# GAZE — Character Browser: LLM Development Guide ## What This Project Is GAZE is a Flask web app for managing AI image generation assets and generating images via ComfyUI. It is a **personal creative tool** for organizing characters, outfits, actions, styles, scenes, and detailers — all of which map to Stable Diffusion LoRAs and prompt fragments — and generating images by wiring those assets into a ComfyUI workflow at runtime. The app is deployed locally, connects to a local ComfyUI instance at `http://127.0.0.1:8188`, and uses SQLite for persistence. LoRA and model files live on `/mnt/alexander/AITools/Image Models/`. --- ## Architecture ### File Structure ``` app.py # ~186 lines: Flask init, config, logging, route registration, startup/migrations models.py # SQLAlchemy models only comfy_workflow.json # ComfyUI workflow template with placeholder strings utils.py # Pure constants + helpers (no Flask/DB deps) services/ __init__.py comfyui.py # ComfyUI HTTP client (queue_prompt, get_history, get_image) workflow.py # Workflow building (_prepare_workflow, _apply_checkpoint_settings) prompts.py # Prompt building + dedup (build_prompt, build_extras_prompt) llm.py # LLM integration + MCP tool calls (call_llm, load_prompt) mcp.py # MCP/Docker server lifecycle (ensure_mcp_server_running) sync.py # All sync_*() functions + preset resolution helpers job_queue.py # Background job queue (_enqueue_job, _make_finalize, worker thread) file_io.py # LoRA/checkpoint scanning, file helpers routes/ __init__.py # register_routes(app) — imports and calls all route modules characters.py # Character CRUD + generation + outfit management outfits.py # Outfit routes actions.py # Action routes styles.py # Style routes scenes.py # Scene routes detailers.py # Detailer routes checkpoints.py # Checkpoint routes looks.py # Look routes presets.py # Preset routes generator.py # Generator mix-and-match page gallery.py # Gallery browsing + image/resource deletion settings.py # Settings page + status APIs + context processors strengths.py # Strengths gallery system transfer.py # Resource transfer system queue_api.py # /api/queue/* endpoints ``` ### Dependency Graph ``` app.py ├── models.py (unchanged) ├── utils.py (no deps except stdlib) ├── services/ │ ├── comfyui.py ← utils (for config) │ ├── prompts.py ← utils, models │ ├── workflow.py ← prompts, utils, models │ ├── llm.py ← mcp (for tool calls) │ ├── mcp.py ← (stdlib only: subprocess, os) │ ├── sync.py ← models, utils │ ├── job_queue.py ← comfyui, models │ └── file_io.py ← models, utils └── routes/ ├── All route modules ← services/*, utils, models └── (routes never import from other routes) ``` **No circular imports**: routes → services → utils/models. Services never import routes. Utils never imports services. ### Route Registration Pattern Routes use a `register_routes(app)` closure pattern — each route module defines a function that receives the Flask `app` object and registers routes via `@app.route()` closures. This preserves all existing `url_for()` endpoint names without requiring Blueprint prefixes. Helper functions used only by routes in that module are defined inside `register_routes()` before the routes that reference them. ### Database SQLite at `instance/database.db`, managed by Flask-SQLAlchemy. The DB is a cache of the JSON files on disk — the JSON files are the source of truth. **Models**: `Character`, `Look`, `Outfit`, `Action`, `Style`, `Scene`, `Detailer`, `Checkpoint`, `Settings` All category models (except Settings and Checkpoint) share this pattern: - `{entity}_id` — canonical ID (from JSON, often matches filename without extension) - `slug` — URL-safe version of the ID (alphanumeric + underscores only, via `re.sub(r'[^a-zA-Z0-9_]', '', id)`) - `name` — display name - `filename` — original JSON filename - `data` — full JSON blob (SQLAlchemy JSON column) - `default_fields` — list of `section::key` strings saved as the user's preferred prompt fields - `image_path` — relative path under `static/uploads/` ### Data Flow: JSON → DB → Prompt → ComfyUI 1. **JSON files** in `data/{characters,clothing,actions,styles,scenes,detailers,looks}/` are loaded by `sync_*()` functions into SQLite. 2. At generation time, `build_prompt(data, selected_fields, default_fields, active_outfit)` converts the character JSON blob into `{"main": ..., "face": ..., "hand": ...}` prompt strings. 3. `_prepare_workflow(workflow, character, prompts, ...)` wires prompts and LoRAs into the loaded `comfy_workflow.json`. 4. `queue_prompt(workflow, client_id)` POSTs the workflow to ComfyUI's `/prompt` endpoint. 5. The app polls `get_history(prompt_id)` and retrieves the image via `get_image(filename, subfolder, type)`. --- ## ComfyUI Workflow Node Map The workflow (`comfy_workflow.json`) uses string node IDs. These are the critical nodes: | Node | Role | |------|------| | `3` | Main KSampler | | `4` | Checkpoint loader | | `5` | Empty latent (width/height) | | `6` | Positive prompt — contains `{{POSITIVE_PROMPT}}` placeholder | | `7` | Negative prompt | | `8` | VAE decode | | `9` | Save image | | `11` | Face ADetailer | | `13` | Hand ADetailer | | `14` | Face detailer prompt — contains `{{FACE_PROMPT}}` placeholder | | `15` | Hand detailer prompt — contains `{{HAND_PROMPT}}` placeholder | | `16` | Character LoRA (or Look LoRA when a Look is active) | | `17` | Outfit LoRA | | `18` | Action LoRA | | `19` | Style / Detailer / Scene LoRA (priority: style > detailer > scene) | LoRA nodes chain: `4 → 16 → 17 → 18 → 19`. Unused LoRA nodes are bypassed by pointing `model_source`/`clip_source` directly to the prior node. All model/clip consumers (nodes 3, 6, 7, 11, 13, 14, 15) are wired to the final `model_source`/`clip_source` at the end of `_prepare_workflow`. --- ## Key Functions by Module ### `utils.py` — Constants and Pure Helpers - **`_IDENTITY_KEYS` / `_WARDROBE_KEYS`** — Lists of canonical field names for the `identity` and `wardrobe` sections. Used by `_ensure_character_fields()`. - **`ALLOWED_EXTENSIONS`** — Permitted upload file extensions. - **`_LORA_DEFAULTS`** — Default LoRA directory paths per category. - **`parse_orientation(orientation_str)`** — Converts orientation codes (`1F`, `2F`, `1M1F`, etc.) into Danbooru tags. - **`_resolve_lora_weight(lora_data)`** — Extracts and validates LoRA weight from a lora data dict. - **`allowed_file(filename)`** — Checks file extension against `ALLOWED_EXTENSIONS`. ### `services/prompts.py` — Prompt Building - **`build_prompt(data, selected_fields, default_fields, active_outfit)`** — Converts a character (or combined) data dict into `{"main", "face", "hand"}` prompt strings. Field selection priority: `selected_fields` → `default_fields` → select all (fallback). Fields are addressed as `"section::key"` strings (e.g. `"identity::hair"`, `"wardrobe::top"`). Characters support a **nested** wardrobe format where `wardrobe` is a dict of outfit names → outfit dicts. - **`build_extras_prompt(actions, outfits, scenes, styles, detailers)`** — Used by the Generator page. Combines prompt text from all checked items across categories into a single string. - **`_cross_dedup_prompts(positive, negative)`** — Cross-deduplicates tags between positive and negative prompt strings. Equal counts cancel completely; excess on one side is retained. - **`_resolve_character(character_slug)`** — Returns a `Character` ORM object for a given slug string. Handles `"__random__"` sentinel. - **`_ensure_character_fields(character, selected_fields, ...)`** — Mutates `selected_fields` in place, appending populated identity/wardrobe keys. Called in every secondary-category generate route after `_resolve_character()`. - **`_append_background(prompts, character=None)`** — Appends `" simple background"` tag to `prompts['main']`. ### `services/workflow.py` — Workflow Wiring - **`_prepare_workflow(workflow, character, prompts, ...)`** — Core workflow wiring function. Replaces prompt placeholders, chains LoRA nodes dynamically, randomises seeds, applies checkpoint settings, runs cross-dedup as the final step. - **`_apply_checkpoint_settings(workflow, ckpt_data)`** — Applies checkpoint-specific sampler/prompt/VAE settings. - **`_get_default_checkpoint()`** — Returns `(checkpoint_path, checkpoint_data)` from session, database Settings, or workflow file fallback. - **`_log_workflow_prompts(label, workflow)`** — Logs the fully assembled workflow prompts in a readable block. ### `services/job_queue.py` — Background Job Queue - **`_enqueue_job(label, workflow, finalize_fn)`** — Adds a generation job to the queue. - **`_make_finalize(category, slug, db_model_class=None, action=None)`** — Factory returning a callback that retrieves the generated image from ComfyUI, saves it, and optionally updates the DB cover image. - **`_prune_job_history(max_age_seconds=3600)`** — Removes old terminal-state jobs from memory. - **`init_queue_worker(flask_app)`** — Stores the app reference and starts the worker thread. ### `services/comfyui.py` — ComfyUI HTTP Client - **`queue_prompt(prompt_workflow, client_id)`** — POSTs workflow to ComfyUI's `/prompt` endpoint. - **`get_history(prompt_id)`** — Polls ComfyUI for job completion. - **`get_image(filename, subfolder, folder_type)`** — Retrieves generated image bytes. - **`_ensure_checkpoint_loaded(checkpoint_path)`** — Forces ComfyUI to load a specific checkpoint. ### `services/llm.py` — LLM Integration - **`call_llm(prompt, system_prompt)`** — OpenAI-compatible chat completion supporting OpenRouter (cloud) and Ollama/LMStudio (local). Implements a tool-calling loop (up to 10 turns) using `DANBOORU_TOOLS` via MCP Docker container. - **`load_prompt(filename)`** — Loads system prompt text from `data/prompts/`. - **`call_mcp_tool()`** — Synchronous wrapper for MCP tool calls. ### `services/sync.py` — Data Synchronization - **`sync_characters()`, `sync_outfits()`, `sync_actions()`, etc.** — Load JSON files from `data/` directories into SQLite. One function per category. - **`_resolve_preset_entity(type, id)`** / **`_resolve_preset_fields(preset_data)`** — Preset resolution helpers. ### `services/file_io.py` — File & DB Helpers - **`get_available_loras(category)`** — Scans filesystem for available LoRA files in a category. - **`get_available_checkpoints()`** — Scans checkpoint directories. - **`_count_look_assignments()`** / **`_count_outfit_lora_assignments()`** — DB aggregate queries. ### `services/mcp.py` — MCP/Docker Lifecycle - **`ensure_mcp_server_running()`** — Ensures the danbooru-mcp Docker container is running. - **`ensure_character_mcp_server_running()`** — Ensures the character-mcp Docker container is running. ### Route-local Helpers Some helpers are defined inside a route module's `register_routes()` since they're only used by routes in that file: - `routes/scenes.py`: `_queue_scene_generation()` — scene-specific workflow builder - `routes/detailers.py`: `_queue_detailer_generation()` — detailer-specific generation helper - `routes/styles.py`: `_build_style_workflow()` — style-specific workflow builder - `routes/checkpoints.py`: `_build_checkpoint_workflow()` — checkpoint-specific workflow builder - `routes/strengths.py`: `_build_strengths_prompts()`, `_prepare_strengths_workflow()` — strengths gallery helpers - `routes/transfer.py`: `_create_minimal_template()` — transfer template builder - `routes/gallery.py`: `_scan_gallery_images()`, `_enrich_with_names()`, `_parse_comfy_png_metadata()` --- ## JSON Data Schemas ### Character (`data/characters/*.json`) ```json { "character_id": "tifa_lockhart", "character_name": "Tifa Lockhart", "identity": { "base_specs": "", "hair": "", "eyes": "", "hands": "", "arms": "", "torso": "", "pelvis": "", "legs": "", "feet": "", "extra": "" }, "defaults": { "expression": "", "pose": "", "scene": "" }, "wardrobe": { "default": { "full_body": "", "headwear": "", "top": "", "bottom": "", "legwear": "", "footwear": "", "hands": "", "gloves": "", "accessories": "" } }, "styles": { "aesthetic": "", "primary_color": "", "secondary_color": "", "tertiary_color": "" }, "lora": { "lora_name": "Illustrious/Looks/tifa.safetensors", "lora_weight": 0.8, "lora_triggers": "" }, "tags": [], "participants": { "orientation": "1F", "solo_focus": "true" } } ``` `participants` is optional; when absent, `(solo:1.2)` is injected. `orientation` is parsed by `parse_orientation()` into Danbooru tags (`1girl`, `hetero`, etc.). ### Outfit (`data/clothing/*.json`) ```json { "outfit_id": "french_maid_01", "outfit_name": "French Maid", "wardrobe": { "full_body": "", "headwear": "", "top": "", "bottom": "", "legwear": "", "footwear": "", "hands": "", "accessories": "" }, "lora": { "lora_name": "Illustrious/Clothing/maid.safetensors", "lora_weight": 0.8, "lora_triggers": "" }, "tags": [] } ``` ### Action (`data/actions/*.json`) ```json { "action_id": "sitting", "action_name": "Sitting", "action": { "full_body": "", "additional": "", "head": "", "eyes": "", "arms": "", "hands": "" }, "lora": { "lora_name": "", "lora_weight": 1.0, "lora_triggers": "" }, "tags": [] } ``` ### Scene (`data/scenes/*.json`) ```json { "scene_id": "beach", "scene_name": "Beach", "scene": { "background": "", "foreground": "", "furniture": "", "colors": "", "lighting": "", "theme": "" }, "lora": { "lora_name": "", "lora_weight": 1.0, "lora_triggers": "" }, "tags": [] } ``` ### Style (`data/styles/*.json`) ```json { "style_id": "watercolor", "style_name": "Watercolor", "style": { "artist_name": "", "artistic_style": "" }, "lora": { "lora_name": "", "lora_weight": 1.0, "lora_triggers": "" } } ``` ### Detailer (`data/detailers/*.json`) ```json { "detailer_id": "detailed_skin", "detailer_name": "Detailed Skin", "prompt": ["detailed skin", "pores"], "focus": { "face": true, "hands": true }, "lora": { "lora_name": "", "lora_weight": 1.0, "lora_triggers": "" } } ``` ### Look (`data/looks/*.json`) ```json { "look_id": "tifa_casual", "look_name": "Tifa Casual", "character_id": "tifa_lockhart", "positive": "casual clothes, jeans", "negative": "revealing", "lora": { "lora_name": "Illustrious/Looks/tifa_casual.safetensors", "lora_weight": 0.85, "lora_triggers": "" }, "tags": [] } ``` Looks occupy LoRA node 16, overriding the character's own LoRA. The Look's `negative` is prepended to the workflow's negative prompt. ### Checkpoint (`data/checkpoints/*.json`) ```json { "checkpoint_path": "Illustrious/model.safetensors", "checkpoint_name": "Model Display Name", "base_positive": "anime", "base_negative": "text, logo", "steps": 25, "cfg": 5, "sampler_name": "euler_ancestral", "scheduler": "normal", "vae": "integrated" } ``` Checkpoint JSONs are keyed by `checkpoint_path`. If no JSON exists for a discovered model file, `_default_checkpoint_data()` provides defaults. --- ## URL Routes ### Characters - `GET /` — character gallery (index) - `GET /character/` — character detail with generation UI - `POST /character//generate` — queue generation (AJAX or form); returns `{"job_id": ...}` - `POST /character//replace_cover_from_preview` — promote preview to cover - `GET/POST /character//edit` — edit character data - `POST /character//upload` — upload cover image - `POST /character//save_defaults` — save default field selection - `POST /character//outfit/switch|add|delete|rename` — manage per-character wardrobe outfits - `GET/POST /create` — create new character (blank or LLM-generated) - `POST /rescan` — sync DB from JSON files ### Category Pattern (Outfits, Actions, Styles, Scenes, Detailers) Each category follows the same URL pattern: - `GET //` — gallery - `GET //` — detail + generation UI - `POST ///generate` — queue generation; returns `{"job_id": ...}` - `POST ///replace_cover_from_preview` - `GET/POST ///edit` - `POST ///upload` - `POST ///save_defaults` - `POST ///clone` — duplicate entry - `POST ///save_json` — save raw JSON (from modal editor) - `POST //rescan` - `POST //bulk_create` — LLM-generate entries from LoRA files on disk ### Looks - `GET /looks` — gallery - `GET /look/` — detail - `GET/POST /look//edit` - `POST /look//generate` — queue generation; returns `{"job_id": ...}` - `POST /look//replace_cover_from_preview` - `GET/POST /look/create` - `POST /looks/rescan` ### Generator (Mix & Match) - `GET/POST /generator` — freeform generator with multi-select accordion UI - `POST /generator/preview_prompt` — AJAX: preview composed prompt without generating ### Checkpoints - `GET /checkpoints` — gallery - `GET /checkpoint/` — detail + generation settings editor - `POST /checkpoint//save_json` - `POST /checkpoints/rescan` ### Job Queue API All generation routes use the background job queue. Frontend polls: - `GET /api/queue//status` — returns `{"status": "pending"|"running"|"done"|"failed", "result": {...}}` Image retrieval is handled server-side by the `_make_finalize()` callback; there are no separate client-facing finalize routes. ### Utilities - `POST /set_default_checkpoint` — save default checkpoint to session and persist to `comfy_workflow.json` - `GET /get_missing_{characters,outfits,actions,scenes,styles,detailers,looks,checkpoints}` — AJAX: list items without cover images (sorted by display name) - `POST /generate_missing` — batch generate covers for all characters missing one (uses job queue) - `POST /clear_all_covers` / `clear_all_{outfit,action,scene,style,detailer,look,checkpoint}_covers` - `GET /gallery` — global image gallery browsing `static/uploads/` - `GET/POST /settings` — LLM provider configuration - `POST /resource///delete` — soft (JSON only) or hard (JSON + safetensors) delete --- ## Frontend - Bootstrap 5.3 (CDN). Custom styles in `static/style.css`. - All templates extend `templates/layout.html`. The base layout provides: - `{% block content %}` — main page content - `{% block scripts %}` — additional JS at end of body - Navbar with links to all sections - Global default checkpoint selector (saves to session via AJAX) - Resource delete modal (soft/hard) shared across gallery pages - `initJsonEditor(saveUrl)` — shared JSON editor modal (simple form + raw textarea tabs) - Context processors inject `all_checkpoints`, `default_checkpoint_path`, and `COMFYUI_WS_URL` into every template. - **No `{% block head %}` exists** in layout.html — do not try to use it. - Generation is async: JS submits the form via AJAX (`X-Requested-With: XMLHttpRequest`), receives a `{"job_id": ...}` response, then polls `/api/queue//status` every ~1.5 seconds until `status == "done"`. The server-side worker handles all ComfyUI polling and image saving via the `_make_finalize()` callback. There are no client-facing finalize HTTP routes. - **Batch generation** (library pages): Uses a two-phase pattern: 1. **Queue phase**: All jobs are submitted upfront via sequential fetch calls, collecting job IDs 2. **Poll phase**: All jobs are polled concurrently via `Promise.all()`, updating UI as each completes 3. **Progress tracking**: Displays currently processing items in real-time using a `Set` to track active jobs 4. **Sorting**: All batch operations sort items by display `name` (not `filename`) for better UX --- ## LLM Integration ### System Prompts Text files in `data/prompts/` define JSON output schemas for LLM-generated entries: - `character_system.txt` — character JSON schema - `outfit_system.txt` — outfit JSON schema - `action_system.txt`, `scene_system.txt`, `style_system.txt`, `detailer_system.txt`, `look_system.txt`, `checkpoint_system.txt` Used by: character/outfit/action/scene/style create forms, and bulk_create routes. ### Danbooru MCP Tools The LLM loop in `call_llm()` provides three tools via a Docker-based MCP server (`danbooru-mcp:latest`): - `search_tags(query, limit, category)` — prefix search - `validate_tags(tags)` — exact-match validation - `suggest_tags(partial, limit, category)` — autocomplete The LLM uses these to verify and discover correct Danbooru-compatible tags for prompts. All system prompts (`character_system.txt`, `outfit_system.txt`, `action_system.txt`, `scene_system.txt`, `style_system.txt`, `detailer_system.txt`, `look_system.txt`, `checkpoint_system.txt`) instruct the LLM to use these tools before finalising any tag values. `checkpoint_system.txt` applies them specifically to the `base_positive` and `base_negative` fields. --- ## LoRA File Paths LoRA filenames in JSON are stored as paths relative to ComfyUI's `models/lora/` root: | Category | Path prefix | Example | |----------|-------------|---------| | Character / Look | `Illustrious/Looks/` | `Illustrious/Looks/tifa_v2.safetensors` | | Outfit | `Illustrious/Clothing/` | `Illustrious/Clothing/maid.safetensors` | | Action | `Illustrious/Poses/` | `Illustrious/Poses/sitting.safetensors` | | Style | `Illustrious/Styles/` | `Illustrious/Styles/watercolor.safetensors` | | Detailer | `Illustrious/Detailers/` | `Illustrious/Detailers/skin.safetensors` | | Scene | `Illustrious/Backgrounds/` | `Illustrious/Backgrounds/beach.safetensors` | Checkpoint paths: `Illustrious/.safetensors` or `Noob/.safetensors`. Absolute paths on disk: - Checkpoints: `/mnt/alexander/AITools/Image Models/Stable-diffusion/{Illustrious,Noob}/` - LoRAs: `/mnt/alexander/AITools/Image Models/lora/Illustrious/{Looks,Clothing,Poses,Styles,Detailers,Backgrounds}/` --- ## Adding a New Category To add a new content category (e.g. "Poses" as a separate concept from Actions), the pattern is: 1. **Model** (`models.py`): Add a new SQLAlchemy model with the standard fields. 2. **Sync function** (`services/sync.py`): Add `sync_newcategory()` following the pattern of `sync_outfits()`. 3. **Data directory** (`app.py`): Add `app.config['NEWCATEGORY_DIR'] = 'data/newcategory'`. 4. **Routes** (`routes/newcategory.py`): Create a new route module with a `register_routes(app)` function. Implement index, detail, edit, generate, replace_cover_from_preview, upload, save_defaults, clone, rescan routes. Follow `routes/outfits.py` or `routes/scenes.py` exactly. 5. **Route registration** (`routes/__init__.py`): Import and call `newcategory.register_routes(app)`. 6. **Templates**: Create `templates/newcategory/{index,detail,edit,create}.html` extending `layout.html`. 7. **Nav**: Add link to navbar in `templates/layout.html`. 8. **Startup** (`app.py`): Import and call `sync_newcategory()` in the `with app.app_context()` block. 9. **Generator page**: Add to `routes/generator.py`, `services/prompts.py` `build_extras_prompt()`, and `templates/generator.html` accordion. --- ## Session Keys The Flask filesystem session stores: - `default_checkpoint` — checkpoint_path string for the global default - `prefs_{slug}` — selected_fields list for character detail page - `preview_{slug}` — relative image path of last character preview - `prefs_outfit_{slug}`, `preview_outfit_{slug}`, `char_outfit_{slug}` — outfit detail state - `prefs_action_{slug}`, `preview_action_{slug}`, `char_action_{slug}` — action detail state - `prefs_scene_{slug}`, `preview_scene_{slug}`, `char_scene_{slug}` — scene detail state - `prefs_detailer_{slug}`, `preview_detailer_{slug}`, `char_detailer_{slug}`, `action_detailer_{slug}`, `extra_pos_detailer_{slug}`, `extra_neg_detailer_{slug}` — detailer detail state (selected fields, preview image, character, action LoRA, extra positive prompt, extra negative prompt) - `prefs_style_{slug}`, `preview_style_{slug}`, `char_style_{slug}` — style detail state - `prefs_look_{slug}`, `preview_look_{slug}` — look detail state --- ## Running the App ### Directly (development) ```bash cd /mnt/alexander/Projects/character-browser source venv/bin/activate python app.py ``` The app runs in debug mode on port 5000 by default. ComfyUI must be running at `http://127.0.0.1:8188`. The DB is initialised and all sync functions are called inside `with app.app_context():` at the bottom of `app.py` before `app.run()`. ### Docker ```bash docker compose up -d ``` The compose file (`docker-compose.yml`) runs two services: - **`danbooru-mcp`** — built from `https://git.liveaodh.com/aodhan/danbooru-mcp.git`; the MCP tag-search container used by `call_llm()`. - **`app`** — the Flask app, exposed on host port **5782** → container port 5000. Key environment variables set by compose: - `COMFYUI_URL=http://10.0.0.200:8188` — points at ComfyUI on the Docker host network. - `SKIP_MCP_AUTOSTART=true` — disables the app's built-in danbooru-mcp launch logic (compose manages it). Volumes mounted into the app container: - `./data`, `./static/uploads`, `./instance`, `./flask_session` — persistent app data. - `/Volumes/ImageModels:/ImageModels:ro` — model files for checkpoint/LoRA scanning (**requires Docker Desktop file sharing enabled for `/Volumes/ImageModels`**). - `/var/run/docker.sock` — Docker socket so the app can exec danbooru-mcp tool containers. --- ## Common Pitfalls - **SQLAlchemy JSON mutation**: After modifying a JSON column dict in place, always call `flag_modified(obj, "data")` or the change won't be detected. - **Dual write**: Every edit route writes back to both the DB (`db.session.commit()`) and the JSON file on disk. Both must be kept in sync. - **Slug generation**: `re.sub(r'[^a-zA-Z0-9_]', '', id)` — note this removes hyphens and dots, not just replaces them. Character IDs like `yuna_(ff10)` become slug `yunaffx10`. This is intentional. - **Checkpoint slugs use underscore replacement**: `re.sub(r'[^a-zA-Z0-9_]', '_', ...)` (replaces with `_`, not removes) to preserve readability in paths. - **LoRA chaining**: If a LoRA node has no LoRA (name is empty/None), the node is skipped and `model_source`/`clip_source` pass through unchanged. Do not set the node inputs for skipped nodes. - **AJAX detection**: `request.headers.get('X-Requested-With') == 'XMLHttpRequest'` determines whether to return JSON or redirect. - **Session must be marked modified for JSON responses**: After setting session values in AJAX-responding routes, set `session.modified = True`. - **Detailer `prompt` is a list**: The `prompt` field in detailer JSON is stored as a list of strings (e.g. `["detailed skin", "pores"]`), not a plain string. When merging into `tags` for `build_prompt`, use `extend` for lists and `append` for strings — never append the list object itself or `", ".join()` will fail on the nested list item. - **`_make_finalize` action semantics**: Pass `action=None` when the route should always update the DB cover (e.g. batch generate, checkpoint generate). Pass `action=request.form.get('action')` for routes that support both "preview" (no DB update) and "replace" (update DB). The factory skips the DB write when `action` is truthy and not `"replace"`.