Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ Turn a character, style, or concept into a ready-to-train LoRA dataset. One refe

_Last verified: 2026-10-08_

- **Status:** in active development; main is v0.18.0 (git tag `v0.18.0`). **Every version bump must ship a GitHub Release** or the in-app update check never fires. The project was renamed twice (lora-dataset-studio → lora-distillery → dataset-deviser); older references under the previous names are stale.
- **Works:** all five stages end to end (preprocess → generate & curate → caption → export → train config); the three dataset types (character, style, concept) with their own shot plans and caption framing; local and cloud paths for every stage; gallery curation with selection carried forward between stages, including shift-click range select; advisory dedupe, quality flags and caption lint; trainer configs for ai-toolkit (incl. SDXL), kohya sd-scripts, musubi-tuner and Fizgig; opt-in private Hugging Face publish. Since 0.15.0: per-image fault isolation in ① (one bad source never ends the batch), a cooperative ⏹ Stop across ①/②/③ with documented resume paths, translated ComfyUI failures (missing node, bad input, unreachable server) and an in-app 🩺 setup check. Since 0.15.1: a ② **shot style** (default = keep the reference's own medium; eight presets + custom text) threaded into both prompt builders, the CLI, ④ metadata and ⑤'s sample prompt, a final-prompt preview, and a prev/next/save-and-next caption editor with the image on screen. Since 0.16.0: **one name box and one trigger box in the header** that ②/③/④/⑤ all read (they used to be per-tab and went stale), ④ stating the pair it stamped, an opt-in ④ **hand-off to [Idiot LoRa Builder](https://github.com/Fablestarexpanse/Idiot-Lora-Builder)** (writes its `.lora-studio/ratings.json` so its grid opens pre-triaged — nothing is launched), a ⑤ advisory when the dataset can't fill the training resolution, every bundled ComfyUI model filename overridable from `.env`, and a `cli build` preflight for the local engine. Since 0.17.0: the local ② engine is **Qwen-Image 2.1** (plain-English prompts shared with Gemini, ~26 s/shot, `LDS_QWEN21_*` filenames) instead of Qwen-Image-Edit 2511 + the Multiple-Angles LoRA, whose graph stays in `studio/comfy_workflows/legacy/`; a dead ComfyUI mid-batch now fails one shot, not the run. Since 0.17.1: the UI port is chosen by `_pick_port` in `app.py` — 7861 if bindable, else an OS-assigned free port (Windows reserves a ~500-port block around 7861), or `LDS_PORT` to pin one. Since 0.17.2: `comfy_api.upload_image` downscales a source over 90 MB before upload (ComfyUI answers 413 above its 100 MB `--max-upload-size`). Since 0.17.3: the cloud image model defaults to **`auto`** (Google shut down the `-preview` ids on 2026-06-25; `resolve_image_model` picks from the cached live list, Nano Banana 2.1 first), Groq captions use `qwen/qwen3.8-27b` (key `groq-qwen`, old key aliased), every API image goes through `config.api_image` (true mime, size cap), musubi commands include the cache steps and the per-architecture network module, ai-toolkit gains Qwen-Image 2.1 and a correct Krea 2 (Raw) preset and keeps every checkpoint, ① applies EXIF rotation, no longer 4x-upscales large JPEGs, and can jump a busy ComfyUI queue like ② (`--front` on `preprocess`/`build`). Since 0.18.0: ⑤ counts **epochs** (one checkpoint and one sample set each) with auto repeats sized to a step target and a live exposure line; every config ships a `validation/` pack (8 fixed prompts at seed 42, `validation.md`, a per-epoch score sheet); **Fizgig** is a ⑤ target (Krea 2 / Qwen Image 2.1 / Klein); ① stamps a `dd_source` provenance PNG chunk so ⑤ names upscaled sources and ④ knows real photos; ④ can hold out N real photos in a `-heldout` sibling as a likeness test.
- **In progress:** a phased LoRA best-practice update. 0.19.0 is next: training copies keep their background, identity policy (outfits vary by default), framing tiers, a coverage panel, a full-body warning, near-duplicate/burst flags, an anchor shot, local multi-reference. Then 0.20.0 (a Quick build tab). Nothing from those is half-built on main. The 0.18.0 optimizer dropdown and attention-only toggle were deferred (ARCHITECTURE.md → Future ideas).
- **Status:** in active development; main is v0.18.1 (git tag `v0.18.1`). **Every version bump must ship a GitHub Release** or the in-app update check never fires. The project was renamed twice (lora-dataset-studio → lora-distillery → dataset-deviser); older references under the previous names are stale.
- **Works:** all five stages end to end (preprocess → generate & curate → caption → export → train config); the three dataset types (character, style, concept) with their own shot plans and caption framing; local and cloud paths for every stage; gallery curation with selection carried forward between stages, including shift-click range select; advisory dedupe, quality flags and caption lint; trainer configs for ai-toolkit (incl. SDXL), kohya sd-scripts, musubi-tuner and Fizgig; opt-in private Hugging Face publish. Since 0.15.0: per-image fault isolation in ① (one bad source never ends the batch), a cooperative ⏹ Stop across ①/②/③ with documented resume paths, translated ComfyUI failures (missing node, bad input, unreachable server) and an in-app 🩺 setup check. Since 0.15.1: a ② **shot style** (default = keep the reference's own medium; eight presets + custom text) threaded into both prompt builders, the CLI, ④ metadata and ⑤'s sample prompt, a final-prompt preview, and a prev/next/save-and-next caption editor with the image on screen. Since 0.16.0: **one name box and one trigger box in the header** that ②/③/④/⑤ all read (they used to be per-tab and went stale), ④ stating the pair it stamped, an opt-in ④ **hand-off to [Idiot LoRa Builder](https://github.com/Fablestarexpanse/Idiot-Lora-Builder)** (writes its `.lora-studio/ratings.json` so its grid opens pre-triaged — nothing is launched), a ⑤ advisory when the dataset can't fill the training resolution, every bundled ComfyUI model filename overridable from `.env`, and a `cli build` preflight for the local engine. Since 0.17.0: the local ② engine is **Qwen-Image 2.1** (plain-English prompts shared with Gemini, ~26 s/shot, `LDS_QWEN21_*` filenames) instead of Qwen-Image-Edit 2511 + the Multiple-Angles LoRA, whose graph stays in `studio/comfy_workflows/legacy/`; a dead ComfyUI mid-batch now fails one shot, not the run. Since 0.17.1: the UI port is chosen by `_pick_port` in `app.py` — 7861 if bindable, else an OS-assigned free port (Windows reserves a ~500-port block around 7861), or `LDS_PORT` to pin one. Since 0.17.2: `comfy_api.upload_image` downscales a source over 90 MB before upload (ComfyUI answers 413 above its 100 MB `--max-upload-size`). Since 0.17.3: the cloud image model defaults to **`auto`** (Google shut down the `-preview` ids on 2026-06-25; `resolve_image_model` picks from the cached live list, Nano Banana 2.1 first), Groq captions use `qwen/qwen3.8-27b` (key `groq-qwen`, old key aliased), every API image goes through `config.api_image` (true mime, size cap), musubi commands include the cache steps and the per-architecture network module, ai-toolkit gains Qwen-Image 2.1 and a correct Krea 2 (Raw) preset and keeps every checkpoint, ① applies EXIF rotation, no longer 4x-upscales large JPEGs, and can jump a busy ComfyUI queue like ② (`--front` on `preprocess`/`build`). Since 0.18.0: ⑤ counts **epochs** (one checkpoint and one sample set each) with auto repeats sized to a step target and a live exposure line; every config ships a `validation/` pack (8 fixed prompts at seed 42, `validation.md`, a per-epoch score sheet); **Fizgig** is a ⑤ target (Krea 2 / Qwen Image 2.1 / Klein); ① stamps a `dd_source` provenance PNG chunk so ⑤ names upscaled sources and ④ knows real photos; ④ can hold out N real photos in a `-heldout` sibling as a likeness test. Since 0.18.1: a ⑤ optimizer dropdown, AdamW8bit by default, Prodigy (lr 1.0, its diffusion args) for ai-toolkit/musubi/kohya; Fizgig stays AdamW8bit.
- **In progress:** a phased LoRA best-practice update. 0.19.0 is next: training copies keep their background, identity policy (outfits vary by default), framing tiers, a coverage panel, a full-body warning, near-duplicate/burst flags, an anchor shot, local multi-reference. Then 0.20.0 (a Quick build tab). Nothing from those is half-built on main. The attention-only toggle planned for 0.18.0 was deferred (ARCHITECTURE.md → Future ideas).
- **Known gaps / next steps:** Auto's model order (`IMAGE_MODEL_PREFERENCE`, Nano Banana 2.1 then Pro) rests on one A/B subject — see ARCHITECTURE.md; local ② still sends only the first reference. Otherwise pick up from `docs/ARCHITECTURE.md` → "Future ideas" / "Deferred", and read its *Maintainer principles* before changing anything; `gradio` is pinned `<6` because of a real stuck-loading-overlay regression — unpinning needs that verified fixed upstream; training is never launched for you and is not planned to be; the default export `output_root` writes into the repo root, so a smoke-test export leaves an untracked dataset folder to delete before committing.
- **Deep docs:** `docs/ARCHITECTURE.md` (module map, stage and backend detail, gotchas, backlog — the deep reference), `docs/comfyui-setup.md`. Worklogs under `docs/` are gitignored and local-only by design.

Expand Down
34 changes: 25 additions & 9 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
)
from studio.jobs import JobControl
from studio.shotplan import Shot, apply_prop_exclusion, apply_wardrobe, plan_for_type
from studio.trainer_configs import TRAINER_MODELS, TRAINERS
from studio.trainer_configs import TRAINER_MODELS, TRAINERS, optimizer_choices

TRAINER_CHOICES = [(label, key) for key, label in TRAINERS.items()]

Expand Down Expand Up @@ -1409,7 +1409,8 @@ def on_trainer_change(trainer: str):

p = TRAINER_MODELS[trainer][0]
return (_model_dropdown(trainer), user_config.get_trainer_path(trainer),
p.resolution, p.rank, p.alpha, p.epochs, p.lr, p.batch_size)
p.resolution, p.rank, p.alpha, p.epochs, p.lr, p.batch_size,
gr.update(choices=optimizer_choices(trainer), value="adamw8bit"))


def on_model_change(trainer: str, model_key: str):
Expand Down Expand Up @@ -1515,7 +1516,7 @@ def do_generate_train_config(trainer: str, model_key: str, dataset_dir: str,
multi_res: bool, dataset_type: str = "character",
style_key: str = shot_style.MATCH,
style_text: str = "", project_name: str = "",
repeats=0) -> str:
repeats=0, optimizer: str = "adamw8bit") -> str:
# ⑤'s "LoRA name" is the trained file's name, not the subject's — the one
# identity-ish field that is legitimately its own (people want "-v2"). Blank
# means "follow the header name", which is why it is not auto-filled: an
Expand Down Expand Up @@ -1545,13 +1546,15 @@ def do_generate_train_config(trainer: str, model_key: str, dataset_dir: str,
preset = _preset(trainer, model_key)
buckets = stats.buckets_for(int(resolution)) if multi_res else []
num_repeats = _repeats(trainer, stats, epochs, batch_size, repeats)
if optimizer not in {k for _, k in optimizer_choices(trainer)}:
optimizer = "adamw8bit"
cfg = TrainConfig(
trainer=trainer, model=preset, dataset_dir=ds,
trigger=trigger.strip(), name=(name.strip() or "lora"),
dataset_type=dataset_type,
resolution=int(resolution), rank=int(rank), alpha=int(alpha),
epochs=max(1, int(epochs)), num_repeats=num_repeats, n_images=stats.n_images,
lr=float(lr), batch_size=int(batch_size),
lr=float(lr), optimizer=optimizer, batch_size=int(batch_size),
buckets=buckets, shot_style=style_key, shot_style_text=style_text)
try:
written, command = write_configs(cfg, install_path.strip())
Expand All @@ -1561,14 +1564,21 @@ def do_generate_train_config(trainer: str, model_key: str, dataset_dir: str,
user_config.set_last_train_settings({
"trainer": trainer, "model": model_key, "resolution": int(resolution),
"rank": int(rank), "alpha": int(alpha), "epochs": cfg.epochs,
"repeats": int(repeats or 0), "lr": float(lr), "batch_size": int(batch_size)})
"repeats": int(repeats or 0), "lr": float(lr), "optimizer": optimizer,
"batch_size": int(batch_size)})
files = "\n".join(str(p) for p in written)
bucket_note = (f"\nBuckets: {buckets} (from the dataset's actual sizes)"
if buckets else f"\nSingle bucket at {int(resolution)}px")
bucket_note += stats.upscale_note(int(resolution))
exposure = exposure_line(trainer, str(ds), cfg.epochs, num_repeats, batch_size)
caveat = ("\n\n📋 validation/validation.md says how to pick the best epoch from "
"the per-epoch samples.")
if optimizer == "prodigy":
caveat += ("\n\nProdigy finds its own step size, so the config uses learning rate "
"1.0 and the Learning rate field is ignored.")
if trainer in ("musubi", "kohya"):
caveat += (f" {trainer} doesn't install Prodigy: run `pip install prodigyopt` in "
"its environment first.")
if trainer in ("musubi", "fizgig"):
caveat += (f"\n\n⚠️ {trainer} needs your local model paths — fill the "
"<<FILL: …>> placeholders in the command before running.")
Expand Down Expand Up @@ -2247,8 +2257,14 @@ def _check_for_update():
tr_repeats = gr.Number(value=0, precision=0, minimum=0,
label="Repeats (0 = auto)",
info="Auto sizes epochs to the target steps.")
tr_lr = gr.Number(value=_ai_presets[0].lr, label="Learning rate",
info="1e-4 is a common starting point.")
with gr.Row():
tr_lr = gr.Number(value=_ai_presets[0].lr, label="Learning rate",
info="1e-4 is a common starting point.")
tr_optimizer = gr.Dropdown(
optimizer_choices("ai-toolkit"), value="adamw8bit",
label="Optimizer",
info="AdamW8bit is every trainer's tested default. Prodigy "
"picks its own step size and ignores the learning rate.")
tr_exposure = gr.Markdown()
tr_multi_res = gr.Checkbox(
value=True, label="Multi-resolution buckets",
Expand Down Expand Up @@ -2447,7 +2463,7 @@ def _cap_cost(key: str, model: str, selected: list[str]) -> str:

tr_hparams = [tr_res, tr_rank, tr_alpha, tr_epochs, tr_lr, tr_batch]
tr_trainer.change(on_trainer_change, [tr_trainer],
[tr_model, tr_path] + tr_hparams)
[tr_model, tr_path] + tr_hparams + [tr_optimizer])
tr_model.change(on_model_change, [tr_trainer, tr_model], tr_hparams)
tr_save_path.click(save_trainer_path, [tr_trainer, tr_path], [tr_path_note])
btn_inspect.click(inspect_dataset, [tr_dataset, dataset_type], [tr_stats])
Expand All @@ -2458,7 +2474,7 @@ def _cap_cost(key: str, model: str, selected: list[str]) -> str:
tr_gen.click(do_generate_train_config,
[tr_trainer, tr_model, tr_dataset, tr_path, tr_name, project_trigger]
+ tr_hparams + [tr_multi_res, dataset_type, gen_style, gen_style_text,
project_name, tr_repeats],
project_name, tr_repeats, tr_optimizer],
[tr_result])

