/** * Popi video generation executor. * Used by workflow node execution paths. */ import type { GenerateVideoNodeData } from "@/types"; import { createPopiserverDefaultVideoModel } from "@/store/utils/defaultImageModel"; import { buildRuntimeExtraTaskParams, buildRuntimeModelParameters, readVideoGenerationConfig, } from "@/utils/generationConfig"; import { isHttpMediaUrl } from "@/utils/mediaResolver"; import type { NodeExecutionContext } from "./types"; import { buildPromptFromConnectedTextItems } from "./textInputPrompt"; import { executePopiMediaGeneration, mediaGenerationErrorMessage, } from "./popiMediaGenerationExecutor"; export interface GenerateVideoOptions { useStoredFallback?: boolean; } function getGeneratedImageUrl(value: unknown): string | undefined { if (typeof value === "string" && value.length > 0) return value; if (!value || typeof value !== "object") return undefined; const record = value as { url?: unknown }; return typeof record.url === "string" && record.url.length > 0 ? record.url : undefined; } export async function executeGenerateVideo( ctx: NodeExecutionContext, options: GenerateVideoOptions = {} ): Promise { const { node, getConnectedInputs, updateNodeData, getFreshNode } = ctx; const { useStoredFallback = false } = options; const { images: connectedImages, videos: connectedVideos, text: connectedText, textItems: connectedTextItems, audio: connectedAudio, } = getConnectedInputs(node.id); const freshNode = getFreshNode(node.id); const nodeData = (freshNode?.data || node.data) as GenerateVideoNodeData; const nodeConfig = readVideoGenerationConfig(nodeData, createPopiserverDefaultVideoModel()); const images = useStoredFallback && connectedImages.length === 0 ? nodeConfig.inputImages : connectedImages; const prompt = buildPromptFromConnectedTextItems(connectedTextItems, connectedText, nodeConfig.prompt); try { updateNodeData(node.id, { inputImages: images, inputPrompt: nodeConfig.prompt, status: "loading", error: null, }); const result = await executePopiMediaGeneration({ ctx, mediaType: "video", model: nodeConfig.selectedModel, prompt: prompt || "", images, videos: connectedVideos, voices: connectedAudio, parameters: buildRuntimeModelParameters({ ...nodeConfig, prompt: prompt || "" }), extraTaskParams: buildRuntimeExtraTaskParams({ ...nodeConfig, prompt: prompt || "" }), subType: nodeConfig.subType, referenceSubjectList: nodeConfig.referenceSubjectList, }); const videoData = result.video || result.videoUrl; const imageData = getGeneratedImageUrl(result.image); const outputContent = videoData || imageData; if (!result.success || !outputContent) { throw new Error(result.error || "Video generation failed"); } const timestamp = Date.now(); const videoId = `${timestamp}`; const newHistoryItem = { id: videoId, video: outputContent, timestamp, prompt: prompt || "", model: nodeConfig.selectedModel.modelId, modelDisplayName: nodeConfig.selectedModel.displayName || nodeConfig.selectedModel.modelId, modelProvider: nodeConfig.selectedModel.provider, }; const isRemoteVideo = Boolean(videoData && outputContent && isHttpMediaUrl(outputContent)); updateNodeData(node.id, { outputVideo: outputContent, outputVideoRef: undefined, outputVideoRemoteUrl: isRemoteVideo ? outputContent : undefined, outputVideoStorageStatus: isRemoteVideo ? "remote-only" : undefined, status: "complete", error: null, videoHistory: [newHistoryItem, ...(nodeData.videoHistory || [])].slice(0, 50), selectedVideoHistoryIndex: 0, lastUsedModel: nodeConfig.selectedModel, }); } catch (error) { if (error instanceof DOMException && error.name === "AbortError") { throw error; } const errorMessage = mediaGenerationErrorMessage(error, "Video generation failed"); updateNodeData(node.id, { status: "error", error: errorMessage, }); throw new Error(errorMessage); } }