Download ComfyUI-MiniMaxH3-Context-Loop/web/h3_reference_preview_core.mjs from dzmdd/Fast_deploy: direct link, hf CLI and curl.
- Browser
- Download file 37.9 kB
-
https://huggingface.co/dzmdd/Fast_deploy/resolve/main/ComfyUI-MiniMaxH3-Context-Loop/web/h3_reference_preview_core.mjs
- Command line
-
hf download hf://dzmdd/Fast_deploy/ComfyUI-MiniMaxH3-Context-Loop/web/h3_reference_preview_core.mjs
-
curl -L -o h3_reference_preview_core.mjs https://huggingface.co/dzmdd/Fast_deploy/resolve/main/ComfyUI-MiniMaxH3-Context-Loop/web/h3_reference_preview_core.mjs
37.9 kB
| 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 = `<Picture ${++ordinal}>`; | |
| } | |
| ordinal = 0; | |
| for (const item of videos) { | |
| if (item.active) item.label = `<Video ${++ordinal}>`; | |
| } | |
| ordinal = 0; | |
| // Core Ref2VA numbers paired video soundtracks before standalone audio. | |
| for (const item of pairedAudios) { | |
| if (item.active) item.label = `<Audio ${++ordinal}>`; | |
| } | |
| for (const item of audios) { | |
| if (item.active) item.label = `<Audio ${++ordinal}>`; | |
| } | |
| return { | |
| wrapper, | |
| mode: "scheduled", | |
| records: [...pictures, ...videos, ...pairedAudios, ...audios] | |
| .filter((item) => item.tag) | |
| .map((item) => ({...item, token: `@${item.tag}`})), | |
| }; | |
| } | |
| function promptTagSet(prompt) { | |
| return new Set([...String(prompt ?? "").matchAll( | |
| /(?<![A-Za-z0-9_])@([A-Za-z][A-Za-z0-9_-]{0,63})/g, | |
| )].map((match) => match[1])); | |
| } | |
| function semanticPromptTagState(prompt) { | |
| const result = new Map(); | |
| for (const match of String(prompt ?? "").matchAll( | |
| /(?<![A-Za-z0-9_])#([A-Za-z][A-Za-z0-9_-]{0,63})(?:\[([0-9]+(?:\.[0-9]+)?)s?\]|(?!\[))(?![A-Za-z0-9_-])/gi, | |
| )) { | |
| const tag = match[1]; | |
| const current = result.get(tag) ?? {untimed:false, timed:false}; | |
| if (match[2] === undefined) current.untimed = true; | |
| else current.timed = true; | |
| result.set(tag, current); | |
| } | |
| return result; | |
| } | |
| function projectCatalog(node) { | |
| if (nodeType(node) !== PROJECT_ASSET_MANAGER_TYPE) return null; | |
| try { | |
| const value = JSON.parse(String(widgetValue(node, "catalog_json", ""))); | |
| return value && Array.isArray(value.assets) ? value : null; | |
| } catch (_error) { | |
| return null; | |
| } | |
| } | |
| function projectPreviewUrl(catalog, asset, variant = "poster") { | |
| const query = new URLSearchParams({ | |
| project: String(catalog?.project ?? ""), | |
| asset: String(asset?.id ?? ""), | |
| variant, | |
| }); | |
| return `/minimax_h3_context_loop/project-assets/media?${query}`; | |
| } | |
| export function projectAssetReferenceRecords(manager, prompt = "") { | |
| const catalog = projectCatalog(manager); | |
| if (!catalog) return []; | |
| const used = promptTagSet(prompt); | |
| const semanticState = semanticPromptTagState(prompt); | |
| const semanticUsed = new Set(semanticState.keys()); | |
| const pictures = []; | |
| const semanticPictures = []; | |
| const videos = []; | |
| const pairedAudios = []; | |
| const audios = []; | |
| for (const asset of catalog.assets) { | |
| if (!asset?.enabled || asset.role === "source_track") continue; | |
| const tag = referenceTag(asset.tag); | |
| if (!tag) continue; | |
| const common = { | |
| node: manager, | |
| tag, | |
| selector: asset.role === "semantic_anchor" | |
| ? "semantic prompt tag" : "prompt tag", | |
| source: manager, | |
| assetId: String(asset.id ?? ""), | |
| asset, | |
| }; | |
| if (asset.role === "picture") { | |
| pictures.push({ | |
| ...common, kind: "picture", | |
| nativeActive: used.has(tag), | |
| semanticActive: semanticUsed.has(tag), | |
| active: used.has(tag) || semanticUsed.has(tag), | |
| previewUrl: projectPreviewUrl(catalog, asset, "poster"), | |
| }); | |
| } else if (asset.role === "semantic_anchor") { | |
| semanticPictures.push({ | |
| ...common, kind: "picture", semanticOnly: true, | |
| nativeActive: false, semanticActive: semanticUsed.has(tag), | |
| active: semanticUsed.has(tag), | |
| previewUrl: projectPreviewUrl(catalog, asset, "poster"), | |
| }); | |
| } else if (["video", "motion"].includes(asset.role)) { | |
| const options = asset.options ?? {}; | |
| const hasAudio = Boolean( | |
| options.use_embedded_audio ?? asset.metadata?.has_audio); | |
| const audioTag = referenceTag(options.audio_tag) || `${tag}_audio`; | |
| const video = { | |
| ...common, kind: "video", | |
| semanticRole: asset.role === "motion" ? "motion" : "video", | |
| tagActive: used.has(tag), | |
| active: used.has(tag) || (hasAudio && used.has(audioTag)), | |
| previewUrl: projectPreviewUrl(catalog, asset, "preview"), | |
| }; | |
| videos.push(video); | |
| if (hasAudio) pairedAudios.push({ | |
| ...common, kind: "audio", tag: audioTag, | |
| active: video.active, pairedWith: video, | |
| previewUrl: projectPreviewUrl(catalog, asset, "original"), | |
| }); | |
| } else if (asset.role === "audio_reference") { | |
| audios.push({ | |
| ...common, kind: "audio", active: used.has(tag), | |
| previewUrl: projectPreviewUrl(catalog, asset, "original"), | |
| }); | |
| } | |
| } | |
| let ordinal = 0; | |
| for (const item of pictures) { | |
| if (item.nativeActive) item.label = `<Picture ${++ordinal}>`; | |
| } | |
| ordinal = 0; | |
| for (const item of videos) { | |
| if (item.active) { | |
| item.sourceLabel = `<Video ${++ordinal}>`; | |
| item.label = item.sourceLabel; | |
| } | |
| } | |
| const usedSubjects = [...String(prompt ?? "").matchAll( | |
| /<Subject\s+(\d+)>/gi, | |
| )].map((match) => Number(match[1])); | |
| let subjectOrdinal = Math.max(0, ...usedSubjects) + 1; | |
| for (const item of videos) { | |
| if (item.active && item.semanticRole === "motion" && item.tagActive) { | |
| item.label = `<Subject ${subjectOrdinal++}>`; | |
| } | |
| } | |
| ordinal = 0; | |
| for (const item of pairedAudios) { | |
| if (item.active) item.label = `<Audio ${++ordinal}>`; | |
| } | |
| for (const item of audios) { | |
| if (item.active) item.label = `<Audio ${++ordinal}>`; | |
| } | |
| const anchorMode = String(widgetValue( | |
| manager, "semantic_anchor_mode", "timestamped_video")); | |
| let pictureOrdinal = pictures.filter((item) => item.nativeActive).length; | |
| let videoOrdinal = videos.filter((item) => item.active).length; | |
| for (const item of [...pictures, ...semanticPictures]) { | |
| if (!item.semanticActive) continue; | |
| const timed = Boolean(semanticState.get(item.tag)?.timed); | |
| item.semanticLabel = anchorMode === "timestamped_video" && timed | |
| ? `<Video ${++videoOrdinal}>` : `<Picture ${++pictureOrdinal}>`; | |
| if (!item.nativeActive) item.label = item.semanticLabel; | |
| } | |
| return [...pictures, ...semanticPictures, ...videos, ...pairedAudios, ...audios] | |
| .map((item) => ({ | |
| ...item, | |
| token: item.semanticOnly | |
| ? taggedPictureReferenceToken(item.tag, "semantic") | |
| : `@${item.tag}`, | |
| nativeToken: item.semanticOnly ? null : `@${item.tag}`, | |
| semanticToken: item.kind === "picture" | |
| ? taggedPictureReferenceToken(item.tag, "semantic") : null, | |
| supportsSemantic: item.kind === "picture" && !item.semanticOnly, | |
| })); | |
| } | |
| export function taggedReferenceRecords(editorNode, prompt = "") { | |
| const wrapper = findTaggedRef2VA(editorNode); | |
| if (!wrapper) { | |
| // Studio workflows can keep the render engine inside a subgraph while | |
| // the Asset Carousel feeds Plan/Plan Studio upstream of this editor. | |
| // The connected Carousel catalog remains authoritative even when no | |
| // downstream Ref2VA wrapper is traversable from the editor. | |
| const manager = findProjectAssetManager(editorNode); | |
| return manager ? { | |
| wrapper: manager, | |
| mode: "tagged", | |
| records: projectAssetReferenceRecords(manager, prompt), | |
| } : {wrapper: null, mode: null, records: []}; | |
| } | |
| const referenceSource = inputSource(wrapper, "references"); | |
| if (nodeType(referenceSource) === PROJECT_ASSET_MANAGER_TYPE) { | |
| return { | |
| wrapper, | |
| mode: "tagged", | |
| records: projectAssetReferenceRecords(referenceSource, prompt), | |
| }; | |
| } | |
| const nodes = collectTaggedNodes(wrapper); | |
| const dedicated = collectSemanticAnchorNodes(wrapper); | |
| const used = promptTagSet(prompt); | |
| const semanticState = semanticPromptTagState(prompt); | |
| const semanticUsed = new Set(semanticState.keys()); | |
| const pictures = []; | |
| const videos = []; | |
| const pairedAudios = []; | |
| const audios = []; | |
| const semanticPictures = []; | |
| for (const node of nodes) { | |
| const type = nodeType(node); | |
| if (type === TAGGED_PICTURE_REF_TYPE) { | |
| const record = baseRecord(node, "picture", 1, "image"); | |
| record.selector = "prompt tag"; | |
| record.nativeActive = used.has(record.tag); | |
| record.semanticActive = semanticUsed.has(record.tag); | |
| record.active = record.nativeActive || record.semanticActive; | |
| pictures.push(record); | |
| } else if (type === TAGGED_VIDEO_REF_TYPE || | |
| type === TAGGED_MOTION_REF_TYPE || | |
| type === TAGGED_MOTION_PATH_REF_TYPE || | |
| type === TAGGED_MOTION_TIMELINE_REF_TYPE) { | |
| const pathBacked = type === TAGGED_MOTION_PATH_REF_TYPE; | |
| const timelineBacked = | |
| type === TAGGED_MOTION_TIMELINE_REF_TYPE; | |
| const video = baseRecord( | |
| node, "video", 1, | |
| timelineBacked ? "source_timeline" | |
| : pathBacked ? "video_path" : "video"); | |
| const nativeVideoSource = pathBacked | |
| ? inputSource(node, "source_video") : null; | |
| const timelineSource = timelineBacked | |
| ? inputSource(node, "source_timeline") : null; | |
| if (pathBacked) video.source = nativeVideoSource || node; | |
| if (timelineBacked) video.source = timelineSource || node; | |
| video.selector = "prompt tag"; | |
| video.semanticRole = ( | |
| type === TAGGED_MOTION_REF_TYPE || pathBacked || timelineBacked) | |
| ? "motion" : "video"; | |
| video.tagActive = used.has(video.tag); | |
| const audioSource = inputSource(node, "audio"); | |
| const embeddedAudio = ( | |
| pathBacked && Boolean( | |
| widgetValue(node, "use_embedded_audio", true))) || ( | |
| timelineBacked && String( | |
| widgetValue(node, "paired_audio", "off")) === "embedded"); | |
| const explicit = referenceTag(widgetValue(node, "audio_tag", "")); | |
| const audioTag = explicit || `${video.tag}_audio`; | |
| video.active = video.tagActive || Boolean( | |
| (audioSource || embeddedAudio) && used.has(audioTag)); | |
| videos.push(video); | |
| if (audioSource || embeddedAudio) { | |
| pairedAudios.push({ | |
| node, | |
| kind: "audio", | |
| tag: audioTag, | |
| selector: "prompt tag", | |
| active: video.active, | |
| source: audioSource || nativeVideoSource | |
| || timelineSource || node, | |
| label: null, | |
| pairedWith: video, | |
| }); | |
| } | |
| } else if (type === TAGGED_AUDIO_REF_TYPE) { | |
| const record = baseRecord(node, "audio", 1, "audio"); | |
| record.selector = "prompt tag"; | |
| record.active = used.has(record.tag); | |
| audios.push(record); | |
| } | |
| } | |
| for (const node of dedicated.nodes) { | |
| const record = baseRecord(node, "picture", 1, "image"); | |
| record.selector = "semantic prompt tag"; | |
| record.nativeActive = false; | |
| record.semanticActive = semanticUsed.has(record.tag); | |
| record.active = record.semanticActive; | |
| record.semanticOnly = true; | |
| semanticPictures.push(record); | |
| } | |
| let ordinal = 0; | |
| for (const item of pictures) { | |
| if (item.nativeActive) item.label = `<Picture ${++ordinal}>`; | |
| } | |
| ordinal = 0; | |
| const activeMotionRecords = []; | |
| for (const item of videos) { | |
| if (!item.active) continue; | |
| item.sourceLabel = `<Video ${++ordinal}>`; | |
| item.label = item.sourceLabel; | |
| if (item.semanticRole === "motion" && item.tagActive) { | |
| activeMotionRecords.push(item); | |
| } | |
| } | |
| const usedSubjectNumbers = [...String(prompt ?? "").matchAll( | |
| /<Subject\s+(\d+)>/gi, | |
| )].map((match) => Number(match[1])); | |
| let subjectOrdinal = Math.max(0, ...usedSubjectNumbers) + 1; | |
| for (const item of activeMotionRecords) { | |
| item.label = `<Subject ${subjectOrdinal++}>`; | |
| } | |
| ordinal = 0; | |
| for (const item of pairedAudios) { | |
| if (item.active) item.label = `<Audio ${++ordinal}>`; | |
| } | |
| for (const item of audios) { | |
| if (item.active) item.label = `<Audio ${++ordinal}>`; | |
| } | |
| const anchorMode = String(widgetValue( | |
| dedicated.bundle, "semantic_anchor_mode", "timestamped_video")); | |
| let pictureOrdinal = pictures.filter((item) => item.nativeActive).length; | |
| let videoOrdinal = videos.filter((item) => item.active).length; | |
| for (const item of [...pictures, ...semanticPictures]) { | |
| if (!item.semanticActive) continue; | |
| const timed = Boolean(semanticState.get(item.tag)?.timed); | |
| item.semanticLabel = anchorMode === "timestamped_video" && timed | |
| ? `<Video ${++videoOrdinal}>` : `<Picture ${++pictureOrdinal}>`; | |
| if (!item.nativeActive) item.label = item.semanticLabel; | |
| } | |
| return { | |
| wrapper, | |
| mode: "tagged", | |
| records: [ | |
| ...pictures, ...semanticPictures, ...videos, ...pairedAudios, ...audios, | |
| ] | |
| .filter((item) => item.tag) | |
| .map((item) => ({ | |
| ...item, | |
| token: item.semanticOnly | |
| ? taggedPictureReferenceToken(item.tag, "semantic") | |
| : `@${item.tag}`, | |
| nativeToken: item.semanticOnly ? null : `@${item.tag}`, | |
| semanticToken: item.kind === "picture" | |
| ? taggedPictureReferenceToken(item.tag, "semantic") : null, | |
| supportsSemantic: item.kind === "picture" && !item.semanticOnly, | |
| })), | |
| }; | |
| } | |
| function numberedInputRecords(wrapper, pattern, kind, labelKind) { | |
| const records = []; | |
| for (const input of wrapper?.inputs ?? []) { | |
| const match = String(input.name ?? "").match(pattern); | |
| if (!match || input.link == null) continue; | |
| const index = Number(match[1]); | |
| const label = `<${labelKind} ${index + 1}>`; | |
| records.push({ | |
| node: wrapper, | |
| kind, | |
| tag: "", | |
| token: label, | |
| selector: "all", | |
| active: true, | |
| source: inputSource(wrapper, input.name), | |
| label, | |
| index, | |
| mode: "native", | |
| }); | |
| } | |
| records.sort((left, right) => left.index - right.index); | |
| return records; | |
| } | |
| export function coreReferenceRecords(editorNode) { | |
| const wrapper = findCoreRef2VA(editorNode); | |
| if (!wrapper) return {wrapper: null, mode: null, records: []}; | |
| const pictures = numberedInputRecords( | |
| wrapper, /^ref_images\.ref_image_(\d+)$/, "picture", "Picture"); | |
| const videos = numberedInputRecords( | |
| wrapper, /^ref_videos\.ref_video_(\d+)$/, "video", "Video"); | |
| const pairedAudios = numberedInputRecords( | |
| wrapper, /^ref_video_audios\.ref_video_audio_(\d+)$/, "audio", "Audio"); | |
| const audios = numberedInputRecords( | |
| wrapper, /^ref_audios\.ref_audio_(\d+)$/, "audio", "Audio"); | |
| // Audio labels are shared across video-paired and standalone references. | |
| [...pairedAudios, ...audios].forEach((item, index) => { | |
| item.label = `<Audio ${index + 1}>`; | |
| item.token = item.label; | |
| }); | |
| return { | |
| wrapper, | |
| mode: "native", | |
| records: [...pictures, ...videos, ...pairedAudios, ...audios], | |
| }; | |
| } | |
| function selectedFrameSource(source, scene) { | |
| if (nodeType(source) !== FRAME_INDEX_SWITCH_TYPE) return source; | |
| const frames = []; | |
| for (let index = 1; index <= 8; index += 1) { | |
| const frame = inputSource(source, `frame_${index}`); | |
| if (frame) frames.push(frame); | |
| } | |
| if (!frames.length) return source; | |
| const requested = Math.max(1, Number.isFinite(Number(scene)) | |
| ? Math.trunc(Number(scene)) : 1); | |
| return frames[(requested - 1) % frames.length]; | |
| } | |
| function keyframePreviewSource(connection, role, scene) { | |
| let source = connection?.source ?? null; | |
| if (!source) return null; | |
| // The frame gate exposes both first_frame and last_frame from the same | |
| // node. Starting a generic upstream search at that node always encounters | |
| // its opening `image` first, which made Picture 2 preview Picture 1. Follow | |
| // the input that corresponds to the I2V socket instead. | |
| if (nodeType(source) === FIRST_SCENE_IMAGE_TYPE) { | |
| source = inputSource(source, role === "first" ? "image" : "last_frame") | |
| ?? source; | |
| } | |
| return selectedFrameSource(source, scene); | |
| } | |
| export function imageToVideoReferenceRecords(editorNode, scene = 1) { | |
| const wrapper = findImageToVideo(editorNode); | |
| if (!wrapper) return {wrapper: null, mode: null, records: []}; | |
| const firstConnection = inputConnection(wrapper, "first_frame"); | |
| const lastConnection = inputConnection(wrapper, "last_frame"); | |
| const firstFrame = firstConnection?.source ?? null; | |
| const lastFrame = lastConnection?.source ?? null; | |
| const records = []; | |
| let firstActive = false; | |
| if (firstFrame) { | |
| const firstSceneOnly = nodeType(firstFrame) === FIRST_SCENE_IMAGE_TYPE; | |
| const active = !firstSceneOnly || Number(scene) === 1; | |
| firstActive = active; | |
| records.push({ | |
| node: wrapper, | |
| kind: "picture", | |
| tag: "", | |
| token: "<Picture 1>", | |
| selector: firstSceneOnly ? "1" : "all", | |
| active, | |
| source: keyframePreviewSource(firstConnection, "first", scene), | |
| label: "<Picture 1>", | |
| mode: "native", | |
| role: "first frame", | |
| }); | |
| } | |
| if (lastFrame) { | |
| const ordinal = firstActive ? 2 : 1; | |
| records.push({ | |
| node: wrapper, | |
| kind: "picture", | |
| tag: "", | |
| token: `<Picture ${ordinal}>`, | |
| selector: "all", | |
| active: true, | |
| source: keyframePreviewSource(lastConnection, "last", scene), | |
| label: `<Picture ${ordinal}>`, | |
| mode: "native", | |
| role: "last frame", | |
| }); | |
| } | |
| return {wrapper, mode: "native_keyframes", records}; | |
| } | |
| export function referencePreviewRecords(editorNode, scene, {prompt = ""} = {}) { | |
| const tagged = taggedReferenceRecords(editorNode, prompt); | |
| if (tagged.wrapper) return tagged; | |
| const scheduled = scheduledReferenceRecords(editorNode, scene); | |
| if (scheduled.wrapper) return scheduled; | |
| const core = coreReferenceRecords(editorNode); | |
| if (core.wrapper) return core; | |
| return imageToVideoReferenceRecords(editorNode, scene); | |
| } | |
| export function availableReferenceRecords( | |
| editorNode, scene, {includeInactive = false, prompt = ""} = {}, | |
| ) { | |
| const result = referencePreviewRecords(editorNode, scene, {prompt}); | |
| return { | |
| ...result, | |
| records: result.records.filter( | |
| (record) => record.source && (includeInactive || record.active), | |
| ), | |
| }; | |
| } | |