comfy-cli 1.9.0__tar.gz → 1.10.0__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.
Files changed (58) hide show
  1. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/PKG-INFO +38 -6
  2. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/README.md +37 -5
  3. comfy_cli-1.10.0/comfy_cli/command/generate/adapters.py +267 -0
  4. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/app.py +22 -2
  5. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/client.py +9 -2
  6. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/output.py +19 -0
  7. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/poll.py +8 -0
  8. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/schema.py +6 -0
  9. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/spec.py +24 -1
  10. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/PKG-INFO +38 -6
  11. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/SOURCES.txt +1 -0
  12. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/pyproject.toml +1 -1
  13. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/LICENSE +0 -0
  14. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/__init__.py +0 -0
  15. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/__main__.py +0 -0
  16. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/cmdline.py +0 -0
  17. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/__init__.py +0 -0
  18. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/code_search.py +0 -0
  19. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/custom_nodes/__init__.py +0 -0
  20. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +0 -0
  21. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/custom_nodes/cm_cli_util.py +0 -0
  22. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/custom_nodes/command.py +0 -0
  23. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/__init__.py +0 -0
  24. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/upload.py +0 -0
  25. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/github/pr_info.py +0 -0
  26. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/install.py +0 -0
  27. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/launch.py +0 -0
  28. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/models/models.py +0 -0
  29. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/pr_command.py +0 -0
  30. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/run.py +0 -0
  31. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/config_manager.py +0 -0
  32. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/constants.py +0 -0
  33. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/cuda_detect.py +0 -0
  34. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/env_checker.py +0 -0
  35. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/file_utils.py +0 -0
  36. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/git_utils.py +0 -0
  37. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/logging.py +0 -0
  38. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/pr_cache.py +0 -0
  39. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/registry/__init__.py +0 -0
  40. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/registry/api.py +0 -0
  41. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/registry/config_parser.py +0 -0
  42. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/registry/types.py +0 -0
  43. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/resolve_python.py +0 -0
  44. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/standalone.py +0 -0
  45. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/tracking.py +0 -0
  46. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/typing.py +0 -0
  47. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/ui.py +0 -0
  48. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/update.py +0 -0
  49. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/utils.py +0 -0
  50. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/uv.py +0 -0
  51. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/workflow_to_api.py +0 -0
  52. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/workspace_manager.py +0 -0
  53. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/dependency_links.txt +0 -0
  54. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/entry_points.txt +0 -0
  55. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/requires.txt +0 -0
  56. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/top_level.txt +0 -0
  57. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/setup.cfg +0 -0
  58. {comfy_cli-1.9.0 → comfy_cli-1.10.0}/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.9.0
3
+ Version: 1.10.0
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
@@ -64,7 +64,7 @@ workflows, and call hosted partner image models, all from your terminal.
64
64
  ## Features
65
65
 
66
66
  - 🚀 One-command ComfyUI install and launch
67
- - 🎨 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
67
+ - 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft, Stability, Gemini/nano-banana, Kling, Luma, Runway, Pika, Vidu, Hailuo, Seedance, …) via `comfy generate`, no workflow JSON required
68
68
  - 🔧 Custom node management — install, update, snapshot, bisect
69
69
  - 📦 Fast dependency resolution with `uv` (`--fast-deps`, `--uv-compile`)
70
70
  - 🗄️ Model downloads from CivitAI, Hugging Face, and direct URLs
@@ -336,10 +336,11 @@ the bisect tool can help you pinpoint the custom node that causes the issue.
336
336
  `comfy generate` calls Comfy's partner nodes directly from the terminal — no
337
337
  local ComfyUI or workflow JSON required. It hits the same hosted partner nodes
338
338
  you'd otherwise wire into a ComfyUI workflow, but as one-shot CLI calls. Image
339
- models (Flux, Ideogram, DALL·E, Recraft, Stability, Runway, Reve, xAI Grok, …)
340
- and video models (Kling, Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo,
341
- Grok video) are all covered; video jobs run async and the CLI polls until the
342
- result is ready.
339
+ models (Flux, Ideogram, DALL·E, Recraft, Stability, Runway, Reve, xAI Grok,
340
+ Google Gemini Flash Image aka **nano-banana**, …) and video models (Kling,
341
+ Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo, Grok video, ByteDance
342
+ **Seedance**) are all covered; video jobs run async and the CLI polls until
343
+ the result is ready.
343
344
 
