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
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
routes/characters.py
| Route |
Current Line |
GET / (index) |
2093 |
POST /rescan |
2098 |
GET /character/<slug> |
2289 |
GET/POST /character/<slug>/transfer |
2305 |
GET/POST /create |
2465 |
GET/POST /character/<slug>/edit |
2718 |
POST /character/<slug>/outfit/* (6 routes) |
2800–2988 |
POST /character/<slug>/upload |
2989 |
POST /character/<slug>/replace_cover_from_preview |
3018 |
POST /character/<slug>/generate |
3344 |
POST /character/<slug>/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/<slug> |
3966 |
GET/POST /outfit/<slug>/edit |
3991 |
POST /outfit/<slug>/upload |
4062 |
POST /outfit/<slug>/generate |
4091 |
POST /outfit/<slug>/replace_cover_from_preview |
4187 |
GET/POST /outfit/create |
4199 |
POST /outfit/<slug>/save_defaults |
4321 |
POST /outfit/<slug>/clone |
4330 |
POST /outfit/<slug>/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/<slug> |
4409 |
GET/POST /action/<slug>/edit |
4430 |
POST /action/<slug>/upload |
4501 |
POST /action/<slug>/generate |
4530 |
POST /action/<slug>/replace_cover_from_preview |
4706 |
POST /action/<slug>/save_defaults |
4718 |
POST /actions/bulk_create |
4727 |
GET/POST /action/create |
4831 |
POST /action/<slug>/clone |
4905 |
POST /action/<slug>/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/<slug> |
4976 |
GET/POST /style/<slug>/edit |
4997 |
POST /style/<slug>/upload |
5060 |
POST /style/<slug>/generate |
5142 |
POST /style/<slug>/save_defaults |
5185 |
POST /style/<slug>/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/<slug>/clone |
5412 |
POST /style/<slug>/save_json |
5453 |
routes/scenes.py
| Route |
Current Line |
GET /scenes |
5471 |
POST /scenes/rescan |
5476 |
GET /scene/<slug> |
5482 |
GET/POST /scene/<slug>/edit |
5503 |
POST /scene/<slug>/upload |
5574 |
POST /scene/<slug>/generate |
5680 |
POST /scene/<slug>/save_defaults |
5721 |
POST /scene/<slug>/replace_cover_from_preview |
5730 |
POST /scenes/bulk_create |
5742 |
GET/POST /scene/create |
5844 |
POST /scene/<slug>/clone |
5902 |
POST /scene/<slug>/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/<slug> |
5972 |
GET/POST /detailer/<slug>/edit |
5999 |
POST /detailer/<slug>/upload |
6062 |
POST /detailer/<slug>/generate |
6154 |
POST /detailer/<slug>/save_defaults |
6206 |
POST /detailer/<slug>/replace_cover_from_preview |
6215 |
POST /detailer/<slug>/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/<slug> |
6407 |
POST /checkpoint/<slug>/upload |
6425 |
POST /checkpoint/<slug>/generate |
6531 |
POST /checkpoint/<slug>/replace_cover_from_preview |
6558 |
POST /checkpoint/<slug>/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/<slug> |
6714 |
GET/POST /look/<slug>/edit |
6748 |
POST /look/<slug>/upload |
6803 |
POST /look/<slug>/generate |
6820 |
POST /look/<slug>/replace_cover_from_preview |
6898 |
POST /look/<slug>/save_defaults |
6910 |
POST /look/<slug>/generate_character |
6919 |
POST /look/<slug>/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/<slug> |
3435 |
POST /preset/<slug>/generate |
3442 |
POST /preset/<slug>/replace_cover_from_preview |
3595 |
POST /preset/<slug>/upload |
3608 |
GET/POST /preset/<slug>/edit |
3628 |
POST /preset/<slug>/save_json |
3710 |
POST /preset/<slug>/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/<category>/<slug>/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/<category>/<slug>/generate |
7796 |
GET /strengths/<category>/<slug>/list |
7892 |
POST /strengths/<category>/<slug>/clear |
7917 |
POST /strengths/<category>/<slug>/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/<category>/<slug>/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/<job_id>/remove |
331 |
POST /api/queue/<job_id>/pause |
348 |
POST /api/queue/clear |
365 |
GET /api/queue/<job_id>/status |
396 |
Dependency Graph
No circular imports: routes → services → utils/models. Services never import routes. Utils never imports services.
Migration Phases
Order matters — extract leaf dependencies first:
-
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.
-
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.
-
services/mcp.py — _ensure_mcp_repo, ensure_mcp_server_running, _ensure_character_mcp_repo, ensure_character_mcp_server_running. Only uses subprocess/os/logging.
-
services/llm.py — DANBOORU_TOOLS, call_mcp_tool, load_prompt, call_llm. Depends on services/mcp for tool calls and models.Settings for config.
-
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.
-
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.
-
services/sync.py — All sync_*() + _default_checkpoint_data, _resolve_preset_entity, _resolve_preset_fields, _PRESET_ENTITY_MAP. Depends on models, utils.
-
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.
-
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.
Order: Start with the smallest/most isolated, work toward characters (largest).
-
routes/queue_api.py — 6 routes, only depends on job_queue globals. Blueprint prefix: none (keeps /api/queue/*).
-
routes/settings.py — Settings page + status APIs + context processors. Register context processors on the app via app.context_processor in register_blueprints().
-
routes/gallery.py — Gallery + resource delete. Depends on file_io, models.
-
routes/transfer.py — Transfer system. Self-contained with its own constants.
-
routes/strengths.py — Strengths system. Self-contained with its own constants + workflow helper.
-
routes/generator.py — Generator page. Depends on prompts.build_extras_prompt.
-
routes/checkpoints.py — Smallest category. Good test case for the category pattern.
-
routes/presets.py — Preset CRUD. Depends on sync._resolve_preset_*.
-
routes/looks.py — Look CRUD + generate_character.
-
routes/detailers.py — Detailer CRUD.
-
routes/scenes.py — Scene CRUD.
-
routes/styles.py — Style CRUD.
-
routes/actions.py — Action CRUD.
-
routes/outfits.py — Outfit CRUD + linked characters helper.
-
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
- Run the app, test every page manually (index, detail, generate, edit, clone, delete for each category).
- Test batch generation, generator page, gallery, settings, strengths, transfer.
- Remove any dead imports from
app.py.
- Update
CLAUDE.md to reflect new file structure.
Blueprint Pattern Template
Each route blueprint follows this pattern:
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 |