import { analyzeProductImages, buildProductSummary, extractSellingPoints, generateCreativeOptions, generateStoryboard, generateVideoPrompts, checkCompliance, type AdVideoUserConfig, } from "../../api/adVideoPlanClient"; import { aiGenerationClient } from "../../api/aiGenerationClient"; import { waitForTask } from "../../api/taskSubscription"; import { resolveVideoRequestModel } from "../../utils/resolveVideoModel"; import type { EcommerceVideoPlanResult, EcommerceVideoSceneTask, PlanStep, } from "./ecommerceVideoTypes"; export interface PlanCallbacks { onStepStart: (step: PlanStep) => void; onStepDone: (step: PlanStep) => void; signal?: AbortSignal; } export async function runVideoPlan( imageDataUrls: string[], manualText: string, config: AdVideoUserConfig, callbacks: PlanCallbacks, ): Promise { const { onStepStart, onStepDone, signal } = callbacks; 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 } } onStepDone("upload"); onStepStart("analyze"); const imageDesc = await analyzeProductImages(imageUrls, signal); onStepDone("analyze"); onStepStart("summary"); const summary = await buildProductSummary(imageDesc, manualText, signal); onStepDone("summary"); onStepStart("selling"); const selling = await extractSellingPoints(summary, signal); onStepDone("selling"); onStepStart("creative"); const creatives = await generateCreativeOptions(selling, config, signal); if (!creatives.length) throw new Error("未能生成有效的广告创意"); onStepDone("creative"); onStepStart("storyboard"); const storyboard = await generateStoryboard(creatives[0], summary, config, signal); onStepDone("storyboard"); onStepStart("prompts"); const videoPrompts = await generateVideoPrompts(storyboard, summary, signal); onStepDone("prompts"); onStepStart("compliance"); const compliance = await checkCompliance(summary, selling, storyboard, signal); onStepDone("compliance"); return { summary, selling, creatives, storyboard, videoPrompts, compliance }; } export interface RenderSceneInput { sceneId: number; prompt: string; durationSeconds: number; imageUrl: string; aspectRatio: string; resolution: string; model?: string; } export interface RenderCallbacks { onSceneSubmitted: (sceneId: number, taskId: string) => void; onSceneProgress: (sceneId: number, progress: number) => void; onSceneCompleted: (sceneId: number, resultUrl: string) => void; onSceneFailed: (sceneId: number, error: string) => void; } export async function renderScene( input: RenderSceneInput, callbacks: RenderCallbacks, abortRef: { current: boolean }, ): Promise { const model = resolveVideoRequestModel({ model: input.model || "happyhorse-1.0", referenceUrls: [input.imageUrl], }); const { taskId } = await aiGenerationClient.createVideoTask({ model, prompt: input.prompt, ratio: input.aspectRatio, duration: input.durationSeconds, quality: input.resolution, resolution: input.resolution, imageUrl: input.imageUrl, referenceUrls: [input.imageUrl], hasReferenceVideo: false, }); callbacks.onSceneSubmitted(input.sceneId, taskId); const resultUrl = await waitForTask(taskId, { abortRef, onProgress: (e) => callbacks.onSceneProgress(input.sceneId, e.progress), }); if (resultUrl) { callbacks.onSceneCompleted(input.sceneId, resultUrl); } else { callbacks.onSceneFailed(input.sceneId, "任务未返回结果"); } } export function buildSceneTasks( plan: EcommerceVideoPlanResult, ): EcommerceVideoSceneTask[] { return plan.storyboard.scenes.map((scene) => { const prompt = plan.videoPrompts.find((p) => p.scene_id === scene.scene_id); return { sceneId: scene.scene_id, prompt: prompt?.positive_prompt || scene.visual_description, durationSeconds: Number.parseInt(scene.duration, 10) || 5, status: "idle", progress: 0, }; }); }