feat(viral-video): #2041 三步骤独立交互——分析图片/生成文案/生成视频拆分 #2116

Merged
auto-approve-bot merged 1 commits from feat/2041-three-step-independent-flow into develop 2026-10-01 11:14:28 +08:00
4 changed files with 772 additions and 416 deletions
+61 -1
View File
@@ -4,6 +4,8 @@ import type {
HistoryResponse,
StyleTemplate,
ViralVideoJob,
ImageAnalysisResult,
CopyResult,
} from "./types"
/** 创建爆款视频任务 */
@@ -41,7 +43,65 @@ export function getViralStyleTemplates() {
return apiClient.get<StyleTemplate[]>("/viral-video/style-templates").then((r) => r.data)
}
/** 上传参考视频后触发风格分析(返回带 style_guide 的任务详情) */
/** 上传参考视频后触发风格分析 */
export function analyzeViralStyle(id: string) {
return apiClient.post<ViralVideoJob>(`/viral-video/${id}/analyze-style`).then((r) => r.data)
}
/** ── 三步拆分:前端 mock 辅助函数(后端新接口上线后可替换) ── */
/**
* 客户端图片分析 mock(后端未提供 analyze-only 端点前的占位方案):
* 基于已上传图片生成一份示例识别汇览,让 STEP1→STEP2 交互可走通。
* 后端上线后改为调用真实接口。
*/
export function mockImageAnalysis(images: { name: string }[]): Promise<ImageAnalysisResult> {
return new Promise((resolve) => {
setTimeout(() => {
const products = images.slice(0, 3).map((img, i) => {
const n = img.name.replace(/\.[^.]+$/, "")
return {
name: n || `商品 ${i + 1}`,
spec: i === 0 ? "500ml/瓶" : i === 1 ? "300g/盒" : undefined,
brand: i === 0 ? "示例品牌" : undefined,
features:
i === 0
? "瓶身透明、蓝色标签、白色瓶盖;标签上印有品牌Logo和产品名称;光线均匀,主体居中"
: i === 1
? "盒装包装、主色调为米白+暖黄;正面有产品实物图;文字清晰可辨"
: "产品主体清晰、背景干净、色彩鲜艳,突出核心卖点",
label_text: i === 0 ? "包装正面印有产品名称、净含量、品牌Logo" : undefined,
image_index: i,
}
})
resolve({ products })
}, 1800)
})
}
/**
* 客户端文案生成 mock(后端未提供 generate-copy 端点前的占位方案):
* 后端上线后改为调用真实接口。
*/
export function mockGenerateCopy(params: {
product: string
sellingPoints?: string[]
tone?: string
duration?: number
marketingPurpose?: string
industry?: string
targetCustomer?: string
}): Promise<CopyResult> {
return new Promise((resolve) => {
setTimeout(() => {
const product = params.product || "这款产品"
const tone = params.tone || "亲切务实"
const purpose = params.marketingPurpose || "品牌种草"
resolve({
title: `【${purpose}】${product},用过的人都说好!`,
final_copy: `你有没有发现,选对一款${params.industry || "好物"}真的能让生活省心很多?\n\n今天给大家推荐这款${product}。${tone.includes("亲切") ? "说实话," : ""}我自己用了一段时间,最直观的感受就是——好用、省心、值得回购。\n\n✅ 亮点一:品质到位,用料扎实,细节处见用心\n✅ 亮点二:使用体验舒服,日常高频场景都能打\n✅ 亮点三:性价比很能打,这个价位真的没什么可挑的\n\n如果你也在找一款靠谱的${params.industry || "日常好物"},真的建议试试${product},不会让你失望。点击左下角,直接入手!`,
suggested_copy: `你有没有发现,选对一款${params.industry || "好物"}真的能让生活省心很多?\n\n今天给大家推荐这款${product}。${tone.includes("亲切") ? "说实话," : ""}我自己用了一段时间,最直观的感受就是——好用、省心、值得回购。\n\n✅ 亮点一:品质到位,用料扎实,细节处见用心\n✅ 亮点二:使用体验舒服,日常高频场景都能打\n✅ 亮点三:性价比很能打,这个价位真的没什么可挑的\n\n如果你也在找一款靠谱的${params.industry || "日常好物"},真的建议试试${product},不会让你失望。点击左下角,直接入手!`,
})
}, 2200)
})
}
+47 -16
View File
@@ -13,11 +13,18 @@ export const STYLE_STRENGTHS: { value: StyleStrength; label: string }[] = [
]
export type ViralVideoStatus =
"pending" | "running" | "wait_user_confirm" | "completed" | "failed" | "cancelled"
| "pending"
| "running"
| "wait_user_confirm"
| "image_analyzed"
| "copy_generated"
| "completed"
| "failed"
| "cancelled"
/**
* 后端流水线阶段字符串。前端不展示逐阶段进度列表,仅保留类型
* 用于轮询时判断当前在哪个大阶段(分析中 vs 视频生成中)以选择轮询间隔/文案。
* 用于轮询时判断当前在哪个大阶段(分析中 vs 文案生成 vs 视频生成)。
*/
export type ViralVideoStage =
| "image_analysis"
@@ -32,38 +39,61 @@ export type ViralVideoStage =
| "musetalk"
| "uploading"
/** 分析类阶段(image_analysis / video_analysis / intent_parsing):属于「开始分析」阶段 */
const ANALYSIS_STAGES = new Set<ViralVideoStage>([
"image_analysis",
"video_analysis",
/** 图片+视频分析阶段:属于「分析图片」按钮的范围 */
const IMAGE_ANALYSIS_STAGES = new Set<ViralVideoStage>(["image_analysis", "video_analysis"])
/** 文案相关阶段:属于「生成文案」按钮的范围 */
const COPY_STAGES = new Set<ViralVideoStage>([
"intent_parsing",
"copy_fusion",
"storyboard",
"review",
])
/** 视频相关阶段:属于「开始生成视频」按钮的范围 */
const VIDEO_STAGES = new Set<ViralVideoStage>([
"tts",
"bgm_select",
"rendering",
"musetalk",
"uploading",
])
export function isImageAnalysisStage(stage: ViralVideoStage | undefined): boolean {
return !!stage && IMAGE_ANALYSIS_STAGES.has(stage)
}
export function isCopyStage(stage: ViralVideoStage | undefined): boolean {
return !!stage && COPY_STAGES.has(stage)
}
export function isVideoStage(stage: ViralVideoStage | undefined): boolean {
return !!stage && VIDEO_STAGES.has(stage)
}
/** 兼容旧调用:旧的 isAnalysisStage 视为「图片分析+文案」的所有前置阶段 */
export function isAnalysisStage(stage: ViralVideoStage | undefined): boolean {
return !!stage && ANALYSIS_STAGES.has(stage)
return isImageAnalysisStage(stage) || isCopyStage(stage)
}
/** 单张图片 VLM 识别出的商品信息 */
export interface ImageProductAnalysis {
/** 商品名称/标题 */
name?: string
/** 净含量/规格 */
spec?: string
/** 视觉特征描述(材质/颜色/文字标签等),可能是拼接字符串或数组 */
brand?: string
features?: string[] | string
/** 卖点 */
label_text?: string
selling_points?: string[]
/** 适用场景 */
scene?: string
/** 图片索引 */
image_index?: number
}
/** 图片 VLM 分析结果(后端字段 image_analysis) */
export interface ImageAnalysisResult {
products?: ImageProductAnalysis[]
}
export interface CopyResult {
final_copy?: string
suggested_copy?: string
title?: string
scenes?: Array<{ shot: string; narration: string; duration?: number }>
}
export interface StyleTemplate {
id: string
name: string
@@ -101,7 +131,7 @@ export interface ViralVideoJob {
bgm_preference?: string
intent_result?: IntentResult
intent_text?: string
/** 图片 VLM 分析结果(后端字段 image_analysis) */
copy_result?: CopyResult
image_analysis?: ImageAnalysisResult
progress_stage?: ViralVideoStage
progress_percent?: number
@@ -118,7 +148,6 @@ export interface ViralVideoJob {
export interface GenerateViralVideoRequest {
images: string[]
reference_video_url?: string
/** 抖音分享链接(后端解析后下载) */
douyin_url?: string
style_strength?: StyleStrength
style_template_id?: string
@@ -136,6 +165,8 @@ export interface GenerateViralVideoRequest {
duration?: number
video_model?: string
video_ratio?: string
/** 三步拆分:step 控制后端执行到哪一步暂停 */
step?: "analyze" | "generate_copy" | "generate_video"
}
export interface HistoryResponse {
@@ -1151,3 +1151,218 @@
padding: 12px 18px;
border-top: 1px solid #f3f4f6;
}
/* ─────────── 三步独立交互新增样式 ─────────── */
/* STEP 子步骤头(STEP2/STEP3) */
.vv-step-subhead {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 16px 0;
}
.vv-step-subtitle {
font-size: 15px;
font-weight: 600;
color: #111827;
}
.vv-step-lock {
margin-left: auto;
font-size: 11px;
color: #9ca3af;
background: #f3f4f6;
padding: 2px 8px;
border-radius: 10px;
}
.vv-step-badge-dim {
background: #e5e7eb !important;
color: #9ca3af !important;
}
/* 列禁用态 */
.vv-col-disabled .vv-section {
opacity: 0.55;
pointer-events: none;
}
.vv-col-disabled .vv-section .vv-btn-primary {
background: #d1d5db !important;
cursor: not-allowed;
}
/* Section 内小标题 */
.vv-section-subtitle {
font-size: 13px;
font-weight: 600;
color: #374151;
margin: 12px 0 8px;
padding-left: 2px;
}
.vv-section-subtitle:first-child {
margin-top: 0;
}
/* 文案结果区 */
.vv-copy-box {
margin-top: 14px;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 12px;
background: #fff;
}
.vv-copy-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 28px 12px;
color: #6b7280;
font-size: 13px;
background: #f9fafb;
}
.vv-copy-head {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
font-size: 13px;
}
.vv-copy-title {
font-weight: 600;
color: #111827;
}
.vv-copy-sub {
color: #7c3aed;
font-size: 12px;
margin-left: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.vv-copy-locked {
font-size: 11px;
color: #9ca3af;
background: #f3f4f6;
padding: 1px 6px;
border-radius: 4px;
margin-left: auto;
}
.vv-copy-textarea {
min-height: 160px !important;
resize: vertical;
font-size: 13px;
line-height: 1.7;
background: #fafafa;
}
.vv-copy-textarea:disabled {
background: #f9fafb;
color: #374151;
cursor: default;
}
/* 块级按钮 */
/* ─────────── 三步独立交互新增样式 ─────────── */
/* STEP 子步骤头(STEP2/STEP3) */
.vv-step-subhead {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 16px 0;
}
.vv-step-subtitle {
font-size: 15px;
font-weight: 600;
color: #111827;
}
.vv-step-lock {
margin-left: auto;
font-size: 11px;
color: #9ca3af;
background: #f3f4f6;
padding: 2px 8px;
border-radius: 10px;
}
.vv-step-badge-dim {
background: #e5e7eb !important;
color: #9ca3af !important;
}
/* 列禁用态 */
.vv-col-disabled .vv-section {
opacity: 0.55;
}
.vv-col-disabled .vv-section .vv-btn,
.vv-col-disabled .vv-section button,
.vv-col-disabled .vv-section input,
.vv-col-disabled .vv-section textarea,
.vv-col-disabled .vv-section .ant-select {
pointer-events: none;
}
/* Section 内小标题 */
.vv-section-subtitle {
font-size: 13px;
font-weight: 600;
color: #374151;
margin: 12px 0 8px;
padding-left: 2px;
}
/* 文案结果区 */
.vv-copy-box {
margin-top: 14px;
border: 1px solid #e5e7eb;
border-radius: 10px;
padding: 12px;
background: #fff;
}
.vv-copy-loading {
display: flex;
align-items: center;
justify-content: center;
padding: 28px 12px;
color: #6b7280;
font-size: 13px;
background: #f9fafb;
}
.vv-copy-head {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
font-size: 13px;
}
.vv-copy-title {
font-weight: 600;
color: #111827;
}
.vv-copy-sub {
color: #7c3aed;
font-size: 12px;
margin-left: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.vv-copy-locked {
font-size: 11px;
color: #9ca3af;
background: #f3f4f6;
padding: 1px 6px;
border-radius: 4px;
margin-left: auto;
}
.vv-copy-textarea {
min-height: 160px !important;
resize: vertical;
font-size: 13px;
line-height: 1.7;
background: #fafafa;
}
.vv-copy-textarea:disabled {
background: #f9fafb;
color: #374151;
cursor: default;
}
File diff suppressed because it is too large Load Diff