344
345
  Prerequisites — a Comfy API key and a credit balance:
345
346
 
@@ -382,6 +383,37 @@ comfy generate luma --prompt "..." --aspect_ratio 16:9 --async
382
383
  comfy generate resume luma <job_id> --download out.mp4
383
384
  ```
384
385
 
386
+ **Gemini Flash Image (nano-banana)** — text-to-image and image edits in one
387
+ alias. Pass `--image` (repeatable) for reference images. The response is
388
+ inline base64, so `--download` is required to save:
389
+
390
+ ```bash
391
+ comfy generate nano-banana --prompt "a watercolor of a sleeping fox" \
392
+ --download fox.png
393
+
394
+ # Image edit — reference accepted as a local path, http(s) URL, or data URI:
395
+ comfy generate nano-banana --prompt "add a top hat" \
396
+ --image ./cat.png --download out.png
397
+
398
+ # Switch model variants:
399
+ comfy generate nano-banana --prompt "..." --model gemini-3-pro-image-preview \
400
+ --download out.png
401
+ ```
402
+
403
+ **Seedance** — text-to-video and image-to-video, up to 1080p / 12s clips.
404
+ Resolution, ratio, duration, fps, etc. get passed through as flags; the CLI
405
+ inlines them into Seedance's prompt syntax for you:
406
+
407
+ ```bash
408
+ comfy generate seedance --prompt "a hummingbird hovering over a flower" \
409
+ --resolution 1080p --duration 5 --download bird.mp4
410
+
411
+ # Image-to-video: pick a lite/i2v variant and pass a first frame.
412
+ comfy generate seedance --model seedance-1-0-lite-i2v-250428 \
413
+ --prompt "the wave crests and crashes" \
414
+ --image ./still.jpg --download wave.mp4
415
+ ```
416
+
385
417
  ### Managing ComfyUI-Manager
386
418
 
387
419
  - Disable ComfyUI-Manager completely (no manager flags passed to ComfyUI):
@@ -19,7 +19,7 @@ workflows, and call hosted partner image models, all from your terminal.
19
19
  ## Features
20
20
 
21
21
  - 🚀 One-command ComfyUI install and launch
22
- - 🎨 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
22
+ - 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft, Stability, Gemini/nano-banana, Kling, Luma, Runway, Pika, Vidu, Hailuo, Seedance, …) via `comfy generate`, no workflow JSON required
23
23
  - 🔧 Custom node management — install, update, snapshot, bisect
24
24
  - 📦 Fast dependency resolution with `uv` (`--fast-deps`, `--uv-compile`)
25
25
  - 🗄️ Model downloads from CivitAI, Hugging Face, and direct URLs
@@ -291,10 +291,11 @@ the bisect tool can help you pinpoint the custom node that causes the issue.
291
291
  `comfy generate` calls Comfy's partner nodes directly from the terminal — no
292
292
  local ComfyUI or workflow JSON required. It hits the same hosted partner nodes
293
293
  you'd otherwise wire into a ComfyUI workflow, but as one-shot CLI calls. Image
294
- models (Flux, Ideogram, DALL·E, Recraft, Stability, Runway, Reve, xAI Grok, …)
295
- and video models (Kling, Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo,
296
- Grok video) are all covered; video jobs run async and the CLI polls until the
297
- result is ready.
294
+ models (Flux, Ideogram, DALL·E, Recraft, Stability, Runway, Reve, xAI Grok,
295
+ Google Gemini Flash Image aka **nano-banana**, …) and video models (Kling,
296
+ Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo, Grok video, ByteDance
297
+ **Seedance**) are all covered; video jobs run async and the CLI polls until
298
+ the result is ready.
298
299
 
299
300
  Prerequisites — a Comfy API key and a credit balance:
300
301
 
@@ -337,6 +338,37 @@ comfy generate luma --prompt "..." --aspect_ratio 16:9 --async
337
338
  comfy generate resume luma <job_id> --download out.mp4
338
339
  ```
339
340
 
