feat(P1): 智能剪辑配音支持从配音库选择 #566
@@ -24,7 +24,7 @@ import {
|
||||
CloseOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import type { AssetItem } from "@/api/assets"
|
||||
import { getAssets, getAssetLibraries } from "@/api/assets"
|
||||
import { getAssets, getAssetLibraries, getAssetsByKind } from "@/api/assets"
|
||||
import {
|
||||
createEditPlan,
|
||||
generateEditPlan,
|
||||
@@ -129,8 +129,15 @@ const GeneratePage: React.FC = () => {
|
||||
/* ── 配音 ── */
|
||||
const [selectedVoice, setSelectedVoice] = useState<string>("")
|
||||
const [playingVoice, setPlayingVoice] = useState<string | null>(null)
|
||||
const [voiceMode, setVoiceMode] = useState<"preset" | "custom" | "clone">("preset")
|
||||
const [voiceMode, setVoiceMode] = useState<"preset" | "custom" | "clone" | "library">("preset")
|
||||
const [customVoiceText, setCustomVoiceText] = useState("")
|
||||
/* ── 从配音库选择 ── */
|
||||
const [selectedVoiceAssetId, setSelectedVoiceAssetId] = useState("")
|
||||
const { data: voiceAssets = [], isLoading: voiceAssetsLoading } = useQuery({
|
||||
queryKey: ["voice-assets-library"],
|
||||
queryFn: () => getAssetsByKind("voice"),
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
/* ── 生成数量 ── */
|
||||
const [generateCount, setGenerateCount] = useState(1)
|
||||
@@ -465,6 +472,10 @@ const GeneratePage: React.FC = () => {
|
||||
message.warning("请先选择一个克隆音色")
|
||||
return
|
||||
}
|
||||
if (voiceMode === "library" && !selectedVoiceAssetId) {
|
||||
message.warning("请从配音库选择一个配音")
|
||||
return
|
||||
}
|
||||
|
||||
setGenerating(true)
|
||||
setProgress(0)
|
||||
@@ -484,6 +495,11 @@ const GeneratePage: React.FC = () => {
|
||||
voiceConfig.voice_id = selectedVoice || undefined
|
||||
if (customAudioUrl) voiceConfig.custom_audio_url = customAudioUrl
|
||||
if (customVoiceText.trim()) voiceConfig.custom_text = customVoiceText.trim()
|
||||
} else if (voiceMode === "library") {
|
||||
const voiceAsset = voiceAssets.find((a) => a.id === selectedVoiceAssetId)
|
||||
if (voiceAsset?.file_url) {
|
||||
voiceConfig.custom_audio_url = voiceAsset.file_url
|
||||
}
|
||||
}
|
||||
|
||||
const plan = await createEditPlan({
|
||||
@@ -678,6 +694,8 @@ const GeneratePage: React.FC = () => {
|
||||
selectedVoice,
|
||||
voiceMode,
|
||||
selectedClonedVoice,
|
||||
selectedVoiceAssetId,
|
||||
voiceAssets,
|
||||
customAudioUrl,
|
||||
customVoiceText,
|
||||
videoRatio,
|
||||
@@ -761,6 +779,10 @@ const GeneratePage: React.FC = () => {
|
||||
const cv = clonedVoices.find((v) => v.id === selectedClonedVoice)
|
||||
return cv ? cv.name : "未选择"
|
||||
}
|
||||
if (voiceMode === "library") {
|
||||
const va = voiceAssets.find((a) => a.id === selectedVoiceAssetId)
|
||||
return va ? va.name : "未选择"
|
||||
}
|
||||
const pv = presetVoices.find((v) => v.voice_id === selectedVoice)
|
||||
return pv ? pv.name : "未选择"
|
||||
}
|
||||
@@ -1114,6 +1136,32 @@ const GeneratePage: React.FC = () => {
|
||||
<p>上传语音样本克隆</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* 从配音库选择 */}
|
||||
<div
|
||||
className={`xx-voice-choice-item ${voiceMode === "library" ? "selected" : ""}`}
|
||||
onClick={() => setVoiceMode("library")}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-pressed={voiceMode === "library"}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault()
|
||||
setVoiceMode("library")
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="xx-voice-choice-check">✓</span>
|
||||
<div
|
||||
className="xx-voice-choice-avatar"
|
||||
style={{ background: "linear-gradient(135deg, #f59e0b, #d97706)" }}
|
||||
>
|
||||
📁
|
||||
</div>
|
||||
<div className="xx-voice-choice-info">
|
||||
<h4>从配音库选择</h4>
|
||||
<p>我上传的配音文件</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 预设音色详细列表(当 voiceMode === preset 时显示 API 返回的音色) */}
|
||||
@@ -1167,6 +1215,113 @@ const GeneratePage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 从配音库选择 */}
|
||||
{voiceMode === "library" && (
|
||||
<div>
|
||||
<div className="xx-form-field">
|
||||
<label>选择配音文件</label>
|
||||
{voiceAssetsLoading ? (
|
||||
<p style={{ color: "var(--text-tertiary)", fontSize: 13 }}>加载中…</p>
|
||||
) : voiceAssets.length === 0 ? (
|
||||
<div style={{ padding: "20px 0", textAlign: "center" }}>
|
||||
<p style={{ color: "var(--text-tertiary)", fontSize: 13 }}>
|
||||
配音库暂无素材,去上传配音吧
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(2, 1fr)",
|
||||
gap: 10,
|
||||
maxHeight: 300,
|
||||
overflowY: "auto",
|
||||
}}
|
||||
>
|
||||
{voiceAssets.map((asset) => (
|
||||
<div
|
||||
key={asset.id}
|
||||
onClick={() => {
|
||||
setSelectedVoiceAssetId(asset.id)
|
||||
}}
|
||||
style={{
|
||||
padding: 12,
|
||||
border: `2px solid ${
|
||||
selectedVoiceAssetId === asset.id
|
||||
? "var(--primary, #6366f1)"
|
||||
: "var(--border-color, #e2e8f0)"
|
||||
}`,
|
||||
borderRadius: "var(--radius-sm, 10px)",
|
||||
cursor: "pointer",
|
||||
background:
|
||||
selectedVoiceAssetId === asset.id
|
||||
? "rgba(99, 102, 241, 0.05)"
|
||||
: "transparent",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
<span style={{ fontSize: 18 }}>🎵</span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: 13,
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
title={asset.name}
|
||||
>
|
||||
{asset.name}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "var(--text-tertiary)",
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
{asset.duration
|
||||
? `${Math.floor(asset.duration / 60)}:${String(asset.duration % 60).padStart(2, "0")}`
|
||||
: "—"}
|
||||
</span>
|
||||
{asset.file_url && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
toggleVoicePlay(asset.id, asset.file_url!)
|
||||
}}
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "var(--primary)",
|
||||
cursor: "pointer",
|
||||
fontSize: 11,
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
{playingVoice === asset.id ? "⏸ 暂停" : "▶ 试听"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 自定义录制 */}
|
||||
{voiceMode === "custom" && (
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user