Skip to content

Commit e47991c

Browse files
authored
Merge pull request #1 from longredzhong/copilot/add-keyword-argument-support
Add keyword argument support to transpiler
2 parents 87ca4ce + 3eb2816 commit e47991c

File tree

3 files changed

+113
-10
lines changed

3 files changed

+113
-10
lines changed

src/comfy_script/transpile/__init__.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
from . import passes
1313

1414
class WorkflowToScriptTranspiler:
15-
def __init__(self, workflow: str | dict, api_endpoint: str = None):
15+
def __init__(self, workflow: str | dict, api_endpoint: str = None, use_keyword_args: bool = False):
1616
'''
1717
- `workflow`: Can be either in the web UI format or the API format.
18+
- `use_keyword_args`: Whether to generate keyword arguments instead of positional arguments.
1819
'''
1920
if api_endpoint is not None:
2021
client.client = client.Client(api_endpoint)
2122
self.nodes_info = client.get_nodes_info()
23+
self.use_keyword_args = use_keyword_args
2224

2325
if isinstance(workflow, str):
2426
workflow = json.loads(workflow)
@@ -47,9 +49,13 @@ def __init__(self, workflow: str | dict, api_endpoint: str = None):
4749
self.links = links
4850

4951
@staticmethod
50-
def from_image(image: Image.Image, comfyui_api: str = None) -> WorkflowToScriptTranspiler:
52+
def from_image(image: Image.Image, comfyui_api: str = None, use_keyword_args: bool = False) -> WorkflowToScriptTranspiler:
5153
'''
5254
Support PNG images generated by ComfyScript/ComfyUI.
55+
56+
- `image`: A PIL image containing embedded workflow or prompt metadata.
57+
- `comfyui_api`: Optional ComfyUI API endpoint to initialize the client with.
58+
- `use_keyword_args`: Whether to generate keyword arguments instead of positional arguments.
5359
'''
5460
# TODO: webp
5561
workflow = None
@@ -60,21 +66,25 @@ def from_image(image: Image.Image, comfyui_api: str = None) -> WorkflowToScriptT
6066
workflow = image_info['prompt']
6167
else:
6268
raise ValueError('No workflow in the image')
63-
return WorkflowToScriptTranspiler(workflow, comfyui_api)
69+
return WorkflowToScriptTranspiler(workflow, comfyui_api, use_keyword_args=use_keyword_args)
6470

6571
@staticmethod
66-
def from_file(path: str | Path, comfyui_api: str = None) -> WorkflowToScriptTranspiler:
72+
def from_file(path: str | Path, comfyui_api: str = None, use_keyword_args: bool = False) -> WorkflowToScriptTranspiler:
6773
'''
6874
Support PNG images generated by ComfyScript/ComfyUI and workflow JSON files either in the web UI format or the API format.
75+
76+
- `path`: Path to a PNG image generated by ComfyScript/ComfyUI or to a workflow JSON file in either the web UI format or the API format.
77+
- `comfyui_api`: Optional ComfyUI API endpoint to use when creating the client.
78+
- `use_keyword_args`: Whether to generate keyword arguments instead of positional arguments.
6979
'''
7080
path = Path(path)
7181
if path.suffix == '.json':
7282
with path.open() as f:
7383
workflow = json.load(f)
74-
return WorkflowToScriptTranspiler(workflow, comfyui_api)
84+
return WorkflowToScriptTranspiler(workflow, comfyui_api, use_keyword_args=use_keyword_args)
7585
else:
7686
image = Image.open(path)
77-
return WorkflowToScriptTranspiler.from_image(image, comfyui_api)
87+
return WorkflowToScriptTranspiler.from_image(image, comfyui_api, use_keyword_args=use_keyword_args)
7888

7989
def _declare_id(self, id: str) -> str:
8090
if id not in self.ids:
@@ -171,6 +181,26 @@ def _keyword_args_to_positional(self, node_type: str, kwargs: dict) -> list:
171181
# Optional inputs
172182
args.append({'exp': 'None', 'value': None})
173183
return args
184+
185+
def _format_args_as_keyword(self, node_type: str, args_dict: dict) -> list[str]:
186+
"""Format arguments as keyword arguments like 'param_name=value'."""
187+
result = []
188+
input_types = self._get_input_types(node_type)
189+
190+
for group in 'required', 'optional':
191+
group_dict: dict = input_types.get(group)
192+
if group_dict is None:
193+
continue
194+
for name in group_dict:
195+
value = args_dict.get(name)
196+
if value is not None:
197+
# Format as keyword argument
198+
result.append(f"{name}={value['exp']}")
199+
elif group == 'required':
200+
# Only include None for required parameters
201+
# Optional parameters with None value are skipped
202+
result.append(f"{name}=None")
203+
return result
174204

