Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5cefbc9c05 | |||
| 41fe2a96a6 |
@@ -1,8 +1,8 @@
|
||||
export type FusionLevel = "full_ai" | "polish" | "as_is"
|
||||
export type FusionLevel = "ai_full" | "ai_polish" | "user_primary"
|
||||
export const FUSION_LEVELS: { value: FusionLevel; label: string; desc: string }[] = [
|
||||
{ value: "full_ai", label: "AI 全写", desc: "给我方向,全由AI创作" },
|
||||
{ value: "polish", label: "AI润色", desc: "我写草稿,AI帮我润色" },
|
||||
{ value: "as_is", label: "按我写的来", desc: "几乎不改我的文案" },
|
||||
{ value: "ai_full", label: "AI 全写", desc: "给我方向,全由AI创作" },
|
||||
{ value: "ai_polish", label: "AI润色", desc: "我写草稿,AI帮我润色" },
|
||||
{ value: "user_primary", label: "按我写的来", desc: "几乎不改我的文案" },
|
||||
]
|
||||
|
||||
export type StyleStrength = "light" | "medium" | "strict"
|
||||
@@ -16,8 +16,8 @@ export type ViralVideoStatus =
|
||||
"pending" | "running" | "wait_user_confirm" | "completed" | "failed" | "cancelled"
|
||||
|
||||
/**
|
||||
* 前端展示的流水线阶段(已移除 muse talk 口型同步;后端枚举保留字段以兼容旧数据,不展示)。
|
||||
* 百分比对齐后端 worker _emit_progress 的真实值。
|
||||
* 后端流水线阶段字符串。前端不展示逐阶段进度列表,仅保留类型
|
||||
* 用于轮询时判断当前在哪个大阶段(分析中 vs 视频生成中)以选择轮询间隔/文案。
|
||||
*/
|
||||
export type ViralVideoStage =
|
||||
| "image_analysis"
|
||||
@@ -29,40 +29,19 @@ export type ViralVideoStage =
|
||||
| "tts"
|
||||
| "bgm_select"
|
||||
| "rendering"
|
||||
| "musetalk"
|
||||
| "uploading"
|
||||
|
||||
export interface StageDisplay {
|
||||
label: string
|
||||
/** 该阶段完成时到达的总体进度百分比 */
|
||||
pct: number
|
||||
}
|
||||
|
||||
export const STAGE_DISPLAYS: Record<ViralVideoStage, StageDisplay> = {
|
||||
image_analysis: { label: "图片分析", pct: 15 },
|
||||
video_analysis: { label: "参考视频风格分析", pct: 20 },
|
||||
intent_parsing: { label: "意图理解", pct: 30 },
|
||||
copy_fusion: { label: "文案融合创作", pct: 50 },
|
||||
storyboard: { label: "分镜生成", pct: 60 },
|
||||
review: { label: "AI审核", pct: 70 },
|
||||
tts: { label: "配音生成", pct: 75 },
|
||||
bgm_select: { label: "BGM匹配", pct: 78 },
|
||||
rendering: { label: "视频渲染", pct: 88 },
|
||||
uploading: { label: "上传成片", pct: 100 },
|
||||
}
|
||||
|
||||
/** 阶段顺序(用于进度列表渲染) */
|
||||
export const STAGE_ORDER: ViralVideoStage[] = [
|
||||
/** 分析类阶段(image_analysis / video_analysis / intent_parsing):属于「开始分析」阶段 */
|
||||
const ANALYSIS_STAGES = new Set<ViralVideoStage>([
|
||||
"image_analysis",
|
||||
"video_analysis",
|
||||
"intent_parsing",
|
||||
"copy_fusion",
|
||||
"storyboard",
|
||||
"review",
|
||||
"tts",
|
||||
"bgm_select",
|
||||
"rendering",
|
||||
"uploading",
|
||||
]
|
||||
])
|
||||
|
||||
export function isAnalysisStage(stage: ViralVideoStage | undefined): boolean {
|
||||
return !!stage && ANALYSIS_STAGES.has(stage)
|
||||
}
|
||||
|
||||
export interface StyleTemplate {
|
||||
id: string
|
||||
|
||||
@@ -639,57 +639,6 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ── 阶段进度列表 ──────────────────────────────────── */
|
||||
.vv-stages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.vv-stage {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
padding: 5px 0;
|
||||
}
|
||||
.vv-stage-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #e5e7eb;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 9px;
|
||||
background: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.vv-stage.active {
|
||||
color: #7c3aed;
|
||||
}
|
||||
.vv-stage.active .vv-stage-dot {
|
||||
border-color: #7c3aed;
|
||||
background: #f3f0ff;
|
||||
}
|
||||
.vv-stage.done {
|
||||
color: #10b981;
|
||||
}
|
||||
.vv-stage.done .vv-stage-dot {
|
||||
border-color: #10b981;
|
||||
background: #10b981;
|
||||
color: #fff;
|
||||
}
|
||||
.vv-stage.failed {
|
||||
color: #ef4444;
|
||||
}
|
||||
.vv-stage.failed .vv-stage-dot {
|
||||
border-color: #ef4444;
|
||||
background: #ef4444;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vv-progress-bar {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
CaretDownOutlined,
|
||||
VideoCameraOutlined,
|
||||
SoundOutlined,
|
||||
CheckOutlined,
|
||||
ReloadOutlined,
|
||||
WarningFilled,
|
||||
BulbFilled,
|
||||
@@ -20,12 +19,10 @@ import { uploadAssetDirect, getAssetLibraries, getAssetsByKind } from "@/api/ass
|
||||
import {
|
||||
FUSION_LEVELS,
|
||||
STYLE_STRENGTHS,
|
||||
STAGE_DISPLAYS,
|
||||
STAGE_ORDER,
|
||||
isAnalysisStage,
|
||||
type FusionLevel,
|
||||
type StyleStrength,
|
||||
type ViralVideoJob,
|
||||
type ViralVideoStage,
|
||||
} from "@/api/viral-video/types"
|
||||
import { generateViralVideo, confirmViralVideoIntent, retryViralVideo } from "@/api/viral-video"
|
||||
import { useViralVideoPolling } from "./hooks/useViralVideoPolling"
|
||||
@@ -124,7 +121,7 @@ const emptyTask = (id: string, title: string): TabTask => ({
|
||||
title,
|
||||
images: [],
|
||||
refVideo: null,
|
||||
fusionLevel: "full_ai",
|
||||
fusionLevel: "ai_full",
|
||||
styleStrength: "medium",
|
||||
voiceId: "",
|
||||
userCopy: "",
|
||||
@@ -428,7 +425,7 @@ const ViralVideoPage: React.FC = () => {
|
||||
images: imgUrls,
|
||||
reference_video_url: task.refVideo?.ossUrl || undefined,
|
||||
style_strength: task.refVideo?.ossUrl ? task.styleStrength : undefined,
|
||||
user_copy_text: task.fusionLevel !== "full_ai" ? task.userCopy : undefined,
|
||||
user_copy_text: task.fusionLevel !== "ai_full" ? task.userCopy : undefined,
|
||||
fusion_level: task.fusionLevel,
|
||||
// 注意:STEP1「开始分析」阶段还没有真正开始生成视频,后端会跑到 WAIT_USER_CONFIRM 暂停。
|
||||
// 配音和视频参数在 STEP2 确认文案时由 confirm-intent 继续使用;但后端接口目前只接收一次 payload,
|
||||
@@ -517,30 +514,11 @@ const ViralVideoPage: React.FC = () => {
|
||||
task.job.status === "running" &&
|
||||
!isAwaitingConfirm &&
|
||||
!task.generating &&
|
||||
(!task.job.progress_stage ||
|
||||
["image_analysis", "video_analysis", "intent_parsing"].includes(task.job.progress_stage)))
|
||||
(!task.job.progress_stage || isAnalysisStage(task.job.progress_stage)))
|
||||
const isGenerating =
|
||||
task.generating ||
|
||||
(task.job?.status === "running" &&
|
||||
task.job.progress_stage &&
|
||||
!["image_analysis", "video_analysis", "intent_parsing"].includes(task.job.progress_stage))
|
||||
task.generating || (task.job?.status === "running" && !isAnalysisStage(task.job.progress_stage))
|
||||
const isDone = task.job?.status === "completed"
|
||||
const isFailed = task.job?.status === "failed"
|
||||
const progressPct = task.job?.progress_percent ?? 0
|
||||
const currentStage: ViralVideoStage | undefined = task.job?.progress_stage as
|
||||
ViralVideoStage | undefined
|
||||
|
||||
/* ── 阶段列表派生显示状态 ── */
|
||||
const stageState = (s: ViralVideoStage): "pending" | "active" | "done" | "failed" => {
|
||||
if (isFailed && s === currentStage) return "failed"
|
||||
if (isDone) return "done"
|
||||
if (!currentStage) return "pending"
|
||||
const cur = STAGE_DISPLAYS[currentStage].pct
|
||||
const mine = STAGE_DISPLAYS[s].pct
|
||||
if (s === currentStage) return "active"
|
||||
if (mine < cur) return "done"
|
||||
return "pending"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="vv-page">
|
||||
@@ -838,14 +816,14 @@ const ViralVideoPage: React.FC = () => {
|
||||
</div>
|
||||
<div className="vv-form-row">
|
||||
<label className="vv-label">
|
||||
{task.fusionLevel === "full_ai"
|
||||
{task.fusionLevel === "ai_full"
|
||||
? "你想拍什么?(给 AI 方向即可,可选)"
|
||||
: "写下你的文案/草稿"}
|
||||
</label>
|
||||
<textarea
|
||||
className="vv-textarea"
|
||||
placeholder={
|
||||
task.fusionLevel === "full_ai"
|
||||
task.fusionLevel === "ai_full"
|
||||
? "例:夏季上新的连衣裙,突出清爽、显白、百搭"
|
||||
: "例:姐妹们!这件连衣裙夏天穿太清爽了…"
|
||||
}
|
||||
@@ -1128,44 +1106,6 @@ const ViralVideoPage: React.FC = () => {
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
title="生成进度"
|
||||
step={3}
|
||||
collapsed={task.collapsed.stages}
|
||||
onToggle={() => toggleCollapse("stages")}
|
||||
>
|
||||
{task.jobId && (isAnalyzing || isGenerating || isDone || isFailed) && (
|
||||
<>
|
||||
<div className="vv-progress-bar">
|
||||
<div className="vv-progress-fill" style={{ width: `${progressPct}%` }} />
|
||||
</div>
|
||||
<div className="vv-progress-meta">
|
||||
<span>{task.job?.progress_message || "处理中…"}</span>
|
||||
<span className="vv-progress-pct">{progressPct}%</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="vv-stages">
|
||||
{STAGE_ORDER.map((s) => {
|
||||
const st = stageState(s)
|
||||
return (
|
||||
<div key={s} className={`vv-stage ${st}`}>
|
||||
<span className="vv-stage-dot">
|
||||
{st === "done" ? (
|
||||
<CheckOutlined style={{ fontSize: 8 }} />
|
||||
) : st === "failed" ? (
|
||||
"×"
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</span>
|
||||
<span>{STAGE_DISPLAYS[s].label}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<div className="vv-credits">
|
||||
<span>
|
||||
<SoundOutlined style={{ marginRight: 4 }} />
|
||||
@@ -1206,7 +1146,7 @@ const ViralVideoPage: React.FC = () => {
|
||||
)}
|
||||
{isGenerating && (
|
||||
<button className="vv-btn vv-btn-primary" disabled>
|
||||
<span className="vv-spinner" /> 视频生成中 {progressPct}%…
|
||||
<span className="vv-spinner" /> AI 正在生成视频中…
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useRef } from "react"
|
||||
import { getViralVideoJob } from "@/api/viral-video"
|
||||
import type { ViralVideoJob, ViralVideoStatus } from "@/api/viral-video/types"
|
||||
import { isAnalysisStage, type ViralVideoJob, type ViralVideoStatus } from "@/api/viral-video/types"
|
||||
|
||||
const TERMINAL: ViralVideoStatus[] = ["completed", "failed", "cancelled"]
|
||||
|
||||
@@ -10,9 +10,9 @@ export interface UseViralVideoPollingOptions {
|
||||
}
|
||||
|
||||
/**
|
||||
* 爆款视频任务 HTTP 轮询 hook(后端暂未暴露 WS 桥,轮询兜底)。
|
||||
* 爆款视频任务 HTTP 轮询 hook。
|
||||
* 负责持续拉取任务状态并回调给上层;上层负责根据状态/阶段切换 UI 文案。
|
||||
* 任务进入终态(completed/failed/cancelled)后自动停止。
|
||||
* 后端进度条通过 progress_percent 真实值推进,前端不再依赖阶段插值。
|
||||
*/
|
||||
export function useViralVideoPolling(
|
||||
jobId: string | null | undefined,
|
||||
@@ -43,14 +43,15 @@ export function useViralVideoPolling(
|
||||
return
|
||||
}
|
||||
if (stoppedRef.current) return
|
||||
// 渲染阶段(Seedance 多段视频生成较慢)拉长轮询间隔
|
||||
// 视频渲染阶段(Seedance 多段视频生成较慢)拉长轮询间隔
|
||||
const inRender = job.progress_stage === "rendering"
|
||||
const nextDelay = inRender ? 3000 : intervalMs
|
||||
// 分析阶段走默认间隔即可
|
||||
const isAnalyzing = isAnalysisStage(job.progress_stage)
|
||||
const nextDelay = inRender ? 3000 : isAnalyzing ? 2000 : intervalMs
|
||||
timerRef.current = setTimeout(() => pollOnce(id), nextDelay)
|
||||
} catch (_err) {
|
||||
failCountRef.current += 1
|
||||
if (stoppedRef.current) return
|
||||
// 指数退避,最多退到 10s
|
||||
const delay = Math.min(intervalMs * 2 ** Math.min(failCountRef.current, 3), 10000)
|
||||
timerRef.current = setTimeout(() => pollOnce(id), delay)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user