From 30669c7a27e39f9137574e8df83f077cc7db0751 Mon Sep 17 00:00:00 2001 From: Towsty Date: Tue, 25 Aug 2026 18:02:25 -0500 Subject: [PATCH] Swap in the custom MiniMax H3 graph and bind prompts, seed, size, steps, and LoRA to its node IDs. Co-authored-by: Cursor --- server/assets/workflow_api.json | 489 +++++++++++++++++++++++--------- server/utils/workflow.ts | 147 ++++++++-- 2 files changed, 480 insertions(+), 156 deletions(-) diff --git a/server/assets/workflow_api.json b/server/assets/workflow_api.json index 874c8f2..684d444 100644 --- a/server/assets/workflow_api.json +++ b/server/assets/workflow_api.json @@ -1,150 +1,381 @@ { - "1": { - "class_type": "LoadImage", + "92": { "inputs": { - "image": "input.png" + "filename_prefix": "video/MiniMax_H3", + "format": "auto", + "codec": "auto", + "video": [ + "105:91", + 0 + ] }, - "_meta": { "title": "Load Image" } + "class_type": "SaveVideo", + "_meta": { + "title": "Save Video" + } }, - "2": { - "class_type": "CLIPLoader", + "114": { + "inputs": { + "image": "Firefly.png" + }, + "class_type": "LoadImage", + "_meta": { + "title": "Load Image" + } + }, + "119": { + "inputs": { + "upscale_method": "nearest-exact", + "megapixels": 0.9, + "resolution_steps": 32 + }, + "class_type": "ImageScaleToTotalPixels", + "_meta": { + "title": "Scale Image to Total Pixels" + } + }, + "120": { + "inputs": { + "image": [ + "119", + 0 + ] + }, + "class_type": "GetImageSize", + "_meta": { + "title": "Get Image Size" + } + }, + "127": { + "inputs": { + "image": [ + "128", + 0 + ] + }, + "class_type": "GetImageSize", + "_meta": { + "title": "Get Image Size" + } + }, + "128": { + "inputs": { + "upscale_method": "nearest-exact", + "megapixels": 0.98, + "resolution_steps": 32, + "image": [ + "114", + 0 + ] + }, + "class_type": "ImageScaleToTotalPixels", + "_meta": { + "title": "Scale Image to Total Pixels" + } + }, + "105:11": { + "inputs": { + "vae_name": "minimax_h3_video_vae_fp16.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "Load VAE" + } + }, + "105:24": { + "inputs": { + "vae_name": "minimax_h3_audio_vae_fp32.safetensors" + }, + "class_type": "VAELoader", + "_meta": { + "title": "Load VAE" + } + }, + "105:23": { + "inputs": { + "samples": [ + "105:14", + 0 + ], + "vae": [ + "105:24", + 0 + ] + }, + "class_type": "VAEDecodeAudio", + "_meta": { + "title": "VAE Decode Audio" + } + }, + "105:10": { + "inputs": { + "samples": [ + "105:14", + 0 + ], + "vae": [ + "105:11", + 0 + ] + }, + "class_type": "VAEDecode", + "_meta": { + "title": "VAE Decode" + } + }, + "105:17": { + "inputs": { + "sampler_name": "res_multistep" + }, + "class_type": "KSamplerSelect", + "_meta": { + "title": "KSamplerSelect" + } + }, + "105:9": { + "inputs": { + "scheduler": "simple", + "steps": [ + "105:123", + 0 + ], + "denoise": 1, + "model": [ + "105:122", + 0 + ] + }, + "class_type": "BasicScheduler", + "_meta": { + "title": "BasicScheduler" + } + }, + "105:14": { + "inputs": { + "noise": [ + "105:15", + 0 + ], + "guider": [ + "105:16", + 0 + ], + "sampler": [ + "105:17", + 0 + ], + "sigmas": [ + "105:9", + 0 + ], + "latent_image": [ + "105:104", + 1 + ] + }, + "class_type": "SamplerCustomAdvanced", + "_meta": { + "title": "SamplerCustomAdvanced" + } + }, + "105:16": { + "inputs": { + "model": [ + "105:122", + 0 + ], + "conditioning": [ + "105:104", + 0 + ] + }, + "class_type": "BasicGuider", + "_meta": { + "title": "Basic Guider" + } + }, + "105:6": { + "inputs": { + "unet_name": "minimax_h3_fl2va_pruned_int8_convrot.safetensors", + "weight_dtype": "default" + }, + "class_type": "UNETLoader", + "_meta": { + "title": "Load Diffusion Model" + } + }, + "105:13": { "inputs": { "clip_name": "qwen3vl_32b_minimax_h3_nvfp4_awq.safetensors", "type": "minimax", "device": "default" }, - "_meta": { "title": "Qwen3-VL CLIP" } + "class_type": "CLIPLoader", + "_meta": { + "title": "Load CLIP" + } }, - "3": { - "class_type": "UNETLoader", + "105:15": { "inputs": { - "unet_name": "minimax_h3_fl2va_pruned_int8_convrot.safetensors", - "weight_dtype": "default" + "noise_seed": 757358688076805 }, - "_meta": { "title": "MiniMax H3 UNET" } - }, - "4": { - "class_type": "VAELoader", - "inputs": { - "vae_name": "minimax_h3_video_vae_fp16.safetensors" - }, - "_meta": { "title": "Video VAE" } - }, - "5": { - "class_type": "VAELoader", - "inputs": { - "vae_name": "minimax_h3_audio_vae_fp32.safetensors" - }, - "_meta": { "title": "Audio VAE" } - }, - "6": { - "class_type": "LoraLoaderModelOnly", - "inputs": { - "model": ["3", 0], - "lora_name": "minimax_h3_fl2v_turbo_8step_v1.0_comfyui_bf16.safetensors", - "strength_model": 1 - }, - "_meta": { "title": "Turbo LoRA" } - }, - "7": { - "class_type": "ComfySwitchNode", - "inputs": { - "switch": true, - "on_false": ["3", 0], - "on_true": ["6", 0] - }, - "_meta": { "title": "LoRA Bypass Switch" } - }, - "8": { - "class_type": "MiniMaxH3ImageToVideo", - "inputs": { - "clip": ["2", 0], - "vae": ["4", 0], - "first_frame": ["1", 0], - "prompt": "", - "width": 1344, - "height": 768, - "length": 124 - }, - "_meta": { "title": "MiniMax H3 Image to Video" } - }, - "9": { "class_type": "RandomNoise", - "inputs": { - "noise_seed": 0 - }, - "_meta": { "title": "Seed" } + "_meta": { + "title": "RandomNoise" + } }, - "10": { - "class_type": "KSamplerSelect", + "105:91": { "inputs": { - "sampler_name": "res_multistep" - }, - "_meta": { "title": "Sampler MiniMax" } - }, - "11": { - "class_type": "BasicScheduler", - "inputs": { - "model": ["7", 0], - "scheduler": "simple", - "steps": 8, - "denoise": 1 - }, - "_meta": { "title": "Scheduler" } - }, - "12": { - "class_type": "BasicGuider", - "inputs": { - "model": ["7", 0], - "conditioning": ["8", 0] - }, - "_meta": { "title": "Guider" } - }, - "13": { - "class_type": "SamplerCustomAdvanced", - "inputs": { - "noise": ["9", 0], - "guider": ["12", 0], - "sampler": ["10", 0], - "sigmas": ["11", 0], - "latent_image": ["8", 1] - }, - "_meta": { "title": "KSampler" } - }, - "14": { - "class_type": "VAEDecode", - "inputs": { - "samples": ["13", 0], - "vae": ["4", 0] - }, - "_meta": { "title": "Decode Video" } - }, - "15": { - "class_type": "VAEDecodeAudio", - "inputs": { - "samples": ["13", 0], - "vae": ["5", 0] - }, - "_meta": { "title": "Decode Audio" } - }, - "16": { - "class_type": "CreateVideo", - "inputs": { - "images": ["14", 0], - "audio": ["15", 0], "fps": 24, - "bit_depth": 8 + "bit_depth": 8, + "images": [ + "105:10", + 0 + ], + "audio": [ + "105:23", + 0 + ] }, - "_meta": { "title": "Create Video" } + "class_type": "CreateVideo", + "_meta": { + "title": "Create Video" + } }, - "17": { - "class_type": "SaveVideo", + "105:104": { "inputs": { - "video": ["16", 0], - "filename_prefix": "video/MiniMax_H3", - "format": "auto", - "codec": "auto" + "prompt": "", + "width": [ + "127", + 0 + ], + "height": [ + "127", + 1 + ], + "length": [ + "105:107", + 1 + ], + "clip": [ + "105:13", + 0 + ], + "vae": [ + "105:11", + 0 + ], + "first_frame": [ + "128", + 0 + ] }, - "_meta": { "title": "Save MP4" } + "class_type": "MiniMaxH3ImageToVideo", + "_meta": { + "title": "MiniMax H3 Image to Video" + } + }, + "105:107": { + "inputs": { + "expression": "max(5, round(a * 24)) + (5 - (max(5, round(a * 24)) % 17)) % 17", + "values.a": [ + "105:111", + 0 + ] + }, + "class_type": "ComfyMathExpression", + "_meta": { + "title": "Math Expression" + } + }, + "105:111": { + "inputs": { + "value": 10 + }, + "class_type": "PrimitiveFloat", + "_meta": { + "title": "Float (duration)" + } + }, + "105:121": { + "inputs": { + "lora_name": "minimax_h3_fl2v_turbo_8step_v1.0_comfyui_bf16.safetensors", + "strength_model": 1, + "model": [ + "105:6", + 0 + ] + }, + "class_type": "LoraLoaderModelOnly", + "_meta": { + "title": "Load LoRA" + } + }, + "105:122": { + "inputs": { + "switch": [ + "105:126", + 0 + ], + "on_false": [ + "105:6", + 0 + ], + "on_true": [ + "105:121", + 0 + ] + }, + "class_type": "ComfySwitchNode", + "_meta": { + "title": "If/Else Switch (Model)" + } + }, + "105:123": { + "inputs": { + "switch": [ + "105:126", + 0 + ], + "on_false": [ + "105:124", + 0 + ], + "on_true": [ + "105:125", + 0 + ] + }, + "class_type": "ComfySwitchNode", + "_meta": { + "title": "If/Else Switch (Steps)" + } + }, + "105:124": { + "inputs": { + "value": 20 + }, + "class_type": "PrimitiveInt", + "_meta": { + "title": "Int" + } + }, + "105:125": { + "inputs": { + "value": 8 + }, + "class_type": "PrimitiveInt", + "_meta": { + "title": "Int" + } + }, + "105:126": { + "inputs": { + "value": false + }, + "class_type": "PrimitiveBoolean", + "_meta": { + "title": "Boolean (Enable Lightning LoRA)" + } } -} +} \ No newline at end of file diff --git a/server/utils/workflow.ts b/server/utils/workflow.ts index 700005a..b3950b4 100644 --- a/server/utils/workflow.ts +++ b/server/utils/workflow.ts @@ -12,38 +12,131 @@ export interface GenerateParams { length: number } -type WorkflowGraph = Record; _meta?: { title?: string } }> +type WorkflowNode = { class_type: string; inputs: Record; _meta?: { title?: string } } +type WorkflowGraph = Record + +const PROMPT_CLASSES = new Set([ + 'MiniMaxH3ImageToVideo', + 'CLIPTextEncode', + 'CLIPTextEncodeQwen3VL', + 'TextEncodeQwen3VL' +]) +const SEED_CLASSES = new Set(['RandomNoise', 'KSampler', 'SamplerMiniMax', 'KSamplerAdvanced']) +const IMAGE_CLASSES = new Set(['LoadImage']) +const LORA_CLASSES = new Set(['LoraLoaderModelOnly', 'LoraLoader']) + +function entriesOf(graph: WorkflowGraph, classType: string) { + return Object.entries(graph).filter(([, node]) => node.class_type === classType) +} + +function linkId(value: unknown): string | null { + return Array.isArray(value) && typeof value[0] === 'string' ? value[0] : null +} + +function snap32(value: number) { + return Math.max(32, Math.round(value / 32) * 32) +} + +function titleOf(node: WorkflowNode) { + return node._meta?.title || '' +} export function buildWorkflow(params: GenerateParams) { const graph = structuredClone(workflowTemplate) as WorkflowGraph - graph['1'].inputs.image = params.imageName - graph['8'].inputs.prompt = params.prompt - graph['8'].inputs.width = params.width - graph['8'].inputs.height = params.height - graph['8'].inputs.length = params.length - graph['9'].inputs.noise_seed = params.seed - graph['11'].inputs.steps = params.steps - graph['7'].inputs.switch = params.turbo - graph['6'].inputs.strength_model = params.turbo ? 1 : 0 + const width = snap32(params.width) + const height = snap32(params.height) + + for (const [, node] of Object.entries(graph)) { + if (IMAGE_CLASSES.has(node.class_type) && 'image' in node.inputs) { + node.inputs.image = params.imageName + } + + if (PROMPT_CLASSES.has(node.class_type) || node.class_type.includes('Qwen3')) { + if ('prompt' in node.inputs) node.inputs.prompt = params.prompt + if ('text' in node.inputs) node.inputs.text = params.prompt + } + + if (node.class_type === 'MiniMaxH3ImageToVideo') { + node.inputs.prompt = params.prompt + node.inputs.width = width + node.inputs.height = height + node.inputs.length = params.length + } + + if (SEED_CLASSES.has(node.class_type)) { + if ('noise_seed' in node.inputs) node.inputs.noise_seed = params.seed + if ('seed' in node.inputs) node.inputs.seed = params.seed + } + + if (LORA_CLASSES.has(node.class_type)) { + node.inputs.strength_model = params.turbo ? 1 : 0 + } + + if (node.class_type === 'PrimitiveBoolean') { + node.inputs.value = params.turbo + } + + if (node.class_type === 'ComfySwitchNode' && typeof node.inputs.switch === 'boolean') { + node.inputs.switch = params.turbo + } + + if (node.class_type === 'BasicScheduler' && typeof node.inputs.steps === 'number') { + node.inputs.steps = params.steps + } + } + + for (const [, node] of entriesOf(graph, 'ComfySwitchNode')) { + if (!/step/i.test(titleOf(node))) continue + const qualityId = linkId(node.inputs.on_false) + const turboId = linkId(node.inputs.on_true) + if (qualityId && graph[qualityId]?.class_type === 'PrimitiveInt') { + graph[qualityId].inputs.value = params.turbo ? 20 : params.steps + } + if (turboId && graph[turboId]?.class_type === 'PrimitiveInt') { + graph[turboId].inputs.value = params.turbo ? params.steps : 8 + } + } + return graph } +const LABEL_OVERRIDES: Record = { + '114': 'Loading image', + '128': 'Scaling image', + '127': 'Reading image size', + '119': 'Scaling image', + '120': 'Reading image size', + '92': 'Saving MP4', + '105:6': 'Loading MiniMax H3 model', + '105:11': 'Loading video VAE', + '105:13': 'Loading Qwen3-VL encoder', + '105:15': 'Preparing seed', + '105:9': 'Building schedule', + '105:10': 'Decoding video frames', + '105:14': 'Sampling', + '105:16': 'Preparing guider', + '105:17': 'Selecting sampler', + '105:23': 'Decoding audio', + '105:24': 'Loading audio VAE', + '105:91': 'Encoding video', + '105:104': 'Encoding prompt & latent', + '105:107': 'Computing duration', + '105:111': 'Setting duration', + '105:121': 'Applying turbo LoRA', + '105:122': 'Selecting model path', + '105:123': 'Selecting steps', + '105:124': 'Quality steps', + '105:125': 'Turbo steps', + '105:126': 'Selecting turbo LoRA' +} + export const NODE_LABELS: Record = { - '1': 'Loading image', - '2': 'Loading Qwen3-VL encoder', - '3': 'Loading MiniMax H3 model', - '4': 'Loading video VAE', - '5': 'Loading audio VAE', - '6': 'Applying turbo LoRA', - '7': 'Selecting model path', - '8': 'Encoding prompt & latent', - '9': 'Preparing seed', - '10': 'Selecting sampler', - '11': 'Building schedule', - '12': 'Preparing guider', - '13': 'Sampling', - '14': 'Decoding video frames', - '15': 'Decoding audio', - '16': 'Encoding video', - '17': 'Saving MP4' + ...Object.fromEntries( + Object.entries(workflowTemplate as WorkflowGraph).map(([id, node]) => [id, node._meta?.title || `Node ${id}`]) + ), + ...LABEL_OVERRIDES +} + +export function isEncodingNode(node: string) { + return node === '92' || node === '105:91' || /encoding|saving mp4/i.test(NODE_LABELS[node] || '') }