# APP_REFACTOR.md — Split app.py into Modules ## Goal Split the 8,478-line `app.py` into a clean module structure using Flask Blueprints for routes and plain Python modules for services/utilities. The JSON files and DB remain the source of truth — no data migration needed. --- ## Target Structure ``` app.py # ~80 lines: Flask init, config, register blueprints, startup models.py # Unchanged utils.py # Pure helpers (no Flask/DB deps beyond current_app) services/ __init__.py comfyui.py # ComfyUI HTTP client workflow.py # Workflow building + checkpoint settings prompts.py # Prompt building + dedup llm.py # LLM integration + MCP tool calls mcp.py # MCP/Docker server lifecycle sync.py # All sync_*() functions job_queue.py # Background job queue + worker thread file_io.py # LoRA/checkpoint scanning, file uploads, image saving routes/ __init__.py # register_blueprints() helper characters.py # Character CRUD + generate + 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 deletion settings.py # Settings page + status API + context processors strengths.py # Strengths gallery system transfer.py # Resource transfer system queue_api.py # /api/queue/* endpoints ``` --- ## Module Contents — Exact Function Mapping ### `app.py` (entry point, ~80 lines) Keep only: - Flask app creation, config, extensions (SQLAlchemy, Session) - Logging setup - `from routes import register_blueprints; register_blueprints(app)` - `with app.app_context():` block (DB init, migrations, sync calls, worker start) ### `utils.py` | Function | Current Line | Notes | |----------|-------------|-------| | `parse_orientation()` | 768 | Pure logic | | `_resolve_lora_weight()` | 833 | Pure logic | | `allowed_file()` | 765 | Pure logic | | `_IDENTITY_KEYS` | 850 | Constant | | `_WARDROBE_KEYS` | 851 | Constant | | `ALLOWED_EXTENSIONS` | 728 | Constant | | `_LORA_DEFAULTS` | 730 | Constant | ### `services/job_queue.py` | Function/Global | Current Line | Notes | |----------------|-------------|-------| | `_job_queue_lock` | 75 | Global | | `_job_queue` | 76 | Global | | `_job_history` | 77 | Global | | `_queue_worker_event` | 78 | Global | | `_enqueue_job()` | 81 | Needs `comfyui.queue_prompt`, `comfyui.get_history` | | `_queue_worker()` | 102 | The background thread loop | | `_make_finalize()` | 227 | Needs `comfyui.get_history`, `comfyui.get_image`, DB models | | `_prune_job_history()` | 292 | Pure logic on globals | ### `services/comfyui.py` | Function | Current Line | Notes | |----------|-------------|-------| | `queue_prompt()` | 1083 | HTTP POST to ComfyUI | | `get_history()` | 1116 | HTTP GET + polling | | `get_image()` | 1148 | HTTP GET for image bytes | | `_ensure_checkpoint_loaded()` | 1056 | HTTP POST to load checkpoint | All use `current_app.config['COMFYUI_URL']` — pass URL as param or read from config. ### `services/workflow.py` | Function | Current Line | Notes | |----------|-------------|-------| | `_prepare_workflow()` | 3119 | Core workflow wiring | | `_apply_checkpoint_settings()` | 6445 | Checkpoint-specific settings | | `_log_workflow_prompts()` | 3030 | Logging helper | | `_build_style_workflow()` | 5089 | Style-specific workflow builder | | `_build_checkpoint_workflow()` | 6501 | Checkpoint-specific workflow builder | | `_queue_scene_generation()` | 5603 | Scene generation helper | | `_queue_detailer_generation()` | 6091 | Detailer generation helper | | `_get_default_checkpoint()` | 3273 | Reads session + DB | ### `services/prompts.py` | Function | Current Line | Notes | |----------|-------------|-------| | `build_prompt()` | 933 | Core prompt builder | | `build_extras_prompt()` | 2104 | Generator page prompt builder | | `_dedup_tags()` | 797 | Tag deduplication | | `_cross_dedup_prompts()` | 808 | Cross-dedup positive/negative | | `_build_strengths_prompts()` | 7577 | Strengths-specific prompt builder | | `_get_character_data_without_lora()` | 7563 | Helper for strengths | | `_resolve_character()` | 853 | Resolve slug → Character | | `_ensure_character_fields()` | 861 | Mutate selected_fields | | `_append_background()` | 889 | Add background tag | ### `services/llm.py` | Function/Constant | Current Line | Notes | |-------------------|-------------|-------| | `DANBOORU_TOOLS` | 1844 | Tool definitions constant | | `call_mcp_tool()` | 1904 | Sync MCP tool call | | `load_prompt()` | 1911 | Load system prompt from file | | `call_llm()` | 1918 | LLM chat completion + tool loop | ### `services/mcp.py` | Function | Current Line | Notes | |----------|-------------|-------| | `_ensure_mcp_repo()` | 423 | Clone danbooru-mcp repo | | `ensure_mcp_server_running()` | 457 | Start danbooru-mcp container | | `_ensure_character_mcp_repo()` | 496 | Clone character-mcp repo | | `ensure_character_mcp_server_running()` | 530 | Start character-mcp container | ### `services/sync.py` | Function | Current Line | Notes | |----------|-------------|-------| | `sync_characters()` | 1158 | | | `sync_outfits()` | 1218 | | | `ensure_default_outfit()` | 1276 | | | `sync_looks()` | 1360 | | | `sync_presets()` | 1415 | | | `sync_actions()` | 1534 | | | `sync_styles()` | 1591 | | | `sync_detailers()` | 1648 | | | `sync_scenes()` | 1705 | | | `sync_checkpoints()` | 1776 | | | `_default_checkpoint_data()` | 1762 | | | `_resolve_preset_entity()` | 1484 | Used by preset routes too | | `_resolve_preset_fields()` | 1494 | Used by preset routes too | | `_PRESET_ENTITY_MAP` | 1472 | Constant | ### `services/file_io.py` | Function | Current Line | Notes | |----------|-------------|-------| | `get_available_loras()` | 739 | Reads Settings + filesystem | | `get_available_checkpoints()` | 750 | Reads Settings + filesystem | | `_count_look_assignments()` | 895 | DB query | | `_count_outfit_lora_assignments()` | 907 | DB query | | `_scan_gallery_images()` | 7233 | Filesystem scan | | `_enrich_with_names()` | 7276 | DB lookups for gallery | | `_parse_comfy_png_metadata()` | 7340 | PNG metadata parsing | ### `routes/__init__.py` ```python def register_blueprints(app): from routes.characters import bp as characters_bp from routes.outfits import bp as outfits_bp # ... etc for all route modules app.register_blueprint(characters_bp) app.register_blueprint(outfits_bp) # ... ``` ### `routes/characters.py` | Route | Current Line | |-------|-------------| | `GET /` (index) | 2093 | | `POST /rescan` | 2098 | | `GET /character/` | 2289 | | `GET/POST /character//transfer` | 2305 | | `GET/POST /create` | 2465 | | `GET/POST /character//edit` | 2718 | | `POST /character//outfit/*` (6 routes) | 2800–2988 | | `POST /character//upload` | 2989 | | `POST /character//replace_cover_from_preview` | 3018 | | `POST /character//generate` | 3344 | | `POST /character//save_defaults` | 3379 | | `GET /get_missing_characters` | 3303 | | `POST /clear_all_covers` | 3308 | | `POST /generate_missing` | 3316 | ### `routes/outfits.py` | Route | Current Line | |-------|-------------| | `GET /outfits` | 3844 | | `POST /outfits/rescan` | 3850 | | `POST /outfits/bulk_create` | 3856 | | `GET /outfit/` | 3966 | | `GET/POST /outfit//edit` | 3991 | | `POST /outfit//upload` | 4062 | | `POST /outfit//generate` | 4091 | | `POST /outfit//replace_cover_from_preview` | 4187 | | `GET/POST /outfit/create` | 4199 | | `POST /outfit//save_defaults` | 4321 | | `POST /outfit//clone` | 4330 | | `POST /outfit//save_json` | 4380 | | Helper: `_get_linked_characters_for_outfit()` | 3956 | ### `routes/actions.py` | Route | Current Line | |-------|-------------| | `GET /actions` | 4398 | | `POST /actions/rescan` | 4403 | | `GET /action/` | 4409 | | `GET/POST /action//edit` | 4430 | | `POST /action//upload` | 4501 | | `POST /action//generate` | 4530 | | `POST /action//replace_cover_from_preview` | 4706 | | `POST /action//save_defaults` | 4718 | | `POST /actions/bulk_create` | 4727 | | `GET/POST /action/create` | 4831 | | `POST /action//clone` | 4905 | | `POST /action//save_json` | 4947 | | `GET /get_missing_actions` | 3401 | | `POST /clear_all_action_covers` | 3406 | ### `routes/styles.py` | Route | Current Line | |-------|-------------| | `GET /styles` | 4965 | | `POST /styles/rescan` | 4970 | | `GET /style/` | 4976 | | `GET/POST /style//edit` | 4997 | | `POST /style//upload` | 5060 | | `POST /style//generate` | 5142 | | `POST /style//save_defaults` | 5185 | | `POST /style//replace_cover_from_preview` | 5194 | | `GET /get_missing_styles` | 5206 | | `POST /clear_all_style_covers` | 5224 | | `POST /styles/generate_missing` | 5232 | | `POST /styles/bulk_create` | 5262 | | `GET/POST /style/create` | 5358 | | `POST /style//clone` | 5412 | | `POST /style//save_json` | 5453 | ### `routes/scenes.py` | Route | Current Line | |-------|-------------| | `GET /scenes` | 5471 | | `POST /scenes/rescan` | 5476 | | `GET /scene/` | 5482 | | `GET/POST /scene//edit` | 5503 | | `POST /scene//upload` | 5574 | | `POST /scene//generate` | 5680 | | `POST /scene//save_defaults` | 5721 | | `POST /scene//replace_cover_from_preview` | 5730 | | `POST /scenes/bulk_create` | 5742 | | `GET/POST /scene/create` | 5844 | | `POST /scene//clone` | 5902 | | `POST /scene//save_json` | 5943 | | `GET /get_missing_scenes` | 3414 | | `POST /clear_all_scene_covers` | 3419 | ### `routes/detailers.py` | Route | Current Line | |-------|-------------| | `GET /detailers` | 5961 | | `POST /detailers/rescan` | 5966 | | `GET /detailer/` | 5972 | | `GET/POST /detailer//edit` | 5999 | | `POST /detailer//upload` | 6062 | | `POST /detailer//generate` | 6154 | | `POST /detailer//save_defaults` | 6206 | | `POST /detailer//replace_cover_from_preview` | 6215 | | `POST /detailer//save_json` | 6227 | | `POST /detailers/bulk_create` | 6243 | | `GET/POST /detailer/create` | 6340 | | `GET /get_missing_detailers` | 5211 | | `POST /clear_all_detailer_covers` | 5216 | ### `routes/checkpoints.py` | Route | Current Line | |-------|-------------| | `GET /checkpoints` | 6396 | | `POST /checkpoints/rescan` | 6401 | | `GET /checkpoint/` | 6407 | | `POST /checkpoint//upload` | 6425 | | `POST /checkpoint//generate` | 6531 | | `POST /checkpoint//replace_cover_from_preview` | 6558 | | `POST /checkpoint//save_json` | 6570 | | `GET /get_missing_checkpoints` | 6586 | | `POST /clear_all_checkpoint_covers` | 6591 | | `POST /checkpoints/bulk_create` | 6598 | ### `routes/looks.py` | Route | Current Line | |-------|-------------| | `GET /looks` | 6702 | | `POST /looks/rescan` | 6708 | | `GET /look/` | 6714 | | `GET/POST /look//edit` | 6748 | | `POST /look//upload` | 6803 | | `POST /look//generate` | 6820 | | `POST /look//replace_cover_from_preview` | 6898 | | `POST /look//save_defaults` | 6910 | | `POST /look//generate_character` | 6919 | | `POST /look//save_json` | 7043 | | `GET/POST /look/create` | 7060 | | `GET /get_missing_looks` | 7103 | | `POST /clear_all_look_covers` | 7108 | | `POST /looks/bulk_create` | 7116 | ### `routes/presets.py` | Route | Current Line | |-------|-------------| | `GET /presets` | 3429 | | `GET /preset/` | 3435 | | `POST /preset//generate` | 3442 | | `POST /preset//replace_cover_from_preview` | 3595 | | `POST /preset//upload` | 3608 | | `GET/POST /preset//edit` | 3628 | | `POST /preset//save_json` | 3710 | | `POST /preset//clone` | 3728 | | `POST /presets/rescan` | 3757 | | `GET/POST /preset/create` | 3764 | | `GET /get_missing_presets` | 3836 | ### `routes/generator.py` | Route | Current Line | |-------|-------------| | `GET/POST /generator` | 2168 | | `POST /generator/preview_prompt` | 2256 | ### `routes/gallery.py` | Route | Current Line | |-------|-------------| | `GET /gallery` | 7296 | | `GET /gallery/prompt-data` | 7414 | | `POST /gallery/delete` | 7434 | | `POST /resource///delete` | 7457 | Constants: `GALLERY_CATEGORIES`, `_MODEL_MAP` ### `routes/settings.py` | Route/Function | Current Line | |----------------|-------------| | `GET/POST /settings` | 2066 | | `POST /set_default_checkpoint` | 588 | | `GET /api/status/comfyui` | 625 | | `GET /api/comfyui/loaded_checkpoint` | 638 | | `GET /api/status/mcp` | 659 | | `GET /api/status/llm` | 674 | | `GET /api/status/character-mcp` | 712 | | `POST /get_openrouter_models` | 2035 | | `POST /get_local_models` | 2051 | | Context processor: `inject_comfyui_ws_url()` | 569 | | Context processor: `inject_default_checkpoint()` | 582 | ### `routes/strengths.py` | Route | Current Line | |-------|-------------| | `POST /strengths///generate` | 7796 | | `GET /strengths///list` | 7892 | | `POST /strengths///clear` | 7917 | | `POST /strengths///save_range` | 7939 | Constants: `_STRENGTHS_MODEL_MAP`, `_CATEGORY_LORA_NODES`, `_STRENGTHS_DATA_DIRS` Helper: `_prepare_strengths_workflow()` (7690) ### `routes/transfer.py` | Route | Current Line | |-------|-------------| | `GET/POST /resource///transfer` | 8118 | Constants: `_RESOURCE_TRANSFER_MAP`, `_TRANSFER_TARGET_CATEGORIES` Helper: `_create_minimal_template()` (8045) ### `routes/queue_api.py` | Route | Current Line | |-------|-------------| | `GET /api/queue` | 306 | | `GET /api/queue/count` | 323 | | `POST /api/queue//remove` | 331 | | `POST /api/queue//pause` | 348 | | `POST /api/queue/clear` | 365 | | `GET /api/queue//status` | 396 | --- ## 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 ← comfyui, prompts, utils, models │ ├── llm.py ← mcp (for tool calls) │ ├── mcp.py ← (stdlib only: subprocess, docker) │ ├── 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. --- ## Migration Phases ### Phase 1 — Extract services (no route changes) **Order matters** — extract leaf dependencies first: 1. **`utils.py`** — Pure constants and helpers. Zero risk. Cut `parse_orientation`, `_resolve_lora_weight`, `allowed_file`, `ALLOWED_EXTENSIONS`, `_LORA_DEFAULTS`, `_IDENTITY_KEYS`, `_WARDROBE_KEYS` out of app.py. Add imports in app.py to keep everything working. 2. **`services/comfyui.py`** — `queue_prompt`, `get_history`, `get_image`, `_ensure_checkpoint_loaded`. These only use `requests` + config URL. Accept `comfyui_url` as parameter or read `current_app.config`. 3. **`services/mcp.py`** — `_ensure_mcp_repo`, `ensure_mcp_server_running`, `_ensure_character_mcp_repo`, `ensure_character_mcp_server_running`. Only uses `subprocess`/`os`/`logging`. 4. **`services/llm.py`** — `DANBOORU_TOOLS`, `call_mcp_tool`, `load_prompt`, `call_llm`. Depends on `services/mcp` for tool calls and `models.Settings` for config. 5. **`services/prompts.py`** — `build_prompt`, `build_extras_prompt`, `_dedup_tags`, `_cross_dedup_prompts`, `_resolve_character`, `_ensure_character_fields`, `_append_background`, `_build_strengths_prompts`, `_get_character_data_without_lora`. Depends on `utils` and `models`. 6. **`services/workflow.py`** — `_prepare_workflow`, `_apply_checkpoint_settings`, `_log_workflow_prompts`, `_build_style_workflow`, `_build_checkpoint_workflow`, `_queue_scene_generation`, `_queue_detailer_generation`, `_get_default_checkpoint`. Depends on `comfyui`, `prompts`, `utils`, `models`. 7. **`services/sync.py`** — All `sync_*()` + `_default_checkpoint_data`, `_resolve_preset_entity`, `_resolve_preset_fields`, `_PRESET_ENTITY_MAP`. Depends on `models`, `utils`. 8. **`services/file_io.py`** — `get_available_loras`, `get_available_checkpoints`, `_count_look_assignments`, `_count_outfit_lora_assignments`, `_scan_gallery_images`, `_enrich_with_names`, `_parse_comfy_png_metadata`. Depends on `models`, `utils`. 9. **`services/job_queue.py`** — Queue globals, `_enqueue_job`, `_queue_worker`, `_make_finalize`, `_prune_job_history`. Depends on `comfyui`, `models`. Extract last because the worker thread references many things. **After Phase 1**: `app.py` still has all routes, but imports helpers from `services/*` and `utils`. Each service module is independently testable. The app should work identically. ### Phase 2 — Extract routes into Blueprints **Order**: Start with the smallest/most isolated, work toward characters (largest). 1. **`routes/queue_api.py`** — 6 routes, only depends on `job_queue` globals. Blueprint prefix: none (keeps `/api/queue/*`). 2. **`routes/settings.py`** — Settings page + status APIs + context processors. Register context processors on the app via `app.context_processor` in `register_blueprints()`. 3. **`routes/gallery.py`** — Gallery + resource delete. Depends on `file_io`, `models`. 4. **`routes/transfer.py`** — Transfer system. Self-contained with its own constants. 5. **`routes/strengths.py`** — Strengths system. Self-contained with its own constants + workflow helper. 6. **`routes/generator.py`** — Generator page. Depends on `prompts.build_extras_prompt`. 7. **`routes/checkpoints.py`** — Smallest category. Good test case for the category pattern. 8. **`routes/presets.py`** — Preset CRUD. Depends on `sync._resolve_preset_*`. 9. **`routes/looks.py`** — Look CRUD + generate_character. 10. **`routes/detailers.py`** — Detailer CRUD. 11. **`routes/scenes.py`** — Scene CRUD. 12. **`routes/styles.py`** — Style CRUD. 13. **`routes/actions.py`** — Action CRUD. 14. **`routes/outfits.py`** — Outfit CRUD + linked characters helper. 15. **`routes/characters.py`** — Character CRUD + outfit management + transfer. Largest blueprint (~1300 lines). Do last since it has the most cross-cutting concerns. **After Phase 2**: `app.py` is ~80 lines. All routes live in blueprints. Full functionality preserved. ### Phase 3 — Verification & cleanup 1. Run the app, test every page manually (index, detail, generate, edit, clone, delete for each category). 2. Test batch generation, generator page, gallery, settings, strengths, transfer. 3. Remove any dead imports from `app.py`. 4. Update `CLAUDE.md` to reflect new file structure. --- ## Blueprint Pattern Template Each route blueprint follows this pattern: ```python from flask import Blueprint, render_template, request, redirect, url_for, flash, session, jsonify from models import db, Outfit # only what's needed from services.workflow import _prepare_workflow from services.prompts import build_prompt, _resolve_character, _ensure_character_fields, _append_background from services.job_queue import _enqueue_job, _make_finalize from services.file_io import get_available_loras from services.llm import call_llm, load_prompt from utils import allowed_file import logging logger = logging.getLogger('gaze') bp = Blueprint('outfits', __name__) @bp.route('/outfits') def outfits_index(): ... ``` --- ## Risk Mitigation - **Circular imports**: Enforced by the dependency graph — routes → services → utils/models. If a service needs something from another service, import it at function level if needed. - **`current_app` vs `app`**: Routes already use `request`, `session`, etc. which are context-local. Services that need app config use `current_app.config[...]` inside function bodies (not at module level). - **Thread safety**: `job_queue.py` keeps the same threading globals. The worker thread is started in `app.py`'s startup block, same as before. - **Session access**: Only route functions access `session`. Services that currently read session (like `_get_default_checkpoint`) will stay in `services/workflow.py` and import `session` from flask — this is fine since they're only called from within request context. - **Testing**: After each phase-1 extraction, verify the app starts and the affected functionality works before proceeding to the next module. --- ## Lines of Code Estimate (per module) | Module | Approx Lines | |--------|-------------| | `app.py` (final) | ~100 | | `utils.py` | ~120 | | `services/comfyui.py` | ~120 | | `services/mcp.py` | ~150 | | `services/llm.py` | ~200 | | `services/prompts.py` | ~350 | | `services/workflow.py` | ~400 | | `services/sync.py` | ~800 | | `services/job_queue.py` | ~250 | | `services/file_io.py` | ~250 | | `routes/characters.py` | ~1300 | | `routes/outfits.py` | ~550 | | `routes/actions.py` | ~570 | | `routes/styles.py` | ~500 | | `routes/scenes.py` | ~500 | | `routes/detailers.py` | ~450 | | `routes/checkpoints.py` | ~350 | | `routes/looks.py` | ~550 | | `routes/presets.py` | ~450 | | `routes/generator.py` | ~150 | | `routes/gallery.py` | ~300 | | `routes/settings.py` | ~250 | | `routes/strengths.py` | ~400 | | `routes/transfer.py` | ~200 | | `routes/queue_api.py` | ~120 | | **Total** | **~8,430** |