341
+ **Gemini Flash Image (nano-banana)** — text-to-image and image edits in one
342
+ alias. Pass `--image` (repeatable) for reference images. The response is
343
+ inline base64, so `--download` is required to save:
344
+
345
+ ```bash
346
+ comfy generate nano-banana --prompt "a watercolor of a sleeping fox" \
347
+ --download fox.png
348
+
349
+ # Image edit — reference accepted as a local path, http(s) URL, or data URI:
350
+ comfy generate nano-banana --prompt "add a top hat" \
351
+ --image ./cat.png --download out.png
352
+
353
+ # Switch model variants:
354
+ comfy generate nano-banana --prompt "..." --model gemini-3-pro-image-preview \
355
+ --download out.png
356
+ ```
357
+
358
+ **Seedance** — text-to-video and image-to-video, up to 1080p / 12s clips.
359
+ Resolution, ratio, duration, fps, etc. get passed through as flags; the CLI
360
+ inlines them into Seedance's prompt syntax for you:
361
+
362
+ ```bash
363
+ comfy generate seedance --prompt "a hummingbird hovering over a flower" \
364
+ --resolution 1080p --duration 5 --download bird.mp4
365
+
366
+ # Image-to-video: pick a lite/i2v variant and pass a first frame.
367
+ comfy generate seedance --model seedance-1-0-lite-i2v-250428 \
368
+ --prompt "the wave crests and crashes" \
369
+ --image ./still.jpg --download wave.mp4
370
+ ```
371
+
340
372
  ### Managing ComfyUI-Manager
341
373
 
342
374
  - Disable ComfyUI-Manager completely (no manager flags passed to ComfyUI):
