feat(workflow): 工作流提供方 - 集成 RunningHub 与 ComfyUI Bridge
This commit is contained in:
@@ -10,6 +10,7 @@ import { getNodeResourceKind } from "@/lib/canvas/node-registry";
|
||||
import { resolveCanvasDrawingReference } from "@/lib/canvas/canvas-drawing-reference";
|
||||
import { compileCharacterReferencePrompt } from "@/lib/canvas/canvas-character-reference";
|
||||
import { nodeReferenceImage } from "@/lib/canvas/canvas-project-generation";
|
||||
import { isCanvasWorkflowProvider } from "@/lib/canvas/canvas-workflow";
|
||||
import type { ModelReferenceLimits } from "@/lib/model-selection";
|
||||
import type { Asset } from "@/stores/use-asset-store";
|
||||
|
||||
@@ -71,8 +72,18 @@ export function buildNodeGenerationContext(nodeId: string, nodes: CanvasNodeData
|
||||
const inputs = [...connectedInputs, ...portraitTextureInput, ...buildAssetGenerationInputs(assets)];
|
||||
const storyboardInputs = getConnectedStoryboardRows(nodeId, nodes, connections);
|
||||
const hasExplicitResourceMention = /@\[(?:node|asset):[^\]]+\]/.test(normalizeLegacyNodeMentions(prompt, inputs));
|
||||
if ((sourceNode?.type === CanvasNodeType.Config && Boolean(sourceNode.metadata?.composerContent?.trim())) || hasExplicitResourceMention) {
|
||||
return buildComposerGenerationContext(inputs, prompt, [sourceNode?.metadata?.videoStartFrameNodeId, sourceNode?.metadata?.videoEndFrameNodeId].filter((id): id is string => Boolean(id)), promptOnly);
|
||||
const isWorkflowSource = sourceNode?.type === CanvasNodeType.Config && isCanvasWorkflowProvider(sourceNode.metadata);
|
||||
if ((Boolean(sourceNode?.metadata?.composerContent?.trim()) && (sourceNode?.type === CanvasNodeType.Config || isWorkflowSource)) || hasExplicitResourceMention) {
|
||||
const autoIncludeWorkflowMedia = isWorkflowSource;
|
||||
return buildComposerGenerationContext(
|
||||
inputs,
|
||||
prompt,
|
||||
// 工作流节点由字段映射接收全部连线媒体;视频节点的历史首尾帧字段不能再额外追加参考图。
|
||||
autoIncludeWorkflowMedia ? [] : [sourceNode?.metadata?.videoStartFrameNodeId, sourceNode?.metadata?.videoEndFrameNodeId].filter((id): id is string => Boolean(id)),
|
||||
promptOnly,
|
||||
autoIncludeWorkflowMedia,
|
||||
connectedInputs,
|
||||
);
|
||||
}
|
||||
|
||||
const isStoryboardMedia = sourceNode?.type === CanvasNodeType.Image || sourceNode?.type === CanvasNodeType.Video;
|
||||
@@ -120,7 +131,14 @@ function removeTrailingInputBlocks(prompt: string, inputs: NodeGenerationInput[]
|
||||
return next;
|
||||
}
|
||||
|
||||
function buildComposerGenerationContext(inputs: NodeGenerationInput[], prompt: string, videoFrameNodeIds: string[] = [], promptOnly = false): NodeGenerationContext {
|
||||
function buildComposerGenerationContext(
|
||||
inputs: NodeGenerationInput[],
|
||||
prompt: string,
|
||||
videoFrameNodeIds: string[] = [],
|
||||
promptOnly = false,
|
||||
autoIncludeWorkflowMedia = false,
|
||||
workflowMediaInputs: NodeGenerationInput[] = [],
|
||||
): NodeGenerationContext {
|
||||
const normalizedPrompt = normalizeLegacyNodeMentions(prompt, inputs);
|
||||
const inputByToken = new Map(inputs.map((input) => [generationInputToken(input), input]));
|
||||
const nodeInputById = new Map(inputs.filter((input) => !input.nodeId.startsWith("asset:")).map((input) => [input.nodeId, input]));
|
||||
@@ -132,6 +150,16 @@ function buildComposerGenerationContext(inputs: NodeGenerationInput[], prompt: s
|
||||
let lastIndex = 0;
|
||||
let nextPrompt = "";
|
||||
|
||||
if (autoIncludeWorkflowMedia) {
|
||||
// 先固定“图片1/视频1”等提示词标签的顺序,和工作流槽位保持一致;
|
||||
// 用户先 @ 第二张图时,提示词也不会把它误标成第一张。
|
||||
workflowMediaInputs.forEach((input) => {
|
||||
if (input.type === "text" || labelByNodeId.has(input.nodeId)) return;
|
||||
const labelKind = input.sourceKind === "drawing" ? "drawing" : input.type;
|
||||
labelByNodeId.set(input.nodeId, generationLabel(labelKind, counts[labelKind]++));
|
||||
});
|
||||
}
|
||||
|
||||
for (const match of normalizedPrompt.matchAll(/@\[(node|asset):([^\]]+)\]/g)) {
|
||||
if (match.index === undefined) continue;
|
||||
hasToken = true;
|
||||
@@ -153,6 +181,24 @@ function buildComposerGenerationContext(inputs: NodeGenerationInput[], prompt: s
|
||||
|
||||
nextPrompt += normalizedPrompt.slice(lastIndex);
|
||||
if (textBlocks.length && !promptOnly) nextPrompt = `${nextPrompt.trim()}\n\n${textBlocks.join("\n\n")}`;
|
||||
if (autoIncludeWorkflowMedia) {
|
||||
// RunningHub/ComfyUI 工作流按保存的字段槽位接收图片、视频和音频;
|
||||
// 配置节点不能因为提示词里没有逐个 @ 就丢失已连接媒体。
|
||||
// 先按连线顺序放入媒体,避免用户在提示词里 @图片2 后改变工作流槽位的索引;
|
||||
// 素材库中的显式 @ 引用仍保留在后面,不会被自动模式吞掉。
|
||||
const explicitInputs = selectedInputs.splice(0);
|
||||
const selectedNodeIds = new Set<string>();
|
||||
workflowMediaInputs.forEach((input) => {
|
||||
if (input.type === "text" || selectedNodeIds.has(input.nodeId)) return;
|
||||
selectedInputs.push(input);
|
||||
selectedNodeIds.add(input.nodeId);
|
||||
});
|
||||
explicitInputs.forEach((input) => {
|
||||
if (selectedNodeIds.has(input.nodeId)) return;
|
||||
selectedInputs.push(input);
|
||||
selectedNodeIds.add(input.nodeId);
|
||||
});
|
||||
}
|
||||
// 首尾帧是结构化生成参数,不受提示词中的 @ 引用筛选影响。
|
||||
const selectedNodeIds = new Set(selectedInputs.map((input) => input.nodeId));
|
||||
videoFrameNodeIds.forEach((nodeId) => {
|
||||
@@ -501,12 +547,12 @@ function readReferenceImage(node: CanvasNodeData, nodes: CanvasNodeData[], conne
|
||||
}
|
||||
|
||||
function readReferenceVideo(node: CanvasNodeData): ReferenceVideo | null {
|
||||
if (node.type !== CanvasNodeType.Video || !node.metadata?.content) return null;
|
||||
if (node.type !== CanvasNodeType.Video || (!node.metadata?.content && !node.metadata?.storageKey)) return null;
|
||||
return {
|
||||
id: node.id,
|
||||
name: `${node.title || node.id}.mp4`,
|
||||
type: node.metadata.mimeType || "video/mp4",
|
||||
url: node.metadata.content,
|
||||
url: node.metadata.content || "",
|
||||
storageKey: node.metadata.storageKey,
|
||||
bytes: node.metadata.bytes,
|
||||
width: node.metadata.naturalWidth,
|
||||
@@ -516,12 +562,12 @@ function readReferenceVideo(node: CanvasNodeData): ReferenceVideo | null {
|
||||
}
|
||||
|
||||
function readReferenceAudio(node: CanvasNodeData): ReferenceAudio | null {
|
||||
if (node.type !== CanvasNodeType.Audio || !node.metadata?.content) return null;
|
||||
if (node.type !== CanvasNodeType.Audio || (!node.metadata?.content && !node.metadata?.storageKey)) return null;
|
||||
return {
|
||||
id: node.id,
|
||||
name: `${node.title || node.id}.mp3`,
|
||||
type: node.metadata.mimeType || "audio/mpeg",
|
||||
url: node.metadata.content,
|
||||
url: node.metadata.content || "",
|
||||
storageKey: node.metadata.storageKey,
|
||||
bytes: node.metadata.bytes,
|
||||
durationMs: node.metadata.durationMs,
|
||||
|
||||
Reference in New Issue
Block a user