diff --git a/apps/web/src/pages/generate/GeneratePage.tsx b/apps/web/src/pages/generate/GeneratePage.tsx index 94bc215fc..ff3baa14e 100755 --- a/apps/web/src/pages/generate/GeneratePage.tsx +++ b/apps/web/src/pages/generate/GeneratePage.tsx @@ -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("") const [playingVoice, setPlayingVoice] = useState(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 = () => {

上传语音样本克隆

+ {/* 从配音库选择 */} +
setVoiceMode("library")} + role="button" + tabIndex={0} + aria-pressed={voiceMode === "library"} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault() + setVoiceMode("library") + } + }} + > + +
+ 📁 +
+
+

从配音库选择

+

我上传的配音文件

+
+
{/* 预设音色详细列表(当 voiceMode === preset 时显示 API 返回的音色) */} @@ -1167,6 +1215,113 @@ const GeneratePage: React.FC = () => { )} + {/* 从配音库选择 */} + {voiceMode === "library" && ( +
+
+ + {voiceAssetsLoading ? ( +

加载中…

+ ) : voiceAssets.length === 0 ? ( +
+

+ 配音库暂无素材,去上传配音吧 +

+
+ ) : ( +
+ {voiceAssets.map((asset) => ( +
{ + 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", + }} + > +
+ 🎵 + + {asset.name} + +
+
+ + {asset.duration + ? `${Math.floor(asset.duration / 60)}:${String(asset.duration % 60).padStart(2, "0")}` + : "—"} + + {asset.file_url && ( + + )} +
+
+ ))} +
+ )} +
+
+ )} + {/* 自定义录制 */} {voiceMode === "custom" && (