@@ -0,0 +1,267 @@
1
+ """Per-endpoint adapters for partners whose request/response shapes don't fit
2
+ the generic schema-driven flag→JSON mold.
3
+
4
+ Two endpoints today:
5
+
6
+ - **Gemini Flash Image (nano-banana)** — Vertex AI's ``contents``/``parts``
7
+ body, inline base64 image input, and inline base64 image output. The model
8
+ variant lives in the URL path, not the body.
9
+ - **Seedance** (ByteDance) — assembles a ``content`` array of typed parts
10
+ (``text`` + optional ``image_url``) and inlines its own knobs (resolution,
11
+ duration, …) into the prompt string.
12
+
13
+ An adapter contributes three optional pieces:
14
+
15
+ - ``flags`` — replaces the schema-derived flag list for the model
16
+ - ``build_body`` — produces the JSON body from parsed flag values
17
+ - ``decode_sync`` — handles a sync response that ships inline blobs (Gemini)
18
+ - ``path_param`` — name of a flag whose value gets substituted into the URL
19
+ path's ``{placeholder}`` (e.g. ``model`` for Gemini's templated path)
20
+ """
21
+
22
+ from __future__ import annotations
23
+
24
+ import base64
25
+ import mimetypes
26
+ from collections.abc import Callable
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+ from typing import Any
30
+
31
+ import httpx
32
+
33
+ from comfy_cli.command.generate.client import ApiError
34
+ from comfy_cli.command.generate.schema import FlagDef
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Adapter:
39
+ flags: list[FlagDef]
40
+ build_body: Callable[[dict, str], dict]
41
+ decode_sync: Callable[[dict, str, str], list[Path]] | None = None
42
+ path_param: str | None = None
43
+
44
+
45
+ # ── Gemini / nano-banana ──────────────────────────────────────────────────
46
+
47
+ GEMINI_IMAGE_MODELS = (
48
+ "gemini-2.5-flash-image",
49
+ "gemini-2.5-flash-image-preview",
50
+ "gemini-3-pro-image-preview",
51
+ )
52
+
53
+
54
+ def _inline_image(value: str) -> tuple[str, str]:
55
+ """Return ``(mime_type, base64_str)`` for a local path, http(s) URL, or
56
+ ``data:`` URI. Gemini accepts inline-only — there's no signed-URL path
57
+ here, so we pull bytes locally rather than going through ``upload.py``."""
58
+ if value.startswith("data:"):
59
+ head, _, b64 = value.partition(",")
60
+ mime = head.split(";", 1)[0].removeprefix("data:") or "image/png"
61
+ return mime, b64
62
+ if value.startswith(("http://", "https://")):
63
+ with httpx.Client(timeout=60.0, follow_redirects=True) as c:
64
+ r = c.get(value)
65
+ r.raise_for_status()
66
+ mime = (r.headers.get("content-type") or "image/png").split(";", 1)[0].strip()
67
+ return mime, base64.b64encode(r.content).decode("ascii")
68
+ path = Path(value).expanduser()
69
+ if not path.is_file():
70
+ raise ApiError(0, "", f"Image not found: {path}")
71
+ mime, _ = mimetypes.guess_type(path.name)
72
+ return mime or "image/png", base64.b64encode(path.read_bytes()).decode("ascii")
73
+
74
+
75
+ def _gemini_build_body(values: dict, api_key: str) -> dict[str, Any]:
76
+ parts: list[dict[str, Any]] = [{"text": str(values["prompt"])}]
77
+ images = values.get("image") or []
78
+ if isinstance(images, str):
79
+ images = [images]
80
+ for img in images:
81
+ mime, b64 = _inline_image(str(img))
82
+ parts.append({"inlineData": {"mimeType": mime, "data": b64}})
83
+ return {
84
+ "contents": [{"role": "user", "parts": parts}],
85
+ "generationConfig": {"responseModalities": ["IMAGE"]},
86
+ }
87
+
88
+
89
+ def _gemini_decode_sync(body: dict, download: str, request_id: str) -> list[Path]:
90
+ """Walk candidates[*].content.parts[*].inlineData; save each blob."""
91
+ from comfy_cli.command.generate import output
92
+
93
+ blobs: list[tuple[str, bytes]] = []
94
+ for cand in body.get("candidates") or []:
95
+ content = cand.get("content") or {}
96
+ for part in content.get("parts") or []:
97
+ inline = part.get("inlineData") or part.get("inline_data")
98
+ if not inline:
99
+ continue
100
+ data_b64 = inline.get("data") or ""
101
+ mime = inline.get("mimeType") or inline.get("mime_type") or "image/png"
102
+ try:
103
+ raw = base64.b64decode(data_b64, validate=False)
104
+ except (ValueError, TypeError):
105
+ continue
106
+ blobs.append((mime, raw))
107
+ if not blobs:
108
+ return []
109
+ return output.save_inline_blobs(blobs, download, request_id)
110
+
111
+
112
+ _gemini_adapter = Adapter(
113
+ flags=[
114
+ FlagDef(
115
+ name="prompt",
116
+ kind="string",
117
+ required=True,
118
+ description="Text instruction. For edits, describe the change.",
119
+ ),
120
+ FlagDef(
121
+ name="image",
122
+ kind="array",
123
+ item_kind="string",
124
+ required=False,
125
+ description="Optional reference image(s): local path, http(s) URL, or data URI.",
126
+ ),
127
+ FlagDef(
128
+ name="model",
129
+ kind="enum",
130
+ required=False,
131
+ default="gemini-2.5-flash-image",
132
+ description="Gemini image-model variant.",
133
+ enum=list(GEMINI_IMAGE_MODELS),
134
+ ),
135
+ ],
136
+ build_body=_gemini_build_body,
137
+ decode_sync=_gemini_decode_sync,
138
+ path_param="model",
139
+ )
140
+
141
+
142
+ # ── Seedance ──────────────────────────────────────────────────────────────
143
+
144
+ SEEDANCE_MODELS = (
145
+ "seedance-1-0-pro-250528",
146
+ "seedance-1-0-pro-fast-251015",
147
+ "seedance-1-5-pro-251215",
148
+ "seedance-1-0-lite-t2v-250428",
149
+ "seedance-1-0-lite-i2v-250428",
150
+ )
151
+
152
+ _SEEDANCE_INLINE_KEYS = ("resolution", "ratio", "duration", "fps", "seed", "camerafixed", "watermark")
153
+
154
+
155
+ def _seedance_text(values: dict) -> str:
156
+ """Compose the ``text`` field, appending Seedance's inline ``--rs/--rt/…``
157
+ style overrides for any flags the user set."""
158
+ prompt = str(values["prompt"])
159
+ extras: list[str] = []
160
+ for key in _SEEDANCE_INLINE_KEYS:
161
+ v = values.get(key)
162
+ if v is None or v == "":
163
+ continue
164
+ if isinstance(v, bool):
165
+ v = "true" if v else "false"
166
+ extras.append(f"--{key} {v}")
167
+ return f"{prompt} {' '.join(extras)}".strip()
168
+
169
+
170
+ def _seedance_image_url(value: str, api_key: str) -> str:
171
+ """Local paths get uploaded; data: and http(s) pass through verbatim."""
172
+ if value.startswith(("http://", "https://", "data:")):
173
+ return value
174
+ from comfy_cli.command.generate import upload
175
+
176
+ return upload.upload_path(Path(value).expanduser(), api_key).url
177
+
178
+
179
+ def _seedance_build_body(values: dict, api_key: str) -> dict[str, Any]:
180
+ content: list[dict[str, Any]] = [{"type": "text", "text": _seedance_text(values)}]
181
+ image = values.get("image")
182
+ if image:
183
+ content.append({"type": "image_url", "image_url": {"url": _seedance_image_url(str(image), api_key)}})
184
+ body: dict[str, Any] = {
185
+ "model": values.get("model") or SEEDANCE_MODELS[0],
186
+ "content": content,
187
+ }
188
+ if "generate_audio" in values:
189
+ body["generate_audio"] = bool(values["generate_audio"])
190
+ if "return_last_frame" in values:
191
+ body["return_last_frame"] = bool(values["return_last_frame"])
192
+ return body
193
+
194
+
195
+ _seedance_adapter = Adapter(
196
+ flags=[
197
+ FlagDef(name="prompt", kind="string", required=True, description="Text prompt for the video."),
198
+ FlagDef(
199
+ name="image",
200
+ kind="string",
201
+ required=False,
202
+ description="Optional first-frame image (URL, local path, or data URI). "
203
+ "Local paths are auto-uploaded via /customers/storage.",
204
+ ),
205
+ FlagDef(
206
+ name="model",
207
+ kind="enum",
208
+ required=False,
209
+ default="seedance-1-0-pro-250528",
210
+ description="Seedance model variant.",
211
+ enum=list(SEEDANCE_MODELS),
212
+ ),
213
+ FlagDef(name="resolution", kind="enum", required=False, enum=["480p", "720p", "1080p"]),
214
+ FlagDef(
215
+ name="ratio",
216
+ kind="enum",
217
+ required=False,
218
+ enum=["21:9", "16:9", "4:3", "1:1", "3:4", "9:16", "9:21", "adaptive"],
219
+ ),
220
+ FlagDef(name="duration", kind="integer", required=False, description="Length in seconds (3–12)."),
221
+ FlagDef(name="fps", kind="integer", required=False, description="Frames per second (default 24)."),
222
+ FlagDef(name="seed", kind="integer", required=False, description="RNG seed (-1 to 2^32-1)."),
223
+ FlagDef(name="camerafixed", kind="boolean", required=False, description="Lock camera position."),
224
+ FlagDef(name="watermark", kind="boolean", required=False, description="Include a watermark."),
225
+ FlagDef(
226
+ name="generate_audio",
227
+ kind="boolean",
228
+ required=False,
229
+ description="Synthesize matching audio (Seedance 1.5 pro only).",
230
+ ),
231
+ FlagDef(
232
+ name="return_last_frame",
233
+ kind="boolean",
234
+ required=False,
235
+ description="Return the last-frame image alongside the video.",
236
+ ),
237
+ ],
238
+ build_body=_seedance_build_body,
239
+ decode_sync=None,
240
+ path_param=None,
241
+ )
242
+
243
+
244
+ _ADAPTERS: dict[str, Adapter] = {
245
+ "vertexai/gemini/{model}": _gemini_adapter,
246
+ "byteplus/api/v3/contents/generations/tasks": _seedance_adapter,
247
+ }
248
+
249
+
250
+ def get(endpoint_id: str) -> Adapter | None:
251
+ return _ADAPTERS.get(endpoint_id)
252
+
253
+
254
+ def resolve_path(template: str, values: dict, adapter: Adapter) -> str:
255
+ """Substitute ``adapter.path_param`` into the URL template, falling back to
256
+ the flag's ``default`` when the user didn't pass it."""
257
+ if not adapter.path_param:
258
+ return template
259
+ val = values.get(adapter.path_param)
260
+ if not val:
261
+ for f in adapter.flags:
262
+ if f.name == adapter.path_param:
263
+ val = f.default
264
+ break
265
+ if not val:
266
+ raise ApiError(0, "", f"Missing --{adapter.path_param}: required to fill in the URL path.")
267
+ return template.replace("{" + adapter.path_param + "}", str(val))
@@ -25,7 +25,7 @@ from rich import print as rprint
25
25
  from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