demo.load(_check_for_update, None, update_notice)
Expand Down
20 changes: 14 additions & 6 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Architecture

Version: 0.18.0
Version: 0.18.1

```
app.py Gradio UI — thin wiring over the stage functions (5 tabs).
Expand Down Expand Up @@ -921,6 +921,15 @@ quietly gets someone else's value.
them. Only photos with provenance qualify (a generated shot is no test of likeness) and at
least one image always stays in training. The dataset naming loop skips a name whose
`-heldout` sibling exists, so a stale held-out folder never pairs with a new dataset.
- **The optimizer defaults to AdamW8bit; Prodigy is the one alternative (0.18.1).** AdamW8bit is
the default and the validated recipe in ai-toolkit's UI, musubi's examples, sd-scripts and
Fizgig. Prodigy is offered because it removes the learning-rate guess: it runs at lr 1.0 with
its README's diffusion advice (`weight_decay=0.01, use_bias_correction, safeguard_warmup`).
ai-toolkit ships `prodigyopt`; sd-scripts (`Prodigy`) and musubi (`prodigyopt.Prodigy`, its
module-path form) need `pip install prodigyopt`, which ⑤ says. Fizgig is AdamW8bit only: it
removed Prodigy because a self-tuning LR fights its adaptive LR. Adafactor, Lion and Automagic
are left out: for a LoRA the optimizer state is small, so their memory savings buy little, and
each needs its own LR recipe.
- **Fizgig is a ⑤ target, not a launch (0.18.0).** It reads musubi's `dataset.toml` shape, so
`render_musubi_toml` serves both; `arch` holds its `--family` (`krea2` / `qwen_image21` /
`klein`). The command is its three `cache`/`train` steps from `docs/CLI.md` with `<<FILL>>`
Expand Down Expand Up @@ -1201,11 +1210,10 @@ ordered by benefit-to-cost.
⑤'s bucket line (idea borrowed from Idiot LoRa Builder's crop tool, which shows the same verdict
per crop). The *transforming* half (pad-to-square / center-crop) is still open and still wants its
own design pass: it mutates pixels, and the advisory covers the case where knowing is enough.
- **⑤ optimizer choice and attention-only training (planned for 0.18.0, deferred).** An
"Advanced" accordion with an optimizer dropdown (AdamW8bit default; Prodigy at lr 1.0, hidden
for Fizgig, which removed it; Automagic where the trainer has it) and a musubi/Fizgig
attention-only toggle via `--network_args "exclude_patterns=[…]"`. Low value for the target
user, who should not need either; add when someone asks.
- **Attention-only training (⑤, planned for 0.18.0, deferred).** A musubi/Fizgig toggle that
puts the LoRA on the attention projections only (via `--network_args "exclude_patterns=[…]"`),
leaving the feed-forward layers, which hold more of the textures, clothes and backgrounds, alone.
Evidence for a likeness gain is anecdotal and per-model; add when an A/B shows it.
- **Fizgig look-score report (⑤).** Fizgig writes `fizgig_look_scores.json` per run; reading it
back into a per-epoch report beside `validation_scores.csv` would close the pick-the-epoch loop.
- **Exact CLIP token count (③/④).** The 77-token warning is a tokenizer-free estimate; loading the
Expand Down
2 changes: 1 addition & 1 deletion studio/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Dataset Deviser: turn a character, style, or concept into a ready-to-train LoRA dataset."""

__version__ = "0.18.0"
__version__ = "0.18.1"
Loading
Loading