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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ HF_TOKEN=
# reserved by Windows (Hyper-V/WSL/Docker). Set it to pin one exactly.
# LDS_PORT=7861
# LDS_OUTPUT_ROOT=./datasets
# LDS_GEMINI_IMAGE_MODEL=gemini-3-pro-image-preview
# LDS_GEMINI_IMAGE_MODEL=auto # auto = best image model your key can use; or pin an id from ② refresh
# LDS_UPDATE_CHECK_ENABLED=true # checks GitHub releases on launch (no data sent); set false to disable

# ComfyUI model filenames, if yours are named differently (see docs/comfyui-setup.md)
Expand Down
10 changes: 5 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ Turn a character, style, or concept into a ready-to-train LoRA dataset. One refe

## Current state

_Last verified: 2026-10-03_
_Last verified: 2026-10-08_

- **Status:** in active development, released at v0.17.2 (git tag `v0.17.2`). **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 and musubi-tuner; 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`).
- **In progress:** nothing half-built — 0.16.0 closed out the hand-off, cross-tab-identity and config-hardening work. `docs/ARCHITECTURE.md` carries the "Future ideas" and "Deferred" sections that hold the real backlog.
- **Known gaps / next steps:** 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.
- **Status:** in active development; main is v0.17.3 (git tag `v0.17.3`). **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 and musubi-tuner; 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`).
- **In progress:** a phased LoRA best-practice update. 0.18.0 is next: epochs as the ⑤ unit with an exposure line, a validation pack, held-out references, a Fizgig trainer target, source-size provenance in a PNG chunk. Then 0.19.0 (training copies keep their background, identity policy, framing tiers, coverage panel, local multi-reference) and 0.20.0 (a Quick build tab). Nothing from those is half-built on main.
- **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.

## Architecture in 60 seconds
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ drop-list. It's advisory: nothing is blocked or rewritten.
| Qwen3-VL-8B NSFW-Caption | your GPU, ~17 GB | explicit-dataset specialist |
| **WD EVA02 / ViT v3** *(tagger)* | GPU or CPU, ONNX | canonical **Danbooru** tags |
| **Z3D e621 ConvNeXt** *(tagger)* | GPU or CPU, ONNX | canonical **e621/furry** tags |
| Gemini Flash | Google API | SFW, ~$0.0007/img est., billed to your key |
| Groq Qwen3.6 27B | Groq API | SFW, free tier, rate-limited |
| Gemini Flash | Google API | SFW, ~$0.002/img est., billed to your key |
| Groq Qwen3.8 27B | Groq API | SFW, free or paid plan, rate-limited |
| LM Studio / Ollama / custom | your choice | any OpenAI-compatible endpoint |

## Shot style
Expand Down
65 changes: 41 additions & 24 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,19 @@
from studio.config import (
CAPTIONERS,
CAPTIONERS_BY_KEY,
CLOUD_IMAGE_PRICES,
friendly_api_error,
list_images,
load_caption_model_cache,
read_caption,
settings,
)
from studio.engines.gemini import (
AUTO_MODEL,
image_model_choices,
image_price,
known_image_models,
resolve_image_model,
)
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
Expand All @@ -57,7 +63,17 @@
("Cloud — Gemini image model (best identity fidelity, SFW only)", "gemini"),
("Local — ComfyUI Qwen Image 2.1 (free, private, uncensored)", "comfyui"),
]
CLOUD_MODEL_CHOICES = [(f"{m} (~${p:.3f}/img est.)", m) for m, p in CLOUD_IMAGE_PRICES.items()]
CLOUD_MODEL_CHOICES = image_model_choices(known_image_models())


def _cloud_model_default(pinned: str, choices: list[tuple[str, str]]) -> str:
"""The ② dropdown's start value: the .env pin (retired ids mapped) if listed, else Auto."""
if pinned != AUTO_MODEL:
pinned = resolve_image_model(pinned)
return pinned if pinned in {m for _, m in choices} else AUTO_MODEL


CLOUD_MODEL_DEFAULT = _cloud_model_default(settings.gemini_image_model, CLOUD_MODEL_CHOICES)
CAPTIONER_CHOICES = [(c.label, c.key) for c in CAPTIONERS]

# Gemini caption-model dropdown seed: use the local cache if present, else a
Expand Down Expand Up @@ -581,7 +597,8 @@ def _preprocess_note(reports, out_dir: Path, alpha_cutout: bool) -> str:
def do_preprocess(files: list[str], folder: str, target: int, restore_mode: str,
restore_backend: str, isolate: bool, isolation_backend: str,
subject_prompt: str, exclude_prompt: str, tighten: bool = False,
alpha_cutout: bool = False, progress=gr.Progress()):
alpha_cutout: bool = False, front: bool = False,
progress=gr.Progress()):
sources = _inputs(files, folder)
out_dir = _stamped("prepped")
force = {"Auto (only if needed)": None, "Always": True, "Never": False}[restore_mode]
Expand All @@ -598,7 +615,7 @@ def report(msg: str):
subject_prompt=subject_prompt or "character",
exclude_prompt=exclude_prompt or "", restore_backend=restore_backend,
isolation_backend=isolation_backend, tighten_crop=tighten,
alpha_cutout=alpha_cutout, should_stop=JOB, progress=report)
alpha_cutout=alpha_cutout, front=front, should_stop=JOB, progress=report)
except OSError as e:
raise gr.Error(f"Couldn't write to '{out_dir}': {e}. Check the output folder "
f"(valid drive, writable, enough space).") from e
Expand Down Expand Up @@ -1358,17 +1375,10 @@ def do_load_plan(plan_name: str):
def estimate_cost(engine: str, cloud_model: str, df: pd.DataFrame) -> str:
n = len(df)
if engine == "gemini":
from studio.config import CLOUD_IMAGE_PRICES, load_cloud_model_cache