26
26
 
27
27
  from comfy_cli import tracking, ui
28
- from comfy_cli.command.generate import client, output, poll, schema, spec, upload
28
+ from comfy_cli.command.generate import adapters, client, output, poll, schema, spec, upload
29
29
 
30
30
  _HELP = "Generate images via ComfyUI partner nodes (Flux, Ideogram, DALL·E, Recraft, Stability, …)."
31
31
 
@@ -251,6 +251,23 @@ def _generate(model: str, extra_args: list[str]) -> None:
251
251
  _emit_result(result, request_id=job_id, download=download, as_json=as_json)
252
252
  return
253
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]")
262
+ 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
+
254
271
  result = poll.sync_result_from_response(resp)
255
272
  _emit_result(result, request_id=request_id, download=download, as_json=as_json)
256
273
 
@@ -360,8 +377,11 @@ def _apply_upload_transforms(values: dict, flags: list[schema.FlagDef], endpoint
360
377
  base64 blob or a URL, transform it transparently.
361
378
 
362
379
  This only applies to JSON endpoints — multipart endpoints already stream
363
- file paths natively via httpx and don't need pre-uploading.
380
+ file paths natively via httpx and don't need pre-uploading. Endpoints with
381
+ a custom adapter handle their own asset shaping inside ``build_body``.
364
382
  """
383
+ if adapters.get(endpoint.id) is not None:
384
+ return
365
385
  if endpoint.request_content_type != "application/json":
366
386
  return
367
387
  flag_by_name = {f.name: f for f in flags}
@@ -103,8 +103,15 @@ def send_request(
103
103
  timeout: float = 120.0,
104
104
  ) -> httpx.Response:
105
105
  """Send the initial request for `endpoint` with the given typed values."""
106
- url = spec.base_url() + endpoint.path
107
- json_body, files, data = _split_payload(values, flags, endpoint.request_content_type)
106
+ from comfy_cli.command.generate import adapters as _adapters
107
+
108
+ adapter = _adapters.get(endpoint.id)
109
+ url_path = _adapters.resolve_path(endpoint.path, values, adapter) if adapter else endpoint.path
110
+ url = spec.base_url() + url_path
111
+ if adapter is not None:
112
+ json_body, files, data = adapter.build_body(values, api_key), None, None
113
+ else:
114
+ json_body, files, data = _split_payload(values, flags, endpoint.request_content_type)
108
115
  headers = _auth_headers(api_key)
109
116
  try:
110
117
  if endpoint.method.lower() == "get":
@@ -70,6 +70,25 @@ def save_urls(urls: list[str], template: str, request_id: str) -> list[Path]:
70
70
  return saved
71
71
 
72
72
 
73
+ def save_inline_blobs(blobs: list[tuple[str, bytes]], template: str, request_id: str) -> list[Path]:
74
+ """Save ``(mime, bytes)`` pairs returned inline (e.g. Gemini's
75
+ ``inlineData``) under the resolved template path. Same auto-indexing rule
76
+ as ``save_urls``: if the template has no ``{index}`` placeholder and isn't
77
+ a directory shorthand, multi-blob responses get ``_<i>`` inserted before
78
+ the suffix so the first blob doesn't get silently overwritten."""
79
+ saved: list[Path] = []
80
+ auto_index = len(blobs) > 1 and "{index}" not in template and not template.endswith(("/", "\\"))
81
+ for i, (mime, data) in enumerate(blobs):
82
+ ext = _EXT_FROM_MIME.get(mime) or (mimetypes.guess_extension(mime) or ".png").lstrip(".") or "png"
83
+ dest = _resolve_template(template, request_id, i, ext)
84
+ if auto_index:
85
+ dest = dest.with_name(f"{dest.stem}_{i}.{ext}")
86
+ dest.parent.mkdir(parents=True, exist_ok=True)
87
+ dest.write_bytes(data)
88
+ saved.append(dest)
89
+ return saved
90
+
91
+
73
92
  def save_binary_response(resp: httpx.Response, template: str, request_id: str) -> Path:
74
93
  """Save a single binary response body (e.g. Stability returns image/* bytes)."""
75
94
  ext = _ext_from_response(resp)
@@ -235,6 +235,14 @@ _POLL_SPECS: dict[str, PollSpec] = {
235
235
  success_values=("done",),
236
236
  failure_values=(),
237
237
  ),
238
+ "seedance": PollSpec(
239
+ name="seedance",
240
+ id_paths=("id",),
241
+ poll_url="/proxy/byteplus/api/v3/contents/generations/tasks/{id}",
242
+ status_paths=("status",),
243
+ success_values=("succeeded",),
244
+ failure_values=("failed", "cancelled"),
245
+ ),
238
246
  }
