fix: 电商视频生成链路稳定性 — AI超时/重试/断点续传 + 404页面 + DashScope Key移除
- adVideoPlanClient: 模型级联降级(qwen-max→plus→turbo), 5xx/网络错误可重试, 超时延长至180s, 错误信息包含上游响应体 - 服务端ai/chat: 超时60s→120s, AbortError返回504(非500), PM2已热重载 - EcommerceVideoWorkspace: 策划失败后支持从断点继续(保留已完成步骤的中间产物), 分镜图/视频生成仅重做失败场景 - scriptEvalClient: 移除客户端DASHSCOPE_API_KEY引用(Nginx代理注入) - NotFoundPage: 未知路由显示404页面(替代兜底跳首页) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
PLAN_STEPS_DISPLAY,
|
||||
type EcommerceVideoStage,
|
||||
type EcommerceVideoSceneTask,
|
||||
type EcommerceVideoPlanProgress,
|
||||
type EcommerceVideoPlanResult,
|
||||
type PlanStep,
|
||||
} from "./ecommerceVideoTypes";
|
||||
@@ -48,6 +49,19 @@ function mapResolutionToQuality(res: string): "720P" | "1080P" {
|
||||
return res.includes("720") ? "720P" : "1080P";
|
||||
}
|
||||
|
||||
function stepCompletedFromProgress(step: PlanStep, p: EcommerceVideoPlanProgress): boolean {
|
||||
switch (step) {
|
||||
case "upload": return Boolean(p.imageUrls?.length);
|
||||
case "analyze": return p.imageDescription !== undefined;
|
||||
case "summary": return Boolean(p.summary);
|
||||
case "selling": return Boolean(p.selling);
|
||||
case "creative": return Boolean(p.creatives?.length);
|
||||
case "storyboard": return Boolean(p.storyboard);
|
||||
case "prompts": return Boolean(p.videoPrompts);
|
||||
case "compliance": return Boolean(p.compliance);
|
||||
}
|
||||
}
|
||||
|
||||
export default function EcommerceVideoWorkspace({
|
||||
isAuthenticated,
|
||||
productImageDataUrls,
|
||||
@@ -60,10 +74,12 @@ export default function EcommerceVideoWorkspace({
|
||||
}: EcommerceVideoWorkspaceProps) {
|
||||
const [stage, setStage] = useState<EcommerceVideoStage>("idle");
|
||||
const [planResult, setPlanResult] = useState<EcommerceVideoPlanResult | null>(null);
|
||||
const [planProgress, setPlanProgress] = useState<EcommerceVideoPlanProgress | null>(null);
|
||||
const [scenes, setScenes] = useState<EcommerceVideoSceneTask[]>([]);
|
||||
const [completedSteps, setCompletedSteps] = useState<PlanStep[]>([]);
|
||||
const [sourceImageUrls, setSourceImageUrls] = useState<string[]>([]);
|
||||
const [currentStep, setCurrentStep] = useState<PlanStep | null>(null);
|
||||
const [failedStep, setFailedStep] = useState<PlanStep | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [actionNotice, setActionNotice] = useState<string | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
@@ -83,6 +99,7 @@ export default function EcommerceVideoWorkspace({
|
||||
setStage(saved.stage);
|
||||
setCompletedSteps(saved.completedSteps || []);
|
||||
setPlanResult(saved.planResult);
|
||||
setPlanProgress((saved as { planProgress?: EcommerceVideoPlanProgress | null }).planProgress || null);
|
||||
setScenes(saved.scenes || []);
|
||||
setSourceImageUrls(saved.sourceImageUrls || saved.planResult?.imageUrls || []);
|
||||
}, []);
|
||||
@@ -90,8 +107,8 @@ export default function EcommerceVideoWorkspace({
|
||||
// ── Keep-alive: save state on changes ───────────────────
|
||||
useEffect(() => {
|
||||
if (stage === "idle" || stage === "cancelled") return;
|
||||
saveEcommerceVideoState({ stage, completedSteps, planResult, scenes, sourceImageUrls });
|
||||
}, [stage, completedSteps, planResult, scenes, sourceImageUrls]);
|
||||
saveEcommerceVideoState({ stage, completedSteps, planResult, planProgress, scenes, sourceImageUrls });
|
||||
}, [stage, completedSteps, planResult, planProgress, scenes, sourceImageUrls]);
|
||||
|
||||
// ── Keep-alive: resume polling for running tasks ──────────
|
||||
useEffect(() => {
|
||||
@@ -253,40 +270,85 @@ export default function EcommerceVideoWorkspace({
|
||||
|
||||
// ── Phase 1: Planning ──────────────────────────────────────
|
||||
|
||||
const handlePlan = async () => {
|
||||
if (!isAuthenticated) { onRequestLogin?.(); return; }
|
||||
if (!productImageDataUrls.length && !requirement.trim()) {
|
||||
setError("请先上传产品图片或填写商品说明"); return;
|
||||
}
|
||||
const runPlanFlow = async (resume: EcommerceVideoPlanProgress | null) => {
|
||||
abortControllerRef.current?.abort();
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current = controller;
|
||||
setStage("planning"); setError(null);
|
||||
setCompletedSteps([]); setCurrentStep(null);
|
||||
setPlanResult(null); setScenes([]); setSourceImageUrls([]);
|
||||
setStage("planning"); setError(null); setFailedStep(null);
|
||||
if (!resume) {
|
||||
setCompletedSteps([]); setPlanResult(null); setScenes([]); setSourceImageUrls([]); setPlanProgress(null);
|
||||
}
|
||||
setCurrentStep(null);
|
||||
// Mutable snapshot — async handlers must persist to localStorage directly since the component may unmount
|
||||
let livePlanProgress: EcommerceVideoPlanProgress = resume ? { ...resume } : {};
|
||||
let liveCompletedSteps: PlanStep[] = resume
|
||||
? ALL_STEPS.filter((s) => stepCompletedFromProgress(s, resume))
|
||||
: [];
|
||||
const persist = (stageNow: EcommerceVideoStage) => {
|
||||
saveEcommerceVideoState({
|
||||
stage: stageNow,
|
||||
completedSteps: liveCompletedSteps,
|
||||
planResult: null,
|
||||
planProgress: livePlanProgress,
|
||||
scenes: [],
|
||||
sourceImageUrls: livePlanProgress.imageUrls || [],
|
||||
});
|
||||
};
|
||||
try {
|
||||
const result = await runVideoPlan(
|
||||
productImageDataUrls, requirement, buildConfig(),
|
||||
{
|
||||
onStepStart: (step) => setCurrentStep(step),
|
||||
onStepDone: (step) => setCompletedSteps((prev) => [...prev, step]),
|
||||
onImagesUploaded: (urls) => { setSourceImageUrls(urls); saveEcommerceVideoState({ stage: "planning", completedSteps: ["upload"], planResult: null, scenes: [], sourceImageUrls: urls }); },
|
||||
onStepDone: (step) => {
|
||||
liveCompletedSteps = [...liveCompletedSteps, step];
|
||||
setCompletedSteps((prev) => [...prev, step]);
|
||||
},
|
||||
onImagesUploaded: (urls) => {
|
||||
setSourceImageUrls(urls);
|
||||
livePlanProgress = { ...livePlanProgress, imageUrls: urls };
|
||||
persist("planning");
|
||||
},
|
||||
onPartialProgress: (progress) => {
|
||||
livePlanProgress = progress;
|
||||
setPlanProgress(progress);
|
||||
persist("planning");
|
||||
},
|
||||
resumeFrom: resume || undefined,
|
||||
signal: controller.signal,
|
||||
},
|
||||
);
|
||||
const builtScenes = buildSceneTasks(result);
|
||||
setPlanResult(result);
|
||||
setPlanProgress(null);
|
||||
setScenes(builtScenes);
|
||||
setStage("planned");
|
||||
// Persist immediately — component may be unmounted by the time React re-renders
|
||||
saveEcommerceVideoState({ stage: "planned", completedSteps: [...ALL_STEPS], planResult: result, scenes: builtScenes, sourceImageUrls: result.imageUrls });
|
||||
saveEcommerceVideoState({ stage: "planned", completedSteps: [...ALL_STEPS], planResult: result, planProgress: null, scenes: builtScenes, sourceImageUrls: result.imageUrls });
|
||||
} catch (err) {
|
||||
if ((err as Error).name === "AbortError") return;
|
||||
setError(err instanceof Error ? err.message : "策划失败");
|
||||
if ((err as Error).name === "AbortError" && controller.signal.aborted) return;
|
||||
const message = err instanceof Error ? err.message : "策划失败";
|
||||
setError(message);
|
||||
// Mark the step that was in-progress as failed so user can resume
|
||||
setFailedStep((prev) => prev || currentStep);
|
||||
setStage("idle");
|
||||
// Persist partial progress so the user can resume after a page switch
|
||||
persist("idle");
|
||||
} finally { setCurrentStep(null); }
|
||||
};
|
||||
|
||||
const handlePlan = async () => {
|
||||
if (!isAuthenticated) { onRequestLogin?.(); return; }
|
||||
if (!productImageDataUrls.length && !requirement.trim()) {
|
||||
setError("请先上传产品图片或填写商品说明"); return;
|
||||
}
|
||||
await runPlanFlow(null);
|
||||
};
|
||||
|
||||
const handleResumePlan = async () => {
|
||||
if (!isAuthenticated) { onRequestLogin?.(); return; }
|
||||
if (!planProgress) { void handlePlan(); return; }
|
||||
await runPlanFlow(planProgress);
|
||||
};
|
||||
|
||||
// ── Phase 2: Image generation per scene ──────────────────────
|
||||
|
||||
const handleGenerateImages = async () => {
|
||||
@@ -302,9 +364,12 @@ export default function EcommerceVideoWorkspace({
|
||||
setScenes(next);
|
||||
saveEcommerceVideoState({ stage: "imaging", completedSteps, planResult, scenes: next, sourceImageUrls });
|
||||
};
|
||||
for (const scene of currentScenes) {
|
||||
// Only redo scenes missing imageUrl — preserves successfully generated images on partial retry
|
||||
const scenesToProcess = currentScenes.filter((s) => !s.imageUrl);
|
||||
if (!scenesToProcess.length) { setStage("imaged"); return; }
|
||||
for (const scene of scenesToProcess) {
|
||||
if (renderAbortRef.current.current) break;
|
||||
persistScenes(currentScenes.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "pending" } : s));
|
||||
persistScenes(currentScenes.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "pending", error: undefined } : s));
|
||||
try {
|
||||
await renderSceneImage(
|
||||
{ sceneId: scene.sceneId, prompt: scene.prompt, aspectRatio: ratio },
|
||||
@@ -331,8 +396,7 @@ export default function EcommerceVideoWorkspace({
|
||||
|
||||
const handleRenderVideos = async () => {
|
||||
if (!scenes.length) return;
|
||||
const firstImage = scenes[0]?.imageUrl;
|
||||
if (!firstImage) { setError("请先生成分镜图片"); return; }
|
||||
if (!scenes.some((s) => s.imageUrl)) { setError("请先生成分镜图片"); return; }
|
||||
setStage("rendering"); setError(null);
|
||||
renderAbortRef.current = { current: false };
|
||||
const quality = mapResolutionToQuality(resolution);
|
||||
@@ -342,10 +406,13 @@ export default function EcommerceVideoWorkspace({
|
||||
setScenes(next);
|
||||
saveEcommerceVideoState({ stage: "rendering", completedSteps, planResult, scenes: next, sourceImageUrls });
|
||||
};
|
||||
for (const scene of currentScenes) {
|
||||
// Only render scenes that haven't completed yet — preserves successful videos on partial retry
|
||||
const scenesToProcess = currentScenes.filter((s) => s.imageUrl && s.status !== "completed");
|
||||
if (!scenesToProcess.length) { setStage(currentScenes.every((s) => s.status === "completed") ? "completed" : "partial_failed"); return; }
|
||||
for (const scene of scenesToProcess) {
|
||||
if (renderAbortRef.current.current) break;
|
||||
if (!scene.imageUrl) continue;
|
||||
persistScenes(currentScenes.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "pending" } : s));
|
||||
persistScenes(currentScenes.map((s) => s.sceneId === scene.sceneId ? { ...s, status: "pending", error: undefined } : s));
|
||||
try {
|
||||
await renderScene(
|
||||
{ sceneId: scene.sceneId, prompt: scene.prompt, durationSeconds: scene.durationSeconds, imageUrl: scene.imageUrl, aspectRatio, resolution: quality },
|
||||
@@ -424,26 +491,32 @@ export default function EcommerceVideoWorkspace({
|
||||
|
||||
<div className="ecom-video-flowbar__actions">
|
||||
{error ? <span className="ecom-video-flowbar__error" role="alert">{error}</span> : null}
|
||||
{stage === "idle" && planProgress && (planProgress.summary || planProgress.creatives || planProgress.storyboard) ? (
|
||||
<button type="button" className="ecom-video-flow-action ecom-video-flow-action--ghost"
|
||||
onClick={() => void handleResumePlan()} title={`从「${failedStep ? PLAN_STEP_LABELS[failedStep] : "已中断处"}」继续策划`}>
|
||||
<ReloadOutlined /> 继续
|
||||
</button>
|
||||
) : null}
|
||||
{stage !== "planning" && stage !== "imaging" && stage !== "rendering" ? (
|
||||
<button type="button" className="ecom-video-flow-action"
|
||||
onClick={() => void handlePlan()} title="一键策划">
|
||||
onClick={() => void handlePlan()} title={planProgress ? "从头重新策划" : "一键策划"}>
|
||||
<PlayCircleOutlined />
|
||||
</button>
|
||||
) : null}
|
||||
{stage === "planned" ? (
|
||||
{stage === "planned" || stage === "imaged" ? (
|
||||
<button type="button" className="ecom-video-flow-action ecom-video-flow-action--ghost"
|
||||
onClick={() => void handleGenerateImages()} title="生成图片">
|
||||
<SendOutlined />
|
||||
onClick={() => void handleGenerateImages()} title={stage === "imaged" ? "重新生成分镜图" : "生成图片"}>
|
||||
{stage === "imaged" ? <ReloadOutlined /> : <SendOutlined />}
|
||||
</button>
|
||||
) : null}
|
||||
{stage === "imaged" ? (
|
||||
{stage === "imaged" || (stage === "partial_failed" && imagedScenes.length > 0) ? (
|
||||
<button type="button" className="ecom-video-flow-action ecom-video-flow-action--ghost"
|
||||
onClick={() => void handleRenderVideos()} title="生成视频">
|
||||
onClick={() => void handleRenderVideos()} title={stage === "partial_failed" ? "重新生成失败的视频" : "生成视频"}>
|
||||
<SendOutlined />
|
||||
</button>
|
||||
) : null}
|
||||
{stage === "planning" ? (
|
||||
<span className="ecom-video-flowbar__stage-label"><LoadingOutlined /> 策划中</span>
|
||||
<span className="ecom-video-flowbar__stage-label"><LoadingOutlined /> {currentStep ? PLAN_STEP_LABELS[currentStep] : "策划中"}</span>
|
||||
) : null}
|
||||
{stage === "imaging" ? (
|
||||
<span className="ecom-video-flowbar__stage-label"><LoadingOutlined /> 生成图片中</span>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
EcommerceVideoStage,
|
||||
EcommerceVideoSceneTask,
|
||||
EcommerceVideoPlanProgress,
|
||||
EcommerceVideoPlanResult,
|
||||
PlanStep,
|
||||
} from "./ecommerceVideoTypes";
|
||||
@@ -11,6 +12,7 @@ interface EcommerceVideoKeepalive {
|
||||
stage: EcommerceVideoStage;
|
||||
completedSteps: PlanStep[];
|
||||
planResult: EcommerceVideoPlanResult | null;
|
||||
planProgress?: EcommerceVideoPlanProgress | null;
|
||||
scenes: EcommerceVideoSceneTask[];
|
||||
sourceImageUrls: string[];
|
||||
savedAt: number;
|
||||
@@ -20,6 +22,7 @@ export function saveEcommerceVideoState(state: {
|
||||
stage: EcommerceVideoStage;
|
||||
completedSteps: PlanStep[];
|
||||
planResult: EcommerceVideoPlanResult | null;
|
||||
planProgress?: EcommerceVideoPlanProgress | null;
|
||||
scenes: EcommerceVideoSceneTask[];
|
||||
sourceImageUrls?: string[];
|
||||
}): void {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { aiGenerationClient } from "../../api/aiGenerationClient";
|
||||
import { waitForTask } from "../../api/taskSubscription";
|
||||
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
|
||||
import type {
|
||||
EcommerceVideoPlanProgress,
|
||||
EcommerceVideoPlanResult,
|
||||
EcommerceVideoSceneTask,
|
||||
PlanStep,
|
||||
@@ -21,66 +22,118 @@ export interface PlanCallbacks {
|
||||
onStepStart: (step: PlanStep) => void;
|
||||
onStepDone: (step: PlanStep) => void;
|
||||
onImagesUploaded?: (urls: string[]) => void;
|
||||
onPartialProgress?: (progress: EcommerceVideoPlanProgress) => void;
|
||||
signal?: AbortSignal;
|
||||
/** Partial state from a previous run; steps with existing data are skipped. */
|
||||
resumeFrom?: EcommerceVideoPlanProgress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the full ad video planning pipeline.
|
||||
* Supports resumption: if `resumeFrom` contains data for a step, that step is skipped.
|
||||
* After each step, `onPartialProgress` fires so callers can persist intermediate state.
|
||||
*/
|
||||
export async function runVideoPlan(
|
||||
imageDataUrls: string[],
|
||||
manualText: string,
|
||||
config: AdVideoUserConfig,
|
||||
callbacks: PlanCallbacks,
|
||||
): Promise<EcommerceVideoPlanResult> {
|
||||
const { onStepStart, onStepDone, signal } = callbacks;
|
||||
const { onStepStart, onStepDone, signal, resumeFrom = {} } = callbacks;
|
||||
const progress: EcommerceVideoPlanProgress = { ...resumeFrom };
|
||||
const emit = () => callbacks.onPartialProgress?.({ ...progress });
|
||||
|
||||
onStepStart("upload");
|
||||
const imageUrls: string[] = [];
|
||||
const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
|
||||
for (const srcUrl of imageDataUrls) {
|
||||
try {
|
||||
const resp = await fetch(srcUrl);
|
||||
const rawBlob = await resp.blob();
|
||||
const mimeType = SUPPORTED_IMAGE_TYPES.has(rawBlob.type) ? rawBlob.type : "image/png";
|
||||
const blob = rawBlob.type === mimeType ? rawBlob : new Blob([rawBlob], { type: mimeType });
|
||||
const result = await aiGenerationClient.uploadAssetBinary(blob, { mimeType, scope: "ecommerce-product" });
|
||||
imageUrls.push(result.url);
|
||||
} catch {
|
||||
// skip images that fail to upload
|
||||
// ── Step: upload ──────────────────────────────────────
|
||||
if (!progress.imageUrls?.length) {
|
||||
onStepStart("upload");
|
||||
const imageUrls: string[] = [];
|
||||
const SUPPORTED_IMAGE_TYPES = new Set(["image/jpeg", "image/png", "image/webp", "image/gif"]);
|
||||
for (const srcUrl of imageDataUrls) {
|
||||
try {
|
||||
const resp = await fetch(srcUrl);
|
||||
const rawBlob = await resp.blob();
|
||||
const mimeType = SUPPORTED_IMAGE_TYPES.has(rawBlob.type) ? rawBlob.type : "image/png";
|
||||
const blob = rawBlob.type === mimeType ? rawBlob : new Blob([rawBlob], { type: mimeType });
|
||||
const result = await aiGenerationClient.uploadAssetBinary(blob, { mimeType, scope: "ecommerce-product" });
|
||||
imageUrls.push(result.url);
|
||||
} catch {
|
||||
// skip images that fail to upload
|
||||
}
|
||||
}
|
||||
if (!imageUrls.length) throw new Error("图片上传失败,请检查图片格式或网络后重试");
|
||||
progress.imageUrls = imageUrls;
|
||||
onStepDone("upload");
|
||||
callbacks.onImagesUploaded?.(imageUrls);
|
||||
emit();
|
||||
}
|
||||
if (!imageUrls.length) throw new Error("图片上传失败,请检查图片格式或网络后重试");
|
||||
onStepDone("upload");
|
||||
callbacks.onImagesUploaded?.(imageUrls);
|
||||
|
||||
onStepStart("analyze");
|
||||
const imageDesc = await analyzeProductImages(imageUrls, signal);
|
||||
onStepDone("analyze");
|
||||
// ── Step: analyze ─────────────────────────────────────
|
||||
if (progress.imageDescription === undefined) {
|
||||
onStepStart("analyze");
|
||||
progress.imageDescription = await analyzeProductImages(progress.imageUrls!, signal);
|
||||
onStepDone("analyze");
|
||||
emit();
|
||||
}
|
||||
|
||||
onStepStart("summary");
|
||||
const summary = await buildProductSummary(imageDesc, manualText, signal);
|
||||
onStepDone("summary");
|
||||
// ── Step: summary ─────────────────────────────────────
|
||||
if (!progress.summary) {
|
||||
onStepStart("summary");
|
||||
progress.summary = await buildProductSummary(progress.imageDescription || "", manualText, signal);
|
||||
onStepDone("summary");
|
||||
emit();
|
||||
}
|
||||
|
||||
onStepStart("selling");
|
||||
const selling = await extractSellingPoints(summary, signal);
|
||||
onStepDone("selling");
|
||||
// ── Step: selling ─────────────────────────────────────
|
||||
if (!progress.selling) {
|
||||
onStepStart("selling");
|
||||
progress.selling = await extractSellingPoints(progress.summary, signal);
|
||||
onStepDone("selling");
|
||||
emit();
|
||||
}
|
||||
|
||||
onStepStart("creative");
|
||||
const creatives = await generateCreativeOptions(selling, config, signal);
|
||||
if (!creatives.length) throw new Error("未能生成有效的广告创意");
|
||||
onStepDone("creative");
|
||||
// ── Step: creative ────────────────────────────────────
|
||||
if (!progress.creatives?.length) {
|
||||
onStepStart("creative");
|
||||
progress.creatives = await generateCreativeOptions(progress.selling, config, signal);
|
||||
if (!progress.creatives.length) throw new Error("未能生成有效的广告创意");
|
||||
onStepDone("creative");
|
||||
emit();
|
||||
}
|
||||
|
||||
onStepStart("storyboard");
|
||||
const storyboard = await generateStoryboard(creatives[0], summary, config, signal);
|
||||
onStepDone("storyboard");
|
||||
// ── Step: storyboard ──────────────────────────────────
|
||||
if (!progress.storyboard) {
|
||||
onStepStart("storyboard");
|
||||
progress.storyboard = await generateStoryboard(progress.creatives[0], progress.summary, config, signal);
|
||||
onStepDone("storyboard");
|
||||
emit();
|
||||
}
|
||||
|
||||
onStepStart("prompts");
|
||||
const videoPrompts = await generateVideoPrompts(storyboard, summary, signal);
|
||||
onStepDone("prompts");
|
||||
// ── Step: prompts ─────────────────────────────────────
|
||||
if (!progress.videoPrompts) {
|
||||
onStepStart("prompts");
|
||||
progress.videoPrompts = await generateVideoPrompts(progress.storyboard, progress.summary, signal);
|
||||
onStepDone("prompts");
|
||||
emit();
|
||||
}
|
||||
|
||||
onStepStart("compliance");
|
||||
const compliance = await checkCompliance(summary, selling, storyboard, signal);
|
||||
onStepDone("compliance");
|
||||
// ── Step: compliance ──────────────────────────────────
|
||||
if (!progress.compliance) {
|
||||
onStepStart("compliance");
|
||||
progress.compliance = await checkCompliance(progress.summary, progress.selling, progress.storyboard, signal);
|
||||
onStepDone("compliance");
|
||||
emit();
|
||||
}
|
||||
|
||||
return { imageUrls, summary, selling, creatives, storyboard, videoPrompts, compliance };
|
||||
return {
|
||||
imageUrls: progress.imageUrls!,
|
||||
imageDescription: progress.imageDescription,
|
||||
summary: progress.summary!,
|
||||
selling: progress.selling!,
|
||||
creatives: progress.creatives!,
|
||||
storyboard: progress.storyboard!,
|
||||
videoPrompts: progress.videoPrompts!,
|
||||
compliance: progress.compliance!,
|
||||
};
|
||||
}
|
||||
|
||||
export interface RenderSceneImageInput {
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface EcommerceVideoSceneTask {
|
||||
|
||||
export interface EcommerceVideoPlanResult {
|
||||
imageUrls: string[];
|
||||
imageDescription?: string;
|
||||
summary: ProductSummary;
|
||||
selling: SellingPointResult;
|
||||
creatives: CreativeOption[];
|
||||
@@ -44,6 +45,18 @@ export interface EcommerceVideoPlanResult {
|
||||
compliance: ComplianceCheck;
|
||||
}
|
||||
|
||||
/** Partial plan state — used as resume input when an earlier run failed mid-flow. */
|
||||
export interface EcommerceVideoPlanProgress {
|
||||
imageUrls?: string[];
|
||||
imageDescription?: string;
|
||||
summary?: ProductSummary;
|
||||
selling?: SellingPointResult;
|
||||
creatives?: CreativeOption[];
|
||||
storyboard?: Storyboard;
|
||||
videoPrompts?: VideoPrompt[];
|
||||
compliance?: ComplianceCheck;
|
||||
}
|
||||
|
||||
export interface EcommerceVideoDelivery {
|
||||
planResult: EcommerceVideoPlanResult | null;
|
||||
scenes: EcommerceVideoSceneTask[];
|
||||
|
||||
Reference in New Issue
Block a user