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")