- Drop unused `import json` from new test module (F401).
- Remove unused `# noqa: BLE001` directives — project ruff config doesn't
enable BLE001 so the suppressions were dead weight (RUF100 x3).
- Replace `×` (U+00D7) with ASCII `x` in console output (RUF001).
- Collapse seed-strategy if/else into ternary (SIM108).
- Use `enumerate(as_completed(...), start=1)` for completion counter
instead of manual `completed = 0; completed += 1` (SIM113).
- Run `ruff format` on touched files.
Pre-existing lint errors on master (PLC0415/PLR0915/SIM113 in unrelated
commands) are untouched — separate cleanup PR if desired. Net delta of
this branch over master: 0 new lint errors.
All 374 tests still passing.
Mirrors the style-sweep --parallel-queue flag on the `generate` command.
When used with --count N > 1, splits the request into N independent
batch_size=1 jobs queued P-at-a-time via ThreadPoolExecutor instead of
a single ComfyUI batch.
Each task receives a distinct seed (incrementing from --seed when set,
freshly randomized per task when --seed=-1) and a distinct output path
following the existing stem_NNN.suffix convention. The GPU still
processes one prompt at a time, but HTTP queueing, websocket polling,
and image-download phases pipeline across tasks for a meaningful
wall-clock speedup on warmed-up models (~30-50% in practice).
Implementation notes:
- count=1 always takes the legacy sequential path regardless of -P.
- -P 1 is also sequential — identical behavior to pre-flag invocations.
- Bare model names (`-m lust_v10`) are resolved to canonical filenames
ONCE in the parent before fanout, so worker tasks (which run with
json_output=True path semantics for stdout) don't each duplicate the
validation step or, worse, forward unresolved names to ComfyUI.
- --json + -P>1 is rejected up-front: the JSON path inside _run_generation
short-circuits the disk-save block, which would silently produce zero
files. Better to fail loud than save nothing.
- parallel_queue is plumbed through --input (JSON/YAML) like every other
generate param, with the usual CLI-flag-wins precedence.
Tests: 15 new in tests/test_generate_parallel.py covering validation,
fanout topology, seed strategies, output naming, --input integration,
partial-failure exit code, and a concurrency assertion that confirms
threads actually overlap.
Manual E2E against ComfyUI on sin: -c 3 -P 3 on FLUX produced 3 distinct
images in ~83s vs the ~195s a pure sequential run would take.
`tsr generate --input <file>` previously only understood JSON, which was
awkward for hand-authored template libraries (e.g. ~/Projects/draw/templates/
ships *.yml scene files with embedded newlines and unquoted keys that mirror
the `tsr template` output shape).
Behavior:
- Files with .yml / .yaml extension parse as YAML; .json (or unknown
extensions whose first non-whitespace char is '{' or '[') parse as JSON.
- Inline strings starting with '{' still parse as JSON (regression-safe).
- Inline strings without leading '{' now parse as YAML, enabling
`tsr generate --input 'prompt: foo\nmodel: bar.safetensors'` without
shell-quoting a JSON object.
- All downstream key-mapping / CLI-override / character / scene / lora
/ count handling is identical to the JSON path — parsing only differs.
Implementation:
- New `_parse_generate_input(value)` helper in tensors/cli.py centralizes
source detection (file vs inline), format selection (extension or
content sniff), and rich-formatted error reporting via typer.Exit(1).
- The pre-existing inline JSON merge block in `generate` is reduced to a
single call to the helper.
- Adds pyyaml>=6.0 as a runtime dep. It was already transitively pulled
in by huggingface_hub, but we depend on it directly so the surface
contract is explicit and survives a hub re-pin.
- mypy override added for the yaml module (no upstream stubs in tree).
Tests:
- 20 new tests in tests/test_generate_input.py covering inline JSON,
inline YAML, file by extension (.json/.yml/.yaml), unknown extension
content sniffing, non-mapping rejection, malformed input handling,
CLI-flag-wins-over-input precedence, and a full smoke against the
exact draw template shape (with embedded newlines in the scene list).
- 359 -> 379 total tests. Lint clean on changed lines.
Co-Authored-By: OpenCode <noreply@anomaly.co>
Extracts the per-name YAML-list storage logic out of tensors/characters.py
into a new generic tensors.fragments.FragmentLibrary class, then layers two
kind-specific function-style modules on top:
- tensors.characters (unchanged public API, now thin wrapper)
- tensors.scenes (new): save_scene / load_scene / list_scenes / delete_scene /
scene_path / resolve_scene, stored in ~/.local/share/tensors/scenes/
CLI additions:
- tsr scene save -o <name> 'elem1, elem2, ...'
- tsr scene list / show / delete
Wired into generate + template + style-sweep:
- tsr generate -S <name> / --scene-prompt 'elems' injects scene elements
into the positive prompt right after the character block (order:
quality_prefix -> character -> scene -> rating -> user)
- tsr template -S <name> / --scene-prompt 'elems' embeds a 'scene' list
field in the dumped JSON template (named + inline merged, deduped)
- style-sweep templates accept 'scene' (str|list) and 'scene_prompt' keys
- generate --input JSON honors both 'scene' (str=name or list=inline)
and 'scene_prompt' keys
FragmentLibrary uses kind-aware error messages (e.g. "Invalid scene name"
vs "Invalid character name") so users see the right context.
28 new tests cover the generic library + scenes. Bumps version to 0.1.24
(supersedes v0.1.23 which failed to publish due to a transient Sigstore
network error).
Adds a 'character' subsystem for reusable prompt fragments stored as YAML
lists in ~/.local/share/tensors/characters/<name>.yml.
New tensors.characters module:
- save_character / load_character / list_characters / delete_character
- parse_elements + resolve_character (named + inline merge with dedup)
- Path-traversal-safe name validation ([A-Za-z0-9_.-]+)
- JSON-encoded YAML scalars on write; tolerant reader (JSON, single-quoted YAML, bare)
New CLI:
- tsr character save -o <name> 'elem1, elem2, ...'
- tsr character list / show / delete
Wired into generate + template + style-sweep:
- tsr generate -C <name> / --character-prompt 'elems' injects character
elements into the positive prompt (after quality_prefix, before user prompt)
- tsr template -C <name> / --character-prompt 'elems' embeds a 'character'
list field in the dumped JSON template (named + inline merged, deduped)
- style-sweep templates accept 'character' (str|list) and 'character_prompt'
keys; lists are passed through verbatim, names are looked up at run-time
- generate --input JSON honors both 'character' (str=name or list=inline)
and 'character_prompt' keys
37 new tests cover module behavior. Bumps version to 0.1.23.
Adds client-side concurrent queueing to style-sweep. -P N submits N
prompts to ComfyUI's HTTP queue concurrently via ThreadPoolExecutor.
The GPU still processes one prompt at a time (ComfyUI's queue is
single-worker), but the HTTP submission, websocket polling, image
download, and disk-write phases pipeline with the next prompt's
submission.
Expected speedup: 5-15% on a typical Flux sweep where per-image GPU
time is ~25-30s and overhead is ~3-5s. Real benefit grows with
slower networks or larger images.
Design choices:
- Default P=1 preserves the exact existing sequential behavior and
log output (no "(submit #N)" suffix in messages).
- P>1 uses ThreadPoolExecutor.as_completed for completion-order
reporting; the manifest is re-sorted to source-list order after.
- Skip-existing + dry-run cases are handled synchronously before the
executor even starts (no point pipelining no-ops).
- --abort-on-error is incompatible with parallelism (can't reliably
stop in-flight workers); we warn and continue.
- Per-task console output WILL interleave under -P>1 because
_run_generation prints its own progress; users are pointed at the
manifest for clean per-slug timing.
Why not full async multi-GPU-workflow parallelism:
- ComfyUI processes its queue strictly sequentially; we can't
actually run two Flux UNets concurrently without a second ComfyUI
instance, second port, second model dir, etc.
- Even with two instances on one GPU, the CUDA cores time-slice and
you get ~1.1x not 2x.
- Memory math is tighter than it looks even on Spark's 80GB unified
pool: two Flux dev instances = 64GB fixed before any activations.
- Maintenance burden is real; speed gain is marginal.
Client-side pipelining gets the practical wins (overhead hiding,
cleaner progress feedback for long sweeps) without the complexity
or OOM risk.
7 new tests covering: invalid P=0, P=1 equivalence with sequential,
multi-style execution, source-order manifest preservation under
chaotic completion, skip-existing in parallel mode, individual
failure containment, and abort-on-error warning.
267 -> 274 tests.
Catches mismatches between local intent and what's actually loaded on the
ComfyUI host. Replaces ComfyUI's generic 400 'prompt_outputs_failed_validation'
with a clear "model X not available on host — did you mean Y?" suggestion.
Why: when a user types `tsr generate -m getphatFLUXReality_v5Hardcore` but
only v11Softcore is installed, they got a 30-line raw API error buried in
node validation output. Now they get one red line plus three fuzzy-matched
candidates from the actual loader bucket.
Implementation:
- Extends get_loaded_models() in comfyui.py to include the diffusion_models
bucket (UNETLoader -> unet_name). Previously only checkpoints, loras, vae,
clip, controlnet, upscale_models were exposed.
- New _validate_model_available() helper in cli.py runs after family
detection, before prompt enhancement. Maps family -> loader bucket:
flux_unet / flux2_klein -> diffusion_models/, else checkpoints/. Uses
difflib.get_close_matches for the "did you mean" hint.
- Validates LoRA presence too when -l is passed.
- Special hint: if the requested file IS in checkpoints/ but the family
requires diffusion_models/, suggests the symlink command the user needs
to run on the host. Common case for newly-uploaded UNet-only checkpoints.
- Network failures are non-fatal — falls through to let ComfyUI surface
the error itself rather than blocking on a stale endpoint.
- Skipped in --json mode (machine callers) and --remote dispatches (the
server validates remotely).
8 new tests covering: unknown model in checkpoints bucket, unknown in
diffusion_models, flux2_klein routing, happy path, missing LoRA, network
failure, symlink hint, and a source-level check that the
diffusion_models bucket is wired into get_loaded_models.
259 -> 267 tests.
Adds a `flux2_klein` model family for Black Forest Labs' Flux.2 Klein 9B
release. Different architecture from Flux.1 D — required a separate
workflow rather than extending flux_unet.
Architecture differences from Flux.1 D:
- Single Qwen3-8B text encoder via CLIPLoader(type=flux2), producing
12288-dim conditioning (3 stacked hidden layers). NOT DualCLIPLoader.
- EmptyFlux2LatentImage instead of EmptySD3LatentImage (different
latent shape).
- Custom-sampling pipeline: Flux2Scheduler -> SIGMAS, fed into
SamplerCustomAdvanced together with BasicGuider + RandomNoise +
KSamplerSelect. No standalone KSampler node, so the caller's
scheduler argument is ignored.
- Dedicated VAE: flux2-vae.safetensors (not Flux.1's ae.safetensors).
Detection:
- New FLUX2_KLEIN_PATTERNS constant lists known Klein filenames
("lust_", "moodydesire"). _is_flux2_klein() checks base_model field
first ("flux.2 klein" / "flux2 klein") then filename pattern.
- detect_model_family() runs the Klein check BEFORE flux_unet, so
Klein checkpoints that also match UNet-only patterns (lust_v10,
moodyDesireMix) correctly route to flux2_klein.
Affected checkpoints reclassified from flux_unet -> flux2_klein:
- lust_v10.safetensors (Flux.2 Klein 9B-base per CivitAI DB)
- moodyDesireMix_v20PRO.safetensors (Flux.2 Klein 9B)
Still flux_unet (genuinely Flux.1 D UNet-only):
- cyberrealisticFlux_v25, fcFluxPonyPerfectBase,
getphatFLUXReality_v11Softcore.
Required ComfyUI host setup (one-time):
- /home/madcat/comfyui/models/text_encoders/qwen_3_8b_fp8mixed.safetensors
(8.1 GB, from Comfy-Org/vae-text-encorder-for-flux-klein-9b on HF)
- /home/madcat/comfyui/models/vae/flux2-vae.safetensors (321 MB)
Verified end-to-end on madcat: lust_v10 generated successfully through
the new flux2_klein workflow (~85s per image at 1024x1024, 20 steps).
7 new tests; 253 -> 259 total. Existing flux_unet tests retargeted to
genuine Flux.1 D checkpoints (getphat, fcFluxPony, cyberrealisticFlux).
Adds a `flux_unet` model family for Flux.1 D checkpoints that ship only the
UNet weights (no baked-in CLIP/T5/VAE) and require external encoder loading.
Affected checkpoints: lust_v10, cyberrealisticFlux_v25,
getphatFLUXReality_v11Softcore, moodyDesireMix_v20PRO,
fcFluxPonyPerfectBase. These live in `models/diffusion_models/` (or
`models/unet/`) on the ComfyUI host and are loaded via UNETLoader instead
of CheckpointLoaderSimple.
Detection:
- New `FLUX_UNET_ONLY_PATTERNS` constant in config.py lists known
UNet-only filename substrings.
- `detect_model_family()` returns `flux_unet` when any pattern matches,
taking precedence over base_model field (same architecture-override
pattern used for FluxPony hybrids).
Workflow:
- New flux_unet workflow in comfyui.py uses UNETLoader + DualCLIPLoader
(clip_l.safetensors + t5xxl_fp16.safetensors, type=flux) + VAELoader
(ae.safetensors) wired into the same Flux KSampler graph as the
monolithic flux family.
- Family defaults inherit from flux (sampler/scheduler/steps/cfg) with
external_clip flag set.
Smoke-tested end-to-end on madcat with getphatFLUXReality_v11Softcore
producing valid output. Flux.2 Klein checkpoints (lust_v10,
moodyDesireMix) still fail because they require a different text encoder
(qwen_3_8b) — see follow-up commit.
13 new tests; 240 -> 253 total.
--list/-L prints the resolved styles list as a two-column rich table
(slug + suffix truncated to ~80 chars) and exits without generating.
Template becomes optional when --list is paired with an explicit
--styles source, so you can inspect any styles file standalone.
--style/-S SLUG selects a single style by exact slug match; repeatable
for multiple. Unknown slugs error red with the available slug list.
Filter applies before --limit and preserves the source file's order.
Both flags compose with --limit and --dry-run; when filtering down to
a subset, the manifest is still written for the smaller run.
New `tsr style-sweep` command renders one image per style suffix from a
template JSON, composing prompt = template.prompt + ', ' + style.suffix
and writing to {output_dir}/{slug}.png.
- Template JSON mirrors `generate --input` keys plus output_dir + styles.
- Styles source can be a path or inline list/object on either CLI or
template. Relative styles paths in the template resolve against the
template's directory (so templates can ship with their styles file).
- Skips existing outputs by default (--no-skip-existing to force).
- --dry-run prints planned prompts/paths without invoking generate.
- --limit N caps the sweep for fast iteration.
- --continue-on-error keeps going on individual failures; final exit code
is non-zero if any style failed and failed slugs are reported.
- --remote propagates to the underlying generation, same as `generate`.
- Writes a manifest {output_dir}/_sweep.json with per-style results
(slug, prompt, output, seed, duration_sec, success, error).
Delegates to the `_run_generation` helper extracted from `generate`.
Models like gonzalomoXLFluxPony are architecturally Flux but CivitAI
tags them as 'Pony', causing the SDXL workflow to be sent to ComfyUI
which fails validation. The filename now overrides base_model when it
contains 'flux'.
Also adds:
- Full Flux Dev/Schnell workflow template (ModelSamplingFlux,
FluxGuidance, ConditioningZeroOut, EmptySD3LatentImage); KSampler
cfg locked to 1.0, caller cfg routed to FluxGuidance
- --family/-F flag to manually override family detection
- queue_prompt now surfaces ComfyUI node_errors from 400 responses
- Tests for Flux workflow builder (8 cases) and updated family defaults
The CLI download flow only set civitai_model_id/version_id on local_files
without caching the full model payload, so 'tsr db list' joined against
empty models/versions/creators tables and showed every linked file as
'unlinked'. The server's _auto_link_file path had additional bugs:
resolved-vs-unresolved path comparison after rescan, redundant CivitAI
hash lookup, and silent failure swallowed by 'completed' status.
- New Database.register_downloaded_file() consolidates hashing, metadata
storage, FK linking, and cache_model() into a single idempotent call
shared by both CLI and server paths.
- Server _do_download now passes version_info straight through and
surfaces db_file_id/db_linked/db_cached/db_error onto _active_downloads.
- Drops the broken _auto_link_file rescan helper.
- Add sampler/scheduler/steps/vae to MODEL_FAMILY_DEFAULTS for all families
- Add zimage family detection for ZImageTurbo models
- Flux and zimage families use ae.safetensors VAE
- SD 1.5 families use checkpoint built-in VAE
- SDXL families use sdxl_vae.safetensors
- API auto-applies family defaults when request uses default values
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add [paths] section to config.toml for custom model directories
- Add get_model_paths() function that merges config with defaults
- Update get_default_output_path() to check config first
- Add --set-path option to tsr config command
- Update download_routes.py to use centralized path function
- Add tests for path configuration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Unified search at /api/search handles both CivitAI and HuggingFace.
CivitAI routes now only provide:
- /api/civitai/model/{id} - get model by ID
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove ProcessManager and process.py
- Add get_sd_server_url() config (env/config/default)
- Update routes to proxy to external sd-server URL
- Remove model switching (handled by external sd-server)
- Update CLI serve command
- Update tests for new architecture
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace management endpoints (/start, /stop, /restart) with a transparent
reverse proxy and hot-reload architecture. The wrapper now sits in front of
sd-server, forwarding all requests and adding a /reload endpoint for model
swapping without restarting the wrapper itself.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>