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>