diff --git a/ComfyUI-MiniMaxH3-Context-Loop/tests/_reference_preview_js_test.mjs b/ComfyUI-MiniMaxH3-Context-Loop/tests/_reference_preview_js_test.mjs new file mode 100644 index 0000000000000000000000000000000000000000..db401c1bc9e8915f440d478737f07ab4e8fe1bd6 --- /dev/null +++ b/ComfyUI-MiniMaxH3-Context-Loop/tests/_reference_preview_js_test.mjs @@ -0,0 +1,781 @@ +import assert from "node:assert/strict"; +import { + availableReferenceRecords, + collectScheduleNodes, + collectSemanticAnchorNodes, + collectTaggedNodes, + convertTaggedPictureReference, + coreReferenceRecords, + findProjectAssetManager, + findScheduledRef2VA, + findTaggedRef2VA, + imageToVideoReferenceRecords, + referencePreviewRecords, + referenceIsActive, + referenceReplacementToken, + replacePromptReferenceOccurrence, + scheduledReferenceRecords, + taggedReferenceRecords, + taggedPictureReferenceMode, + taggedPictureReferenceToken, +} from "../web/h3_reference_preview_core.mjs"; + +assert.equal(taggedPictureReferenceToken("@hero", "native"), "@hero"); +assert.equal(taggedPictureReferenceToken("hero", "semantic"), "#hero"); +assert.equal( + taggedPictureReferenceToken("hero", "semantic", 2.5), + "#hero[2.50s]", +); +assert.equal(taggedPictureReferenceMode("Use @hero.", "hero"), "native"); +assert.equal(taggedPictureReferenceMode("Use @Hero.", "hero"), "unused"); +assert.equal(taggedPictureReferenceMode("Use #hero.", "hero"), "semantic"); +assert.equal(taggedPictureReferenceMode("Use #Hero.", "hero"), "unused"); +assert.equal( + taggedPictureReferenceMode("Use #hero[2.50s].", "hero"), + "semantic", +); +assert.equal( + taggedPictureReferenceMode("Use @hero then #hero[2.50s].", "hero"), + "mixed", +); +assert.equal( + convertTaggedPictureReference( + "Use @hero, but keep @heroine and mail x@hero.", + "hero", "semantic", 1.25, + ), + "Use #hero[1.25s], but keep @heroine and mail x@hero.", +); +assert.equal( + convertTaggedPictureReference("Use @hero.", "hero", "semantic"), + "Use #hero.", +); +assert.equal( + convertTaggedPictureReference("Keep @Hero unchanged.", "hero", "semantic"), + "Keep @Hero unchanged.", +); +assert.equal( + convertTaggedPictureReference( + "Use #hero, #hero[0.00s], then #hero[2.5].", "hero", "native", + ), + "Use @hero, @hero, then @hero.", +); +const heroRecord = { + kind: "picture", tag: "hero", token: "@hero", nativeToken: "@hero", +}; +const alternateRecord = { + kind: "picture", tag: "alternate", token: "@alternate", + nativeToken: "@alternate", +}; +assert.equal( + referenceReplacementToken(alternateRecord, "semantic", 3.125), + "#alternate[3.13s]", +); +assert.equal( + replacePromptReferenceOccurrence( + "Use @hero, then @hero.", 16, 21, alternateRecord, "native", + ), + "Use @hero, then @alternate.", +); +assert.equal( + replacePromptReferenceOccurrence( + "Use #hero[1.00s] now.", 4, 16, heroRecord, "semantic", 2.5, + ), + "Use #hero[2.50s] now.", +); +assert.equal( + replacePromptReferenceOccurrence( + "Use @hero.", -1, 9, alternateRecord, "native", + ), + "Use @hero.", +); +assert.equal( + referenceReplacementToken( + {kind: "audio", tag: "voice", token: "@voice"}, "semantic", 1, + ), + "", +); +assert.equal( + referenceReplacementToken({ + kind: "picture", tag: "only", token: "#only[0.00s]", + nativeToken: null, semanticOnly: true, + }, "native"), + "", +); + +function makeNode(id, type, widgets = {}) { + return { + id, + type, + title: type, + widgets: Object.entries(widgets).map(([name, value]) => ({name, value})), + inputs: [], + outputs: [{name: "output", links: []}], + }; +} + +const graph = { + _nodes: [], + links: {}, + getNodeById(id) { return this._nodes.find((node) => node.id === id); }, +}; +let nextLink = 1; +function add(node) { + node.graph = graph; + graph._nodes.push(node); + return node; +} +function connect(source, target, targetName, sourceSlot = 0) { + const id = nextLink++; + while (source.outputs.length <= sourceSlot) { + source.outputs.push({name: `output_${source.outputs.length}`, links: []}); + } + source.outputs[sourceSlot].links.push(id); + target.inputs.push({name: targetName, link: id}); + graph.links[id] = { + origin_id: source.id, + origin_slot: sourceSlot, + target_id: target.id, + target_slot: target.inputs.length - 1, + }; +} + +function connectExistingInput(source, target, targetName, sourceSlot = 0) { + const targetSlot = target.inputs.findIndex( + (input) => input.name === targetName, + ); + assert.notEqual(targetSlot, -1); + const id = nextLink++; + source.outputs[sourceSlot].links.push(id); + target.inputs[targetSlot].link = id; + graph.links[id] = { + origin_id: source.id, + origin_slot: sourceSlot, + target_id: target.id, + target_slot: targetSlot, + }; +} + +function makeSubgraph(host, inputs = [], outputs = []) { + const subgraph = { + _nodes: [], + links: {}, + inputs: inputs.map((name) => ({name, linkIds: []})), + outputs: outputs.map((name) => ({name, linkIds: []})), + rootGraph: graph, + getNodeById(id) { + return this._nodes.find((node) => node.id === id); + }, + }; + host.subgraph = subgraph; + host.inputs = inputs.map((name) => ({name, link: null})); + host.outputs = outputs.map((name) => ({name, links: []})); + return subgraph; +} + +function addInside(subgraph, node) { + node.graph = subgraph; + subgraph._nodes.push(node); + return node; +} + +function connectInside(source, target, targetName, sourceSlot = 0) { + const owner = source.graph; + const id = nextLink++; + while (source.outputs.length <= sourceSlot) { + source.outputs.push({name: `output_${source.outputs.length}`, links: []}); + } + source.outputs[sourceSlot].links.push(id); + target.inputs.push({name: targetName, link: id}); + owner.links[id] = { + origin_id: source.id, + origin_slot: sourceSlot, + target_id: target.id, + target_slot: target.inputs.length - 1, + }; +} + +function connectSubgraphInput(subgraph, inputSlot, target, targetName) { + const id = nextLink++; + target.inputs.push({name: targetName, link: id}); + subgraph.inputs[inputSlot].linkIds.push(id); + subgraph.links[id] = { + origin_id: -10, + origin_slot: inputSlot, + target_id: target.id, + target_slot: target.inputs.length - 1, + }; +} + +function connectSubgraphOutput(subgraph, source, sourceSlot, outputSlot) { + const id = nextLink++; + source.outputs[sourceSlot].links.push(id); + subgraph.outputs[outputSlot].linkIds.push(id); + subgraph.links[id] = { + origin_id: source.id, + origin_slot: sourceSlot, + target_id: -20, + target_slot: outputSlot, + }; +} + +const editor = add(makeNode(1, "MiniMaxH3ChainScenePromptEditor")); +const relay = add(makeNode(2, "MiniMaxH3ChainCurrent")); +const wrapper = add(makeNode(3, "MiniMaxH3ScheduledReferenceToVideo")); +const firstImage = add(makeNode(4, "LoadImage", {image: "first.png"})); +const secondImage = add(makeNode(5, "LoadImage", {image: "second.png"})); +const audioFile = add(makeNode(6, "LoadAudio", {audio: "score.wav"})); +const first = add(makeNode(7, "MiniMaxH3ScheduledPictureReference", { + tag: "picture_1", scenes: "1", +})); +const second = add(makeNode(8, "MiniMaxH3ScheduledPictureReference", { + tag: "picture_2", scenes: "", +})); +const audio = add(makeNode(9, "MiniMaxH3ScheduledAudioReference", { + tag: "score", scenes: "all", +})); + +connect(editor, relay, "state"); +connect(relay, wrapper, "prompt"); +connect(firstImage, first, "image"); +connect(first, second, "previous"); +connect(secondImage, second, "image"); +connect(second, audio, "previous"); +connect(audioFile, audio, "audio"); +connect(audio, wrapper, "reference_schedule"); + +assert.equal(findScheduledRef2VA(editor), wrapper); +assert.deepEqual(collectScheduleNodes(wrapper), [first, second, audio]); +assert.equal(referenceIsActive("1,3:5", 4), true); +assert.equal(referenceIsActive("1,3:5", 2), false); + +const sceneOne = scheduledReferenceRecords(editor, 1).records; +assert.deepEqual(sceneOne.map(({tag, label, active}) => ({tag, label, active})), [ + {tag: "picture_1", label: "", active: true}, + {tag: "picture_2", label: "", active: true}, + {tag: "score", label: "