comfy-cli 1.10.0__tar.gz → 1.10.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/PKG-INFO +2 -1
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/cmdline.py +110 -29
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/generate/app.py +216 -121
- comfy_cli-1.10.2/comfy_cli/command/generate/spec/openapi.yml +31636 -0
- comfy_cli-1.10.2/comfy_cli/command/run.py +955 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/env_checker.py +5 -2
- comfy_cli-1.10.2/comfy_cli/tracking.py +261 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli.egg-info/PKG-INFO +2 -1
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli.egg-info/SOURCES.txt +1 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli.egg-info/requires.txt +1 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/pyproject.toml +5 -1
- comfy_cli-1.10.0/comfy_cli/command/run.py +0 -369
- comfy_cli-1.10.0/comfy_cli/tracking.py +0 -128
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/LICENSE +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/README.md +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/__init__.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/__main__.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/__init__.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/code_search.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/custom_nodes/__init__.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/custom_nodes/cm_cli_util.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/custom_nodes/command.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/generate/__init__.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/generate/adapters.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/generate/client.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/generate/output.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/generate/poll.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/generate/schema.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/generate/spec.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/generate/upload.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/github/pr_info.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/install.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/launch.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/models/models.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/command/pr_command.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/config_manager.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/constants.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/cuda_detect.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/file_utils.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/git_utils.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/logging.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/pr_cache.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/registry/__init__.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/registry/api.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/registry/config_parser.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/registry/types.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/resolve_python.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/standalone.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/typing.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/ui.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/update.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/utils.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/uv.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/workflow_to_api.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli/workspace_manager.py +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli.egg-info/dependency_links.txt +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli.egg-info/entry_points.txt +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/comfy_cli.egg-info/top_level.txt +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/setup.cfg +0 -0
- {comfy_cli-1.10.0 → comfy_cli-1.10.2}/tests/test_file_utils_network.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: comfy-cli
|
|
3
|
-
Version: 1.10.
|
|
3
|
+
Version: 1.10.2
|
|
4
4
|
Summary: A CLI tool for installing and using ComfyUI.
|
|
5
5
|
Maintainer-email: Yoland Yan <yoland@drip.art>, James Kwon <hongilkwon316@gmail.com>, Robin Huang <robin@drip.art>, "Dr.Lt.Data" <dr.lt.data@gmail.com>
|
|
6
6
|
License: GPL-3.0-only
|
|
@@ -24,6 +24,7 @@ Requires-Dist: httpx
|
|
|
24
24
|
Requires-Dist: mixpanel
|
|
25
25
|
Requires-Dist: packaging
|
|
26
26
|
Requires-Dist: pathspec
|
|
27
|
+
Requires-Dist: posthog<8,>=6
|
|
27
28
|
Requires-Dist: psutil
|
|
28
29
|
Requires-Dist: pyyaml
|
|
29
30
|
Requires-Dist: questionary
|
|
@@ -423,10 +423,24 @@ def update(
|
|
|
423
423
|
rprint(f"[yellow]Failed to update node id cache: {e}[/yellow]")
|
|
424
424
|
|
|
425
425
|
|
|
426
|
-
@app.command(
|
|
427
|
-
|
|
426
|
+
@app.command(
|
|
427
|
+
help=(
|
|
428
|
+
"Run a workflow on the ComfyUI launched by `comfy launch --background`. "
|
|
429
|
+
"Accepts both ComfyUI API format and exported UI workflow JSON; "
|
|
430
|
+
"UI workflows are converted to API format client-side via /object_info."
|
|
431
|
+
)
|
|
432
|
+
)
|
|
428
433
|
def run(
|
|
429
|
-
workflow: Annotated[
|
|
434
|
+
workflow: Annotated[
|
|
435
|
+
str,
|
|
436
|
+
typer.Option(
|
|
437
|
+
help=(
|
|
438
|
+
"Path to the workflow JSON file. Both ComfyUI API format and "
|
|
439
|
+
"exported UI format are accepted; UI workflows are converted "
|
|
440
|
+
"to API format client-side."
|
|
441
|
+
)
|
|
442
|
+
),
|
|
443
|
+
],
|
|
430
444
|
wait: Annotated[
|
|
431
445
|
bool,
|
|
432
446
|
typer.Option(help="If the command should wait until execution completes."),
|
|
@@ -444,9 +458,17 @@ def run(
|
|
|
444
458
|
typer.Option(help="The port where the ComfyUI instance is running, e.g. 8188."),
|
|
445
459
|
] = None,
|
|
446
460
|
timeout: Annotated[
|
|
447
|
-
int
|
|
448
|
-
typer.Option(
|
|
449
|
-
|
|
461
|
+
int,
|
|
462
|
+
typer.Option(
|
|
463
|
+
help=(
|
|
464
|
+
"Per-event timeout in seconds: bails out if the server is silent "
|
|
465
|
+
"for this long. Also caps HTTP connect, /prompt POST, and websocket "
|
|
466
|
+
"handshake. NOT a wall-clock execution deadline — a workflow that "
|
|
467
|
+
"streams progress events faster than the timeout can run "
|
|
468
|
+
"indefinitely."
|
|
469
|
+
),
|
|
470
|
+
),
|
|
471
|
+
] = 120,
|
|
450
472
|
api_key: Annotated[
|
|
451
473
|
str | None,
|
|
452
474
|
typer.Option(
|
|
@@ -454,40 +476,99 @@ def run(
|
|
|
454
476
|
envvar="COMFY_API_KEY",
|
|
455
477
|
help=(
|
|
456
478
|
"Comfy API key for API Nodes (Partner Nodes). "
|
|
457
|
-
"Embedded in the prompt body as extra_data.api_key_comfy_org
|
|
479
|
+
"Embedded in the POST /prompt request body as extra_data.api_key_comfy_org. "
|
|
458
480
|
"For scripting, prefer the COMFY_API_KEY environment variable so the secret "
|
|
459
481
|
"stays out of shell history."
|
|
460
482
|
),
|
|
461
483
|
),
|
|
462
484
|
] = None,
|
|
485
|
+
json_output: Annotated[
|
|
486
|
+
bool,
|
|
487
|
+
typer.Option(
|
|
488
|
+
"--json",
|
|
489
|
+
help=(
|
|
490
|
+
"Emit NDJSON events to stdout instead of human-readable output. "
|
|
491
|
+
"One JSON object per line, terminated by \\n. See docs/json-output.md "
|
|
492
|
+
"for the event reference and stability contract. In this mode, "
|
|
493
|
+
"--verbose has no effect and Rich progress is suppressed. "
|
|
494
|
+
"Workflow input accepts both API and UI format JSON (UI input "
|
|
495
|
+
"triggers a `converted` event before `queued`). The converted "
|
|
496
|
+
"workflow graph is always emitted as a `prompt_preview` event "
|
|
497
|
+
"before `queued`, so agents have a full audit trail of what "
|
|
498
|
+
"the CLI submitted."
|
|
499
|
+
),
|
|
500
|
+
),
|
|
501
|
+
] = False,
|
|
502
|
+
print_prompt: Annotated[
|
|
503
|
+
bool,
|
|
504
|
+
typer.Option(
|
|
505
|
+
"--print-prompt",
|
|
506
|
+
help=(
|
|
507
|
+
"Print the API-format workflow graph that WOULD be sent to /prompt and exit. "
|
|
508
|
+
"Does not POST and does not execute. For UI-format input the workflow is "
|
|
509
|
+
"converted first (requires a reachable ComfyUI for /object_info); API input "
|
|
510
|
+
"is printed as-is with no server hit. In --json mode emits a `prompt_preview` "
|
|
511
|
+
"event; otherwise pretty-prints to stdout."
|
|
512
|
+
),
|
|
513
|
+
),
|
|
514
|
+
] = False,
|
|
463
515
|
):
|
|
464
|
-
|
|
465
|
-
|
|
516
|
+
# Snapshot kwargs before the body mutates api_key/host/port — analytics should record what user actually supplied.
|
|
517
|
+
_track_props = tracking.filter_command_kwargs(dict(locals()))
|
|
518
|
+
tracking.track_event("execution_start", _track_props, mixpanel_name="run")
|
|
466
519
|
|
|
467
|
-
|
|
520
|
+
try:
|
|
521
|
+
if api_key:
|
|
522
|
+
api_key = api_key.strip() or None
|
|
468
523
|
|
|
469
|
-
|
|
470
|
-
s = host.split(":")
|
|
471
|
-
host = s[0]
|
|
472
|
-
if not port and len(s) == 2:
|
|
473
|
-
port = int(s[1])
|
|
524
|
+
config = ConfigManager()
|
|
474
525
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if port:
|
|
481
|
-
local_paths = False
|
|
482
|
-
else:
|
|
483
|
-
port = config.background[1]
|
|
526
|
+
if host:
|
|
527
|
+
s = host.split(":")
|
|
528
|
+
host = s[0]
|
|
529
|
+
if not port and len(s) == 2:
|
|
530
|
+
port = int(s[1])
|
|
484
531
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
532
|
+
if config.background:
|
|
533
|
+
bg_host, bg_port = config.background[0], config.background[1]
|
|
534
|
+
if not host:
|
|
535
|
+
host = bg_host
|
|
536
|
+
if not port:
|
|
537
|
+
port = bg_port
|
|
489
538
|
|
|
490
|
-
|
|
539
|
+
if not host:
|
|
540
|
+
host = "127.0.0.1"
|
|
541
|
+
if not port:
|
|
542
|
+
port = 8188
|
|
543
|
+
|
|
544
|
+
run_inner.execute(
|
|
545
|
+
workflow,
|
|
546
|
+
host,
|
|
547
|
+
port,
|
|
548
|
+
wait,
|
|
549
|
+
verbose,
|
|
550
|
+
timeout,
|
|
551
|
+
api_key=api_key,
|
|
552
|
+
json_mode=json_output,
|
|
553
|
+
print_prompt=print_prompt,
|
|
554
|
+
)
|
|
555
|
+
except typer.Exit as e:
|
|
556
|
+
if (e.exit_code or 0) == 0:
|
|
557
|
+
tracking.track_event("execution_success", _track_props)
|
|
558
|
+
else:
|
|
559
|
+
tracking.track_event(
|
|
560
|
+
"execution_error",
|
|
561
|
+
{**_track_props, "error_type": type(e).__name__, "exit_code": e.exit_code},
|
|
562
|
+
)
|
|
563
|
+
raise
|
|
564
|
+
except Exception as e:
|
|
565
|
+
tracking.track_event(
|
|
566
|
+
"execution_error",
|
|
567
|
+
{**_track_props, "error_type": type(e).__name__},
|
|
568
|
+
)
|
|
569
|
+
raise
|
|
570
|
+
else:
|
|
571
|
+
tracking.track_event("execution_success", _track_props)
|
|
491
572
|
|
|
492
573
|
|
|
493
574
|
def validate_comfyui(_env_checker):
|
|
@@ -43,7 +43,6 @@ def register_with(parent: typer.Typer) -> None:
|
|
|
43
43
|
subcommand name and error."""
|
|
44
44
|
|
|
45
45
|
@parent.command(name="generate", help=_HELP, context_settings=_CONTEXT_SETTINGS)
|
|
46
|
-
@tracking.track_command()
|
|
47
46
|
def _generate_entry(
|
|
48
47
|
ctx: typer.Context,
|
|
49
48
|
target: Annotated[
|
|
@@ -57,17 +56,29 @@ def register_with(parent: typer.Typer) -> None:
|
|
|
57
56
|
if target is None or target in {"-h", "--help"}:
|
|
58
57
|
_print_top_help()
|
|
59
58
|
raise typer.Exit(code=0)
|
|
59
|
+
extra = list(ctx.args)
|
|
60
60
|
if target == "list":
|
|
61
|
-
|
|
61
|
+
tracking.track_event("generate:list")
|
|
62
|
+
return _list_models(extra)
|
|
62
63
|
if target == "schema":
|
|
63
|
-
|
|
64
|
+
model_arg = extra[0] if extra and not extra[0].startswith("-") else None
|
|
65
|
+
tracking.track_event("generate:schema", {"model": model_arg})
|
|
66
|
+
return _schema(extra)
|
|
64
67
|
if target == "refresh":
|
|
68
|
+
tracking.track_event("generate:refresh")
|
|
65
69
|
return _refresh()
|
|
66
70
|
if target == "upload":
|
|
67
|
-
|
|
71
|
+
tracking.track_event("generate:upload")
|
|
72
|
+
return _upload(extra)
|
|
68
73
|
if target == "resume":
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
resume_model = extra[0] if extra and not extra[0].startswith("-") else None
|
|
75
|
+
resume_job_id = extra[1] if len(extra) >= 2 and not extra[1].startswith("-") else None
|
|
76
|
+
tracking.track_event(
|
|
77
|
+
"generate:resume",
|
|
78
|
+
{"model": resume_model, "job_id": resume_job_id},
|
|
79
|
+
)
|
|
80
|
+
return _resume(extra)
|
|
81
|
+
_generate(target, extra)
|
|
71
82
|
|
|
72
83
|
|
|
73
84
|
def _separate_meta_flags(extra_args: list[str]) -> tuple[list[str], dict[str, str | bool]]:
|
|
@@ -146,130 +157,214 @@ def _emit_result(result: poll.PollResult, *, request_id: str, download: str | No
|
|
|
146
157
|
|
|
147
158
|
|
|
148
159
|
def _generate(model: str, extra_args: list[str]) -> None:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
remaining, meta = _separate_meta_flags(extra_args)
|
|
161
|
-
except schema.SchemaError as e:
|
|
162
|
-
rprint(f"[bold red]{e}[/bold red]")
|
|
163
|
-
raise typer.Exit(code=1)
|
|
164
|
-
|
|
165
|
-
flags = schema.flags_for(ep)
|
|
166
|
-
try:
|
|
167
|
-
values = schema.parse_args(flags, remaining)
|
|
168
|
-
except schema.SchemaError as e:
|
|
169
|
-
rprint(f"[bold red]{e}[/bold red]")
|
|
170
|
-
name = spec.preferred_alias(ep.id) or ep.id
|
|
171
|
-
rprint(f"[dim]Run `comfy generate schema {name}` for the full parameter list.[/dim]")
|
|
172
|
-
raise typer.Exit(code=1)
|
|
173
|
-
|
|
174
|
-
try:
|
|
175
|
-
api_key = client.resolve_api_key(meta.get("api-key") if isinstance(meta.get("api-key"), str) else None)
|
|
176
|
-
except client.ApiError as e:
|
|
177
|
-
rprint(f"[bold red]{e}[/bold red]")
|
|
178
|
-
raise typer.Exit(code=1)
|
|
179
|
-
|
|
180
|
-
timeout_raw = meta.get("timeout", "300")
|
|
181
|
-
try:
|
|
182
|
-
timeout = float(timeout_raw) if isinstance(timeout_raw, str) else 300.0
|
|
183
|
-
except ValueError:
|
|
184
|
-
rprint(f"[bold red]--timeout: expected number, got {timeout_raw!r}[/bold red]")
|
|
185
|
-
raise typer.Exit(code=1)
|
|
186
|
-
|
|
187
|
-
do_async = bool(meta.get("async", False))
|
|
188
|
-
download = meta.get("download") if isinstance(meta.get("download"), str) else None
|
|
189
|
-
as_json = bool(meta.get("json", False))
|
|
190
|
-
|
|
191
|
-
try:
|
|
192
|
-
_apply_upload_transforms(values, flags, ep, api_key)
|
|
193
|
-
except (client.ApiError, httpx.HTTPError) as e:
|
|
194
|
-
rprint(f"[bold red]Upload failed: {e}[/bold red]")
|
|
195
|
-
raise typer.Exit(code=1)
|
|
160
|
+
# --help short-circuits before tracking — it's a help-display action, not an execution attempt.
|
|
161
|
+
# If the model is unknown, fall through so the tracking path records the schema error.
|
|
162
|
+
asks_help = any(a in {"--help", "-h"} for a in extra_args)
|
|
163
|
+
if asks_help:
|
|
164
|
+
try:
|
|
165
|
+
help_ep = spec.get_endpoint(model)
|
|
166
|
+
except spec.SpecError:
|
|
167
|
+
help_ep = None
|
|
168
|
+
if help_ep is not None:
|
|
169
|
+
_show_schema_help(help_ep)
|
|
170
|
+
raise typer.Exit(code=0)
|
|
196
171
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
172
|
+
# generate:start fires at entry so every invocation has a paired start/end lifecycle.
|
|
173
|
+
# Props are filled in progressively as model_alias / partner / async / has_download become known.
|
|
174
|
+
gen_props: dict[str, object | None] = {
|
|
175
|
+
"model": model,
|
|
176
|
+
"model_alias": None,
|
|
177
|
+
"async": None,
|
|
178
|
+
"has_download": None,
|
|
179
|
+
"partner": None,
|
|
180
|
+
}
|
|
181
|
+
tracking.track_event("generate:start", gen_props)
|
|
182
|
+
|
|
183
|
+
def _track_error(error_kind: str, exc: BaseException) -> None:
|
|
184
|
+
tracking.track_event(
|
|
185
|
+
"generate:error",
|
|
186
|
+
{**gen_props, "error_type": type(exc).__name__, "error_kind": error_kind},
|
|
187
|
+
)
|
|
203
188
|
|
|
204
189
|
try:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
190
|
+
try:
|
|
191
|
+
ep = spec.get_endpoint(model)
|
|
192
|
+
except spec.SpecError as e:
|
|
193
|
+
rprint(f"[bold red]{e}[/bold red]")
|
|
194
|
+
_track_error("schema", e)
|
|
195
|
+
raise typer.Exit(code=1)
|
|
196
|
+
|
|
197
|
+
gen_props["model_alias"] = spec.preferred_alias(ep.id)
|
|
198
|
+
gen_props["partner"] = getattr(ep, "partner", None)
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
remaining, meta = _separate_meta_flags(extra_args)
|
|
202
|
+
except schema.SchemaError as e:
|
|
203
|
+
rprint(f"[bold red]{e}[/bold red]")
|
|
204
|
+
_track_error("schema", e)
|
|
205
|
+
raise typer.Exit(code=1)
|
|
206
|
+
|
|
207
|
+
do_async = bool(meta.get("async", False))
|
|
208
|
+
download = meta.get("download") if isinstance(meta.get("download"), str) else None
|
|
209
|
+
as_json = bool(meta.get("json", False))
|
|
210
|
+
gen_props["async"] = do_async
|
|
211
|
+
gen_props["has_download"] = bool(download)
|
|
212
|
+
|
|
213
|
+
flags = schema.flags_for(ep)
|
|
214
|
+
try:
|
|
215
|
+
values = schema.parse_args(flags, remaining)
|
|
216
|
+
except schema.SchemaError as e:
|
|
217
|
+
rprint(f"[bold red]{e}[/bold red]")
|
|
218
|
+
name = gen_props["model_alias"] or ep.id
|
|
219
|
+
rprint(f"[dim]Run `comfy generate schema {name}` for the full parameter list.[/dim]")
|
|
220
|
+
_track_error("schema", e)
|
|
221
|
+
raise typer.Exit(code=1)
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
api_key = client.resolve_api_key(meta.get("api-key") if isinstance(meta.get("api-key"), str) else None)
|
|
225
|
+
except client.ApiError as e:
|
|
226
|
+
rprint(f"[bold red]{e}[/bold red]")
|
|
227
|
+
_track_error("api", e)
|
|
228
|
+
raise typer.Exit(code=1)
|
|
229
|
+
|
|
230
|
+
timeout_raw = meta.get("timeout", "300")
|
|
231
|
+
try:
|
|
232
|
+
timeout = float(timeout_raw) if isinstance(timeout_raw, str) else 300.0
|
|
233
|
+
except ValueError as e:
|
|
234
|
+
rprint(f"[bold red]--timeout: expected number, got {timeout_raw!r}[/bold red]")
|
|
235
|
+
_track_error("schema", e)
|
|
236
|
+
raise typer.Exit(code=1)
|
|
237
|
+
|
|
238
|
+
try:
|
|
239
|
+
_apply_upload_transforms(values, flags, ep, api_key)
|
|
240
|
+
except (client.ApiError, httpx.HTTPError) as e:
|
|
241
|
+
rprint(f"[bold red]Upload failed: {e}[/bold red]")
|
|
242
|
+
_track_error("upload", e)
|
|
243
|
+
raise typer.Exit(code=1)
|
|
244
|
+
|
|
245
|
+
request_id = str(uuid.uuid4())[:8]
|
|
246
|
+
try:
|
|
247
|
+
resp = client.send_request(ep, values, flags, api_key, timeout=timeout)
|
|
248
|
+
except httpx.HTTPError as e:
|
|
249
|
+
rprint(f"[bold red]Network error contacting {spec.base_url()}: {e}[/bold red]")
|
|
250
|
+
_track_error("network", e)
|
|
251
|
+
raise typer.Exit(code=1) from e
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
client.raise_for_status(resp)
|
|
255
|
+
except client.ApiError as e:
|
|
256
|
+
rprint(f"[bold red]API error {e.status}[/bold red]\n{e.body}")
|
|
257
|
+
_track_error("api", e)
|
|
258
|
+
raise typer.Exit(code=1) from e
|
|
259
|
+
|
|
260
|
+
if resp.headers.get("content-type", "").startswith("image/"):
|
|
261
|
+
if download:
|
|
262
|
+
saved = output.save_binary_response(resp, download, request_id)
|
|
263
|
+
output.print_saved([saved])
|
|
264
|
+
else:
|
|
265
|
+
rprint(
|
|
266
|
+
"[yellow]Binary image response; nothing saved. Pass --download <path> to write it to disk.[/yellow]"
|
|
267
|
+
)
|
|
268
|
+
tracking.track_event("generate:success", gen_props)
|
|
269
|
+
return
|
|
217
270
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
271
|
+
try:
|
|
272
|
+
body = resp.json()
|
|
273
|
+
except ValueError as e:
|
|
274
|
+
rprint("[bold red]Unexpected non-JSON response.[/bold red]")
|
|
275
|
+
rprint(resp.text[:500])
|
|
276
|
+
_track_error("non_json_response", e)
|
|
277
|
+
raise typer.Exit(code=1)
|
|
278
|
+
|
|
279
|
+
if ep.polling:
|
|
280
|
+
job_id = poll.extract_job_id(ep.polling, body) or request_id
|
|
281
|
+
name = gen_props["model_alias"] or ep.id
|
|
282
|
+
if do_async:
|
|
283
|
+
if as_json:
|
|
284
|
+
output.print_json(body)
|
|
285
|
+
else:
|
|
286
|
+
rprint(f"[bold green]Submitted:[/bold green] {name}")
|
|
287
|
+
rprint(f" job id: {job_id}")
|
|
288
|
+
rprint(f" resume: comfy generate resume {name} {job_id}")
|
|
289
|
+
# Submitted, not succeeded — the workflow runs on the partner side and completion is
|
|
290
|
+
# observed server-side via partner_node:api_call_*. No generate:success pair here.
|
|
291
|
+
tracking.track_event(
|
|
292
|
+
"generate:submitted",
|
|
293
|
+
{
|
|
294
|
+
"model": model,
|
|
295
|
+
"model_alias": gen_props["model_alias"],
|
|
296
|
+
"job_id": job_id,
|
|
297
|
+
"partner": gen_props["partner"],
|
|
298
|
+
},
|
|
299
|
+
)
|
|
300
|
+
return
|
|
301
|
+
|
|
302
|
+
poller = poll.get_poller(ep.polling)
|
|
303
|
+
with _spinner() as prog:
|
|
304
|
+
task = prog.add_task(f"Generating with {name} (job {job_id})", total=None)
|
|
305
|
+
|
|
306
|
+
def _on_progress(p: float) -> None:
|
|
307
|
+
prog.update(task, description=f"Generating ({p * 100:.0f}%)")
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
result = poller(
|
|
311
|
+
body,
|
|
312
|
+
api_key=api_key,
|
|
313
|
+
timeout=timeout,
|
|
314
|
+
on_progress=_on_progress,
|
|
315
|
+
create_path=ep.path,
|
|
316
|
+
)
|
|
317
|
+
except (client.ApiError, httpx.HTTPError) as e:
|
|
318
|
+
_track_error("network" if isinstance(e, httpx.HTTPError) else "api", e)
|
|
319
|
+
raise typer.Exit(code=1) from e
|
|
320
|
+
try:
|
|
321
|
+
_emit_result(result, request_id=job_id, download=download, as_json=as_json)
|
|
322
|
+
tracking.track_event("generate:success", gen_props)
|
|
323
|
+
except typer.Exit as e:
|
|
324
|
+
if (e.exit_code or 0) == 0:
|
|
325
|
+
tracking.track_event("generate:success", gen_props)
|
|
326
|
+
else:
|
|
327
|
+
_track_error("api", e)
|
|
328
|
+
raise
|
|
329
|
+
return
|
|
224
330
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if do_async:
|
|
331
|
+
adapter = adapters.get(ep.id)
|
|
332
|
+
if adapter is not None and adapter.decode_sync is not None:
|
|
333
|
+
body = resp.json()
|
|
229
334
|
if as_json:
|
|
230
335
|
output.print_json(body)
|
|
336
|
+
tracking.track_event("generate:success", gen_props)
|
|
337
|
+
return
|
|
338
|
+
if not download:
|
|
339
|
+
rprint("[yellow]Image data returned inline. Pass --download <path> to save.[/yellow]")
|
|
340
|
+
tracking.track_event("generate:success", gen_props)
|
|
341
|
+
return
|
|
342
|
+
saved = adapter.decode_sync(body, download, request_id)
|
|
343
|
+
if saved:
|
|
344
|
+
output.print_saved(saved)
|
|
231
345
|
else:
|
|
232
|
-
rprint(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return
|
|
236
|
-
|
|
237
|
-
poller = poll.get_poller(ep.polling)
|
|
238
|
-
with _spinner() as prog:
|
|
239
|
-
task = prog.add_task(f"Generating with {name} (job {job_id})", total=None)
|
|
240
|
-
|
|
241
|
-
def _on_progress(p: float) -> None:
|
|
242
|
-
prog.update(task, description=f"Generating ({p * 100:.0f}%)")
|
|
243
|
-
|
|
244
|
-
result = poller(
|
|
245
|
-
body,
|
|
246
|
-
api_key=api_key,
|
|
247
|
-
timeout=timeout,
|
|
248
|
-
on_progress=_on_progress,
|
|
249
|
-
create_path=ep.path,
|
|
250
|
-
)
|
|
251
|
-
_emit_result(result, request_id=job_id, download=download, as_json=as_json)
|
|
252
|
-
return
|
|
253
|
-
|
|
254
|
-
adapter = adapters.get(ep.id)
|
|
255
|
-
if adapter is not None and adapter.decode_sync is not None:
|
|
256
|
-
body = resp.json()
|
|
257
|
-
if as_json:
|
|
258
|
-
output.print_json(body)
|
|
259
|
-
return
|
|
260
|
-
if not download:
|
|
261
|
-
rprint("[yellow]Image data returned inline. Pass --download <path> to save.[/yellow]")
|
|
346
|
+
rprint("[yellow]No image data found in response.[/yellow]")
|
|
347
|
+
output.print_json(body)
|
|
348
|
+
tracking.track_event("generate:success", gen_props)
|
|
262
349
|
return
|
|
263
|
-
saved = adapter.decode_sync(body, download, request_id)
|
|
264
|
-
if saved:
|
|
265
|
-
output.print_saved(saved)
|
|
266
|
-
else:
|
|
267
|
-
rprint("[yellow]No image data found in response.[/yellow]")
|
|
268
|
-
output.print_json(body)
|
|
269
|
-
return
|
|
270
350
|
|
|
271
|
-
|
|
272
|
-
|
|
351
|
+
try:
|
|
352
|
+
result = poll.sync_result_from_response(resp)
|
|
353
|
+
_emit_result(result, request_id=request_id, download=download, as_json=as_json)
|
|
354
|
+
tracking.track_event("generate:success", gen_props)
|
|
355
|
+
except typer.Exit as e:
|
|
356
|
+
if (e.exit_code or 0) == 0:
|
|
357
|
+
tracking.track_event("generate:success", gen_props)
|
|
358
|
+
else:
|
|
359
|
+
_track_error("api", e)
|
|
360
|
+
raise
|
|
361
|
+
except typer.Exit:
|
|
362
|
+
# Inline raise sites already emitted their lifecycle event.
|
|
363
|
+
raise
|
|
364
|
+
except Exception as e:
|
|
365
|
+
# Safety net so an unexpected exception still pairs generate:start with a terminal generate:error.
|
|
366
|
+
_track_error("unknown", e)
|
|
367
|
+
raise
|
|
273
368
|
|
|
274
369
|
|
|
275
370
|
def _arg_value(args: list[str], *names: str) -> str | None:
|
|
@@ -410,7 +505,7 @@ def _apply_upload_transforms(values: dict, flags: list[schema.FlagDef], endpoint
|
|
|
410
505
|
|
|
411
506
|
|
|
412
507
|
def _resume(extra_args: list[str]) -> None:
|
|
413
|
-
if len(extra_args) < 2:
|
|
508
|
+
if len(extra_args) < 2 or extra_args[0].startswith("-") or extra_args[1].startswith("-"):
|
|
414
509
|
rprint("[bold red]Usage: comfy generate resume <model> <job_id> [--download PATH] [--json][/bold red]")
|
|
415
510
|
raise typer.Exit(code=1)
|
|
416
511
|
model, job_id = extra_args[0], extra_args[1]
|