239
247
 
240
248
 
@@ -87,6 +87,12 @@ def _detect_upload_mode(name: str, prop: dict[str, Any]) -> str | None:
87
87
 
88
88
 
89
89
  def flags_for(endpoint: Endpoint) -> list[FlagDef]:
90
+ # Lazy import: adapters depends on FlagDef, so the top-level import would cycle.
91
+ from comfy_cli.command.generate import adapters as _adapters
92
+
93
+ adapter = _adapters.get(endpoint.id)
94
+ if adapter is not None:
95
+ return list(adapter.flags)
90
96
  schema = endpoint.request_schema or {}
91
97
  props = schema.get("properties") or {}
92
98
  required = set(schema.get("required") or [])
@@ -136,6 +136,24 @@ _ALIASES: dict[str, str] = {
136
136
  "vidu-extend": "vidu/extend",
137
137
  # Video — xAI Grok
138
138
  "grok-video": "xai/v1/videos/generations",
139
+ # Google Gemini Flash Image (nano-banana). The model variant lives in the
140
+ # URL path; the adapter substitutes ``--model`` at send time.
141
+ "nano-banana": "vertexai/gemini/{model}",
142
+ # ByteDance Seedance (video).
143
+ "seedance": "byteplus/api/v3/contents/generations/tasks",
144
+ }
145
+
146
+
147
+ # Used in the `list` table for endpoints whose openapi summary is empty or too
148
+ # generic to convey what the model is.
149
+ _SUMMARY_OVERRIDES: dict[str, str] = {
150
+ "vertexai/gemini/{model}": (
151
+ "Google Gemini Flash Image (nano-banana) — text-to-image and image edits "
152
+ "from a prompt plus optional reference images."
153
+ ),
154
+ "byteplus/api/v3/contents/generations/tasks": (
155
+ "ByteDance Seedance — text-to-video and image-to-video (3–12s clips, up to 1080p)."
156
+ ),
139
157
  }
