lcode-cli 0.41.0__tar.gz → 0.42.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.41.0 → lcode_cli-0.42.0}/CHANGELOG.md +19 -1
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/PKG-INFO +1 -1
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/configuration.md +1 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/modes.md +57 -8
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/quickstart.md +1 -1
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/usage.md +1 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/__init__.py +1 -1
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/agent.py +13 -1
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/api.py +1 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/cli.py +8 -7
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/comfy.py +5 -3
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/config.py +6 -0
- lcode_cli-0.42.0/src/lcode/imageopts.py +207 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/media.py +195 -15
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/media.toml +6 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/media_catalog.py +9 -0
- lcode_cli-0.42.0/src/lcode/preview.py +152 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/repl.py +107 -3
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/sessions.py +2 -2
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/comfy_standin.py +3 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_comfy.py +6 -6
- lcode_cli-0.42.0/tests/test_image_generate.py +253 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.editorconfig +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.github/CODEOWNERS +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.github/ISSUE_TEMPLATE/model_request.yml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.github/dependabot.yml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.github/workflows/ci.yml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.github/workflows/docs.yml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.github/workflows/release.yml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.gitignore +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/.pre-commit-config.yaml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/AGENTS.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/CODE_OF_CONDUCT.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/CONTRIBUTING.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/LICENSE +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/README.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/SECURITY.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/action.yml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/agents.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/assets/demo.svg +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/assets/extra.css +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/automation.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/bench.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/changelog.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/commands.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/contributing.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/editors.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/git.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/hooks.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/how-it-works.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/index.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/installation.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/lsp.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/mcp.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/memory.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/models.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/sandbox.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/search.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/servers.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/docs/troubleshooting.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/examples/README.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/examples/api/fix_tests.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/examples/commands/review.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/examples/skills/commit-message/SKILL.md +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/examples/skills/commit-message/scripts/staged.sh +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/install.sh +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/mkdocs.yml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/overrides/main.html +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/pyproject.toml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/scripts/make_logos.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/scripts/record_demo.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/scripts/social-preview.html +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/__main__.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/acp.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/action.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/backends.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/bench.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/bench_backend.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/bench_runtime.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/catalog.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/checkpoints.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/checks.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/clipboard.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/cloud.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/codesearch.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/context.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/escalate.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/extensions.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/frontmatter.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/gitflow.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/hardware.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/hooks.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/jobs.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/limits.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/lsp.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/matching.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/__init__.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/auth.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/catalog.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/catalog.toml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/client.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/commands.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/config.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/http.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/manager.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/protocol.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/server.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/mcp/stdio.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/memory.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/models.toml +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/modes.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/notify.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/ollama.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/permissions.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/planning.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/render.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/repeats.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/repomap.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/residency.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/review.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/sandbox.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/screenshots.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/secretscan.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/servers/__init__.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/servers/encord.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/servers/valohai.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/servers/xai.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/steer.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/subagents.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/tools.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/unreachable.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/update.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/vision.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/web.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/workflows/__init__.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/workflows/flux2-klein-4b-t2i.json +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/src/lcode/workflows/z-image-turbo-t2i.json +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/conftest.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/fake_lsp_server.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/fake_mcp_server.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_acp.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_action.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_agent.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_api.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_backends.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_bench.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_catalog.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_checkpoints.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_checks.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_cli.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_clipboard.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_cloud.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_codesearch.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_config.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_context.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_context_picker.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_escalate.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_extensions.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_gitflow.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_hooks.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_jobs.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_look.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_lsp.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_matching.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_mcp.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_media_catalog.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_memory.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_model_picker.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_modes.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_permissions.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_plan_mode.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_repeats.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_repomap.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_residency.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_review.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_robustness.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_sandbox.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_screenshots.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_secretscan.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_servers.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_sessions.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_steer.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_stuck.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_subagents.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_tools.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_update.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_vision.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_web.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/tests/test_xai.py +0 -0
- {lcode_cli-0.41.0 → lcode_cli-0.42.0}/uv.lock +0 -0
|
@@ -6,6 +6,23 @@ All notable changes to lcode are documented here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.42.0] - 2026-10-24
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Image mode's options, results and follow-ups** (#151). See
|
|
14
|
+
[Image mode](https://nasser1941.github.io/lcode/modes/#image-mode).
|
|
15
|
+
- **Options in a request** (`a red fox --n 4 --ar 16:9 --seed 42 --quality best --style photo`)
|
|
16
|
+
or for the session (`/options n=4 ar=16:9`): how many images, the shape, the seed (counting up
|
|
17
|
+
over several images), a quality preset (0.5, 1 or 2 megapixels), ten style presets, and a
|
|
18
|
+
negative prompt for models that use one.
|
|
19
|
+
- **Numbered results, shown in the terminal:** the picture itself in kitty, Ghostty, iTerm2 and
|
|
20
|
+
WezTerm, and a half-block colour preview elsewhere (the `preview` setting).
|
|
21
|
+
- **Follow-ups:** `/again` (new seeds), `/vary 3` (four variations of image #3), `/gallery` and
|
|
22
|
+
`/open 3`. The gallery and options are saved with the session.
|
|
23
|
+
- `lcode --mode image -p "… --n 2"` reports every image's file and seed.
|
|
24
|
+
- One image is now named `<time>-<prompt>.png` (no `-1`).
|
|
25
|
+
|
|
9
26
|
## [0.41.0] - 2026-10-23
|
|
10
27
|
|
|
11
28
|
### Added
|
|
@@ -923,7 +940,8 @@ First public release.
|
|
|
923
940
|
- `AGENTS.md` project instructions and `/init` to generate them.
|
|
924
941
|
- One-line installer for Linux and macOS.
|
|
925
942
|
|
|
926
|
-
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.
|
|
943
|
+
[Unreleased]: https://github.com/nasser1941/lcode/compare/v0.42.0...HEAD
|
|
944
|
+
[0.42.0]: https://github.com/nasser1941/lcode/compare/v0.41.0...v0.42.0
|
|
927
945
|
[0.41.0]: https://github.com/nasser1941/lcode/compare/v0.40.0...v0.41.0
|
|
928
946
|
[0.40.0]: https://github.com/nasser1941/lcode/compare/v0.39.0...v0.40.0
|
|
929
947
|
[0.39.0]: https://github.com/nasser1941/lcode/compare/v0.38.0...v0.39.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: lcode-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.42.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
|
+
| `preview` | `auto` | Images in the terminal: `auto`, `kitty`, `iterm`, `blocks` (colour text) or `off` ([results](modes.md#results)) |
|
|
30
31
|
| `image_model` | `flux2-klein-4b` | The model image mode uses: a key from `lcode media` ([image models](modes.md#image-models)) |
|
|
31
32
|
| `think` | `true` | Let the model reason before answering |
|
|
32
33
|
| `web` | `on` | Web search and page fetching: `on`, `ask` (before each search/website) or `off` |
|
|
@@ -15,7 +15,7 @@ Unloaded qwen3.6-35b to free the GPU for images.
|
|
|
15
15
|
Starting ComfyUI (~/comfy/ComfyUI) in the background.
|
|
16
16
|
Describe an image and press Enter: FLUX.2 klein 4B makes it on your GPU, into .lcode/media.
|
|
17
17
|
image ❯ a red fox sitting in fresh snow at dusk
|
|
18
|
-
◆ .lcode/media/20261023-184531-a-red-fox-sitting-in-fresh-snow-at-dusk
|
|
18
|
+
◆ #1 .lcode/media/20261023-184531-a-red-fox-sitting-in-fresh-snow-at-dusk.png 1024×1024 · FLUX.2 klein 4B · seed 3714779554 · 8.0 s
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
- **Switching:** `/mode code`, `/mode image` and `/mode video` switch in a session. `lcode --mode image`
|
|
@@ -33,9 +33,53 @@ in code mode even if they're still in your `mcp.json`. It says so once.
|
|
|
33
33
|
## Image mode
|
|
34
34
|
|
|
35
35
|
In image mode, what you type is a description, and lcode makes the picture. No language model is
|
|
36
|
-
involved: lcode fills in its own ComfyUI workflow for the image model and follows the job itself
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
involved: lcode fills in its own ComfyUI workflow for the image model and follows the job itself,
|
|
37
|
+
with the image model you choose (see [Image models](#image-models)).
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
image ❯ a red fox in fresh snow --n 2 --ar 16:9 --seed 7
|
|
41
|
+
◆ #1 .lcode/media/20261023-194019-a-red-fox-in-fresh-snow.png 1360×768 · FLUX.2 klein 4B · seed 7 · 4.9 s
|
|
42
|
+
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ (the picture, in colour)
|
|
43
|
+
◆ #2 .lcode/media/20261023-194024-a-red-fox-in-fresh-snow.png 1360×768 · FLUX.2 klein 4B · seed 8 · 2.7 s
|
|
44
|
+
image ❯ /vary 1 --n 2
|
|
45
|
+
Variations of #1: a red fox in fresh snow
|
|
46
|
+
◆ #3 …
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Options
|
|
50
|
+
|
|
51
|
+
Add them to a request, or set them for the session with `/options` (`/options n=4 ar=16:9
|
|
52
|
+
quality=best style=photo`). A flag in a request wins over `/options` for that request. `/options`
|
|
53
|
+
alone shows them.
|
|
54
|
+
|
|
55
|
+
| Flag | `/options` | What it does |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `--n 4` | `n=4` | How many images: 1 to 8. Each gets its own seed. |
|
|
58
|
+
| `--ar 16:9` | `ar=16:9` | The shape: a ratio (`3:2`, `9:16`, up to 4:1), a name (`square`, `portrait`, `landscape`, `wide`, `tall`, `photo`, `cinema`), or pixels (`--size 1344x768`). |
|
|
59
|
+
| `--seed 42` | `seed=42` | The same seed and settings make the same image. With `--n`, the seeds count up (42, 43, …). `seed=random` goes back to a new one each time. |
|
|
60
|
+
| `--quality best` | `quality=best` | `fast` (about 0.5 megapixels, fewer steps for models that allow it), `balanced` (1 megapixel, the default), `best` (2 megapixels, more steps where they help). |
|
|
61
|
+
| `--style photo` | `style=photo` | Adds a style to the description: `photo`, `cinematic`, `anime`, `watercolor`, `oil`, `pixel`, `3d`, `sketch`, `comic`, `flat`. `style=none` takes it off. |
|
|
62
|
+
| `--no "text, people"` | `no="text"` | A negative prompt, for models that use one. FLUX.2 klein and Z-Image Turbo don't (they're distilled), and lcode says so: describe what you want instead. |
|
|
63
|
+
|
|
64
|
+
### Results
|
|
65
|
+
|
|
66
|
+
- **Numbered:** every image in the session gets a number (`#3`), shown with its file, size, model,
|
|
67
|
+
seed and time.
|
|
68
|
+
- **Shown in the terminal:** in kitty, Ghostty, iTerm2 and WezTerm the picture itself appears
|
|
69
|
+
under its line. Other terminals get a small colour preview drawn with half blocks. The
|
|
70
|
+
`preview` setting chooses: `auto`, `kitty`, `iterm`, `blocks` or `off`. Inside tmux or screen,
|
|
71
|
+
lcode draws the half-block preview.
|
|
72
|
+
- **Follow-ups:**
|
|
73
|
+
|
|
74
|
+
| Command | What it does |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `/again` | The last request again with new seeds; flags change it (`/again --n 4 --ar 9:16`) |
|
|
77
|
+
| `/vary 3` | Four variations of image #3: its prompt and options, new seeds (`/vary 3 --n 2`; the last image without a number) |
|
|
78
|
+
| `/gallery` | The session's images with their prompt, size, model and seed; `/gallery all` lists the latest 50 in the media folder |
|
|
79
|
+
| `/open 3` | Opens image #3 in your image viewer (the last one without a number) |
|
|
80
|
+
|
|
81
|
+
- **Sessions:** the gallery and `/options` are saved with the session, so `lcode -c` or `/resume`
|
|
82
|
+
brings them back. An image session is listed under its first prompt.
|
|
39
83
|
|
|
40
84
|
- **Progress:** the status line says what ComfyUI is doing (loading the model, reading the
|
|
41
85
|
prompt, drawing, decoding), the step, the time left and the time so far. If other jobs are ahead
|
|
@@ -43,14 +87,15 @@ For now that's one image per request, 1024×1024, with the image model you choos
|
|
|
43
87
|
- **Cancel** with Esc or Ctrl+C: lcode cancels the job in ComfyUI too, so it stops using the GPU.
|
|
44
88
|
- **Type ahead:** what you type while an image is being made waits in the next prompt.
|
|
45
89
|
- **Files:** each image goes to `.lcode/media/` in the project (the `media_dir` setting), named
|
|
46
|
-
after the time and the prompt, with a `.json` next to it holding the prompt, model, seed, size
|
|
47
|
-
steps. PNGs carry the same in their own metadata (an `lcode` text chunk), and ComfyUI's workflow
|
|
90
|
+
after the time and the prompt, with a `.json` next to it holding the prompt, model, seed, size,
|
|
91
|
+
steps, quality and style. PNGs carry the same in their own metadata (an `lcode` text chunk), and ComfyUI's workflow
|
|
48
92
|
as well. `.lcode/media/` keeps itself out of git; a folder of your own (`lcode config set
|
|
49
93
|
media_dir assets/generated`) doesn't.
|
|
50
94
|
- **Nothing is lost:** lcode writes each job down while it runs. If lcode crashes or the terminal
|
|
51
95
|
closes before an image arrives, the next request in image mode saves it.
|
|
52
|
-
- **From scripts:** `lcode --mode image -p "a red fox in the snow"` makes
|
|
53
|
-
with `--output json` it prints a result with the file paths, model,
|
|
96
|
+
- **From scripts:** `lcode --mode image -p "a red fox in the snow --n 2"` makes the images and
|
|
97
|
+
exits; with `--output json` it prints a result with the file paths, the model, the seeds, the size
|
|
98
|
+
and the time.
|
|
54
99
|
- **No Ollama needed:** `lcode --mode image` starts even when Ollama isn't running or the coding
|
|
55
100
|
model isn't installed; `/mode code` then says what's missing.
|
|
56
101
|
|
|
@@ -123,6 +168,10 @@ lcode media install z-image-turbo # download what's missing (asks first; --yes
|
|
|
123
168
|
|
|
124
169
|
### Measured
|
|
125
170
|
|
|
171
|
+
On an RTX 4080 Laptop GPU (12 GB), FLUX.2 klein 4B makes a 1360×768 image in 2.6 to 2.8 s and a
|
|
172
|
+
`--quality fast` image (720×720) in 1.3 to 2.0 s once it's loaded.
|
|
173
|
+
|
|
174
|
+
|
|
126
175
|
On an RTX 4080 Laptop GPU (12 GB) with 32 GB of RAM, ComfyUI 0.38, 1024×1024, four different
|
|
127
176
|
prompts each:
|
|
128
177
|
|
|
@@ -81,6 +81,7 @@ reasoning is on.
|
|
|
81
81
|
| `/think [on\|off]` | Toggle reasoning |
|
|
82
82
|
| `/verbose` | Toggle showing the reasoning text |
|
|
83
83
|
| `/mode [code\|image\|video]` | Switch what lcode makes: [code, images or videos](modes.md) |
|
|
84
|
+
| `/options`, `/again`, `/vary N`, `/gallery`, `/open N` | Image mode: [options for the session, follow-ups on results](modes.md#results) |
|
|
84
85
|
| `/permissions [ask\|plan\|auto-edit\|yolo]` | Set the permission mode |
|
|
85
86
|
| `/cd DIR` | Change the working directory |
|
|
86
87
|
| `/todos` | Show the model's task list |
|
|
@@ -309,6 +309,7 @@ class Settings:
|
|
|
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
311
|
image_model: str = "flux2-klein-4b" # the media catalog's model image mode uses (lcode.media_catalog)
|
|
312
|
+
preview: str = "auto" # images in the terminal: auto, kitty, iterm, blocks or off (lcode.preview)
|
|
312
313
|
num_batch: int | None = None
|
|
313
314
|
keep_alive: str = "30m"
|
|
314
315
|
think: bool = True
|
|
@@ -375,6 +376,11 @@ class Agent:
|
|
|
375
376
|
self.interactive = True # False for `lcode -p`: no end-of-session questions
|
|
376
377
|
self.comfy_process = None # the ComfyUI lcode started for image and video mode, stopped when it exits
|
|
377
378
|
self.no_coding_model = "" # why there's no coding model, when lcode started in image or video mode without
|
|
379
|
+
from lcode.imageopts import Options as ImageOptions
|
|
380
|
+
|
|
381
|
+
self.image_options = ImageOptions() # image mode's defaults for this session (/options)
|
|
382
|
+
self.media_results: list[dict] = [] # the session's images, numbered (/gallery, /vary, /open)
|
|
383
|
+
self.last_request: tuple[str, ImageOptions] | None = None # for /again
|
|
378
384
|
self.reflected = 0 # messages before this index were already checked for notes to remember
|
|
379
385
|
self._memory: memory_notes.Memory | None = None
|
|
380
386
|
self._agent_types: tuple[Path, dict[str, subagents.AgentType], list[str]] | None = None
|
|
@@ -679,8 +685,10 @@ class Agent:
|
|
|
679
685
|
return any(m.get("role") == "user" for m in self.messages)
|
|
680
686
|
|
|
681
687
|
def save(self) -> None:
|
|
682
|
-
if not (self.has_conversation() or self.session_name):
|
|
688
|
+
if not (self.has_conversation() or self.session_name or self.media_results):
|
|
683
689
|
return # nothing worth resuming
|
|
690
|
+
if not self.session_title and not self.has_conversation() and self.media_results:
|
|
691
|
+
self.session_title = " ".join(self.media_results[0]["prompt"].split())[:60] # an image session
|
|
684
692
|
self.session_title = self.session_title or sessions.title_from(self.messages)
|
|
685
693
|
f = self.session_file()
|
|
686
694
|
f.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -689,6 +697,7 @@ class Agent:
|
|
|
689
697
|
"model": self.settings.model,
|
|
690
698
|
"mode": self.settings.mode,
|
|
691
699
|
"image_model": self.settings.image_model,
|
|
700
|
+
"media": {"results": self.media_results, "options": self.image_options.to_json()},
|
|
692
701
|
"name": self.session_name,
|
|
693
702
|
"title": self.session_title,
|
|
694
703
|
"messages": self.messages,
|
|
@@ -739,6 +748,9 @@ class Agent:
|
|
|
739
748
|
self.settings.mode = data["mode"] # a session belongs to the mode it was made in
|
|
740
749
|
if isinstance(data.get("image_model"), str):
|
|
741
750
|
self.settings.image_model = data["image_model"]
|
|
751
|
+
media = data.get("media") or {}
|
|
752
|
+
self.media_results = [r for r in media.get("results") or [] if isinstance(r, dict) and "path" in r]
|
|
753
|
+
self.image_options = type(self.image_options).from_json(media.get("options") or {})
|
|
742
754
|
self.session_title = data.get("title") or sessions.title_from(self.messages)
|
|
743
755
|
self.tools.read_mtimes.clear() # files may have changed since; the model must read them again
|
|
744
756
|
self.tools.todos = list(data.get("todos") or [])
|
|
@@ -201,6 +201,7 @@ def open_agent(cwd: Path, options: Options, console: Console, hw: Hardware | Non
|
|
|
201
201
|
comfyui_path=cfg["comfyui_path"],
|
|
202
202
|
media_dir=cfg["media_dir"],
|
|
203
203
|
image_model=cfg["image_model"],
|
|
204
|
+
preview=cfg["preview"],
|
|
204
205
|
web=switch(options.web, cfg["web"], "on"),
|
|
205
206
|
search_backend=cfg["search_backend"],
|
|
206
207
|
searxng_url=cfg["searxng_url"],
|
|
@@ -812,7 +812,7 @@ def media_request(agent, prompt: str, out: Console, quiet: bool) -> None:
|
|
|
812
812
|
api.close_agent(agent)
|
|
813
813
|
sys.exit(2)
|
|
814
814
|
try:
|
|
815
|
-
made = media.
|
|
815
|
+
made = media.request(agent, prompt)
|
|
816
816
|
except (media.MediaError, KeyboardInterrupt) as e:
|
|
817
817
|
message = str(e) or "Cancelled."
|
|
818
818
|
if quiet:
|
|
@@ -826,12 +826,13 @@ def media_request(agent, prompt: str, out: Console, quiet: bool) -> None:
|
|
|
826
826
|
"type": "result",
|
|
827
827
|
"status": "success",
|
|
828
828
|
"mode": "image",
|
|
829
|
-
"files": [str(path) for path in
|
|
830
|
-
"model": made.model,
|
|
831
|
-
"seed": made.seed,
|
|
832
|
-
"
|
|
833
|
-
"
|
|
834
|
-
"
|
|
829
|
+
"files": [str(path) for one in made for path in one.files],
|
|
830
|
+
"model": made[0].model,
|
|
831
|
+
"seed": made[0].seed,
|
|
832
|
+
"seeds": [one.seed for one in made],
|
|
833
|
+
"width": made[0].width,
|
|
834
|
+
"height": made[0].height,
|
|
835
|
+
"seconds": round(sum(one.seconds for one in made), 1),
|
|
835
836
|
}
|
|
836
837
|
print(json.dumps(result, ensure_ascii=False), flush=True)
|
|
837
838
|
api.close_agent(agent)
|
|
@@ -560,13 +560,15 @@ def describe_history_error(entry: dict) -> str:
|
|
|
560
560
|
|
|
561
561
|
|
|
562
562
|
def save(server: Server, outputs: dict, folder: Path, stem: str, meta: dict) -> list[Path]:
|
|
563
|
-
"""Fetch the job's files into `folder`: stem-
|
|
563
|
+
"""Fetch the job's files into `folder`: stem.png (stem-2.png if that's taken), or stem-1.png, … for several,
|
|
564
|
+
each with its settings in a .json next to it."""
|
|
564
565
|
folder.mkdir(parents=True, exist_ok=True)
|
|
565
566
|
items = [item for node in outputs.values() for kind in OUTPUT_KINDS for item in node.get(kind, [])]
|
|
567
|
+
items = [it for it in items if it.get("type", "output") == "output"]
|
|
566
568
|
saved = []
|
|
567
|
-
for i, item in enumerate(
|
|
569
|
+
for i, item in enumerate(items, 1):
|
|
568
570
|
ext = Path(item["filename"]).suffix or ".png"
|
|
569
|
-
path = unique(folder / f"{stem}-{i}{ext}")
|
|
571
|
+
path = unique(folder / (f"{stem}{ext}" if len(items) == 1 else f"{stem}-{i}{ext}"))
|
|
570
572
|
data = server.fetch(item)
|
|
571
573
|
if ext.lower() == ".png":
|
|
572
574
|
data = with_text(data, "lcode", json.dumps(meta, ensure_ascii=False))
|
|
@@ -46,6 +46,7 @@ SETTINGS: dict[str, tuple[object, type, str]] = {
|
|
|
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
48
|
"image_model": ("flux2-klein-4b", str, "the model image mode uses: a key from `lcode media`"),
|
|
49
|
+
"preview": ("auto", str, "images in the terminal: auto | kitty | iterm | blocks (colour text) | off"),
|
|
49
50
|
"think": (True, bool, "let the model reason before answering (slower, better)"),
|
|
50
51
|
"web": ("on", str, "web search and page fetching for the model: on | ask | off"),
|
|
51
52
|
"search_backend": ("auto", str, "auto | ollama | brave | tavily | searxng (keys come from environment variables)"),
|
|
@@ -211,6 +212,11 @@ def coerce(key: str, value: object) -> object:
|
|
|
211
212
|
raise ConfigError(f"permission_mode must be one of {', '.join(PERMISSION_MODES)}")
|
|
212
213
|
if key == "mode" and value not in WORK_MODES:
|
|
213
214
|
raise ConfigError(f"mode must be one of {', '.join(WORK_MODES)}")
|
|
215
|
+
if key == "preview":
|
|
216
|
+
from lcode.preview import PROTOCOLS
|
|
217
|
+
|
|
218
|
+
if value not in PROTOCOLS:
|
|
219
|
+
raise ConfigError(f"preview must be one of {', '.join(PROTOCOLS)}")
|
|
214
220
|
if key == "image_model":
|
|
215
221
|
from lcode import media_catalog
|
|
216
222
|
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Image options: how many, what shape, which seed, how good, what to avoid, which style.
|
|
2
|
+
|
|
3
|
+
They're typed in the request (`a red fox --n 4 --ar 16:9 --seed 42 --quality best --style photo --no "people"`), or
|
|
4
|
+
set for the session with `/options n=4 ar=16:9`. Flags in a request win over the session's options for that request.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import math
|
|
10
|
+
import re
|
|
11
|
+
import shlex
|
|
12
|
+
from dataclasses import asdict, dataclass, replace
|
|
13
|
+
|
|
14
|
+
QUALITIES = ("fast", "balanced", "best")
|
|
15
|
+
MAX_COUNT = 8
|
|
16
|
+
SIZES = { # names for aspect ratios
|
|
17
|
+
"square": "1:1",
|
|
18
|
+
"portrait": "3:4",
|
|
19
|
+
"landscape": "4:3",
|
|
20
|
+
"wide": "16:9",
|
|
21
|
+
"tall": "9:16",
|
|
22
|
+
"photo": "3:2",
|
|
23
|
+
"cinema": "21:9",
|
|
24
|
+
}
|
|
25
|
+
STYLES = {
|
|
26
|
+
"photo": "professional photograph, natural light, sharp focus, 35mm lens",
|
|
27
|
+
"cinematic": "cinematic film still, dramatic lighting, shallow depth of field, color graded",
|
|
28
|
+
"anime": "anime illustration, clean line art, vibrant cel shading",
|
|
29
|
+
"watercolor": "watercolor painting, soft washes, visible paper texture",
|
|
30
|
+
"oil": "oil painting, visible brush strokes, rich colors",
|
|
31
|
+
"pixel": "pixel art, 16-bit, crisp pixels, limited palette",
|
|
32
|
+
"3d": "3D render, soft global illumination, studio lighting",
|
|
33
|
+
"sketch": "pencil sketch, graphite, cross-hatching, on paper",
|
|
34
|
+
"comic": "comic book art, bold ink outlines, halftone shading",
|
|
35
|
+
"flat": "flat vector illustration, minimal, bold shapes",
|
|
36
|
+
}
|
|
37
|
+
ALIASES = {
|
|
38
|
+
"n": "n",
|
|
39
|
+
"count": "n",
|
|
40
|
+
"ar": "size",
|
|
41
|
+
"aspect": "size",
|
|
42
|
+
"size": "size",
|
|
43
|
+
"seed": "seed",
|
|
44
|
+
"quality": "quality",
|
|
45
|
+
"q": "quality",
|
|
46
|
+
"no": "negative",
|
|
47
|
+
"negative": "negative",
|
|
48
|
+
"style": "style",
|
|
49
|
+
}
|
|
50
|
+
FLAG = re.compile(r"(?:^|\s)--?(" + "|".join(sorted(ALIASES, key=len, reverse=True)) + r")\b(?=[\s=]|$)")
|
|
51
|
+
DIMENSIONS = re.compile(r"^(\d{2,4})\s*[x×]\s*(\d{2,4})$")
|
|
52
|
+
RATIO = re.compile(r"^(\d{1,2})\s*:\s*(\d{1,2})$")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class OptionError(ValueError):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class Options:
|
|
61
|
+
n: int = 1
|
|
62
|
+
size: str = "1:1" # an aspect ratio (16:9), a name (wide) or pixels (1344x768)
|
|
63
|
+
seed: int | None = None # None: a new random seed for each image
|
|
64
|
+
quality: str = "balanced"
|
|
65
|
+
negative: str = ""
|
|
66
|
+
style: str = ""
|
|
67
|
+
|
|
68
|
+
def describe(self) -> str:
|
|
69
|
+
seed = "random" if self.seed is None else str(self.seed)
|
|
70
|
+
parts = [f"n={self.n}", f"size={self.size}", f"seed={seed}", f"quality={self.quality}"]
|
|
71
|
+
if self.style:
|
|
72
|
+
parts.append(f"style={self.style}")
|
|
73
|
+
if self.negative:
|
|
74
|
+
parts.append(f"no={self.negative!r}")
|
|
75
|
+
return " ".join(parts)
|
|
76
|
+
|
|
77
|
+
def to_json(self) -> dict:
|
|
78
|
+
return asdict(self)
|
|
79
|
+
|
|
80
|
+
@classmethod
|
|
81
|
+
def from_json(cls, data: dict) -> Options:
|
|
82
|
+
known = {k: v for k, v in (data or {}).items() if k in cls.__dataclass_fields__}
|
|
83
|
+
try:
|
|
84
|
+
return set_values(cls(), known)
|
|
85
|
+
except OptionError:
|
|
86
|
+
return cls()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def set_values(options: Options, values: dict[str, object]) -> Options:
|
|
90
|
+
"""A copy of `options` with `values` (by name or alias) applied. Raises OptionError saying what's wrong."""
|
|
91
|
+
out = replace(options)
|
|
92
|
+
for raw_key, raw in values.items():
|
|
93
|
+
key = ALIASES.get(str(raw_key).lower().lstrip("-"))
|
|
94
|
+
if key is None:
|
|
95
|
+
raise OptionError(f"unknown option {raw_key!r}: use n, ar (or size), seed, quality, style or no")
|
|
96
|
+
value = str(raw).strip() if raw is not None else ""
|
|
97
|
+
if key == "n":
|
|
98
|
+
if not value.isdigit() or not 1 <= int(value) <= MAX_COUNT:
|
|
99
|
+
raise OptionError(f"n is how many images: 1 to {MAX_COUNT}")
|
|
100
|
+
out.n = int(value)
|
|
101
|
+
elif key == "size":
|
|
102
|
+
out.size = check_size(value)
|
|
103
|
+
elif key == "seed":
|
|
104
|
+
if value.lower() in ("random", "none", ""):
|
|
105
|
+
out.seed = None
|
|
106
|
+
elif value.isdigit() and int(value) < 2**63:
|
|
107
|
+
out.seed = int(value)
|
|
108
|
+
else:
|
|
109
|
+
raise OptionError("seed is a whole number, or random")
|
|
110
|
+
elif key == "quality":
|
|
111
|
+
if value.lower() not in QUALITIES:
|
|
112
|
+
raise OptionError(f"quality is {', '.join(QUALITIES)}")
|
|
113
|
+
out.quality = value.lower()
|
|
114
|
+
elif key == "style":
|
|
115
|
+
if value.lower() in ("none", "off", ""):
|
|
116
|
+
out.style = ""
|
|
117
|
+
elif value.lower() in STYLES:
|
|
118
|
+
out.style = value.lower()
|
|
119
|
+
else:
|
|
120
|
+
raise OptionError(f"style is one of {', '.join(STYLES)} (or none)")
|
|
121
|
+
elif key == "negative":
|
|
122
|
+
out.negative = "" if value.lower() in ("none", "off") else value
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def check_size(value: str) -> str:
|
|
127
|
+
v = value.lower().replace(" ", "")
|
|
128
|
+
if v in SIZES:
|
|
129
|
+
return v
|
|
130
|
+
if RATIO.match(v):
|
|
131
|
+
w, h = (int(x) for x in RATIO.match(v).groups())
|
|
132
|
+
if w and h and 1 / 4 <= w / h <= 4:
|
|
133
|
+
return f"{w}:{h}"
|
|
134
|
+
m = DIMENSIONS.match(v)
|
|
135
|
+
if m:
|
|
136
|
+
w, h = (int(x) for x in m.groups())
|
|
137
|
+
if 256 <= w <= 2048 and 256 <= h <= 2048:
|
|
138
|
+
return f"{w}x{h}"
|
|
139
|
+
raise OptionError("a size in pixels is 256 to 2048 on each side, e.g. 1344x768")
|
|
140
|
+
raise OptionError(f"size is a ratio (16:9), pixels (1344x768) or one of {', '.join(SIZES)}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def parse(text: str, defaults: Options) -> tuple[str, Options]:
|
|
144
|
+
"""The prompt without its flags, and the options for this request. Raises OptionError."""
|
|
145
|
+
matches = list(FLAG.finditer(text))
|
|
146
|
+
if not matches:
|
|
147
|
+
return text.strip(), defaults
|
|
148
|
+
prompt = text[: matches[0].start()]
|
|
149
|
+
values: dict[str, str] = {}
|
|
150
|
+
for i, m in enumerate(matches):
|
|
151
|
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(text)
|
|
152
|
+
rest = text[m.end() : end].strip().lstrip("=").strip()
|
|
153
|
+
try:
|
|
154
|
+
words = shlex.split(rest)
|
|
155
|
+
except ValueError:
|
|
156
|
+
words = rest.split()
|
|
157
|
+
key = ALIASES[m.group(1).lower()]
|
|
158
|
+
if key == "negative":
|
|
159
|
+
values[key] = " ".join(words)
|
|
160
|
+
else:
|
|
161
|
+
if not words:
|
|
162
|
+
raise OptionError(f"--{m.group(1)} needs a value")
|
|
163
|
+
values[key] = words[0]
|
|
164
|
+
if len(words) > 1: # words after a flag's value belong to the prompt
|
|
165
|
+
prompt += " " + " ".join(words[1:])
|
|
166
|
+
return prompt.strip(), set_values(defaults, values)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def parse_assignments(text: str) -> dict[str, str]:
|
|
170
|
+
"""`n=4 ar=16:9 no="blurry, text"` -> {"n": "4", "ar": "16:9", "no": "blurry, text"}."""
|
|
171
|
+
try:
|
|
172
|
+
words = shlex.split(text)
|
|
173
|
+
except ValueError as e:
|
|
174
|
+
raise OptionError(f"can't read {text!r}: {e}") from e
|
|
175
|
+
out = {}
|
|
176
|
+
for word in words:
|
|
177
|
+
key, sep, value = word.partition("=")
|
|
178
|
+
if not sep:
|
|
179
|
+
raise OptionError(f"write options as name=value, e.g. n=4 ar=16:9 (not {word!r})")
|
|
180
|
+
out[key] = value
|
|
181
|
+
return out
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def dimensions(size: str, megapixels: float, multiple: int = 16) -> tuple[int, int]:
|
|
185
|
+
"""Width and height in pixels: from a ratio and the quality's megapixels, or exact pixels as given."""
|
|
186
|
+
m = DIMENSIONS.match(size)
|
|
187
|
+
if m:
|
|
188
|
+
w, h = (int(x) for x in m.groups())
|
|
189
|
+
return round_to(w, multiple), round_to(h, multiple)
|
|
190
|
+
ratio = SIZES.get(size, size)
|
|
191
|
+
w, h = (int(x) for x in ratio.split(":"))
|
|
192
|
+
area = megapixels * 1024 * 1024
|
|
193
|
+
width = math.sqrt(area * w / h)
|
|
194
|
+
return round_to(width, multiple), round_to(width * h / w, multiple)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def round_to(value: float, multiple: int) -> int:
|
|
198
|
+
return max(multiple, round(value / multiple) * multiple)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def replace_seed(options: Options, n: int | None = None) -> Options:
|
|
202
|
+
"""The options with a random seed again (for /again and /vary), and optionally another count."""
|
|
203
|
+
return replace(options, seed=None, n=n or options.n)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def styled(prompt: str, style: str) -> str:
|
|
207
|
+
return f"{prompt}, {STYLES[style]}" if style else prompt
|