Swap in the custom MiniMax H3 graph and bind prompts, seed, size, steps, and LoRA to its node IDs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-25 18:02:25 -05:00
co-authored by Cursor
parent a9bf4f6a95
commit 30669c7a27
2 changed files with 480 additions and 156 deletions
+120 -27
View File
@@ -12,38 +12,131 @@ export interface GenerateParams {
length: number
}
type WorkflowGraph = Record<string, { class_type: string; inputs: Record<string, unknown>; _meta?: { title?: string } }>
type WorkflowNode = { class_type: string; inputs: Record<string, unknown>; _meta?: { title?: string } }
type WorkflowGraph = Record<string, WorkflowNode>
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<string, string> = {
'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<string, string> = {
'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] || '')
}