export const SCHEDULED_REF2VA_TYPE = "MiniMaxH3ScheduledReferenceToVideo"; export const TAGGED_REF2VA_TYPE = "MiniMaxH3TaggedReferenceToVideo"; export const CURRENT_TAGGED_REF2VA_TYPE = "MiniMaxH3CurrentTaggedReferenceScene"; export const CORE_REF2VA_TYPE = "MiniMaxH3ReferenceToVideo"; export const IMAGE_TO_VIDEO_TYPE = "MiniMaxH3ImageToVideo"; export const FIRST_SCENE_IMAGE_TYPE = "MiniMaxH3ChainFirstSceneImage"; export const FRAME_INDEX_SWITCH_TYPE = "MiniMaxH3ChainFrameIndexSwitch"; export const PICTURE_REF_TYPE = "MiniMaxH3ScheduledPictureReference"; export const VIDEO_REF_TYPE = "MiniMaxH3ScheduledVideoReference"; export const AUDIO_REF_TYPE = "MiniMaxH3ScheduledAudioReference"; export const TAGGED_PICTURE_REF_TYPE = "MiniMaxH3TaggedPictureReference"; export const TAGGED_VIDEO_REF_TYPE = "MiniMaxH3TaggedVideoReference"; export const TAGGED_MOTION_REF_TYPE = "MiniMaxH3TaggedMotionReference"; export const TAGGED_MOTION_PATH_REF_TYPE = "MiniMaxH3TaggedMotionReferencePath"; export const TAGGED_MOTION_TIMELINE_REF_TYPE = "MiniMaxH3TaggedMotionReferenceTimeline"; export const TAGGED_AUDIO_REF_TYPE = "MiniMaxH3TaggedAudioReference"; export const SEMANTIC_ANCHOR_BUNDLE_TYPE = "MiniMaxH3SemanticAnchorBundle"; export const SEMANTIC_PICTURE_ANCHOR_TYPE = "MiniMaxH3SemanticPictureAnchor"; export const PROJECT_ASSET_MANAGER_TYPE = "MiniMaxH3ProjectAssetManager"; const SCHEDULE_TYPES = new Set([ PICTURE_REF_TYPE, VIDEO_REF_TYPE, AUDIO_REF_TYPE, ]); const TAGGED_TYPES = new Set([ TAGGED_PICTURE_REF_TYPE, TAGGED_VIDEO_REF_TYPE, TAGGED_MOTION_REF_TYPE, TAGGED_MOTION_PATH_REF_TYPE, TAGGED_MOTION_TIMELINE_REF_TYPE, TAGGED_AUDIO_REF_TYPE, ]); const TAGGED_REF2VA_TYPES = new Set([ TAGGED_REF2VA_TYPE, CURRENT_TAGGED_REF2VA_TYPE, ]); export function nodeType(node) { return node?.comfyClass ?? node?.type ?? null; } function widgetValue(node, name, fallback = "") { const connected = connectedInputValue(node, name); if (connected !== undefined) return connected; return node?.widgets?.find((item) => item.name === name)?.value ?? fallback; } export function referenceTag(value) { return String(value ?? "").trim().replace(/^@+/, ""); } function escapedPattern(value) { return String(value ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); } export function taggedPictureReferenceToken(tag, mode = "native", timestamp = null) { const cleanTag = referenceTag(tag); if (!cleanTag) return ""; if (mode !== "semantic") return `@${cleanTag}`; if (timestamp === null || timestamp === undefined || String(timestamp).trim() === "") return `#${cleanTag}`; const seconds = Number(timestamp); if (!Number.isFinite(seconds)) return `#${cleanTag}`; const safeSeconds = Math.max(0, seconds); return `#${cleanTag}[${safeSeconds.toFixed(2)}s]`; } export function taggedPictureReferenceMode(prompt, tag) { const cleanTag = referenceTag(tag); if (!cleanTag) return "unused"; const escaped = escapedPattern(cleanTag); const native = new RegExp( `(^|[^A-Za-z0-9_])@${escaped}(?![A-Za-z0-9_-])`, ).test(String(prompt ?? "")); const semantic = new RegExp( `(^|[^A-Za-z0-9_])#${escaped}(?:\\[[0-9]+(?:\\.[0-9]+)?s?\\]|(?!\\[))(?![A-Za-z0-9_-])`, ).test(String(prompt ?? "")); if (native && semantic) return "mixed"; if (semantic) return "semantic"; if (native) return "native"; return "unused"; } export function convertTaggedPictureReference( prompt, tag, mode = "native", timestamp = null) { const source = String(prompt ?? ""); const cleanTag = referenceTag(tag); if (!cleanTag || !["native", "semantic"].includes(mode)) return source; const escaped = escapedPattern(cleanTag); if (mode === "semantic") { const replacement = taggedPictureReferenceToken( cleanTag, "semantic", timestamp, ); return source.replace( new RegExp( `(^|[^A-Za-z0-9_])@${escaped}(?![A-Za-z0-9_-])`, "g", ), (_match, prefix) => `${prefix}${replacement}`, ); } const replacement = taggedPictureReferenceToken(cleanTag, "native"); return source.replace( new RegExp( `(^|[^A-Za-z0-9_])#${escaped}(?:\\[[0-9]+(?:\\.[0-9]+)?s?\\]|(?!\\[))(?![A-Za-z0-9_-])`, "g", ), (_match, prefix) => `${prefix}${replacement}`, ); } export function referenceReplacementToken( record, mode = "native", timestamp = null) { if (!record || !["native", "semantic"].includes(mode)) return ""; if (mode === "semantic") { if (record.kind !== "picture" || !record.tag) return ""; return taggedPictureReferenceToken(record.tag, "semantic", timestamp); } if (record.nativeToken === null) return ""; return String(record.nativeToken ?? record.token ?? ""); } export function replacePromptReferenceOccurrence( prompt, start, end, record, mode = "native", timestamp = null) { const source = String(prompt ?? ""); const first = Number(start); const last = Number(end); if (!Number.isInteger(first) || !Number.isInteger(last) || first < 0 || last < first || last > source.length) { return source; } const replacement = referenceReplacementToken(record, mode, timestamp); if (!replacement) return source; return `${source.slice(0, first)}${replacement}${source.slice(last)}`; } const TRANSPARENT_REROUTE_TYPES = new Set([ "Reroute", "Reroute (rgthree)", ]); const SUBGRAPH_INPUT_ID = "-10"; const SUBGRAPH_OUTPUT_ID = "-20"; function graphLink(graph, linkId) { if (linkId == null) return null; return graph?.links?.get?.(linkId) ?? graph?.links?.[linkId] ?? null; } function graphLinks(graph) { if (graph?.links?.values) return [...graph.links.values()]; return Object.values(graph?.links ?? {}); } function isGraphIoNode(id, expected) { return String(id) === expected; } function subgraphHostNode(graph) { if (!graph) return null; const root = rootGraph(graph); if (!root || graph === root) return null; const graphs = [root, ...graphDescendants(root)]; for (const candidate of graphs) { const host = candidate?._nodes?.find((node) => node?.subgraph === graph); if (host) return host; } return null; } function connectionFromLink(graph, link) { if (!graph || !link) return null; if (isGraphIoNode(link.origin_id, SUBGRAPH_INPUT_ID)) { const host = subgraphHostNode(graph); const input = host?.inputs?.[Number(link.origin_slot ?? 0)]; const parentLink = graphLink(host?.graph, input?.link); return connectionFromLink(host?.graph, parentLink); } const source = graph.getNodeById?.(link.origin_id) ?? null; return source ? {source, originSlot: Number(link.origin_slot ?? 0)} : null; } function subgraphOutputConnection(node, originSlot) { const graph = node?.subgraph; if (!graph) return null; const slotIndex = Number(originSlot ?? 0); const slot = graph.outputs?.[slotIndex] ?? graph.outputNode?.slots?.[slotIndex]; const linked = (slot?.linkIds ?? []) .map((linkId) => graphLink(graph, linkId)) .filter(Boolean); const candidates = linked.length ? linked : graphLinks(graph).filter( (link) => isGraphIoNode(link?.target_id, SUBGRAPH_OUTPUT_ID) && Number(link?.target_slot ?? -1) === slotIndex, ); const link = candidates.find( (item) => isGraphIoNode(item?.target_id, SUBGRAPH_OUTPUT_ID) && Number(item?.target_slot ?? -1) === slotIndex, ) ?? candidates[0]; return connectionFromLink(graph, link); } function directInputConnection(node, name = null) { const input = name === null ? node?.inputs?.find((item) => item.link != null) : node?.inputs?.find((item) => item.name === name); const link = graphLink(node?.graph, input?.link); return connectionFromLink(node?.graph, link); } function rootGraph(graph) { return graph?.rootGraph ?? graph ?? null; } function graphAncestors(graph) { if (!graph) return []; const root = rootGraph(graph); if (!root || graph === root) return root ? [root] : []; const result = [graph]; const seen = new Set(result); let current = graph; while (current !== root) { let parent = null; const candidates = [root]; const subgraphs = root?._subgraphs ?? root?.subgraphs; if (subgraphs?.values) candidates.push(...subgraphs.values()); for (const candidate of candidates) { if (!candidate?._nodes || candidate === current) continue; if (candidate._nodes.some((node) => node.subgraph === current)) { parent = candidate; break; } } if (!parent || seen.has(parent)) { if (!seen.has(root)) result.push(root); break; } result.push(parent); seen.add(parent); current = parent; } return result; } function graphDescendants(graph, seen = new Set()) { if (!graph?._nodes || seen.has(graph)) return []; seen.add(graph); const result = []; for (const node of graph._nodes) { if (!node?.subgraph || seen.has(node.subgraph)) continue; result.push(node.subgraph); result.push(...graphDescendants(node.subgraph, seen)); } return result; } function setGetName(node) { return String(node?.widgets?.[0]?.value ?? ""); } function setNodeFor(getNode) { const name = setGetName(getNode); if (!name) return null; for (const graph of graphAncestors(getNode?.graph)) { const setter = graph?._nodes?.find( (node) => nodeType(node) === "SetNode" && setGetName(node) === name, ); if (setter) return setter; } return null; } function getNodesFor(setNode) { const name = setGetName(setNode); if (!name || !setNode?.graph) return []; const graphs = [setNode.graph, ...graphDescendants(setNode.graph)]; return graphs.flatMap((graph) => (graph?._nodes ?? []).filter( (node) => nodeType(node) === "GetNode" && setGetName(node) === name, )); } function transparentInputConnection(node, originSlot = 0) { if (node?.subgraph) { return subgraphOutputConnection(node, originSlot); } const type = nodeType(node); if (type === "GetNode") { const setter = setNodeFor(node); return setter ? directInputConnection(setter) : null; } if (type === "SetNode" || TRANSPARENT_REROUTE_TYPES.has(type)) { return directInputConnection(node); } return null; } function resolveInputConnection(connection) { let current = connection; const seen = new Set(); while (current?.source && !seen.has(current.source)) { seen.add(current.source); const next = transparentInputConnection( current.source, current.originSlot, ); if (!next) break; current = next; } return current; } function inputConnection(node, name) { return resolveInputConnection(directInputConnection(node, name)); } function connectedInputValue(node, name) { const connection = inputConnection(node, name); if (!connection?.source) return undefined; const {source, originSlot} = connection; const output = source.outputs?.[originSlot]; if (output?.value !== undefined) return output.value; if (source.output_values?.[originSlot] !== undefined) { return source.output_values[originSlot]; } const widgets = source.widgets ?? []; const outputName = String(output?.name ?? "").trim().toLowerCase(); const matching = outputName ? widgets.find((widget) => String( widget?.name ?? "", ).trim().toLowerCase() === outputName) : null; if (matching?.value !== undefined) return matching.value; for (const candidate of ["value", "string", "text", "tag"]) { const widget = widgets.find((item) => String( item?.name ?? "", ).trim().toLowerCase() === candidate); if (widget?.value !== undefined) return widget.value; } if (widgets.length === 1 && widgets[0]?.value !== undefined) { return widgets[0].value; } return undefined; } function inputSource(node, name) { return inputConnection(node, name)?.source ?? null; } function outputTargets(node) { const targets = []; for (const output of node?.outputs ?? []) { for (const linkId of output.links ?? []) { const link = graphLink(node?.graph, linkId); const target = link ? node.graph?.getNodeById?.(link.target_id) : null; if (target) targets.push(target); } } if (nodeType(node) === "SetNode") targets.push(...getNodesFor(node)); return targets; } function findDownstreamTypes(start, wantedTypes) { const queue = [start]; const seen = new Set(); while (queue.length) { const node = queue.shift(); if (!node || seen.has(node)) continue; seen.add(node); if (node !== start && wantedTypes.has(nodeType(node))) return node; queue.push(...outputTargets(node)); } return null; } function findDownstreamType(start, wantedType) { return findDownstreamTypes(start, new Set([wantedType])); } function findUpstreamType(start, wantedType) { const queue = [start]; const seen = new Set(); while (queue.length) { const node = queue.shift(); if (!node || seen.has(node)) continue; seen.add(node); if (node !== start && nodeType(node) === wantedType) return node; for (const input of node.inputs ?? []) { const parent = inputConnection(node, input.name)?.source ?? null; if (parent) queue.push(parent); } } return null; } export function findScheduledRef2VA(start) { return findDownstreamType(start, SCHEDULED_REF2VA_TYPE); } export function findTaggedRef2VA(start) { return findDownstreamTypes(start, TAGGED_REF2VA_TYPES); } export function findCoreRef2VA(start) { return findDownstreamType(start, CORE_REF2VA_TYPE); } export function findImageToVideo(start) { return findDownstreamType(start, IMAGE_TO_VIDEO_TYPE); } export function findProjectAssetManager(start) { return findUpstreamType(start, PROJECT_ASSET_MANAGER_TYPE); } export function collectScheduleNodes(wrapper) { const result = []; const seen = new Set(); let current = inputSource(wrapper, "reference_schedule"); while (current && SCHEDULE_TYPES.has(nodeType(current)) && !seen.has(current)) { seen.add(current); result.unshift(current); current = inputSource(current, "previous"); } return result; } export function collectTaggedNodes(wrapper) { const result = []; const seen = new Set(); let current = inputSource(wrapper, "references"); if (nodeType(current) === SEMANTIC_ANCHOR_BUNDLE_TYPE) { current = inputSource(current, "references"); } while (current && TAGGED_TYPES.has(nodeType(current)) && !seen.has(current)) { seen.add(current); result.unshift(current); current = inputSource(current, "previous"); } return result; } export function collectSemanticAnchorNodes(wrapper) { const candidates = [ inputSource(wrapper, "references"), inputSource(wrapper, "tagged_references"), ]; const bundle = candidates.find( (node) => nodeType(node) === SEMANTIC_ANCHOR_BUNDLE_TYPE, ); if (nodeType(bundle) !== SEMANTIC_ANCHOR_BUNDLE_TYPE) { return {bundle: null, nodes: []}; } const nodes = []; const seen = new Set(); let current = inputSource(bundle, "anchors"); while (current && nodeType(current) === SEMANTIC_PICTURE_ANCHOR_TYPE && !seen.has(current)) { seen.add(current); nodes.unshift(current); current = inputSource(current, "previous"); } return {bundle, nodes}; } export function referenceIsActive(selector, scene) { const text = String(selector ?? "").trim().toLowerCase(); if (!text || text === "all" || text === "*") return true; const target = Number(scene); if (!Number.isInteger(target) || target < 1) return false; return text.split(",").some((piece) => { const match = piece.trim().match(/^(\d+)(?::(\d+))?$/); if (!match) return false; const first = Number(match[1]); const last = Number(match[2] ?? match[1]); return first <= target && target <= last; }); } function baseRecord(node, kind, scene, inputName, tagName = "tag") { const selector = String(widgetValue(node, "scenes", "")); return { node, kind, tag: referenceTag(widgetValue(node, tagName, "")), selector: selector.trim() || "all", active: referenceIsActive(selector, scene), source: inputSource(node, inputName), label: null, }; } export function scheduledReferenceRecords(editorNode, scene) { const wrapper = findScheduledRef2VA(editorNode); if (!wrapper) return {wrapper: null, records: []}; const nodes = collectScheduleNodes(wrapper); const pictures = []; const videos = []; const pairedAudios = []; const audios = []; for (const node of nodes) { const type = nodeType(node); if (type === PICTURE_REF_TYPE) { pictures.push(baseRecord(node, "picture", scene, "image")); } else if (type === VIDEO_REF_TYPE) { const video = baseRecord(node, "video", scene, "video"); videos.push(video); const audioSource = inputSource(node, "audio"); if (audioSource) { const explicit = referenceTag(widgetValue(node, "audio_tag", "")); pairedAudios.push({ node, kind: "audio", tag: explicit || `${video.tag}_audio`, selector: video.selector, active: video.active, source: audioSource, label: null, pairedWith: video, }); } } else if (type === AUDIO_REF_TYPE) { audios.push(baseRecord(node, "audio", scene, "audio")); } } let ordinal = 0; for (const item of pictures) { if (item.active) item.label = ``; } ordinal = 0; for (const item of videos) { if (item.active) item.label = `