140
158
 
141
159
  _PREFERRED_ALIAS: dict[str, str] = {v: k for k, v in _ALIASES.items()}
@@ -231,6 +249,10 @@ _ENDPOINT_ALLOWLIST: list[tuple[str, str, str | None]] = [
231
249
  ("vidu/extend", "video-extend", "vidu"),
232
250
  # Video — xAI Grok
233
251
  ("xai/v1/videos/generations", "text-to-video", "xai_video"),
252
+ # Google Gemini Flash Image (nano-banana). Sync; adapter decodes inline data.
253
+ ("vertexai/gemini/{model}", "image-edit", None),
254
+ # ByteDance Seedance (video) — async, custom poller.
255
+ ("byteplus/api/v3/contents/generations/tasks", "text-to-video", "seedance"),
234
256
  ]
235
257
 
236
258
 
@@ -341,7 +363,8 @@ def _registry() -> dict[str, Endpoint]:
341
363
  path=path,
342
364
  method=method,
343
365
  partner=partner,
344
- summary=str(op.get("summary") or op.get("description") or "").strip(),
366
+ summary=_SUMMARY_OVERRIDES.get(endpoint_id)
367
+ or str(op.get("summary") or op.get("description") or "").strip(),
345
368
  category=category,
346
369
  request_schema=req_schema if isinstance(req_schema, dict) else {},
347
370
  request_content_type=ctype,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: comfy-cli
3
- Version: 1.9.0
3
+ Version: 1.10.0
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
@@ -64,7 +64,7 @@ workflows, and call hosted partner image models, all from your terminal.
64
64
  ## Features
65
65
 
66
66
  - 🚀 One-command ComfyUI install and launch
67
- - 🎨 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
67
+ - 🎨 Direct calls to partner image and video nodes (Flux, Ideogram, DALL·E, Recraft, Stability, Gemini/nano-banana, Kling, Luma, Runway, Pika, Vidu, Hailuo, Seedance, …) via `comfy generate`, no workflow JSON required
68
68
  - 🔧 Custom node management — install, update, snapshot, bisect
