import type { CreatePreviewTaskInput } from "../../api/webGenerationGateway"; import type { WebCanvasWorkflow, WebCanvasWorkflowNode, WebCanvasWorkflowTaskRef } from "../../types"; import { toHappyHorseDisplayModel } from "../../utils/happyHorseRouting"; import { resolveVideoRequestModel } from "../../utils/resolveVideoModel"; import { normalizeCanvasWorkflowSchema } from "./canvasWorkflowSchema"; export interface CanvasExecutionImageReference { nodeId: string; title: string; url: string; ossKey?: string | null; } export interface CanvasNodeExecutionContext { node: WebCanvasWorkflowNode; prompt: string; imageReferences: CanvasExecutionImageReference[]; upstreamTaskRefs: WebCanvasWorkflowTaskRef[]; } function getNodeText(node: WebCanvasWorkflowNode | undefined): string { if (!node) return ""; return String(node.detail || node.params?.prompt || "").trim(); } function getNodeAssetUrl(node: WebCanvasWorkflowNode | undefined): string { return String(node?.assetRef?.url || node?.previewUrl || "").trim(); } function incomingEdges(workflow: WebCanvasWorkflow, nodeId: string) { return workflow.edges.filter((edge) => edge.target === nodeId); } export function resolveCanvasNodeExecutionContext( workflowInput: WebCanvasWorkflow, nodeId: string, ): CanvasNodeExecutionContext { const workflow = normalizeCanvasWorkflowSchema(workflowInput); const nodeById = new Map(workflow.nodes.map((node) => [node.id, node])); const node = nodeById.get(nodeId); if (!node) throw new Error(`Canvas node not found: ${nodeId}`); const imageReferences: CanvasExecutionImageReference[] = []; const upstreamPrompts: string[] = []; const upstreamTaskRefs: WebCanvasWorkflowTaskRef[] = []; const seenImages = new Set(); incomingEdges(workflow, nodeId).forEach((edge) => { const sourceNode = nodeById.get(edge.source); if (!sourceNode) return; if (sourceNode.taskRef) upstreamTaskRefs.push(sourceNode.taskRef); const sourceKind = sourceNode.kind; if (sourceKind === "image") { const url = getNodeAssetUrl(sourceNode); if (url && !seenImages.has(`${sourceNode.id}:${url}`)) { seenImages.add(`${sourceNode.id}:${url}`); imageReferences.push({ nodeId: sourceNode.id, title: sourceNode.label, url, ossKey: sourceNode.assetRef?.ossKey || null, }); } return; } const text = getNodeText(sourceNode); if (text) upstreamPrompts.push(text); }); return { node, prompt: getNodeText(node) || upstreamPrompts.join("\n"), imageReferences, upstreamTaskRefs, }; } function normalizeDuration(value: unknown, fallback = 5): number { const parsed = Number(String(value || "").replace(/s$/i, "")); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } export function hasRunningCanvasTaskRef(node: WebCanvasWorkflowNode): boolean { const status = node.taskRef?.status; return status === "queued" || status === "pending" || status === "running"; } export function buildCanvasVideoTaskInput(workflow: WebCanvasWorkflow, nodeId: string): CreatePreviewTaskInput { const context = resolveCanvasNodeExecutionContext(workflow, nodeId); const params = context.node.params || {}; const referenceUrls = context.imageReferences.map((item) => item.url); const displayModel = toHappyHorseDisplayModel(String(params.model || workflow.settings.model || "happyhorse-1.0")); const model = resolveVideoRequestModel({ model: displayModel, referenceUrls }); return { title: context.node.label || "视频节点生成", type: "video", prompt: context.prompt || (referenceUrls.length ? "根据参考图片生成视频" : ""), params: { model, ratio: String(params.aspectRatio || params.ratio || workflow.settings.ratio || "16:9"), quality: String(params.resolution || workflow.settings.resolution || "720P"), resolution: String(params.resolution || workflow.settings.resolution || "720P"), duration: normalizeDuration(params.duration || workflow.settings.duration), frameMode: params.videoMode === "firstlast" ? "start-end" : "omni", referenceUrls: referenceUrls.length ? referenceUrls : undefined, muted: false, hasReferenceVideo: false, }, }; } export function buildCanvasImageTaskInput(workflow: WebCanvasWorkflow, nodeId: string): CreatePreviewTaskInput { const context = resolveCanvasNodeExecutionContext(workflow, nodeId); const params = context.node.params || {}; const referenceUrls = context.imageReferences.map((item) => item.url); return { title: context.node.label || "图片节点生成", type: "image", prompt: context.prompt || (referenceUrls.length ? "根据参考图片生成图片" : ""), params: { model: String(params.model || workflow.settings.model || "nano-banana-pro"), ratio: String(params.aspectRatio || params.ratio || workflow.settings.ratio || "16:9"), quality: String(params.imageSize || params.resolution || workflow.settings.resolution || "1K"), gridMode: "single", referenceUrls: referenceUrls.length ? referenceUrls : undefined, }, }; }