export const H3_BASE_SECTIONS = Object.freeze([ "integrated_multimodal_description", "overall_soundscape", "non_diegetic_music", ]); export const H3_REFERENCE_SECTIONS = Object.freeze([ "subject_definitions", "summary", "retention_analysis", "detailed_description", "overall_soundscape", "non_diegetic_music", ]); export const H3_ALL_SECTIONS = Object.freeze([ ...new Set([...H3_BASE_SECTIONS, ...H3_REFERENCE_SECTIONS]), ]); // Exact case-sensitive tokens registered by MiniMax-H3's tokenizer_config and // by ComfyUI PR #15808. Keep this order aligned with tokenizer ids 151669–151675. export const H3_MINIMAX_SPECIAL_TOKENS = Object.freeze([ "", "", "<|cutoff|>", "<|lyrics_start|>", "<|lyrics_end|>", "<|caption_start|>", "<|caption_end|>", ]); export const H3_MODES = Object.freeze([ {id:"auto", label:"Auto schema"}, {id:"t2va", label:"T2VA"}, {id:"i2va", label:"I2VA"}, {id:"fl2va", label:"FL2VA"}, {id:"l2va", label:"L2VA"}, {id:"ref2va", label:"Ref2VA"}, ]); // Common valid summary prefixes from the H3 full-reference format. The // validator also accepts other non-conflicting combinations of the six // canonical task types, so this list is an authoring catalog rather than an // artificial restriction on mixed tasks. export const H3_TASK_DIRECTIVES = Object.freeze([ "[keyframe completion]", "[reference generation]", "[video editing]", "[video continuation]", "[audio reference]", "[audio reuse]", "[keyframe completion + reference generation]", "[keyframe completion + reference generation + audio reference]", "[keyframe completion + reference generation + audio reuse]", "[video editing + keyframe completion]", "[video editing + keyframe completion + audio reference]", "[video editing + keyframe completion + audio reuse]", "[video editing + reference generation]", "[video editing + reference generation + audio reference]", "[video editing + reference generation + audio reuse]", "[video continuation + keyframe completion]", "[video continuation + keyframe completion + audio reference]", "[video continuation + keyframe completion + audio reuse]", "[video continuation + reference generation]", "[video continuation + reference generation + audio reference]", "[video continuation + reference generation + audio reuse]", ]); export const H3_VISUAL_RETENTION_MARKERS = Object.freeze([ "fully_preserved", "partially_preserved", "attribute_transfer", "weak_reference", ]); export const H3_AUDIO_RETENTION_MARKERS = Object.freeze([ "fully_copy", "partially_copy", "reference", "weak_reference", ]); const MODE_IDS = new Set(H3_MODES.map((item) => item.id)); const ALIGNMENT_PREFIX = /^(?:For the target video, at 0\.00 seconds into the target video,|How the reference pictures align with the target video —).*$/m; const SECTION_PATTERN = new RegExp( `^(${H3_ALL_SECTIONS.join("|")}):`, "gm", ); const TASK_TYPES = new Set([ "keyframe completion", "reference generation", "video editing", "video continuation", "audio reference", "audio reuse", ]); const TASK_ORDER = new Map([ ["video editing", 0], ["video continuation", 0], ["keyframe completion", 1], ["reference generation", 2], ["audio reference", 3], ["audio reuse", 3], ]); const REFERENCE_LABEL_PATTERN = /<(?:Subject|Picture|Video|Audio)\s+\d+>/g; export function normalizeH3Mode(value) { const mode = String(value ?? "auto").toLowerCase(); return MODE_IDS.has(mode) ? mode : "auto"; } export function h3ModeLabel(value) { const mode = normalizeH3Mode(value); return H3_MODES.find((item) => item.id === mode)?.label ?? "Auto schema"; } export function detectH3Mode(value) { const text = String(value ?? "").trimStart(); if (/^(?:subject_definitions|summary|retention_analysis|detailed_description):/m.test(text)) { return "ref2va"; } const firstLine = text.split("\n", 1)[0]; if (firstLine.startsWith("For the target video, at 0.00 seconds")) return "i2va"; if (firstLine.startsWith("How the reference pictures align") && /Picture 2/.test(firstLine)) return "fl2va"; if (firstLine.startsWith("How the reference pictures align")) return "l2va"; return "t2va"; } export function effectiveH3Mode(value, selectedMode = "auto") { const selected = normalizeH3Mode(selectedMode); return selected === "auto" ? detectH3Mode(value) : selected; } export function h3SectionsForMode(mode) { return normalizeH3Mode(mode) === "ref2va" ? H3_REFERENCE_SECTIONS : H3_BASE_SECTIONS; } function normalizedDuration(value) { const number = Number(value); return Number.isFinite(number) ? Math.max(0.01, number).toFixed(2) : "6.00"; } function normalizedShot(value) { const number = Math.trunc(Number(value)); return Number.isFinite(number) ? Math.max(1, Math.min(99, number)) : 1; } export function h3AlignmentInstruction(mode, {duration = 6, finalShot = 1} = {}) { const selected = normalizeH3Mode(mode); const seconds = normalizedDuration(duration); const shot = normalizedShot(finalShot); if (selected === "i2va") { return "For the target video, at 0.00 seconds into the target video, (from [Shot 1]) is fully referenced."; } if (selected === "fl2va") { return `How the reference pictures align with the target video — Picture 1 (from Shot 1) aligns with the 0.00-second mark of the target video; Picture 2 (from Shot ${shot}) aligns with the ${seconds}-second mark of the target video.`; } if (selected === "l2va") { return `How the reference pictures align with the target video — (from [Shot ${shot}]) aligns with the ${seconds}-second mark of the target video.`; } return ""; } export function parseH3Sections(value) { const text = String(value ?? ""); const records = []; SECTION_PATTERN.lastIndex = 0; for (const match of text.matchAll(SECTION_PATTERN)) { records.push({ name:match[1], start:match.index ?? 0, headerEnd:(match.index ?? 0) + match[0].length, }); } records.forEach((record, index) => { record.end = records[index + 1]?.start ?? text.length; record.contentStart = record.headerEnd; while (record.contentStart < record.end && /[ \t]/.test(text[record.contentStart])) record.contentStart += 1; if (text.slice(record.contentStart, record.contentStart + 2) === "\r\n") record.contentStart += 2; else if (text[record.contentStart] === "\n") record.contentStart += 1; while (record.contentStart < record.end && /[ \t]/.test(text[record.contentStart])) record.contentStart += 1; }); return records; } function malformedSectionLines(text) { const known = new Set(H3_ALL_SECTIONS); const result = []; String(text ?? "").split("\n").forEach((line, index) => { const candidate = line.match(/^([A-Za-z][A-Za-z _-]+)\s*:/); if (!candidate) return; const normalized = candidate[1].trim().toLowerCase().replace(/[ -]+/g, "_"); if (known.has(normalized) && !line.startsWith(`${normalized}:`)) { result.push({line:index + 1, expected:`${normalized}:`}); } }); return result; } export function validateH3TaskDirective(value) { const text = String(value ?? "").trim(); const match = text.match(/^\[([^\]]+)\]/); if (!match) return {valid:false, message:"summary must begin with a square-bracketed H3 task type"}; const parts = match[1].split("+").map((item) => item.trim()).filter(Boolean); if (!parts.length || parts.some((item) => !TASK_TYPES.has(item))) { return {valid:false, message:"summary contains an unknown H3 task type"}; } if (new Set(parts).size !== parts.length) { return {valid:false, message:"summary repeats an H3 task type"}; } if (parts.includes("video editing") && parts.includes("video continuation")) { return {valid:false, message:"summary cannot combine video editing and video continuation"}; } if (parts.includes("audio reference") && parts.includes("audio reuse")) { return {valid:false, message:"summary cannot combine audio reference and audio reuse"}; } const order = parts.map((item) => TASK_ORDER.get(item)); if (order.some((value, index) => index && value < order[index - 1])) { return {valid:false, message:"summary task types are not in canonical H3 order"}; } return {valid:true, directive:`[${parts.join(" + ")}]`}; } function uniqueMatches(value, pattern) { pattern.lastIndex = 0; return [...String(value ?? "").matchAll(pattern)] .map((match) => match[0]) .filter((item, index, all) => all.indexOf(item) === index); } function analyzeShotBody(body, problems) { const shots = [...String(body ?? "").matchAll(/\[Shot\s+(\d+)\]/g)]; if (!shots.length || Number(shots[0][1]) !== 1) { problems.push({severity:"error", code:"shots", message:"The main description must begin its shot sequence with [Shot 1]"}); return; } const numbers = shots.map((match) => Number(match[1])); if (numbers.some((number, index) => number !== index + 1)) { problems.push({severity:"error", code:"shots", message:"Shot markers must be sequential: [Shot 1], [Shot 2], and so on"}); } let previousTime = -1; for (let index = 1; index < shots.length; index += 1) { const match = shots[index]; const after = body.slice((match.index ?? 0) + match[0].length); const timestamp = after.match(/^ At (\d{2}):(\d{2})\.(\d{3}),/); if (!timestamp) { problems.push({severity:"error", code:"timestamp", message:`${match[0]} must immediately use At MM:SS.mmm,`}); continue; } const seconds = Number(timestamp[1]) * 60 + Number(timestamp[2]) + Number(timestamp[3]) / 1000; if (seconds <= previousTime) { problems.push({severity:"error", code:"timestamp", message:`${match[0]} cut time must be strictly later than the previous cut`}); } previousTime = seconds; } } function analyzeDialogue(text, problems) { const ranges = []; for (const match of String(text ?? "").matchAll(/([\s\S]*?)<\/d>/gi)) { const start = match.index ?? 0; ranges.push([start, start + match[0].length]); const language = match[1].match(/^\s*\[([A-Za-z][^\]]*)\]\s+/); if (!language || language[1].toLowerCase() === "unclear") { problems.push({severity:"warning", code:"language", message:"Each span should begin with a [Language] marker"}); } } for (const match of String(text ?? "").matchAll(/<(?:scenetrans|cutoff)>|<\|cutoff\|>/gi)) { const position = match.index ?? 0; if (!ranges.some(([start, end]) => start < position && position < end)) { problems.push({severity:"error", code:"dialogue_flow", message:`${match[0]} must be inside a dialogue span`}); } } if (//i.test(String(text ?? ""))) { problems.push({severity:"warning", code:"legacy_token", message:"Use tokenizer-native <|cutoff|> instead of legacy "}); } } function analyzeMiniMaxSpecialTokens(text, problems) { const source = String(text ?? ""); const pattern = /<\/?d>|<\|(?:cutoff|lyrics_start|lyrics_end|caption_start|caption_end)\|>/gi; for (const match of source.matchAll(pattern)) { const canonical = H3_MINIMAX_SPECIAL_TOKENS.find( (token) => token.toLowerCase() === match[0].toLowerCase(), ); if (canonical && match[0] !== canonical) { problems.push({severity:"error", code:"special_token", message:`Use exact case-sensitive MiniMax token ${canonical}`}); } } for (const pair of [ ["lyrics", "<|lyrics_start|>", "<|lyrics_end|>"], ["caption", "<|caption_start|>", "<|caption_end|>"], ]) { const [kind, start, end] = pair; const boundaryPattern = kind === "lyrics" ? /<\|lyrics_(?:start|end)\|>/g : /<\|caption_(?:start|end)\|>/g; let open = false; let invalid = false; for (const match of source.matchAll(boundaryPattern)) { if (match[0] === start) { if (open) invalid = true; open = true; } else if (!open) invalid = true; else open = false; } if (open || invalid) { problems.push({severity:"error", code:"special_pair", message:`Balance ${start} with ${end} in presentation order`}); } } } export function analyzeH3Prompt(value, selectedMode = "auto", options = {}) { const text = String(value ?? ""); const mode = effectiveH3Mode(text, selectedMode); const required = h3SectionsForMode(mode); const records = parseH3Sections(text); const byName = new Map(); for (const record of records) { const entries = byName.get(record.name) ?? []; entries.push(record); byName.set(record.name, entries); } const missing = required.filter((section) => !byName.has(section)); const duplicates = [...byName.entries()].filter(([, entries]) => entries.length > 1) .map(([section]) => section); const unexpected = [...byName.keys()].filter((section) => !required.includes(section)); const canonicalIndexes = records.filter((record) => required.includes(record.name)) .map((record) => required.indexOf(record.name)); const outOfOrder = canonicalIndexes.some((value, index) => index && value < canonicalIndexes[index - 1]); const malformed = malformedSectionLines(text); const problems = []; for (const section of missing) problems.push({severity:"error", code:"missing", section, message:`Missing ${section}:`}); for (const section of duplicates) problems.push({severity:"error", code:"duplicate", section, message:`Duplicate ${section}:`}); for (const section of unexpected) problems.push({severity:"error", code:"unexpected", section, message:`${section}: does not belong in ${h3ModeLabel(mode)}`}); if (outOfOrder) problems.push({severity:"error", code:"order", message:"H3 sections are not in their required order"}); for (const item of malformed) problems.push({severity:"error", code:"format", message:`Line ${item.line}: use exact ${item.expected}`}); const alignment = h3AlignmentInstruction(mode, options); const firstLine = text.trimStart().split("\n", 1)[0]; if (alignment) { if (firstLine !== alignment) { problems.push({severity:"error", code:"alignment", message:`${h3ModeLabel(mode)} requires its exact first-line picture alignment`}); } } else if (ALIGNMENT_PREFIX.test(firstLine)) { problems.push({severity:"error", code:"alignment", message:`${h3ModeLabel(mode)} must not begin with a keyframe-alignment instruction`}); } if (mode === "ref2va" && byName.has("summary")) { const summary = byName.get("summary")[0]; const body = text.slice(summary.contentStart, summary.end).trim(); const directive = validateH3TaskDirective(body); if (!directive.valid) problems.push({severity:"error", code:"summary", section:"summary", message:directive.message}); } for (const section of required) { const record = byName.get(section)?.[0]; if (!record) continue; const body = text.slice(record.contentStart, record.end).trim(); const meaningful = body .replace(/^\[reference generation\]\s*$/, "") .replace(/^\[Shot 1\]\s*$/, "") .trim(); if (!body || (!meaningful && section !== "non_diegetic_music")) { problems.push({severity:"warning", code:"empty", section, message:`${section}: still needs content`}); } } const mainSection = mode === "ref2va" ? "detailed_description" : "integrated_multimodal_description"; const mainRecord = byName.get(mainSection)?.[0]; if (mainRecord) { const body = text.slice(mainRecord.contentStart, mainRecord.end).trim(); analyzeShotBody(body, problems); if (mode === "ref2va") { const firstShot = body.indexOf("[Shot 1]"); if (firstShot <= 0 || !body.slice(0, firstShot).trim()) { problems.push({severity:"warning", code:"style", section:mainSection, message:"Ref2VA detailed_description needs 1–2 style sentences before [Shot 1]"}); } } } const openDialogue = (text.match(//gi) ?? []).length; const closeDialogue = (text.match(/<\/d>/gi) ?? []).length; if (openDialogue !== closeDialogue) { problems.push({severity:"error", code:"dialogue", message:"Unbalanced and dialogue tags"}); } analyzeDialogue(text, problems); analyzeMiniMaxSpecialTokens(text, problems); if (mode === "ref2va") { const definitions = byName.get("subject_definitions")?.[0]; const retention = byName.get("retention_analysis")?.[0]; if (definitions && retention) { const definitionBody = text.slice(definitions.contentStart, definitions.end); const retentionBody = text.slice(retention.contentStart, retention.end); const retained = uniqueMatches(retentionBody, REFERENCE_LABEL_PATTERN); const used = uniqueMatches(text.slice(definitions.end), REFERENCE_LABEL_PATTERN); const definitionLines = [...definitionBody.matchAll(/^\s*(<(?:Subject|Picture|Video|Audio)\s+\d+>)/gm)] .map((match) => match[1]); const defined = definitionLines.filter((label, index, all) => all.indexOf(label) === index); const duplicateDefinitions = definitionLines.filter((label, index, all) => all.indexOf(label) !== index) .filter((label, index, all) => all.indexOf(label) === index); for (const label of duplicateDefinitions) { problems.push({severity:"error", code:"definition", message:`${label} is defined more than once`}); } for (const label of used.filter((item) => !defined.includes(item))) { problems.push({severity:"warning", code:"definition", message:`${label} is used but not defined in subject_definitions`}); } for (const label of defined.filter((item) => !retained.includes(item))) { problems.push({severity:"warning", code:"retention", message:`${label} has no retention_analysis entry`}); } } } const connected = new Set(); for (const reference of options.connectedReferences ?? []) { const candidates = typeof reference === "string" ? [reference] : [reference?.token, reference?.label]; for (const rawToken of candidates) { const match = String(rawToken ?? "").match(/^<(Picture|Video|Audio)\s+(\d+)>$/i); if (match) connected.add(`<${match[1][0].toUpperCase()}${match[1].slice(1).toLowerCase()} ${Number(match[2])}>`); } } for (const ordinal of options.connectedPictures ?? []) { const value = Number(ordinal); if (Number.isInteger(value)) connected.add(``); } const connectedPictures = new Set( [...connected].filter((token) => token.startsWith("/gi)] .map((match) => `<${match[1][0].toUpperCase()}${match[1].slice(1).toLowerCase()} ${Number(match[2])}>`) .filter((token, index, all) => all.indexOf(token) === index); for (const token of usedExternalReferences.filter((item) => !connected.has(item))) { problems.push({severity:"warning", code:"reference", message:`${token} has no connected authoring reference`}); } return { mode, required, records, missing, duplicates, unexpected, outOfOrder, problems, valid:!problems.some((problem) => problem.severity === "error"), }; } function sectionStarter(section, mode) { if (normalizeH3Mode(mode) === "ref2va") { if (section === "summary") return "summary:\n[reference generation] "; if (section === "detailed_description") return "detailed_description:\n"; if (section === "non_diegetic_music") return "non_diegetic_music:\nN/A"; return `${section}:\n`; } if (section === "integrated_multimodal_description") { return "integrated_multimodal_description: [Shot 1] "; } if (section === "non_diegetic_music") return "non_diegetic_music: N/A"; return `${section}: `; } function cleanJoin(before, inserted, after) { const left = before.replace(/[ \t]+$/g, "").replace(/\n*$/g, ""); const right = after.replace(/^\n*/g, ""); if (!left) return right ? `${inserted}\n\n${right}` : inserted; if (!right) return `${left}\n\n${inserted}`; return `${left}\n\n${inserted}\n\n${right}`; } export function insertH3Section(value, section, mode) { const text = String(value ?? ""); const selected = normalizeH3Mode(mode) === "auto" ? detectH3Mode(text) : normalizeH3Mode(mode); const required = h3SectionsForMode(selected); if (!required.includes(section)) return {text, caret:text.length, added:false}; const records = parseH3Sections(text); const existing = records.find((record) => record.name === section); if (existing) return {text, caret:existing.contentStart, added:false}; const wanted = required.indexOf(section); const next = records.find((record) => required.indexOf(record.name) > wanted); const split = next?.start ?? text.length; const starter = sectionStarter(section, selected); const result = cleanJoin(text.slice(0, split), starter, text.slice(split)); const inserted = parseH3Sections(result).find((record) => record.name === section); return {text:result, caret:inserted?.contentStart ?? result.length, added:true}; } function removeKnownAlignment(value) { const text = String(value ?? "").trimStart(); const firstLine = text.split("\n", 1)[0]; if (!ALIGNMENT_PREFIX.test(firstLine)) return text; return text.slice(firstLine.length).replace(/^\s*/, ""); } export function ensureH3Structure(value, mode, options = {}) { const original = String(value ?? ""); const selected = normalizeH3Mode(mode) === "auto" ? detectH3Mode(original) : normalizeH3Mode(mode); let text = removeKnownAlignment(original); const alignment = h3AlignmentInstruction(selected, options); if (alignment) text = text ? `${alignment}\n\n${text}` : alignment; const added = []; for (const section of h3SectionsForMode(selected)) { const result = insertH3Section(text, section, selected); text = result.text; if (result.added) added.push(section); } return {text, mode:selected, added, caret:text.length}; }