175205
def _node_to_assign_st(self, node):
176206
G = self.G
@@ -290,7 +320,13 @@ def _node_to_assign_st(self, node):
290320
if len(vars) != 0:
291321
c += f"{astutil.to_assign_target_list(vars)} = "
292322
if mode != 4:
293-
c += f"{class_id}({', '.join(arg['exp'] for arg in args)})"
323+
if self.use_keyword_args:
324+
# Generate keyword arguments
325+
formatted_args = self._format_args_as_keyword(v.type, args_dict)
326+
c += f"{class_id}({', '.join(formatted_args)})"
327+
else:
328+
# Generate positional arguments (default behavior)
329+
c += f"{class_id}({', '.join(arg['exp'] for arg in args)})"
294330
else:
295331
# Bypass
296332
if len(vars) > 0 and len(vars_args_of_same_type) == len(vars):

src/comfy_script/transpile/__main__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77
@click.argument('workflow', type=click.File('r', encoding='utf-8'))
88
@click.option('--api', type=click.STRING, default='http://127.0.0.1:8188/', show_default=True)
99
@click.option('--runtime', is_flag=True, default=False, show_default=True, help='Wrap the script with runtime imports and workflow context.')
10-
def cli(workflow: TextIO, api: str, runtime: bool):
10+
@click.option('--use-keyword-args', is_flag=True, default=False, show_default=True, help='Generate keyword arguments instead of positional arguments.')
11+
def cli(workflow: TextIO, api: str, runtime: bool, use_keyword_args: bool):
1112
workflow = workflow.read()
12-
script = WorkflowToScriptTranspiler(workflow, api).to_script(runtime=runtime)
13+
script = WorkflowToScriptTranspiler(workflow, api, use_keyword_args=use_keyword_args).to_script(runtime=runtime)
1314
print(script)
1415

1516
cli()

