#!/usr/bin/env python3 """Compile fresh 0.6 workflows from named recipes and this branch's schemas. No archived workflow, widget array, position, or frontend metadata is an input. Settings and wires use names. --check detects stale generated documents. """ from __future__ import annotations import argparse import copy import json import re import uuid from pathlib import Path from workflow_schema import DATA, ROOT, fields, is_widget, widget_fields, validate_value, load_schemas, options EXAMPLES = ROOT / 'example_workflows' NAMESPACE = uuid.UUID('aedf0e28-4e77-4ec8-98d8-7662561e4cf4') TITLE_HEIGHT, GAP = 30, 70 AUTHOR = {'MiniMaxH3ChainPlanModern', 'MiniMaxH3GenerationProfile', 'MiniMaxH3ChainPlanStudio', 'MiniMaxH3ProjectAssetManager', 'MiniMaxH3ChainScenePromptEditor', 'MiniMaxH3ChainRichScenePromptEditor', 'MiniMaxH3ChainCheckpointManager'} SIZES = { 'MiniMaxH3ChainPlanModern': (1000,900), 'MiniMaxH3ChainPlanStudio': (1080,940), 'MiniMaxH3ProjectAssetManager': (1080,880), 'MiniMaxH3ChainCheckpointManager': (1200,900), 'MiniMaxH3ChainScenePromptEditor': (1000,840), 'MiniMaxH3ChainRichScenePromptEditor': (1200,940), 'MiniMaxH3ChainReview': (760,880), 'MiniMaxH3ChainCurrent': (460,400), 'MiniMaxH3ChainUpscaleCurrent': (480,520), 'MiniMaxH3TaggedReferenceToVideo': (520,540), 'MiniMaxH3ReferenceToVideo': (520,420), 'MiniMaxH3ImageToVideo': (500,380), 'MiniMaxH3ChainSegmentSave': (460,220), 'MiniMaxH3ChainAssemble': (440,360), 'MiniMaxH3ChainContext': (420,260), 'MiniMaxH3LoopTrim': (420,250), 'MiniMaxH3ChainLoopEnd': (420,220), 'MiniMaxH3ChainManifestLoad': (440,200), 'LoadImage': (400,420), 'LoadImageMask': (400,420), 'LoadVideo': (400,380), 'VHS_LoadVideo': (400,580), 'PreviewImage': (420,380), 'PreviewAny': (420,180), 'SaveVideo': (480,440), 'SeedVR2VideoPathUpscaler': (520,580), 'Note': (1000,330), 'MiniMaxH3ChainUpscalePixelConditioning': (600,520), 'MiniMaxH3ChainUpscalePixelCurrent': (500,300), 'UltimateSDUpscaleNoUpscaleGuider': (500,720), } LABELS = { 'MiniMaxH3ChainPlanModern':'Production Plan', 'MiniMaxH3ChainPlanStudio':'Plan Studio', 'MiniMaxH3ProjectAssetManager':'Project Asset Carousel', 'MiniMaxH3ChainCheckpointManager':'Checkpoint Manager', 'MiniMaxH3ChainScenePromptEditor':'Scene Prompts', 'MiniMaxH3ChainRichScenePromptEditor':'Scene Prompts + References', 'MiniMaxH3GenerationProfile':'Generation Profile', 'MiniMaxH3ChainLoopStart':'Loop Start', 'MiniMaxH3ChainCurrent':'Current Scene', 'MiniMaxH3ChainContext':'Apply Scene Context', 'MiniMaxH3TaggedReferenceToVideo':'Tagged Ref2VA Conditioning', 'MiniMaxH3LoopTrim':'Trim Carried Overlap', 'MiniMaxH3ChainSegmentSave':'Save Segment + Checkpoint', 'MiniMaxH3ChainReview':'Review Candidates', 'MiniMaxH3ChainLoopEnd':'Loop End', 'MiniMaxH3ChainAssemble':'Assemble Final Video', 'MiniMaxH3ChainManifestLoad':'Load Saved Clips', 'MiniMaxH3ImageToVideo':'Image / Keyframe Conditioning', 'MiniMaxH3ReferenceToVideo':'Reference Conditioning', 'UNETLoader':'H3 Diffusion Model', 'CLIPLoader':'H3 Text Encoder', 'RandomNoise':'Scene Seed', 'SamplerCustomAdvanced':'Sample Video + Audio', 'BasicScheduler':'Sampling Schedule', 'KSamplerSelect':'Sampler', 'MinimaxH3LatentUpscaler3D':'LBH 3D Latent Upscaler', 'SeedVR2VideoPathUpscaler':'SeedVR2 Video Path Upscaler', 'MiniMaxH3ChainUpscalePixelCurrent':'Pixel Current Scene • Experimental', 'MiniMaxH3ChainUpscalePixelConditioning':'Pixel Conditioning • Actual Image Size', 'UltimateSDUpscaleNoUpscaleGuider':'USDU H3 • Refine Upscaled Images', } def socket_type(spec): return 'COMBO' if isinstance(spec[0], (list,tuple)) else spec[0] def make_node(recipe, schema, number): typ, settings = recipe['type'], recipe['settings'] controls = list(widget_fields(schema, settings)) assert not (set(settings)-{key for key,_ in controls}), (typ,'unknown settings') values=[] for key,spec in controls: assert key in settings, (typ,key,'recipe must explicitly name every widget') validate_value(settings[key],spec,(typ,key)) values.append(copy.deepcopy(settings[key])) inputs=[] for name,spec in fields(schema).items(): if spec[0]=='COMFY_AUTOGROW_V3': template=options(spec)['template'] item_spec=next(iter(template['input']['required'].values())) names=[key for key in recipe['inputs'] if key.startswith(name+'.')] names.append(name+'.'+template['prefix']+str(len(names))) inputs.extend({'name':key,'type':socket_type(item_spec),'link':None} for key in names) elif not is_widget(spec) or name in recipe['inputs']: value={'name':name,'type':socket_type(spec),'link':None} if is_widget(spec): value['widget']={'name':name} inputs.append(value) label=LABELS.get(typ) or re.sub(r'(?<=[a-z])(?=[A-Z])',' ',typ.replace('MiniMaxH3','').replace('Contex','Context')) if typ=='VAELoader': label='Audio VAE' if 'audio' in settings['vae_name'] else 'Video VAE' if typ in ('LoadImage','LoadVideo','LoadImageMask'): label={'LoadImage':'Reference Image','LoadVideo':'Source Video','LoadImageMask':'Edit Mask'}[typ] if recipe.get('mode')==2:label='RECOVERY • '+label elif recipe.get('mode')==4:label='OPTIONAL • '+label outputs=[dict(name=name,type=kind,links=None) for name,kind in zip(schema['output_name'],schema['output'])] input_width=max((len(i['name']) for i in inputs),default=0)*7 output_width=max((len(o['name']) for o in outputs),default=0)*7 width=max(360,len(label)*8+48,input_width+output_width+80) height=64+max(len(inputs),len(outputs))*20+len(controls)*24 if any(options(spec).get('multiline') for _,spec in controls):height+=180 explicit=SIZES.get(typ,(width,height)) width,height=max(width,explicit[0]),max(height,explicit[1]) if typ=='VHS_LoadVideo':values=copy.deepcopy(settings) # VHS uses a named serializer. return dict(id=number,type=typ,pos=[0,0],size=[width,height],flags={},order=number-1, mode=recipe.get('mode',0),inputs=inputs,outputs=outputs,title=label, properties={'Node name for S&R':typ},widgets_values=values) def group(title,members,color): left=min(n['pos'][0] for n in members)-40 top=min(n['pos'][1] for n in members)-90 right=max(n['pos'][0]+n['size'][0] for n in members)+40 bottom=max(n['pos'][1]+n['size'][1] for n in members)+40 return dict(title=title,bounding=[left,top,right-left,bottom-top],color=color,font_size=24,flags={}) def stack(nodes,x,y): for node in nodes: node['pos']=[x,y] y+=node['size'][1]+TITLE_HEIGHT+GAP return y def layout(nodes,links): groups=[] author=[n for n in nodes if n['type'] in AUTHOR or n['type']=='Note'] studio=any(n['type']=='MiniMaxH3ChainPlanStudio' for n in author) columns=[[],[],[]] if studio else [[],[]] priority={'Note':0,'MiniMaxH3GenerationProfile':1,'MiniMaxH3ChainPlanModern':2, 'MiniMaxH3ChainPlanStudio':3,'MiniMaxH3ChainScenePromptEditor':3, 'MiniMaxH3ChainRichScenePromptEditor':3,'MiniMaxH3ProjectAssetManager':4, 'MiniMaxH3ChainCheckpointManager':5} for n in sorted(author,key=lambda n:priority.get(n['type'],9)): t=n['type']; col=0 if studio: if t in ('MiniMaxH3ChainPlanStudio','MiniMaxH3ProjectAssetManager'):col=1 elif t in ('MiniMaxH3ChainRichScenePromptEditor','MiniMaxH3ChainCheckpointManager'):col=2 elif t in ('MiniMaxH3ChainScenePromptEditor','MiniMaxH3ChainRichScenePromptEditor'):col=1 columns[col].append(n) x=80 for col in columns: if not col:continue stack(col,x,180); x+=max(n['size'][0] for n in col)+120 if author:groups.append(group('01 • PROJECT & SCENES',author,'#315566')) x+=100 runtime=[n for n in nodes if n not in author and n['mode']!=2] recovery=sorted([n for n in nodes if n not in author and n['mode']==2], key=lambda n:n['type']!='MiniMaxH3ChainManifestLoad') runtime_ids={n['id'] for n in runtime} predecessors={n['id']:set() for n in runtime} for _,source,_,target,_,_ in links: if source in runtime_ids and target in runtime_ids:predecessors[target].add(source) depths={} while len(depths)