69
69
  - 📦 Fast dependency resolution with `uv` (`--fast-deps`, `--uv-compile`)
70
70
  - 🗄️ Model downloads from CivitAI, Hugging Face, and direct URLs
@@ -336,10 +336,11 @@ the bisect tool can help you pinpoint the custom node that causes the issue.
336
336
  `comfy generate` calls Comfy's partner nodes directly from the terminal — no
337
337
  local ComfyUI or workflow JSON required. It hits the same hosted partner nodes
338
338
  you'd otherwise wire into a ComfyUI workflow, but as one-shot CLI calls. Image
339
- models (Flux, Ideogram, DALL·E, Recraft, Stability, Runway, Reve, xAI Grok, …)
340
- and video models (Kling, Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo,
341
- Grok video) are all covered; video jobs run async and the CLI polls until the
342
- result is ready.
339
+ models (Flux, Ideogram, DALL·E, Recraft, Stability, Runway, Reve, xAI Grok,
340
+ Google Gemini Flash Image aka **nano-banana**, …) and video models (Kling,
341
+ Luma, Runway Gen-3, Pika, Vidu, Moonvalley, Hailuo, Grok video, ByteDance
342
+ **Seedance**) are all covered; video jobs run async and the CLI polls until
343
+ the result is ready.
343
344
 
344
345
  Prerequisites — a Comfy API key and a credit balance:
345
346
 
@@ -382,6 +383,37 @@ comfy generate luma --prompt "..." --aspect_ratio 16:9 --async
382
383
  comfy generate resume luma <job_id> --download out.mp4
383
384
  ```
384
385
 
386
+ **Gemini Flash Image (nano-banana)** — text-to-image and image edits in one
387
+ alias. Pass `--image` (repeatable) for reference images. The response is
388
+ inline base64, so `--download` is required to save:
389
+
390
+ ```bash
391
+ comfy generate nano-banana --prompt "a watercolor of a sleeping fox" \
392
+ --download fox.png
393
+
394
+ # Image edit — reference accepted as a local path, http(s) URL, or data URI:
395
+ comfy generate nano-banana --prompt "add a top hat" \
396
+ --image ./cat.png --download out.png
397
+
398
+ # Switch model variants:
399
+ comfy generate nano-banana --prompt "..." --model gemini-3-pro-image-preview \
400
+ --download out.png
401
+ ```
402
+
403
+ **Seedance** — text-to-video and image-to-video, up to 1080p / 12s clips.
404
+ Resolution, ratio, duration, fps, etc. get passed through as flags; the CLI
405
+ inlines them into Seedance's prompt syntax for you:
406
+
407
+ ```bash
408
+ comfy generate seedance --prompt "a hummingbird hovering over a flower" \
409
+ --resolution 1080p --duration 5 --download bird.mp4
410
+
411
+ # Image-to-video: pick a lite/i2v variant and pass a first frame.
412
+ comfy generate seedance --model seedance-1-0-lite-i2v-250428 \
413
+ --prompt "the wave crests and crashes" \
414
+ --image ./still.jpg --download wave.mp4
415
+ ```
416
+
385
417
  ### Managing ComfyUI-Manager
386
418
 
387
419
  - Disable ComfyUI-Manager completely (no manager flags passed to ComfyUI):
@@ -39,6 +39,7 @@ comfy_cli/command/custom_nodes/bisect_custom_nodes.py
39
39
  comfy_cli/command/custom_nodes/cm_cli_util.py
40
40
  comfy_cli/command/custom_nodes/command.py
41
41
  comfy_cli/command/generate/__init__.py
42
+ comfy_cli/command/generate/adapters.py
42
43
  comfy_cli/command/generate/app.py
43
44
  comfy_cli/command/generate/client.py
44
45
  comfy_cli/command/generate/output.py
@@ -5,7 +5,7 @@ requires = [ "setuptools>=61" ]
5
5
 
6
6
  [project]
7
7
  name = "comfy-cli"
8
- version = "1.9.0" # Will be filled in by the CI/CD pipeline. Check publish_package.py.
8
+ version = "1.10.0" # Will be filled in by the CI/CD pipeline. Check publish_package.py.
9
9
  description = "A CLI tool for installing and using ComfyUI."
10
10
  readme = "README.md"
11
11
  keywords = [ "comfyui", "stable diffusion" ]
File without changes
File without changes
File without changes
File without changes
File without changes