fix(ai-avatar): 恢复原始UI样式,页面拆分为两步骤仅做UI调整
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 19s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m5s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m28s
CI/CD Pipeline / Integration Tests (push) Successful in 1m33s
CI/CD Pipeline / Validate - Style (push) Successful in 2m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m28s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m38s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m55s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m29s
CI/CD Pipeline / Validate - Security (push) Successful in 5m50s
CI/CD Pipeline / Unit Tests (push) Successful in 7m46s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 145h59m33s
CI/CD Pipeline / CI Gate (push) Failing after 145h59m36s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 145h59m36s
CI/CD Pipeline / Build Production API Image (push) Failing after 145h59m36s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 146h6m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 146h6m13s
CI/CD Pipeline / Frontend Lint (push) Failing after 146h7m22s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 146h6m14s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 146h7m26s
CI/CD Pipeline / PR Build Web Image (push) Failing after 146h7m26s
CI/CD Pipeline / PR Build API Image (push) Failing after 146h7m26s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 146h7m27s
CI/CD Pipeline / Deploy Production (push) Failing after 146h34m20s
CI/CD Pipeline / Build Production Web Image (push) Failing after 146h34m23s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 3s
CI/CD Pipeline / Build Staging API Image (push) Successful in 15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 19s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m5s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 1m28s
CI/CD Pipeline / Integration Tests (push) Successful in 1m33s
CI/CD Pipeline / Validate - Style (push) Successful in 2m3s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m7s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m28s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 1m38s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 4m55s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m29s
CI/CD Pipeline / Validate - Security (push) Successful in 5m50s
CI/CD Pipeline / Unit Tests (push) Successful in 7m46s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 145h59m33s
CI/CD Pipeline / CI Gate (push) Failing after 145h59m36s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 145h59m36s
CI/CD Pipeline / Build Production API Image (push) Failing after 145h59m36s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 146h6m14s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 146h6m13s
CI/CD Pipeline / Frontend Lint (push) Failing after 146h7m22s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 146h6m14s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 146h7m26s
CI/CD Pipeline / PR Build Web Image (push) Failing after 146h7m26s
CI/CD Pipeline / PR Build API Image (push) Failing after 146h7m26s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 146h7m27s
CI/CD Pipeline / Deploy Production (push) Failing after 146h34m20s
CI/CD Pipeline / Build Production Web Image (push) Failing after 146h34m23s
- 恢复 AiAvatar.css 为原始版本(1178行逐字节一致),仅在末尾追加70行步骤导航/分步面板宽度样式 - 保持原始横向 flex 布局与全部原有 class/样式,撤销上一版纵向堆叠改动 - 步骤1:出镜视频/配音库/文案(3个横向面板)+ 下一步按钮 - 步骤2:对口型预览(含插入画面)/标题配置/封面&生成(3个横向面板)+ 上一步按钮 - PanelScriptAndLipsync 拆为 PanelScript + PanelLipsyncPreview,业务逻辑/props/弹窗全部原样保留 - 不改任何功能,不动其他页面
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* AI数字人 — 主页面(两步骤版)
|
||||
* AI数字人 — 主页面(v3 两步骤版)
|
||||
* 步骤1:出镜视频 / 配音库 / 文案
|
||||
* 步骤2:插入画面 / 对口型预览 / 标题配置 / 封面&生成
|
||||
* 步骤2:对口型预览(含插入画面)/ 标题配置 / 封面&生成
|
||||
*/
|
||||
import React, { useState, useCallback, useEffect, useRef } from "react"
|
||||
import { message } from "antd"
|
||||
@@ -30,17 +30,11 @@ 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()
|
||||
const [currentStep, setCurrentStep] = useState(1)
|
||||
const [currentStep, setCurrentStep] = useState<1 | 2>(1)
|
||||
const [collapsed, setCollapsed] = useState<Record<PanelKey, boolean>>({
|
||||
video: false,
|
||||
voice: false,
|
||||
@@ -52,11 +46,11 @@ 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)
|
||||
|
||||
/* ── 对口型轮询 ── */
|
||||
@@ -67,24 +61,25 @@ const AiAvatarPage: React.FC = () => {
|
||||
}, [])
|
||||
|
||||
/* ── 步骤切换 ── */
|
||||
const canGoToStep2 = useCallback(() => {
|
||||
return state.selectedVideo && state.selectedVoice && state.scriptText.trim()
|
||||
}, [state.selectedVideo, state.selectedVoice, state.scriptText])
|
||||
|
||||
const handleNextStep = useCallback(() => {
|
||||
if (!canGoToStep2()) {
|
||||
message.warning("请先完成出镜视频、配音和文案的选择")
|
||||
const missing: string[] = []
|
||||
if (!state.selectedVideo) missing.push("出镜视频")
|
||||
if (!state.selectedVoice) missing.push("配音")
|
||||
if (!state.scriptText.trim()) missing.push("文案")
|
||||
if (missing.length > 0) {
|
||||
message.warning(`请先完成${missing.join("、")}`)
|
||||
return
|
||||
}
|
||||
setCurrentStep(2)
|
||||
}, [canGoToStep2])
|
||||
}, [state.selectedVideo, state.selectedVoice, state.scriptText])
|
||||
|
||||
const handlePrevStep = useCallback(() => {
|
||||
setCurrentStep(1)
|
||||
}, [])
|
||||
|
||||
/* ── 对口型 ─ */
|
||||
/* ── 对口型 ── */
|
||||
const handleGenerateLipsync = useCallback(async () => {
|
||||
// ② 缺项明确提示(#1809):不再静默 return
|
||||
const video = state.selectedVideo
|
||||
const voice = state.selectedVoice
|
||||
const text = state.scriptText.trim()
|
||||
@@ -97,10 +92,12 @@ const AiAvatarPage: React.FC = () => {
|
||||
return
|
||||
}
|
||||
try {
|
||||
// 显示生成弹窗
|
||||
setShowLipsyncModal(true)
|
||||
setLipsyncStatus("generating")
|
||||
setLipsyncErrorMessage("")
|
||||
|
||||
// ① 先按素材 id 拿 file_url(#1809 补充:对齐后端新参数 video_url)
|
||||
console.log("[对口型] 开始生成:", {
|
||||
videoId: video.id,
|
||||
voiceId: voice.voice_id,
|
||||
@@ -119,20 +116,19 @@ const AiAvatarPage: React.FC = () => {
|
||||
message.error("获取出镜视频播放地址失败,请重新选择素材")
|
||||
return
|
||||
}
|
||||
// ② 模式A TTS直生:video_url + voice_id + script_text,语速/情绪英文枚举透传(#1822)
|
||||
const payload = {
|
||||
voice_id: voice.voice_id,
|
||||
script_text: state.scriptText,
|
||||
video_url: videoUrl,
|
||||
speed: state.speed,
|
||||
emotion: normalizeEmotion(state.emotion),
|
||||
speed: state.speed, // 语速 0.5~2.0
|
||||
emotion: normalizeEmotion(state.emotion), // natural/excited/calm/friendly
|
||||
}
|
||||
console.log("[对口型] createLipsyncJob 请求:", payload)
|
||||
const job = await createLipsyncJob(payload)
|
||||
console.log("[对口型] createLipsyncJob 响应:", {
|
||||
id: job.id,
|
||||
status: job.status,
|
||||
})
|
||||
console.log("[对口型] createLipsyncJob 响应:", { id: job.id, status: job.status })
|
||||
state.setLipsyncJob(job)
|
||||
// 开始轮询
|
||||
if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current)
|
||||
lipsyncTimerRef.current = setInterval(async () => {
|
||||
try {
|
||||
@@ -153,9 +149,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,19 +162,12 @@ 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) {
|
||||
clearInterval(lipsyncTimerRef.current)
|
||||
@@ -191,6 +178,7 @@ const AiAvatarPage: React.FC = () => {
|
||||
setLipsyncErrorMessage("")
|
||||
}, [])
|
||||
|
||||
// 清理轮询
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (lipsyncTimerRef.current) clearInterval(lipsyncTimerRef.current)
|
||||
@@ -199,6 +187,7 @@ const AiAvatarPage: React.FC = () => {
|
||||
|
||||
/* ── 生成视频 ── */
|
||||
const handleGenerate = useCallback(async () => {
|
||||
// ② 前置条件提示(#1809)
|
||||
if (!state.lipsyncJob || state.lipsyncJob.status !== "completed") {
|
||||
message.warning("请先生成对口型视频,待对口型完成后再提交渲染")
|
||||
return
|
||||
@@ -209,19 +198,16 @@ const AiAvatarPage: React.FC = () => {
|
||||
lipsync_job_id: state.lipsyncJob.id,
|
||||
script_id: state.script?.id,
|
||||
b_roll_segments: state.bRollSegments as never,
|
||||
// 字段映射:build_title_drawtext_filter 真实口径 text/font_size/font_color/position/...
|
||||
title_config: buildTitleConfigPayload(state.titleConfig),
|
||||
cover_config: buildCoverConfigPayload(
|
||||
state.coverConfig,
|
||||
state.coverConfig.smart_cover_url,
|
||||
),
|
||||
// cover_config:智能封面 cover_url + 截帧 timestamp
|
||||
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)
|
||||
}
|
||||
@@ -235,8 +221,9 @@ const AiAvatarPage: React.FC = () => {
|
||||
state.resolution,
|
||||
])
|
||||
|
||||
/* ── 智能封面 ── */
|
||||
/* ── 智能封面:调后端 MediaKit 选帧接口(#1822) ── */
|
||||
const handleSmartCover = useCallback(async () => {
|
||||
// 基于对口型成片抽帧,必须先完成对口型
|
||||
const videoUrl = state.lipsyncJob?.output_video_url
|
||||
if (state.lipsyncJob?.status !== "completed" || !videoUrl) {
|
||||
message.warning("请先生成对口型视频,完成后再智能获取封面")
|
||||
@@ -258,9 +245,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)
|
||||
}
|
||||
@@ -281,36 +266,26 @@ const AiAvatarPage: React.FC = () => {
|
||||
return (
|
||||
<div className="aa-page">
|
||||
<div className="aa-page-header">
|
||||
<h1>AI 数字人</h1>
|
||||
<h1>AI数字人</h1>
|
||||
</div>
|
||||
|
||||
{/* 步骤指示器 */}
|
||||
<div className="aa-steps">
|
||||
<div className={`aa-step ${currentStep >= 1 ? "active" : ""}`}>
|
||||
<span className="aa-step__number">1</span>
|
||||
<span className="aa-step__label">视频 / 配音 / 文案</span>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
{/* 步骤切换导航条 */}
|
||||
<div className="aa-step-nav">
|
||||
<span className={`aa-step-nav__item${currentStep === 1 ? " active" : ""}`}>
|
||||
1. 视频 / 配音 / 文案
|
||||
</span>
|
||||
<span className={`aa-step-nav__item${currentStep === 2 ? " active" : ""}`}>
|
||||
2. 对口型 / 标题 / 封面 / 生成
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="aa-page-body">
|
||||
{/* ════ 步骤 1:出镜视频 / 配音库 / 文案 ═══ */}
|
||||
{/* ════ 步骤 1:出镜视频 / 配音库 / 文案 ════ */}
|
||||
{currentStep === 1 && (
|
||||
<>
|
||||
{/* 面板 1:出镜视频 */}
|
||||
<div
|
||||
className={`aa-panel${collapsed.video ? " collapsed" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="aa-panel__header"
|
||||
onClick={() => togglePanel("video")}
|
||||
>
|
||||
{/* 面板1:出镜视频 */}
|
||||
<div className={`aa-panel aa-panel--s1${collapsed.video ? " collapsed" : ""}`}>
|
||||
<div className="aa-panel__header" onClick={() => togglePanel("video")}>
|
||||
<span className="aa-panel__title">出镜视频</span>
|
||||
<span className="aa-panel__toggle">▼</span>
|
||||
</div>
|
||||
@@ -324,14 +299,9 @@ const AiAvatarPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 面板 2:配音库 */}
|
||||
<div
|
||||
className={`aa-panel${collapsed.voice ? " collapsed" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="aa-panel__header"
|
||||
onClick={() => togglePanel("voice")}
|
||||
>
|
||||
{/* 面板2:配音库 */}
|
||||
<div className={`aa-panel aa-panel--s1${collapsed.voice ? " collapsed" : ""}`}>
|
||||
<div className="aa-panel__header" onClick={() => togglePanel("voice")}>
|
||||
<span className="aa-panel__title">配音库</span>
|
||||
<span className="aa-panel__toggle">▼</span>
|
||||
</div>
|
||||
@@ -351,15 +321,10 @@ const AiAvatarPage: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 面板 3:文案 */}
|
||||
<div
|
||||
className={`aa-panel${collapsed.script ? " collapsed" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="aa-panel__header"
|
||||
onClick={() => togglePanel("script")}
|
||||
>
|
||||
<span className="aa-panel__title">文案</span>
|
||||
{/* 面板3:文案 */}
|
||||
<div className={`aa-panel aa-panel--s1-wide${collapsed.script ? " collapsed" : ""}`}>
|
||||
<div className="aa-panel__header" onClick={() => togglePanel("script")}>
|
||||
<span className="aa-panel__title">文案 & 对口型</span>
|
||||
<span className="aa-panel__toggle">▼</span>
|
||||
</div>
|
||||
<div className="aa-panel__body">
|
||||
@@ -368,35 +333,23 @@ const AiAvatarPage: React.FC = () => {
|
||||
onScriptTextChange={state.setScriptText}
|
||||
onOpenScriptModal={() => state.setShowScriptModal(true)}
|
||||
/>
|
||||
<div className="aa-step-btn-row">
|
||||
<button type="button" className="aa-btn aa-btn--primary" onClick={handleNextStep}>
|
||||
下一步 →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 步骤 1 底部按钮 */}
|
||||
<div className="aa-step-actions">
|
||||
<button
|
||||
className="aa-btn aa-btn--primary"
|
||||
onClick={handleNextStep}
|
||||
>
|
||||
下一步
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ════ 步骤 2:插入画面 / 对口型预览 / 标题配置 / 封面&生成 ═══ */}
|
||||
{/* ════ 步骤 2:对口型预览(含插入画面)/ 标题配置 / 封面&生成 ════ */}
|
||||
{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 aa-panel--s2-wide${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">
|
||||
@@ -407,36 +360,28 @@ const AiAvatarPage: React.FC = () => {
|
||||
onOpenBRollModal={() => state.setShowBRollModal(true)}
|
||||
onRemoveBRoll={state.removeBRollSegment}
|
||||
/>
|
||||
<div className="aa-step-btn-row">
|
||||
<button type="button" className="aa-btn" onClick={handlePrevStep}>
|
||||
← 上一步
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 面板 5:标题配置 */}
|
||||
<div
|
||||
className={`aa-panel${collapsed.title ? " collapsed" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="aa-panel__header"
|
||||
onClick={() => togglePanel("title")}
|
||||
>
|
||||
{/* 面板4:标题配置 */}
|
||||
<div className={`aa-panel aa-panel--s2${collapsed.title ? " collapsed" : ""}`}>
|
||||
<div className="aa-panel__header" onClick={() => togglePanel("title")}>
|
||||
<span className="aa-panel__title">标题配置</span>
|
||||
<span className="aa-panel__toggle">▼</span>
|
||||
</div>
|
||||
<div className="aa-panel__body">
|
||||
<PanelTitleConfig
|
||||
titleConfig={state.titleConfig}
|
||||
onUpdate={state.updateTitleConfig}
|
||||
/>
|
||||
<PanelTitleConfig titleConfig={state.titleConfig} onUpdate={state.updateTitleConfig} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 面板 6:封面 & 生成 */}
|
||||
<div
|
||||
className={`aa-panel${collapsed.cover ? " collapsed" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="aa-panel__header"
|
||||
onClick={() => togglePanel("cover")}
|
||||
>
|
||||
{/* 面板5:封面 & 生成 */}
|
||||
<div className={`aa-panel aa-panel--s2${collapsed.cover ? " collapsed" : ""}`}>
|
||||
<div className="aa-panel__header" onClick={() => togglePanel("cover")}>
|
||||
<span className="aa-panel__title">封面 & 生成</span>
|
||||
<span className="aa-panel__toggle">▼</span>
|
||||
</div>
|
||||
@@ -444,16 +389,11 @@ const AiAvatarPage: React.FC = () => {
|
||||
<PanelCoverAndGenerate
|
||||
coverConfig={state.coverConfig}
|
||||
onCoverConfigChange={(partial) =>
|
||||
state.setCoverConfig((prev) => ({
|
||||
...prev,
|
||||
...partial,
|
||||
}))
|
||||
state.setCoverConfig((prev) => ({ ...prev, ...partial }))
|
||||
}
|
||||
onSmartCover={handleSmartCover}
|
||||
smartCoverLoading={smartCoverLoading}
|
||||
canSmartCover={
|
||||
state.lipsyncJob?.status === "completed"
|
||||
}
|
||||
canSmartCover={state.lipsyncJob?.status === "completed"}
|
||||
resolution={state.resolution}
|
||||
onResolutionChange={state.setResolution}
|
||||
isGenerating={state.isGenerating}
|
||||
@@ -462,13 +402,6 @@ const AiAvatarPage: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 步骤 2 底部按钮 */}
|
||||
<div className="aa-step-actions">
|
||||
<button className="aa-btn" onClick={handlePrevStep}>
|
||||
上一步
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -511,10 +444,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>
|
||||
@@ -530,22 +460,10 @@ const AiAvatarPage: React.FC = () => {
|
||||
{lipsyncStatus === "generating" && (
|
||||
<>
|
||||
<div className="aa-lipsync-spinner" />
|
||||
<div
|
||||
style={{
|
||||
marginTop: 20,
|
||||
fontSize: 15,
|
||||
color: "#1a1a2e",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginTop: 20, fontSize: 15, color: "#1a1a2e" }}>
|
||||
对口型视频生成中…
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
fontSize: 13,
|
||||
color: "#8c8ca1",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginTop: 8, fontSize: 13, color: "#8c8ca1" }}>
|
||||
请勿关闭页面,完成后将自动提示
|
||||
</div>
|
||||
</>
|
||||
@@ -553,13 +471,7 @@ const AiAvatarPage: React.FC = () => {
|
||||
{lipsyncStatus === "completed" && (
|
||||
<>
|
||||
<div style={{ fontSize: 48 }}>✅</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
fontSize: 15,
|
||||
color: "#1a1a2e",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginTop: 16, fontSize: 15, color: "#1a1a2e" }}>
|
||||
对口型视频生成完成
|
||||
</div>
|
||||
</>
|
||||
@@ -567,23 +479,11 @@ const AiAvatarPage: React.FC = () => {
|
||||
{lipsyncStatus === "failed" && (
|
||||
<>
|
||||
<div style={{ fontSize: 48 }}>❌</div>
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
fontSize: 15,
|
||||
color: "#1a1a2e",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginTop: 16, fontSize: 15, color: "#1a1a2e" }}>
|
||||
对口型生成失败
|
||||
</div>
|
||||
{lipsyncErrorMessage && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 8,
|
||||
fontSize: 13,
|
||||
color: "#ff4d4f",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginTop: 8, fontSize: 13, color: "#ff4d4f" }}>
|
||||
{lipsyncErrorMessage}
|
||||
</div>
|
||||
)}
|
||||
@@ -592,10 +492,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 +560,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>
|
||||
|
||||
@@ -43,7 +43,7 @@ export function PanelLipsyncPreview({
|
||||
: "对口型生成中…"
|
||||
|
||||
return (
|
||||
<div className="aa-lipsync-preview-panel">
|
||||
<div className="aa-script-lipsync">
|
||||
{/* ── B-roll 画面 ── */}
|
||||
<div className="aa-lipsync-section">
|
||||
<div className="aa-lipsync-section__title">
|
||||
|
||||
@@ -20,7 +20,7 @@ export function PanelScript({
|
||||
const [scriptTab, setScriptTab] = useState<ScriptTab>("library")
|
||||
|
||||
return (
|
||||
<div className="aa-script-panel">
|
||||
<div className="aa-script-lipsync">
|
||||
{/* ── Tab 切换 ── */}
|
||||
<div className="aa-script-tabs">
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user