diff --git a/apps/api/app/services/ai_avatar_cover_service.py b/apps/api/app/services/ai_avatar_cover_service.py index 2b3422f90..02cb34b59 100644 --- a/apps/api/app/services/ai_avatar_cover_service.py +++ b/apps/api/app/services/ai_avatar_cover_service.py @@ -31,13 +31,14 @@ BEST_FRAME_DOWNLOAD_TIMEOUT = 30 def _sign_video_url_for_mediakit(video_url: str) -> str: """如果 video_url 是自家 OSS 私有桶 URL,重新签名为长有效期预签名 URL。 - + MediaKit GPU worker 需要能公网访问 video_url,裸 public_url 在私有桶下会 403。 """ if not video_url: return video_url try: from packages.shared.storage import get_shared_storage_service + storage = get_shared_storage_service() public_base = getattr(storage, "public_url", "") if not isinstance(public_base, str) or not public_base: @@ -82,7 +83,10 @@ def select_best_cover_frame(video_url: str, *, max_frames: int = 5) -> str: logger.info( "[数字人封面] 开始抽帧: video_url=%s max_frames=%d poll_interval=%.1f max_poll=%d", - video_url[:80], max_frames, COVER_POLL_INTERVAL, COVER_MAX_POLL_ATTEMPTS, + video_url[:80], + max_frames, + COVER_POLL_INTERVAL, + COVER_MAX_POLL_ATTEMPTS, ) snapshots = mk.extract_frames( diff --git a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx index 06bf75464..bd2fe67f7 100644 --- a/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx +++ b/apps/web/src/pages/ai-avatar/AiAvatarPage.tsx @@ -30,13 +30,7 @@ import { } from "./utils/contract" /** 面板折叠状态 */ -type PanelKey = - | "video" - | "voice" - | "script" - | "lipsync" - | "title" - | "cover" +type PanelKey = "video" | "voice" | "script" | "lipsync" | "title" | "cover" const AiAvatarPage: React.FC = () => { const state = useAiAvatar() @@ -52,9 +46,9 @@ const AiAvatarPage: React.FC = () => { /* ── 对口型生成弹窗 ── */ const [showLipsyncModal, setShowLipsyncModal] = useState(false) - const [lipsyncStatus, setLipsyncStatus] = useState< - "generating" | "completed" | "failed" - >("generating") + const [lipsyncStatus, setLipsyncStatus] = useState<"generating" | "completed" | "failed">( + "generating", + ) const [lipsyncErrorMessage, setLipsyncErrorMessage] = useState("") /* ─ 智能封面加载态 ── */ const [smartCoverLoading, setSmartCoverLoading] = useState(false) @@ -153,9 +147,7 @@ const AiAvatarPage: React.FC = () => { } else if (updated.status === "failed") { if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current) setLipsyncStatus("failed") - setLipsyncErrorMessage( - updated.error_message || "对口型生成失败", - ) + setLipsyncErrorMessage(updated.error_message || "对口型生成失败") } } catch (err) { console.error("[对口型] 轮询错误:", err) @@ -168,18 +160,10 @@ const AiAvatarPage: React.FC = () => { message: err instanceof Error ? err.message : String(err), }) setShowLipsyncModal(false) - message.error( - err instanceof Error ? err.message : "对口型任务提交失败,请重试", - ) + message.error(err instanceof Error ? err.message : "对口型任务提交失败,请重试") } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - state.selectedVideo, - state.selectedVoice, - state.scriptText, - state.speed, - state.emotion, - ]) + }, [state.selectedVideo, state.selectedVoice, state.scriptText, state.speed, state.emotion]) const handleCancelLipsync = useCallback(() => { if (lipsyncTimerRef.current) { @@ -210,18 +194,13 @@ const AiAvatarPage: React.FC = () => { script_id: state.script?.id, b_roll_segments: state.bRollSegments as never, title_config: buildTitleConfigPayload(state.titleConfig), - cover_config: buildCoverConfigPayload( - state.coverConfig, - state.coverConfig.smart_cover_url, - ), + cover_config: buildCoverConfigPayload(state.coverConfig, state.coverConfig.smart_cover_url), resolution: state.resolution, }) message.success("渲染任务已提交,可在视频管理中查看进度") } catch (err) { console.error("渲染任务提交失败:", err) - message.error( - err instanceof Error ? err.message : "渲染任务提交失败,请重试", - ) + message.error(err instanceof Error ? err.message : "渲染任务提交失败,请重试") } finally { state.setIsGenerating(false) } @@ -258,9 +237,7 @@ const AiAvatarPage: React.FC = () => { } } catch (err) { console.error("智能封面生成失败:", err) - message.error( - err instanceof Error ? err.message : "智能封面生成失败,请重试", - ) + message.error(err instanceof Error ? err.message : "智能封面生成失败,请重试") } finally { setSmartCoverLoading(false) } @@ -293,9 +270,7 @@ const AiAvatarPage: React.FC = () => {
= 2 ? "active" : ""}`}> 2 - - 对口型 / 标题 / 封面 / 生成 - + 对口型 / 标题 / 封面 / 生成
@@ -304,13 +279,8 @@ const AiAvatarPage: React.FC = () => { {currentStep === 1 && ( <> {/* 面板 1:出镜视频 */} -
-
togglePanel("video")} - > +
+
togglePanel("video")}> 出镜视频 ▼
@@ -325,13 +295,8 @@ const AiAvatarPage: React.FC = () => {
{/* 面板 2:配音库 */} -
-
togglePanel("voice")} - > +
+
togglePanel("voice")}> 配音库 ▼
@@ -352,13 +317,8 @@ const AiAvatarPage: React.FC = () => {
{/* 面板 3:文案 */} -
-
togglePanel("script")} - > +
+
togglePanel("script")}> 文案 ▼
@@ -373,10 +333,7 @@ const AiAvatarPage: React.FC = () => { {/* 步骤 1 底部按钮 */}
-
@@ -387,16 +344,9 @@ const AiAvatarPage: React.FC = () => { {currentStep === 2 && ( <> {/* 面板 4:插入画面 & 对口型预览 */} -
-
togglePanel("lipsync")} - > - - 对口型预览 - +
+
togglePanel("lipsync")}> + 对口型预览 ▼
@@ -411,13 +361,8 @@ const AiAvatarPage: React.FC = () => {
{/* 面板 5:标题配置 */} -
-
togglePanel("title")} - > +
+
togglePanel("title")}> 标题配置 ▼
@@ -430,13 +375,8 @@ const AiAvatarPage: React.FC = () => {
{/* 面板 6:封面 & 生成 */} -
-
togglePanel("cover")} - > +
+
togglePanel("cover")}> 封面 & 生成 ▼
@@ -451,9 +391,7 @@ const AiAvatarPage: React.FC = () => { } onSmartCover={handleSmartCover} smartCoverLoading={smartCoverLoading} - canSmartCover={ - state.lipsyncJob?.status === "completed" - } + canSmartCover={state.lipsyncJob?.status === "completed"} resolution={state.resolution} onResolutionChange={state.setResolution} isGenerating={state.isGenerating} @@ -511,10 +449,7 @@ const AiAvatarPage: React.FC = () => {
e.stopPropagation()}>
对口型生成 -
@@ -592,10 +527,7 @@ const AiAvatarPage: React.FC = () => {
{lipsyncStatus === "generating" && ( - )} @@ -663,17 +595,12 @@ const ScriptSelectModalLazy: React.FC<{ ) : (
{filtered.map((s) => ( -
onSelect(s)} - > +
onSelect(s)}> 📄
{s.title}
- {s.char_count}字 ·{" "} - {new Date(s.created_at).toLocaleDateString()} + {s.char_count}字 · {new Date(s.created_at).toLocaleDateString()}
diff --git a/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx b/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx index 1e32c5186..26789632d 100755 --- a/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx +++ b/apps/web/src/pages/ai-avatar/components/PanelLipsyncPreview.tsx @@ -30,8 +30,7 @@ export function PanelLipsyncPreview({ onOpenBRollModal, onRemoveBRoll, }: PanelLipsyncPreviewProps) { - const isGenerating = - lipsyncJob?.status === "pending" || lipsyncJob?.status === "processing" + const isGenerating = lipsyncJob?.status === "pending" || lipsyncJob?.status === "processing" const isDone = lipsyncJob?.status === "completed" const isFailed = lipsyncJob?.status === "failed" @@ -49,9 +48,7 @@ export function PanelLipsyncPreview({
🎞️ 插入画面 {bRollSegments.length > 0 && ( - - 🎬 {bRollSegments.length} 个画面 - + 🎬 {bRollSegments.length} 个画面 )}
@@ -75,10 +72,7 @@ export function PanelLipsyncPreview({ alt={seg.asset.name} /> ) : ( - + 🎬 )} @@ -157,11 +151,7 @@ export function PanelLipsyncPreview({
{isDone ? ( - ) : isGenerating ? ( diff --git a/apps/web/src/pages/ai-avatar/components/PanelScript.tsx b/apps/web/src/pages/ai-avatar/components/PanelScript.tsx index 7ff8b91d7..551e87131 100755 --- a/apps/web/src/pages/ai-avatar/components/PanelScript.tsx +++ b/apps/web/src/pages/ai-avatar/components/PanelScript.tsx @@ -55,9 +55,7 @@ export function PanelScript({ value={scriptText} readOnly={scriptTab === "library"} placeholder={ - scriptTab === "library" - ? "点击上方按钮,从文案库选择文案…" - : "请输入数字人口播文案…" + scriptTab === "library" ? "点击上方按钮,从文案库选择文案…" : "请输入数字人口播文案…" } onChange={(e) => onScriptTextChange(e.target.value)} /> diff --git a/tests/unit/test_ai_avatar_emotion_tts_lipsync.py b/tests/unit/test_ai_avatar_emotion_tts_lipsync.py index 3df80e903..217b0d6ba 100644 --- a/tests/unit/test_ai_avatar_emotion_tts_lipsync.py +++ b/tests/unit/test_ai_avatar_emotion_tts_lipsync.py @@ -289,7 +289,9 @@ def test_sign_video_url_resigns_own_oss_url(): with patch("packages.shared.storage.get_shared_storage_service") as storage_patch: storage = MagicMock() storage.public_url = "https://xiaoxia-autocut.oss-cn-hangzhou.aliyuncs.com" - storage.get_download_url.return_value = "https://xiaoxia-autocut.oss-cn-hangzhou.aliyuncs.com/vid.mp4?Expires=999&Signature=abc" + storage.get_download_url.return_value = ( + "https://xiaoxia-autocut.oss-cn-hangzhou.aliyuncs.com/vid.mp4?Expires=999&Signature=abc" + ) storage_patch.return_value = storage result = cov._sign_video_url_for_mediakit("https://xiaoxia-autocut.oss-cn-hangzhou.aliyuncs.com/vid.mp4")