tests/transpile/test_transpiler.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,70 @@
6969
])
7070
def test_workflow(workflow, script):
7171
with open(Path(__file__).parent / workflow) as f:
72-
assert transpile.WorkflowToScriptTranspiler(f.read()).to_script() == script
72+
assert transpile.WorkflowToScriptTranspiler(f.read()).to_script() == script
73+
74+
@pytest.mark.parametrize('workflow, script', [
75+
('default.json',
76+
r"""model, clip, vae = CheckpointLoaderSimple(ckpt_name='v1-5-pruned-emaonly.ckpt')
77+
conditioning = CLIPTextEncode(text='beautiful scenery nature glass bottle landscape, , purple galaxy bottle,', clip=clip)
78+
conditioning2 = CLIPTextEncode(text='text, watermark', clip=clip)
79+
latent = EmptyLatentImage(width=512, height=512, batch_size=1)
80+
latent = KSampler(model=model, seed=156680208700286, steps=20, cfg=8, sampler_name='euler', scheduler='normal', positive=conditioning, negative=conditioning2, latent_image=latent, denoise=1)
81+
image = VAEDecode(samples=latent, vae=vae)
82+
SaveImage(images=image, filename_prefix='ComfyUI')
83+
"""),
84+
('bypass.json',
85+
r"""image, _ = LoadImage(image='ComfyUI_temp_rcuxh_00001_.png')
86+
image2 = ImageScaleToSide(image=image, side_length=1024, side='Longest', upscale_method='nearest-exact', crop='disabled')
87+
PreviewImage(images=image2)
88+
image3, _ = CRUpscaleImage(image=image2, upscale_model='8x_NMKD-Superscale_150000_G.pth', mode='rescale', rescale_factor=2, resize_width=1024, resampling_method='lanczos', supersample='true', rounding_modulus=8)
89+
segs = ImpactMakeTileSEGS(image=image3, bbox_size=600, crop_factor=1.5, min_overlap=200, max_overlap=100, sub_batch_size_for_dilation=0, filter_segs_dilation='Reuse fast', mask_irregularity=None, irregular_mask_mode=None)
90+
# _ = SEGSPreview(segs=segs, alpha_mode=True, falloff=0.1, image=image3)
91+
image4 = image3
92+
PreviewImage(images=image4)
93+
segs2 = segs
94+
model, clip, vae = CheckpointLoaderSimple(ckpt_name=r'XL\turbovisionxlSuperFastXLBasedOnNew_alphaV0101Bakedvae.safetensors')
95+
lora_stack, _ = CRLoRAStack(switch_1='On', lora_name_1=r'xl\LCMTurboMix_LCM_Sampler.safetensors', model_weight_1=1, clip_weight_1=1, switch_2='On', lora_name_2=r'xl\xl_more_art-full_v1.safetensors', model_weight_2=1, clip_weight_2=1, switch_3='On', lora_name_3=r'xl\add-detail-xl.safetensors', model_weight_3=1, clip_weight_3=1, lora_stack=None)
96+
model, clip, _ = CRApplyLoRAStack(model=model, clip=clip, lora_stack=lora_stack)
97+
conditioning = CLIPTextEncode(text='Shot Size - extreme wide shot,( Marrakech market at night time:1.5), Moroccan young beautiful woman, smiling, exotic, (loose hijab:0.1)', clip=clip)
98+
conditioning2 = CLIPTextEncode(text='(worst quality, low quality, normal quality:2), blurry, depth of field, nsfw', clip=clip)
99+
basic_pipe = ToBasicPipe(model=model, clip=clip, vae=vae, positive=conditioning, negative=conditioning2)
100+
image5, _, _, _ = DetailerForEachPipe(image=image3, segs=segs2, guide_size=1024, guide_size_for=True, max_size=1024, seed=403808226377311, steps=10, cfg=3, sampler_name='lcm', scheduler='ddim_uniform', denoise=0.1, feather=50, noise_mask=True, force_inpaint=True, basic_pipe=basic_pipe, wildcard='', cycle=0, inpaint_model=1, noise_mask_feather=None, scheduler_func_opt=None, detailer_hook=True, refiner_ratio=50)
101+
PreviewImage(images=image5)
102+
PreviewImage(images=image)
103+
"""),
104+
('rgthree-comfy.json',
105+
r"""model, clip, vae = CheckpointLoaderSimple(ckpt_name='v1-5-pruned-emaonly.ckpt')
106+
# _ = CLIPTextEncode(text='n', clip=clip)
107+
conditioning = CLIPTextEncode(text='p', clip=clip)
108+
latent = EmptyLatentImage(width=512, height=512, batch_size=1)
109+
latent = KSampler(model=model, seed=0, steps=20, cfg=8, sampler_name='euler', scheduler='normal', positive=conditioning, negative=conditioning, latent_image=latent, denoise=1)
110+
image = VAEDecode(samples=latent, vae=vae)
111+
SaveImage(images=image, filename_prefix='ComfyUI')
112+
"""),
113+
('SplitSigmasDenoise.api.json',
114+
r"""noise = DisableNoise()
115+
width, height, _, _, _, empty_latent, _ = CRAspectRatio(width=512, height=768, aspect_ratio='custom', swap_dimensions='Off', upscale_factor=1, prescale_factor=1, batch_size=1)
116+
model = UNETLoader(unet_name='flux1-dev.safetensors', weight_dtype='fp8_e4m3fn')
117+
model = LoraLoaderModelOnly(model=model, lora_name='a.safetensors', strength_model=0.7000000000000001)
118+
model = LoraLoaderModelOnly(model=model, lora_name='b.safetensors', strength_model=0.7000000000000001)
119+
model = ModelSamplingFlux(model=model, max_shift=1.1500000000000001, base_shift=0.5, width=width, height=height)
120+
clip = DualCLIPLoader(clip_name1='t5.safetensors', clip_name2='clip_l.safetensors', type='flux')
121+
conditioning = CLIPTextEncode(text='prompt text', clip=clip)
122+
conditioning = FluxGuidance(conditioning=conditioning, guidance=3.5)
123+
guider = BasicGuider(model=model, conditioning=conditioning)
124+
sampler = KSamplerSelect(sampler_name='deis')
125+
sigmas = BasicScheduler(model=model, scheduler='beta', steps=30, denoise=1)
126+
sigmas, low_sigmas = SplitSigmasDenoise(sigmas=sigmas, denoise=0.4)
127+
noise2 = RandomNoise(noise_seed=149684926930931)
128+
empty_latent, _ = SamplerCustomAdvanced(noise=noise2, guider=guider, sampler=sampler, sigmas=sigmas, latent_image=empty_latent)
129+
empty_latent = InjectLatentNoise(latent=empty_latent, seed=49328841076664, strength=0.3, normalize='true', average=None)
130+
empty_latent, _ = SamplerCustomAdvanced(noise=noise, guider=guider, sampler=sampler, sigmas=low_sigmas, latent_image=empty_latent)
131+
vae = VAELoader(vae_name='ae.safetensors')
132+
image = VAEDecode(samples=empty_latent, vae=vae)
133+
SaveImage(images=image, filename_prefix='ComfyUI')
134+
""")
135+
])
136+
def test_workflow_with_keyword_args(workflow, script):
137+
with open(Path(__file__).parent / workflow) as f:
138+
assert transpile.WorkflowToScriptTranspiler(f.read(), use_keyword_args=True).to_script() == script

0 commit comments

Comments
 (0)