lcode-cli 0.40.0__tar.gz → 0.41.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.
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/CHANGELOG.md +21 -1
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/PKG-INFO +1 -1
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/configuration.md +1 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/modes.md +63 -10
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/quickstart.md +1 -1
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/usage.md +1 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/agent.py +4 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/api.py +1 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/cli.py +64 -7
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/comfy.py +13 -19
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/config.py +7 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/media.py +46 -15
- lcode_cli-0.41.0/src/lcode/media.toml +74 -0
- lcode_cli-0.41.0/src/lcode/media_catalog.py +331 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/modes.py +1 -1
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/repl.py +43 -9
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/workflows/flux2-klein-4b-t2i.json +2 -9
- lcode_cli-0.41.0/src/lcode/workflows/z-image-turbo-t2i.json +31 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/comfy_standin.py +12 -2
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_comfy.py +4 -5
- lcode_cli-0.41.0/tests/test_media_catalog.py +312 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_modes.py +2 -1
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.editorconfig +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/dependabot.yml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/workflows/ci.yml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.gitignore +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/AGENTS.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/LICENSE +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/README.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/SECURITY.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/action.yml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/agents.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/assets/extra.css +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/automation.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/bench.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/changelog.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/commands.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/contributing.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/editors.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/git.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/hooks.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/how-it-works.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/index.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/installation.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/lsp.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/mcp.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/memory.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/models.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/sandbox.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/search.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/servers.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/examples/README.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/examples/api/fix_tests.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/examples/commands/review.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/examples/skills/commit-message/SKILL.md +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/install.sh +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/mkdocs.yml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/overrides/main.html +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/pyproject.toml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/scripts/make_logos.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/scripts/record_demo.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/scripts/social-preview.html +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/acp.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/action.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/backends.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/bench.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/bench_backend.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/bench_runtime.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/checks.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/clipboard.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/cloud.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/codesearch.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/context.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/escalate.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/extensions.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/frontmatter.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/gitflow.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/hooks.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/jobs.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/limits.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/lsp.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/matching.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/catalog.toml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/config.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/manager.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/protocol.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/server.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/memory.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/models.toml +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/notify.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/ollama.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/permissions.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/planning.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/render.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/repeats.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/repomap.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/residency.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/review.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/sandbox.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/screenshots.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/secretscan.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/servers/__init__.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/servers/encord.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/servers/valohai.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/servers/xai.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/sessions.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/steer.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/subagents.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/tools.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/unreachable.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/update.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/vision.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/web.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/src/lcode/workflows/__init__.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/conftest.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/fake_lsp_server.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_acp.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_action.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_agent.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_api.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_backends.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_bench.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_catalog.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_checks.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_cli.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_clipboard.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_cloud.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_codesearch.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_config.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_context.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_escalate.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_extensions.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_gitflow.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_hooks.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_jobs.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_look.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_lsp.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_matching.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_mcp.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_memory.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_model_picker.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_permissions.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_plan_mode.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_repeats.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_repomap.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_residency.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_review.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_robustness.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_sandbox.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_screenshots.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_secretscan.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_servers.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_sessions.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_steer.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_stuck.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_subagents.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_tools.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_update.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_vision.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_web.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/tests/test_xai.py +0 -0
- {lcode_cli-0.40.0 → lcode_cli-0.41.0}/uv.lock +0 -0
|
@@ -6,6 +6,25 @@ All notable changes to lcode are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.41.0] - 2026-10-23
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Image models to choose from, installed by lcode** (#150). lcode's media catalog lists the image
|
|
14
|
+
models it can run, with their size, license, measured speed and whether they fit your GPU.
|
|
15
|
+
**Z-Image Turbo** (photorealistic) joins FLUX.2 klein 4B (fast): 17 s and 3.5 s per 1024×1024
|
|
16
|
+
image on a 12 GB laptop GPU. See
|
|
17
|
+
[Image models](https://nasser1941.github.io/lcode/modes/#image-models).
|
|
18
|
+
- **Choose one:** `/models` in image mode (numbered) or `/model z-image-turbo` switches; the
|
|
19
|
+
`image_model` setting is the default, and sessions keep theirs.
|
|
20
|
+
- **Installed on demand:** a model that isn't installed downloads with your next image, after you
|
|
21
|
+
say yes, or ahead with `lcode media install <model>`. lcode downloads only the missing files
|
|
22
|
+
(models share text encoders and VAEs) into ComfyUI's model folders, checks the disk first,
|
|
23
|
+
verifies each file's SHA-256, and resumes interrupted downloads. `HF_TOKEN` works for gated
|
|
24
|
+
models.
|
|
25
|
+
- **`lcode media`** lists image and video models with what fits and what's installed, and
|
|
26
|
+
`lcode doctor` says how to install the image model when it's missing.
|
|
27
|
+
|
|
9
28
|
## [0.40.0] - 2026-10-23
|
|
10
29
|
|
|
11
30
|
### Added
|
|
@@ -904,7 +923,8 @@ First public release.
|
|
|
904
923
|
- `AGENTS.md` project instructions and `/init` to generate them.
|
|
905
924
|
- One-line installer for Linux and macOS.
|
|
906
925
|
|
|
907
|
-
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.
|
|
926
|
+
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.41.0...HEAD
|
|
927
|
+
[0.41.0]: https://github.com/nasser1941/lcode/compare/v0.40.0...v0.41.0
|
|
908
928
|
[0.40.0]: https://github.com/nasser1941/lcode/compare/v0.39.0...v0.40.0
|
|
909
929
|
[0.39.0]: https://github.com/nasser1941/lcode/compare/v0.38.0...v0.39.0
|
|
910
930
|
[0.38.0]: https://github.com/nasser1941/lcode/compare/v0.37.0...v0.38.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lcode-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.41.0
|
|
4
4
|
Summary: A local-first terminal coding agent powered by open-weight models on your own GPU or Mac, via Ollama.
|
|
5
5
|
Project-URL: Homepage, https://nasser1941.github.io/lcode/
|
|
6
6
|
Project-URL: Documentation, https://nasser1941.github.io/lcode/
|
|
@@ -27,6 +27,7 @@ lcode config path # print the file location
|
|
|
27
27
|
| `comfyui_url` | `http://127.0.0.1:8188` | ComfyUI's address, for image and video mode |
|
|
28
28
|
| `comfyui_path` | (found) | Your ComfyUI folder, if lcode doesn't find it; lcode starts it when needed ([ComfyUI](modes.md#comfyui)) |
|
|
29
29
|
| `media_dir` | `.lcode/media` | Where images and videos go: a folder in the project, or an absolute path |
|
|
30
|
+
| `image_model` | `flux2-klein-4b` | The model image mode uses: a key from `lcode media` ([image models](modes.md#image-models)) |
|
|
30
31
|
| `think` | `true` | Let the model reason before answering |
|
|
31
32
|
| `web` | `on` | Web search and page fetching: `on`, `ask` (before each search/website) or `off` |
|
|
32
33
|
| `search_backend` | `auto` | `auto`, `ollama`, `brave`, `tavily` or `searxng`; `auto` uses the first one configured |
|
|
@@ -34,9 +34,8 @@ in code mode even if they're still in your `mcp.json`. It says so once.
|
|
|
34
34
|
|
|
35
35
|
In image mode, what you type is a description, and lcode makes the picture. No language model is
|
|
36
36
|
involved: lcode fills in its own ComfyUI workflow for the image model and follows the job itself.
|
|
37
|
-
For now that's one image per request, 1024×1024, with
|
|
38
|
-
|
|
39
|
-
comes next (#150, #151).
|
|
37
|
+
For now that's one image per request, 1024×1024, with the image model you choose (see
|
|
38
|
+
[Image models](#image-models)). Sizes, seeds and variations come next (#151).
|
|
40
39
|
|
|
41
40
|
- **Progress:** the status line says what ComfyUI is doing (loading the model, reading the
|
|
42
41
|
prompt, drawing, decoding), the step, the time left and the time so far. If other jobs are ahead
|
|
@@ -73,12 +72,67 @@ ComfyUI runs the image and video models; lcode drives it through its API.
|
|
|
73
72
|
uvx --from comfy-cli comfy install
|
|
74
73
|
```
|
|
75
74
|
|
|
76
|
-
- **The image model** has to be in ComfyUI's model folders for now: for FLUX.2 klein 4B,
|
|
77
|
-
`diffusion_models/flux-2-klein-4b-fp8.safetensors`, `text_encoders/qwen_3_4b.safetensors` and
|
|
78
|
-
`vae/flux2-vae.safetensors` (from ComfyUI's FLUX.2 klein template). lcode names any that are
|
|
79
|
-
missing. Installing models from lcode comes with #150.
|
|
80
75
|
- **`lcode doctor`** shows whether ComfyUI is running or found, and whether the image model is
|
|
81
|
-
|
|
76
|
+
installed.
|
|
77
|
+
|
|
78
|
+
## Image models
|
|
79
|
+
|
|
80
|
+
lcode's media catalog lists the image models it can run, with what each needs and whether it fits
|
|
81
|
+
your machine. `/models` in image mode shows them, numbered; pick one with its number, or `/model
|
|
82
|
+
z-image-turbo`. `lcode config set image_model z-image-turbo` makes one the default.
|
|
83
|
+
|
|
84
|
+
```text
|
|
85
|
+
image ❯ /models
|
|
86
|
+
1. ● FLUX.2 klein 4B (flux2-klein-4b) text-to-image · 12.4 GB · installed · fits (~9 GB of 12 GB GPU memory)
|
|
87
|
+
1024×1024: 3.5 s per image, 14 s for the first with ComfyUI starting (RTX 4080 Laptop, 12 GB)
|
|
88
|
+
2. Z-Image Turbo (z-image-turbo) text-to-image · 14.6 GB · 6.5 GB to download · fits (~9 GB of 12 GB GPU memory)
|
|
89
|
+
1024×1024: 17 s per image (1.9 s per step), 23 s for the first (RTX 4080 Laptop, 12 GB)
|
|
90
|
+
Switch to which model? (number or name, Enter to keep FLUX.2 klein 4B): 2
|
|
91
|
+
Image model: Z-Image Turbo.
|
|
92
|
+
Not installed yet: 6.5 GB download with your next image (you're asked first).
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
| Model | Made by | License | Steps | Good at |
|
|
96
|
+
|---|---|---|---|---|
|
|
97
|
+
| **FLUX.2 klein 4B** (default) | Black Forest Labs | Apache 2.0 | 4 | Speed: a few seconds per image |
|
|
98
|
+
| **Z-Image Turbo** | Tongyi-MAI (Alibaba) | Apache 2.0 | 8 | Photorealism, text in images; slower |
|
|
99
|
+
|
|
100
|
+
Measured speeds are in the list (`/models`, `lcode media`) and in [Measured](#measured).
|
|
101
|
+
|
|
102
|
+
- **Installing:** a model that isn't installed downloads with your next image, after you say yes.
|
|
103
|
+
You can also install it ahead with `lcode media install z-image-turbo`. lcode downloads only
|
|
104
|
+
the files ComfyUI doesn't have: models share text encoders and VAEs, so Z-Image Turbo needs
|
|
105
|
+
6.5 GB once FLUX.2 klein is there. Files go into ComfyUI's model folders, and the folders of
|
|
106
|
+
`extra_model_paths.yaml` count when ComfyUI is running.
|
|
107
|
+
- **Safe downloads:** lcode checks the disk first and keeps 2 GB free. The progress shows the
|
|
108
|
+
speed and the time left. Each file is checked against Hugging Face's SHA-256 before it's used.
|
|
109
|
+
- **Interrupted?** Ctrl+C pauses a download, and so does a dropped connection. Run the same install
|
|
110
|
+
again and it continues where it stopped (a `.part` file holds what arrived).
|
|
111
|
+
- **Gated models:** for a model whose license you must accept on Hugging Face, accept it there
|
|
112
|
+
and set `HF_TOKEN`. lcode sends the token to huggingface.co only.
|
|
113
|
+
- **Will it fit?** On NVIDIA GPUs the list compares an estimate of the GPU memory the model needs
|
|
114
|
+
with your GPU: the largest of its files plus 1.5 GB. ComfyUI loads the text encoder, the model and
|
|
115
|
+
the VAE one after another, so the largest file sets the need. A model too big for the GPU still
|
|
116
|
+
runs, slower, because ComfyUI keeps part of it in RAM. On Apple Silicon, the list compares it
|
|
117
|
+
with unified memory.
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
lcode media # image and video models, what fits, what's installed
|
|
121
|
+
lcode media install z-image-turbo # download what's missing (asks first; --yes doesn't)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Measured
|
|
125
|
+
|
|
126
|
+
On an RTX 4080 Laptop GPU (12 GB) with 32 GB of RAM, ComfyUI 0.38, 1024×1024, four different
|
|
127
|
+
prompts each:
|
|
128
|
+
|
|
129
|
+
| Model | First image (ComfyUI starting, model loading) | Each next image | Per step |
|
|
130
|
+
|---|---|---|---|
|
|
131
|
+
| FLUX.2 klein 4B (fp8) | 14.3 s | 3.5 s | 0.6 s (4 steps) |
|
|
132
|
+
| Z-Image Turbo (int8) | 22.9 s | 17.4 s | 1.9 s (8 steps) |
|
|
133
|
+
|
|
134
|
+
ComfyUI uses whatever GPU memory is free to keep models loaded (11.3 to 11.5 GB here), so the
|
|
135
|
+
memory a model *needs* is lower than what `nvidia-smi` shows while it runs.
|
|
82
136
|
|
|
83
137
|
## One model on the GPU at a time
|
|
84
138
|
|
|
@@ -95,8 +149,7 @@ themselves:
|
|
|
95
149
|
|
|
96
150
|
Video mode doesn't generate yet: a request there says what's coming instead of going anywhere.
|
|
97
151
|
|
|
98
|
-
- **#150 Media model catalog
|
|
99
|
-
demand.
|
|
152
|
+
- **#150 Media model catalog** (image models done; video models come with video generation).
|
|
100
153
|
- **#151, #152 Image mode:** generating, variations, sizes and seeds, a gallery, editing,
|
|
101
154
|
references, inpainting, upscaling.
|
|
102
155
|
- **#153, #154 Video mode:** from text or a start image, first and last frames, several
|
|
@@ -7,6 +7,7 @@ lcode [options] # interactive session in the current directory
|
|
|
7
7
|
lcode setup [model] # pick, download and configure a model
|
|
8
8
|
lcode models # the model catalog and what fits this machine
|
|
9
9
|
lcode doctor # check the installation (include this in bug reports)
|
|
10
|
+
lcode media [install MODEL] # image and video models: what fits here, what's installed; install one
|
|
10
11
|
lcode config [set|unset] # show or change settings
|
|
11
12
|
lcode bench [models] # score models on small coding tasks on this machine
|
|
12
13
|
lcode mcp [catalog|add|list] # connect MCP servers: Jira, GitHub, AWS, databases… (see MCP servers)
|
|
@@ -308,6 +308,7 @@ class Settings:
|
|
|
308
308
|
comfyui_url: str = "http://127.0.0.1:8188"
|
|
309
309
|
comfyui_path: str | None = None # a ComfyUI install lcode doesn't find by itself (lcode.comfy)
|
|
310
310
|
media_dir: str = ".lcode/media" # where image and video mode save their files (lcode.media)
|
|
311
|
+
image_model: str = "flux2-klein-4b" # the media catalog's model image mode uses (lcode.media_catalog)
|
|
311
312
|
num_batch: int | None = None
|
|
312
313
|
keep_alive: str = "30m"
|
|
313
314
|
think: bool = True
|
|
@@ -687,6 +688,7 @@ class Agent:
|
|
|
687
688
|
"cwd": str(self.workspace), # where `lcode -c` there finds it, wherever the shell moved
|
|
688
689
|
"model": self.settings.model,
|
|
689
690
|
"mode": self.settings.mode,
|
|
691
|
+
"image_model": self.settings.image_model,
|
|
690
692
|
"name": self.session_name,
|
|
691
693
|
"title": self.session_title,
|
|
692
694
|
"messages": self.messages,
|
|
@@ -735,6 +737,8 @@ class Agent:
|
|
|
735
737
|
self.session_name = data.get("name", "")
|
|
736
738
|
if data.get("mode") in ("code", "image", "video"):
|
|
737
739
|
self.settings.mode = data["mode"] # a session belongs to the mode it was made in
|
|
740
|
+
if isinstance(data.get("image_model"), str):
|
|
741
|
+
self.settings.image_model = data["image_model"]
|
|
738
742
|
self.session_title = data.get("title") or sessions.title_from(self.messages)
|
|
739
743
|
self.tools.read_mtimes.clear() # files may have changed since; the model must read them again
|
|
740
744
|
self.tools.todos = list(data.get("todos") or [])
|
|
@@ -200,6 +200,7 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
|
|
|
200
200
|
comfyui_url=cfg["comfyui_url"],
|
|
201
201
|
comfyui_path=cfg["comfyui_path"],
|
|
202
202
|
media_dir=cfg["media_dir"],
|
|
203
|
+
image_model=cfg["image_model"],
|
|
203
204
|
web=switch(options.web, cfg["web"], "on"),
|
|
204
205
|
search_backend=cfg["search_backend"],
|
|
205
206
|
searxng_url=cfg["searxng_url"],
|
|
@@ -777,9 +777,10 @@ def exit_on_hangup() -> None:
|
|
|
777
777
|
|
|
778
778
|
def comfyui_status(cfg: dict) -> tuple[str, bool | None]:
|
|
779
779
|
"""lcode doctor's ComfyUI line: running or found, and whether the image model is there."""
|
|
780
|
-
from lcode import comfy
|
|
780
|
+
from lcode import comfy
|
|
781
|
+
from lcode import media_catalog as catalog
|
|
781
782
|
|
|
782
|
-
|
|
783
|
+
model = catalog.load().find(cfg.get("image_model") or "") or catalog.load().models[catalog.DEFAULT_IMAGE]
|
|
783
784
|
server = comfy.Server(cfg["comfyui_url"])
|
|
784
785
|
stats = server.stats()
|
|
785
786
|
install = None if stats else comfy.find_install(cfg["comfyui_path"])
|
|
@@ -787,14 +788,13 @@ def comfyui_status(cfg: dict) -> tuple[str, bool | None]:
|
|
|
787
788
|
return "not found: image mode offers to install it (or lcode config set comfyui_path <folder>)", None
|
|
788
789
|
if stats:
|
|
789
790
|
where = f"{(stats.get('system') or {}).get('comfyui_version', '?')} running at {server.url}"
|
|
790
|
-
installed = {folder: set(server.models(folder)) for folder in workflow.models}
|
|
791
791
|
else:
|
|
792
792
|
where = f"{install.path} (lcode starts it in image mode)"
|
|
793
|
-
|
|
794
|
-
missing = [f"{f}/{names[0]}" for f, names in workflow.models.items() if not installed[f] & set(names)]
|
|
793
|
+
_, missing = catalog.chosen_files(model, catalog.lister(server if stats else None, install))
|
|
795
794
|
if missing:
|
|
796
|
-
|
|
797
|
-
|
|
795
|
+
size = sum(f.gb for f in missing)
|
|
796
|
+
return f"{where} · {model.name} not installed: lcode media install {model.key} ({size:.1f} GB)", None
|
|
797
|
+
return f"{where} · image model: {model.name}", True
|
|
798
798
|
|
|
799
799
|
|
|
800
800
|
def media_request(agent, prompt: str, out: Console, quiet: bool) -> None:
|
|
@@ -909,6 +909,13 @@ def build_subparsers() -> dict[str, argparse.ArgumentParser]:
|
|
|
909
909
|
subs["setup"] = p
|
|
910
910
|
subs["models"] = argparse.ArgumentParser(prog="lcode models", description="List models and how they fit here.")
|
|
911
911
|
subs["doctor"] = argparse.ArgumentParser(prog="lcode doctor", description="Check the installation.")
|
|
912
|
+
p = argparse.ArgumentParser(
|
|
913
|
+
prog="lcode media", description="Image and video models: list them, with how they fit here, or install one."
|
|
914
|
+
)
|
|
915
|
+
p.add_argument("action", nargs="?", choices=["list", "install"], default="list")
|
|
916
|
+
p.add_argument("model", nargs="?", help="with install: a model key from the list, e.g. z-image-turbo")
|
|
917
|
+
p.add_argument("-y", "--yes", action="store_true", help="don't ask before downloading")
|
|
918
|
+
subs["media"] = p
|
|
912
919
|
p = argparse.ArgumentParser(prog="lcode config", description="Show or change settings.")
|
|
913
920
|
p.add_argument("action", nargs="?", choices=["show", "set", "unset", "path"], default="show")
|
|
914
921
|
p.add_argument("key", nargs="?", choices=list(config.SETTINGS))
|
|
@@ -1065,6 +1072,55 @@ def cmd_index(args) -> None:
|
|
|
1065
1072
|
)
|
|
1066
1073
|
|
|
1067
1074
|
|
|
1075
|
+
def cmd_media(args) -> None:
|
|
1076
|
+
"""lcode media [list] | install <model>: the media catalog, what's installed in ComfyUI, downloads."""
|
|
1077
|
+
from rich.prompt import Confirm
|
|
1078
|
+
|
|
1079
|
+
from lcode import comfy, media
|
|
1080
|
+
from lcode import media_catalog as catalog
|
|
1081
|
+
|
|
1082
|
+
try:
|
|
1083
|
+
cfg = config.load()
|
|
1084
|
+
except ConfigError as e:
|
|
1085
|
+
fail(str(e))
|
|
1086
|
+
cat, hw = catalog.load(), detect()
|
|
1087
|
+
server = comfy.Server(cfg["comfyui_url"])
|
|
1088
|
+
install = comfy.find_install(cfg["comfyui_path"])
|
|
1089
|
+
running = server.stats() is not None
|
|
1090
|
+
installed = catalog.lister(server if running else None, install) if running or install else None
|
|
1091
|
+
if args.action == "install":
|
|
1092
|
+
model = cat.find(args.model or "")
|
|
1093
|
+
if model is None:
|
|
1094
|
+
fail(f"no media model {args.model!r}: lcode media lists them")
|
|
1095
|
+
if not running and install is None:
|
|
1096
|
+
fail(f"lcode needs ComfyUI to install models into. {comfy.install_hint()}")
|
|
1097
|
+
ask = None if args.yes else (lambda q: Confirm.ask(q, default=True, console=console))
|
|
1098
|
+
try:
|
|
1099
|
+
saved = catalog.install(model, server if running else None, install, console, ask)
|
|
1100
|
+
except catalog.CatalogError as e:
|
|
1101
|
+
fail(str(e))
|
|
1102
|
+
how = f" ({len(saved)} file(s) downloaded)." if saved else "."
|
|
1103
|
+
console.print(f"[green]{model.name} is installed[/]{how}")
|
|
1104
|
+
return
|
|
1105
|
+
where = (
|
|
1106
|
+
f"ComfyUI at {server.url}" if running else (f"{install.path} (not running)" if install else "no ComfyUI found")
|
|
1107
|
+
)
|
|
1108
|
+
console.print(f"[dim]{hw.describe()} · {where} · image model: {cfg['image_model']}[/]\n")
|
|
1109
|
+
for mode in ("image", "video"):
|
|
1110
|
+
models = cat.for_mode(mode)
|
|
1111
|
+
console.print(f"[bold]{mode.capitalize()} models[/]")
|
|
1112
|
+
if not models:
|
|
1113
|
+
console.print(f" [dim]{media.SOON[mode]}[/]\n")
|
|
1114
|
+
continue
|
|
1115
|
+
for m in models:
|
|
1116
|
+
line = escape(catalog.describe(m, hw, installed))
|
|
1117
|
+
console.print(f" [bold]{m.key:<16}[/] {escape(m.name)} [dim]· {line}[/]")
|
|
1118
|
+
if m.measured:
|
|
1119
|
+
console.print(f" {'':<16} [dim]{escape(m.measured)}[/]")
|
|
1120
|
+
console.print()
|
|
1121
|
+
console.print("[dim]Install one: lcode media install <model> · choose in image mode: /model <model>[/]")
|
|
1122
|
+
|
|
1123
|
+
|
|
1068
1124
|
def cmd_mcp(args) -> None:
|
|
1069
1125
|
from lcode.mcp.commands import run
|
|
1070
1126
|
|
|
@@ -1075,6 +1131,7 @@ COMMANDS = {
|
|
|
1075
1131
|
"setup": cmd_setup,
|
|
1076
1132
|
"models": cmd_models,
|
|
1077
1133
|
"doctor": cmd_doctor,
|
|
1134
|
+
"media": cmd_media,
|
|
1078
1135
|
"config": cmd_config,
|
|
1079
1136
|
"bench": cmd_bench,
|
|
1080
1137
|
"mcp": cmd_mcp,
|
|
@@ -254,6 +254,14 @@ class Server:
|
|
|
254
254
|
except (requests.RequestException, ValueError):
|
|
255
255
|
return []
|
|
256
256
|
|
|
257
|
+
def folder_paths(self) -> dict[str, list[str]]:
|
|
258
|
+
"""Where ComfyUI keeps each kind of model (its own folders and extra_model_paths.yaml's)."""
|
|
259
|
+
try:
|
|
260
|
+
r = requests.get(self.url + "/internal/folder_paths", timeout=10)
|
|
261
|
+
return r.json() if r.status_code == 200 else {}
|
|
262
|
+
except (requests.RequestException, ValueError):
|
|
263
|
+
return {}
|
|
264
|
+
|
|
257
265
|
def upload(self, path: Path) -> str:
|
|
258
266
|
"""Upload an input image; the name to use in a workflow."""
|
|
259
267
|
with path.open("rb") as f:
|
|
@@ -420,32 +428,18 @@ class Events:
|
|
|
420
428
|
|
|
421
429
|
@dataclass
|
|
422
430
|
class Workflow:
|
|
431
|
+
"""A graph in ComfyUI's API format with `$name` placeholders: the model's files (by role, from media.toml),
|
|
432
|
+
the prompt, the seed and the settings in `defaults`."""
|
|
433
|
+
|
|
423
434
|
key: str
|
|
424
|
-
name: str
|
|
425
|
-
task: str
|
|
426
|
-
models: dict[str, list[str]] # ComfyUI folder -> file names it can use, preferred first
|
|
427
|
-
defaults: dict
|
|
428
435
|
graph: dict
|
|
436
|
+
defaults: dict = field(default_factory=dict)
|
|
429
437
|
labels: dict[str, str] = field(default_factory=dict) # node id -> what it does, for progress
|
|
430
438
|
|
|
431
439
|
|
|
432
440
|
def load_workflow(key: str) -> Workflow:
|
|
433
441
|
data = json.loads(resources.files("lcode.workflows").joinpath(f"{key}.json").read_text())
|
|
434
|
-
return Workflow(key, data["
|
|
435
|
-
data.get("labels", {})) # fmt: skip
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
def choose_files(server: Server, workflow: Workflow) -> tuple[dict[str, str], list[str]]:
|
|
439
|
-
"""For each model the workflow needs, the installed file to use; and the folders where none is installed."""
|
|
440
|
-
chosen, missing = {}, []
|
|
441
|
-
for folder, names in workflow.models.items():
|
|
442
|
-
installed = set(server.models(folder))
|
|
443
|
-
found = next((n for n in names if n in installed), None)
|
|
444
|
-
if found:
|
|
445
|
-
chosen[folder] = found
|
|
446
|
-
else:
|
|
447
|
-
missing.append(f"{folder}/{names[0]}")
|
|
448
|
-
return chosen, missing
|
|
442
|
+
return Workflow(key, data["graph"], data.get("defaults", {}), data.get("labels", {}))
|
|
449
443
|
|
|
450
444
|
|
|
451
445
|
PLACEHOLDER = re.compile(r"^\$([a-z_]+)$")
|
|
@@ -45,6 +45,7 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
|
|
|
45
45
|
"comfyui_url": ("http://127.0.0.1:8188", str, "ComfyUI, for image and video mode"),
|
|
46
46
|
"comfyui_path": (None, str, "your ComfyUI folder, if lcode doesn't find it (lcode starts it when needed)"),
|
|
47
47
|
"media_dir": (".lcode/media", str, "where images and videos go: a folder in the project, or an absolute path"),
|
|
48
|
+
"image_model": ("flux2-klein-4b", str, "the model image mode uses: a key from `lcode media`"),
|
|
48
49
|
"think": (True, bool, "let the model reason before answering (slower, better)"),
|
|
49
50
|
"web": ("on", str, "web search and page fetching for the model: on | ask | off"),
|
|
50
51
|
"search_backend": ("auto", str, "auto | ollama | brave | tavily | searxng (keys come from environment variables)"),
|
|
@@ -210,6 +211,12 @@ def coerce(key: str, value: object) -> object:
|
|
|
210
211
|
raise ConfigError(f"permission_mode must be one of {', '.join(PERMISSION_MODES)}")
|
|
211
212
|
if key == "mode" and value not in WORK_MODES:
|
|
212
213
|
raise ConfigError(f"mode must be one of {', '.join(WORK_MODES)}")
|
|
214
|
+
if key == "image_model":
|
|
215
|
+
from lcode import media_catalog
|
|
216
|
+
|
|
217
|
+
keys = [m.key for m in media_catalog.load().for_mode("image")]
|
|
218
|
+
if value not in keys:
|
|
219
|
+
raise ConfigError(f"image_model must be one of {', '.join(keys)} (see lcode media)")
|
|
213
220
|
if typ is bool and isinstance(value, str):
|
|
214
221
|
if value.lower() not in ("true", "false", "1", "0", "yes", "no", "on", "off"):
|
|
215
222
|
raise ConfigError(f"{key} must be true or false")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Image and video mode: a request becomes media, made by ComfyUI (lcode.comfy) with no language model in between.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
variations and a gallery come with #
|
|
3
|
+
Image mode makes one image per request with the image model chosen from the media catalog (lcode.media_catalog);
|
|
4
|
+
sizes, variations and a gallery come with #151, video with #153.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
@@ -16,11 +16,12 @@ from typing import TYPE_CHECKING
|
|
|
16
16
|
from rich.text import Text
|
|
17
17
|
|
|
18
18
|
from lcode import comfy
|
|
19
|
+
from lcode import media_catalog as catalog
|
|
19
20
|
|
|
20
21
|
if TYPE_CHECKING:
|
|
21
22
|
from lcode.agent import Agent
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
SOON = {"video": "Video models come with video generation (#153)."}
|
|
24
25
|
IGNORED = "# lcode's images and videos: not part of the project unless you add them\n*\n"
|
|
25
26
|
|
|
26
27
|
|
|
@@ -28,8 +29,19 @@ class MediaError(Exception):
|
|
|
28
29
|
pass
|
|
29
30
|
|
|
30
31
|
|
|
31
|
-
def
|
|
32
|
-
|
|
32
|
+
def installed_lister(agent: Agent) -> catalog.Installed | None:
|
|
33
|
+
"""What ComfyUI has installed: asked from it when it's running, else read from its folders; None: unknown."""
|
|
34
|
+
server = comfy.Server(agent.settings.comfyui_url)
|
|
35
|
+
if server.stats():
|
|
36
|
+
return catalog.lister(server)
|
|
37
|
+
install = comfy.find_install(agent.settings.comfyui_path)
|
|
38
|
+
return catalog.lister(install=install) if install else None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def current(agent: Agent) -> catalog.MediaModel:
|
|
42
|
+
"""The image model the agent uses (the image_model setting, or /model in image mode)."""
|
|
43
|
+
cat = catalog.load()
|
|
44
|
+
return cat.find(agent.settings.image_model) or cat.models[catalog.DEFAULT_IMAGE]
|
|
33
45
|
|
|
34
46
|
|
|
35
47
|
@dataclass
|
|
@@ -183,6 +195,28 @@ class Line:
|
|
|
183
195
|
return text
|
|
184
196
|
|
|
185
197
|
|
|
198
|
+
def ensure_installed(
|
|
199
|
+
agent: Agent, model: catalog.MediaModel, server: comfy.Server, ask: Callable[[str], bool] | None
|
|
200
|
+
) -> dict[str, str]:
|
|
201
|
+
"""The model's files (role -> installed name), downloading the missing ones first when the user agrees."""
|
|
202
|
+
files, missing = catalog.chosen_files(model, catalog.lister(server))
|
|
203
|
+
if not missing:
|
|
204
|
+
return files
|
|
205
|
+
size = sum(f.gb for f in missing)
|
|
206
|
+
if ask is None:
|
|
207
|
+
raise MediaError(f"{model.name} isn't installed ({size:.1f} GB to download): lcode media install {model.key}")
|
|
208
|
+
install = comfy.find_install(agent.settings.comfyui_path)
|
|
209
|
+
try:
|
|
210
|
+
catalog.install(model, server=server, install=install, console=agent.console, ask=ask)
|
|
211
|
+
except catalog.CatalogError as e:
|
|
212
|
+
raise MediaError(str(e)) from e
|
|
213
|
+
files, missing = catalog.chosen_files(model, catalog.lister(server))
|
|
214
|
+
if missing:
|
|
215
|
+
raise MediaError(f"ComfyUI doesn't see {', '.join(f.name for f in missing)} yet: restart it and try again")
|
|
216
|
+
agent.say(f"Installed {model.name}.")
|
|
217
|
+
return files
|
|
218
|
+
|
|
219
|
+
|
|
186
220
|
def generate_image(
|
|
187
221
|
agent: Agent, prompt: str, cancelled: Callable[[], bool] = lambda: False, ask: Callable[[str], bool] | None = None
|
|
188
222
|
) -> Made:
|
|
@@ -193,20 +227,17 @@ def generate_image(
|
|
|
193
227
|
server = engine(agent, ask, cancelled)
|
|
194
228
|
for path in comfy.collect(server):
|
|
195
229
|
agent.say(f"Saved {shown(agent, path)}: it finished while lcode wasn't watching.")
|
|
196
|
-
|
|
197
|
-
files
|
|
198
|
-
|
|
199
|
-
raise MediaError(
|
|
200
|
-
f"{workflow.name} isn't installed in ComfyUI (missing {', '.join(missing)}). lcode installs image "
|
|
201
|
-
"models from #150 on; until then put the files in ComfyUI's models folder."
|
|
202
|
-
)
|
|
230
|
+
model = current(agent)
|
|
231
|
+
files = ensure_installed(agent, model, server, ask)
|
|
232
|
+
workflow = comfy.load_workflow(model.workflow)
|
|
203
233
|
free_gpu(agent)
|
|
204
234
|
seed = random.randrange(2**32)
|
|
205
235
|
values = {**workflow.defaults, **files, "prompt": prompt, "seed": seed}
|
|
206
236
|
graph = comfy.fill(workflow.graph, values)
|
|
207
237
|
meta = {
|
|
208
238
|
"prompt": prompt,
|
|
209
|
-
"model":
|
|
239
|
+
"model": model.name,
|
|
240
|
+
"model_key": model.key,
|
|
210
241
|
"workflow": workflow.key,
|
|
211
242
|
"seed": seed,
|
|
212
243
|
"width": values["width"],
|
|
@@ -217,7 +248,7 @@ def generate_image(
|
|
|
217
248
|
}
|
|
218
249
|
out = folder(agent)
|
|
219
250
|
stem = f"{time.strftime('%Y%m%d-%H%M%S')}-{comfy.slug(prompt)}"
|
|
220
|
-
line = Line(
|
|
251
|
+
line = Line(model.name, "Esc or Ctrl+C cancels" if agent.interactive else "")
|
|
221
252
|
job = {"id": ""}
|
|
222
253
|
|
|
223
254
|
def on_event(event: comfy.Event) -> None:
|
|
@@ -252,7 +283,7 @@ def generate_image(
|
|
|
252
283
|
comfy.forget(prompt_id)
|
|
253
284
|
if not saved:
|
|
254
285
|
raise MediaError("ComfyUI finished but returned no image.")
|
|
255
|
-
made = Made(saved,
|
|
286
|
+
made = Made(saved, model.name, seed, values["width"], values["height"], time.monotonic() - line.started)
|
|
256
287
|
for path in saved:
|
|
257
288
|
agent.console.print(
|
|
258
289
|
Text("◆ ", style="green")
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Image and video models lcode can run through ComfyUI (lcode.media_catalog).
|
|
2
|
+
#
|
|
3
|
+
# [files.<key>] one file in ComfyUI's model folders, listed once: models share text encoders and VAEs.
|
|
4
|
+
# folder ComfyUI's model folder (diffusion_models, text_encoders, vae, loras, …)
|
|
5
|
+
# name the file name there
|
|
6
|
+
# url where lcode downloads it from (Hugging Face; HF_TOKEN is used for gated files)
|
|
7
|
+
# gb its size in GB (10^9 bytes)
|
|
8
|
+
# alternatives other file names that do the same job: an installed one is used instead of downloading
|
|
9
|
+
#
|
|
10
|
+
# [models.<key>] one model.
|
|
11
|
+
# mode image or video
|
|
12
|
+
# tasks text-to-image, edit, inpaint, text-to-video, image-to-video, first-last-frame, reference-to-video, audio
|
|
13
|
+
# workflow lcode's workflow for it (lcode/workflows/<workflow>.json)
|
|
14
|
+
# files role -> file key; the workflow's $role placeholders get the file names
|
|
15
|
+
# gpu_gb GPU memory it needs, when known better than lcode's estimate (the largest file + 1.5 GB); ComfyUI
|
|
16
|
+
# fills whatever is free with cached models, so nvidia-smi's peak doesn't tell
|
|
17
|
+
# measured what it did on real hardware; models without it haven't been run yet
|
|
18
|
+
|
|
19
|
+
[files.flux-2-klein-4b-fp8]
|
|
20
|
+
folder = "diffusion_models"
|
|
21
|
+
name = "flux-2-klein-4b-fp8.safetensors"
|
|
22
|
+
url = "https://huggingface.co/black-forest-labs/FLUX.2-klein-4b-fp8/resolve/main/flux-2-klein-4b-fp8.safetensors"
|
|
23
|
+
gb = 4.07
|
|
24
|
+
alternatives = ["flux-2-klein-4b.safetensors"]
|
|
25
|
+
|
|
26
|
+
[files.qwen-3-4b]
|
|
27
|
+
folder = "text_encoders"
|
|
28
|
+
name = "qwen_3_4b.safetensors"
|
|
29
|
+
url = "https://huggingface.co/Comfy-Org/flux2-klein/resolve/main/split_files/text_encoders/qwen_3_4b.safetensors"
|
|
30
|
+
gb = 8.04
|
|
31
|
+
alternatives = ["qwen_3_4b_fp8_mixed.safetensors"]
|
|
32
|
+
|
|
33
|
+
[files.flux2-vae]
|
|
34
|
+
folder = "vae"
|
|
35
|
+
name = "flux2-vae.safetensors"
|
|
36
|
+
url = "https://huggingface.co/Comfy-Org/flux2-dev/resolve/main/split_files/vae/flux2-vae.safetensors"
|
|
37
|
+
gb = 0.34
|
|
38
|
+
|
|
39
|
+
[files.z-image-turbo-int8]
|
|
40
|
+
folder = "diffusion_models"
|
|
41
|
+
name = "z_image_turbo_int8_convrot.safetensors"
|
|
42
|
+
url = "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/diffusion_models/z_image_turbo_int8_convrot.safetensors"
|
|
43
|
+
gb = 6.20
|
|
44
|
+
alternatives = ["z_image_turbo_bf16.safetensors"]
|
|
45
|
+
|
|
46
|
+
[files.flux-ae]
|
|
47
|
+
folder = "vae"
|
|
48
|
+
name = "ae.safetensors"
|
|
49
|
+
url = "https://huggingface.co/Comfy-Org/z_image_turbo/resolve/main/split_files/vae/ae.safetensors"
|
|
50
|
+
gb = 0.34
|
|
51
|
+
|
|
52
|
+
[models.flux2-klein-4b]
|
|
53
|
+
name = "FLUX.2 klein 4B"
|
|
54
|
+
mode = "image"
|
|
55
|
+
publisher = "Black Forest Labs"
|
|
56
|
+
license = "Apache 2.0"
|
|
57
|
+
tasks = ["text-to-image"]
|
|
58
|
+
workflow = "flux2-klein-4b-t2i"
|
|
59
|
+
files = { model = "flux-2-klein-4b-fp8", text_encoder = "qwen-3-4b", vae = "flux2-vae" }
|
|
60
|
+
gpu_gb = 0.0
|
|
61
|
+
measured = "1024×1024: 3.5 s per image, 14 s for the first with ComfyUI starting (RTX 4080 Laptop, 12 GB)"
|
|
62
|
+
notes = "fast and small: 4 steps"
|
|
63
|
+
|
|
64
|
+
[models.z-image-turbo]
|
|
65
|
+
name = "Z-Image Turbo"
|
|
66
|
+
mode = "image"
|
|
67
|
+
publisher = "Tongyi-MAI (Alibaba)"
|
|
68
|
+
license = "Apache 2.0"
|
|
69
|
+
tasks = ["text-to-image"]
|
|
70
|
+
workflow = "z-image-turbo-t2i"
|
|
71
|
+
files = { model = "z-image-turbo-int8", text_encoder = "qwen-3-4b", vae = "flux-ae" }
|
|
72
|
+
gpu_gb = 0.0
|
|
73
|
+
measured = "1024×1024: 17 s per image (1.9 s per step), 23 s for the first (RTX 4080 Laptop, 12 GB)"
|
|
74
|
+
notes = "photorealistic, writes text well: 8 steps"
|