You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

117 lines
4.1 KiB

/**
* 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<void> {
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);
}
}