Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ workflows, and call hosted partner image models, all from your terminal.
## Features

- 🚀 One-command ComfyUI install and launch
- 🎨 Direct calls to partner image nodes (Flux, Ideogram, DALL·E, Recraft, Stability, …) via `comfy generate`, no workflow JSON required
- 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft, Stability, Kling, Luma, Runway, Pika, Vidu, Hailuo, …) via `comfy generate`, no workflow JSON required
- 🔧 Custom node management — install, update, snapshot, bisect
- 📦 Fast dependency resolution with `uv` (`--fast-deps`, `--uv-compile`)
- 🗄️ Model downloads from CivitAI, Hugging Face, and direct URLs
Expand Down Expand Up @@ -290,8 +290,11 @@ the bisect tool can help you pinpoint the custom node that causes the issue.

`comfy generate` calls Comfy's partner nodes directly from the terminal — no
local ComfyUI or workflow JSON required. It hits the same hosted partner nodes
(Flux, Ideogram, DALL·E, Recraft, Stability, Runway, Reve, xAI Grok, …) you'd
otherwise wire into a ComfyUI workflow, but as one-shot CLI calls.
you'd otherwise wire into a ComfyUI workflow, but as one-shot CLI calls. Image
models (Flux, Ideogram, DALL·E, Recraft, Stability, Runway, Reve, xAI Grok, …)
and video models (Kling, Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo,
Grok video) are all covered; video jobs run async and the CLI polls until the
result is ready.

Prerequisites — a Comfy API key and a credit balance:

Expand Down Expand Up @@ -321,8 +324,18 @@ comfy generate flux-kontext --prompt "add a top hat" \
comfy generate upload ./photo.jpg # explicit upload
```

Async models block until ready by default. Pass `--async` to return immediately
with a job id, then resume later with `comfy generate resume <model> <job_id>`.
Async models (every video model plus the Flux family) block until ready by
default. Pass `--async` to return immediately with a job id, then resume later
with `comfy generate resume <model> <job_id>`. Examples:

```bash
comfy generate kling --prompt "a paper boat drifting on a river at dusk" \
--duration 5 --download boat.mp4

comfy generate luma --prompt "..." --aspect_ratio 16:9 --async
# → prints job id; resume with:
comfy generate resume luma <job_id> --download out.mp4
```

### Managing ComfyUI-Manager

Expand Down
26 changes: 19 additions & 7 deletions comfy_cli/command/generate/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ def _generate(model: str, extra_args: list[str]) -> None:
raise typer.Exit(code=1)

if ep.polling:
job_id = str(body.get("id") or (body.get("data") or {}).get("task_id") or request_id)
job_id = poll.extract_job_id(ep.polling, body) or request_id
name = spec.preferred_alias(ep.id) or ep.id
if do_async:
if as_json:
Expand All @@ -241,7 +241,13 @@ def _generate(model: str, extra_args: list[str]) -> None:
def _on_progress(p: float) -> None:
prog.update(task, description=f"Generating ({p * 100:.0f}%)")

result = poller(body, api_key=api_key, timeout=timeout, on_progress=_on_progress)
result = poller(
body,
api_key=api_key,
timeout=timeout,
on_progress=_on_progress,
create_path=ep.path,
)
_emit_result(result, request_id=job_id, download=download, as_json=as_json)
return

Expand Down Expand Up @@ -411,10 +417,10 @@ def _resume(extra_args: list[str]) -> None:
download = meta.get("download") if isinstance(meta.get("download"), str) else None
as_json = bool(meta.get("json", False))

if ep.polling == "bfl":
initial = {"polling_url": f"{spec.base_url()}/proxy/bfl/get_result?id={job_id}"}
else:
rprint(f"[bold red]Resume not implemented for partner {ep.partner}[/bold red]")
try:
initial = poll.build_synthetic_initial(ep.polling, job_id, base_url=spec.base_url())
except client.ApiError as e:
rprint(f"[bold red]{e}[/bold red]")
raise typer.Exit(code=1)

poller = poll.get_poller(ep.polling)
Expand All @@ -424,7 +430,13 @@ def _resume(extra_args: list[str]) -> None:
def _on_progress(p: float) -> None:
prog.update(task, description=f"Job {job_id} ({p * 100:.0f}%)")

result = poller(initial, api_key=api_key, timeout=timeout, on_progress=_on_progress)
result = poller(
initial,
api_key=api_key,
timeout=timeout,
on_progress=_on_progress,
create_path=ep.path,
)
_emit_result(result, request_id=job_id, download=download, as_json=as_json)


Expand Down
12 changes: 11 additions & 1 deletion comfy_cli/command/generate/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,21 @@ def _resolve_template(template: str, request_id: str, index: int, ext: str) -> P


def save_urls(urls: list[str], template: str, request_id: str) -> list[Path]:
"""Download each URL and save under the resolved template path. Returns saved paths."""
"""Download each URL and save under the resolved template path. Returns saved paths.

Multi-URL responses (a video + thumbnail from Luma, for example) need a
per-output filename. If the template has no ``{index}`` placeholder and
isn't a directory shorthand, we auto-insert ``_<i>`` before the suffix
and switch the extension to whatever the URL actually points at — so a
user who typed ``--download out.mp4`` doesn't silently get a thumbnail
JPEG written into ``out.mp4`` because the model returned two URLs."""
saved: list[Path] = []
auto_index = len(urls) > 1 and "{index}" not in template and not template.endswith(("/", "\\"))
for i, url in enumerate(urls):
ext = _ext_from_url(url)
dest = _resolve_template(template, request_id, i, ext)
if auto_index:
dest = dest.with_name(f"{dest.stem}_{i}.{ext}")
dest.parent.mkdir(parents=True, exist_ok=True)
data = client.download_bytes(url)
dest.write_bytes(data)
Expand Down
Loading