style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 11s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 25s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 43s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m40s
AI Code Review / AI Code Review (pull_request) Successful in 6m17s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m0s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Failing after 14m16s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m6s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m7s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m16s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m30s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m52s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 10m44s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 142h31m55s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 142h31m56s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 142h31m56s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 142h31m56s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 142h42m39s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 142h56m27s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 142h56m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 142h56m32s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 142h56m37s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 142h56m38s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 142h56m44s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 142h56m44s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 142h56m53s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 143h6m43s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 143h31m16s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 143h31m26s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 143h31m33s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 11s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 24s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 25s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 43s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m40s
AI Code Review / AI Code Review (pull_request) Successful in 6m17s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m0s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Failing after 14m16s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m6s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 1m7s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 1m16s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 1m30s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 1m52s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 10m44s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 142h31m55s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 142h31m56s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 142h31m56s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 142h31m56s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 142h42m39s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 142h56m27s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 142h56m27s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 142h56m32s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 142h56m37s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 142h56m38s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 142h56m44s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 142h56m44s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 142h56m53s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 143h6m43s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 143h31m16s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 143h31m26s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 143h31m33s
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 = () => {
|
||||
<div className="aa-step__connector" />
|
||||
<div className={`aa-step ${currentStep >= 2 ? "active" : ""}`}>
|
||||
<span className="aa-step__number">2</span>
|
||||
<span className="aa-step__label">
|
||||
对口型 / 标题 / 封面 / 生成
|
||||
</span>
|
||||
<span className="aa-step__label">对口型 / 标题 / 封面 / 生成</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -304,13 +279,8 @@ const AiAvatarPage: React.FC = () => {
|
||||
{currentStep === 1 && (
|
||||
<>
|
||||
{/* 面板 1:出镜视频 */}
|
||||
<div
|
||||
className={`aa-panel${collapsed.video ? " collapsed" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="aa-panel__header"
|
||||
onClick={() => togglePanel("video")}
|
||||
>
|
||||
<div className={`aa-panel${collapsed.video ? " collapsed" : ""}`}>
|
||||
<div className="aa-panel__header" onClick={() => togglePanel("video")}>
|
||||
<span className="aa-panel__title">出镜视频</span>
|
||||
<span className="aa-panel__toggle">▼</span>
|
||||
</div>
|
||||
@@ -325,13 +295,8 @@ const AiAvatarPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 面板 2:配音库 */}
|
||||
<div
|
||||
className={`aa-panel${collapsed.voice ? " collapsed" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="aa-panel__header"
|
||||
onClick={() => togglePanel("voice")}
|
||||
>
|
||||
<div className={`aa-panel${collapsed.voice ? " collapsed" : ""}`}>
|
||||
<div className="aa-panel__header" onClick={() => togglePanel("voice")}>
|
||||
<span className="aa-panel__title">配音库</span>
|
||||
<span className="aa-panel__toggle">▼</span>
|
||||
</div>
|
||||
@@ -352,13 +317,8 @@ const AiAvatarPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 面板 3:文案 */}
|
||||
<div
|
||||
className={`aa-panel${collapsed.script ? " collapsed" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="aa-panel__header"
|
||||
onClick={() => togglePanel("script")}
|
||||
>
|
||||
<div className={`aa-panel${collapsed.script ? " collapsed" : ""}`}>
|
||||
<div className="aa-panel__header" onClick={() => togglePanel("script")}>
|
||||
<span className="aa-panel__title">文案</span>
|
||||
<span className="aa-panel__toggle">▼</span>
|
||||
</div>
|
||||
@@ -373,10 +333,7 @@ const AiAvatarPage: React.FC = () => {
|
||||
|
||||
{/* 步骤 1 底部按钮 */}
|
||||
<div className="aa-step-actions">
|
||||
<button
|
||||
className="aa-btn aa-btn--primary"
|
||||
onClick={handleNextStep}
|
||||
>
|
||||
<button className="aa-btn aa-btn--primary" onClick={handleNextStep}>
|
||||
下一步
|
||||
</button>
|
||||
</div>
|
||||
@@ -387,16 +344,9 @@ const AiAvatarPage: React.FC = () => {
|
||||
{currentStep === 2 && (
|
||||
<>
|
||||
{/* 面板 4:插入画面 & 对口型预览 */}
|
||||
<div
|
||||
className={`aa-panel${collapsed.lipsync ? " collapsed" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="aa-panel__header"
|
||||
onClick={() => togglePanel("lipsync")}
|
||||
>
|
||||
<span className="aa-panel__title">
|
||||
对口型预览
|
||||
</span>
|
||||
<div className={`aa-panel${collapsed.lipsync ? " collapsed" : ""}`}>
|
||||
<div className="aa-panel__header" onClick={() => togglePanel("lipsync")}>
|
||||
<span className="aa-panel__title">对口型预览</span>
|
||||
<span className="aa-panel__toggle">▼</span>
|
||||
</div>
|
||||
<div className="aa-panel__body">
|
||||
@@ -411,13 +361,8 @@ const AiAvatarPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 面板 5:标题配置 */}
|
||||
<div
|
||||
className={`aa-panel${collapsed.title ? " collapsed" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="aa-panel__header"
|
||||
onClick={() => togglePanel("title")}
|
||||
>
|
||||
<div className={`aa-panel${collapsed.title ? " collapsed" : ""}`}>
|
||||
<div className="aa-panel__header" onClick={() => togglePanel("title")}>
|
||||
<span className="aa-panel__title">标题配置</span>
|
||||
<span className="aa-panel__toggle">▼</span>
|
||||
</div>
|
||||
@@ -430,13 +375,8 @@ const AiAvatarPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* 面板 6:封面 & 生成 */}
|
||||
<div
|
||||
className={`aa-panel${collapsed.cover ? " collapsed" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="aa-panel__header"
|
||||
onClick={() => togglePanel("cover")}
|
||||
>
|
||||
<div className={`aa-panel${collapsed.cover ? " collapsed" : ""}`}>
|
||||
<div className="aa-panel__header" onClick={() => togglePanel("cover")}>
|
||||
<span className="aa-panel__title">封面 & 生成</span>
|
||||
<span className="aa-panel__toggle">▼</span>
|
||||
</div>
|
||||
@@ -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 = () => {
|
||||
<div className="aa-modal" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="aa-modal__header">
|
||||
<span className="aa-modal__title">对口型生成</span>
|
||||
<button
|
||||
className="aa-modal__close"
|
||||
onClick={handleCancelLipsync}
|
||||
>
|
||||
<button className="aa-modal__close" onClick={handleCancelLipsync}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
@@ -592,10 +527,7 @@ const AiAvatarPage: React.FC = () => {
|
||||
</div>
|
||||
<div className="aa-modal__footer">
|
||||
{lipsyncStatus === "generating" && (
|
||||
<button
|
||||
className="aa-btn aa-btn--danger"
|
||||
onClick={handleCancelLipsync}
|
||||
>
|
||||
<button className="aa-btn aa-btn--danger" onClick={handleCancelLipsync}>
|
||||
取消生成
|
||||
</button>
|
||||
)}
|
||||
@@ -663,17 +595,12 @@ const ScriptSelectModalLazy: React.FC<{
|
||||
) : (
|
||||
<div className="aa-script-list">
|
||||
{filtered.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
className="aa-script-item"
|
||||
onClick={() => onSelect(s)}
|
||||
>
|
||||
<div key={s.id} className="aa-script-item" onClick={() => onSelect(s)}>
|
||||
<span className="aa-script-item__icon">📄</span>
|
||||
<div className="aa-script-item__info">
|
||||
<div className="aa-script-item__title">{s.title}</div>
|
||||
<div className="aa-script-item__meta">
|
||||
{s.char_count}字 ·{" "}
|
||||
{new Date(s.created_at).toLocaleDateString()}
|
||||
{s.char_count}字 · {new Date(s.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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({
|
||||
<div className="aa-lipsync-section__title">
|
||||
<span style={{ marginRight: 8 }}>🎞️ 插入画面</span>
|
||||
{bRollSegments.length > 0 && (
|
||||
<span className="aa-broll-badge">
|
||||
🎬 {bRollSegments.length} 个画面
|
||||
</span>
|
||||
<span className="aa-broll-badge">🎬 {bRollSegments.length} 个画面</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="aa-lipsync-actions">
|
||||
@@ -75,10 +72,7 @@ export function PanelLipsyncPreview({
|
||||
alt={seg.asset.name}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
className="aa-broll-item__thumb"
|
||||
style={{ padding: "6px 4px" }}
|
||||
>
|
||||
<span className="aa-broll-item__thumb" style={{ padding: "6px 4px" }}>
|
||||
🎬
|
||||
</span>
|
||||
)}
|
||||
@@ -157,11 +151,7 @@ export function PanelLipsyncPreview({
|
||||
|
||||
<div className="aa-lipsync-actions">
|
||||
{isDone ? (
|
||||
<button
|
||||
type="button"
|
||||
className="aa-btn aa-btn--full"
|
||||
onClick={onGenerateLipsync}
|
||||
>
|
||||
<button type="button" className="aa-btn aa-btn--full" onClick={onGenerateLipsync}>
|
||||
🔄 重新生成对口型
|
||||
</button>
|
||||
) : isGenerating ? (
|
||||
|
||||
@@ -55,9 +55,7 @@ export function PanelScript({
|
||||
value={scriptText}
|
||||
readOnly={scriptTab === "library"}
|
||||
placeholder={
|
||||
scriptTab === "library"
|
||||
? "点击上方按钮,从文案库选择文案…"
|
||||
: "请输入数字人口播文案…"
|
||||
scriptTab === "library" ? "点击上方按钮,从文案库选择文案…" : "请输入数字人口播文案…"
|
||||
}
|
||||
onChange={(e) => onScriptTextChange(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user