price = CLOUD_IMAGE_PRICES.get(cloud_model)
cached = load_cloud_model_cache() or []
for m in cached:
if m.get("model_id") == cloud_model and m.get("price") is not None:
price = m["price"]
break
model, price = image_price(cloud_model)
if price is None:
return f"**Cost:** {n} images on `{cloud_model}` (price unknown — billed to your API key)"
return (f"**Cost:** ~${n * price:.2f} for {n} images on `{cloud_model}` "
return f"**Cost:** {n} images on `{model}` (price unknown — billed to your API key)"
return (f"**Cost:** ~${n * price:.2f} for {n} images on `{model}` "
f"(estimate at build time — billed to your own Google API key)")
return f"**Cost:** {n} images, $0 (local generation)"

Expand Down Expand Up @@ -1534,15 +1544,16 @@ def do_generate_train_config(trainer: str, model_key: str, dataset_dir: str,
f"trainer's own docs before a long run.")


def refresh_cloud_models(force: bool = False):
def refresh_cloud_models(current: str = "", force: bool = False):
"""New choices from the API; keeps the user's pick when it is still offered."""
from studio.engines.gemini import list_image_models

try:
models = list_image_models(force_refresh=force)
choices = image_model_choices(list_image_models(force_refresh=force))
except Exception as e:
raise gr.Error(f"Could not list models: {e}") from e
return gr.Dropdown(choices=models,
value=models[0][1] if models else settings.gemini_image_model)
keep = current if current in {m for _, m in choices} else AUTO_MODEL
return gr.Dropdown(choices=choices, value=keep)


def refresh_caption_models():
Expand Down Expand Up @@ -1749,6 +1760,10 @@ def _check_for_update():
"own compositing workflows. Builtin SAM3 backend only. Leave off "
"(default) if you're continuing to ② Generate — it expects a white "
"background reference. Needs isolation on.")
pre_front = gr.Checkbox(
value=False, label="Prioritize this app's ComfyUI jobs",
info="Puts ComfyUI restore/isolation jobs at the head of its "
"pending queue. Does not interrupt a job already running.")
btn_pre = gr.Button("① Preprocess", variant="primary")
with gr.Column(scale=2):
pre_note = gr.Markdown()
Expand Down Expand Up @@ -1793,8 +1808,10 @@ def _check_for_update():
label="Generation engine",
info="Cloud Gemini needs no GPU (best identity, SFW); "
"local ComfyUI is free, private, uncensored.")
# A pinned id the list lacks (a retired preview in .env) would
# make Gradio reject every event that reads this dropdown.
cloud_model = gr.Dropdown(CLOUD_MODEL_CHOICES,
value=settings.gemini_image_model,
value=CLOUD_MODEL_DEFAULT,
label="Cloud image model",
info="Only used by the Cloud engine. Prices are "
"build-time estimates.")
Expand Down Expand Up @@ -2200,7 +2217,7 @@ def _check_for_update():
do_preprocess,
[pre_files, pre_folder, target, restore_mode, restore_backend, isolate,
isolation_backend, subject_prompt, exclude_prompt, pre_tighten,
pre_alpha_cutout],
pre_alpha_cutout, pre_front],
[prep_gallery, pre_note, log_box, gen_src_folder, cap_folder]) \
.then(lambda s, e: (s, e), [subject_prompt, exclude_prompt],
[gen_subject, gen_exclude])
Expand Down Expand Up @@ -2249,12 +2266,12 @@ def _check_for_update():
btn_outfits_clear.click(clear_outfits, [plan], [plan, plan_note])
btn_save_plan.click(do_save_plan, [plan, plan_name], [plan_note])
btn_load_plan.click(do_load_plan, [plan_name], [plan, plan_note])
refresh_models.click(refresh_cloud_models, [], [cloud_model])
refresh_models.click(refresh_cloud_models, [cloud_model], [cloud_model])

def _force_refresh():
return refresh_cloud_models(force=True)
def _force_refresh(current: str):
return refresh_cloud_models(current, force=True)

force_refresh_models.click(_force_refresh, [], [cloud_model])
force_refresh_models.click(_force_refresh, [cloud_model], [cloud_model])
engine.change(estimate_cost, [engine, cloud_model, plan], [cost])
cloud_model.change(estimate_cost, [engine, cloud_model, plan], [cost])
plan.change(estimate_cost, [engine, cloud_model, plan], [cost])
Expand Down
Loading
Loading