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.
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/PKG-INFO +38 -6
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/README.md +37 -5
- comfy_cli-1.10.0/comfy_cli/command/generate/adapters.py +267 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/app.py +22 -2
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/client.py +9 -2
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/output.py +19 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/poll.py +8 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/schema.py +6 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/spec.py +24 -1
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/PKG-INFO +38 -6
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/SOURCES.txt +1 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/pyproject.toml +1 -1
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/LICENSE +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/__init__.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/__main__.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/cmdline.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/__init__.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/code_search.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/custom_nodes/__init__.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/custom_nodes/bisect_custom_nodes.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/custom_nodes/cm_cli_util.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/custom_nodes/command.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/__init__.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/generate/upload.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/github/pr_info.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/install.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/launch.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/models/models.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/pr_command.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/command/run.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/config_manager.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/constants.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/cuda_detect.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/env_checker.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/file_utils.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/git_utils.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/logging.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/pr_cache.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/registry/__init__.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/registry/api.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/registry/config_parser.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/registry/types.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/resolve_python.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/standalone.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/tracking.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/typing.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/ui.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/update.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/utils.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/uv.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/workflow_to_api.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli/workspace_manager.py +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/dependency_links.txt +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/entry_points.txt +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/requires.txt +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/comfy_cli.egg-info/top_level.txt +0 -0
- {comfy_cli-1.9.0 → comfy_cli-1.10.0}/setup.cfg +0 -0
- {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.
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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=
|
|
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.
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|