Add extra prompts, endless generation, random character default, and small fixes

- Add extra positive/negative prompt textareas to all 9 detail pages with session persistence
- Add Endless generation button to all detail pages (continuous preview generation until stopped)
- Default character selector to "Random Character" on all secondary detail pages
- Fix queue clear endpoint (remove spurious auth check)
- Refactor app.py into routes/ and services/ modules
- Update CLAUDE.md with new architecture documentation
- Various data file updates and cleanup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Aodhan Collins
2026-03-13 02:07:16 +00:00
parent 1b8a798c31
commit 5e4348ebc1
170 changed files with 17367 additions and 9781 deletions

571
plans/APP_REFACTOR.md Normal file
View File

@@ -0,0 +1,571 @@
# 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/<slug>` | 2289 |
| `GET/POST /character/<slug>/transfer` | 2305 |
| `GET/POST /create` | 2465 |
| `GET/POST /character/<slug>/edit` | 2718 |
| `POST /character/<slug>/outfit/*` (6 routes) | 28002988 |
| `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
```
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** |

File diff suppressed because it is too large Load Diff

149
plans/TRANSFER.md Normal file
View File

@@ -0,0 +1,149 @@
# Resource Transfer (Non-Character) — Implementation Plan
## Scope
### In scope
- Add **Transfer** support and UI to these resource types:
- **Looks**
- **Outfits**
- **Actions**
- **Styles**
- **Scenes**
- **Detailers**
### Out of scope / explicitly excluded
- **Characters** (existing flow may remain separate)
- **Checkpoints**
- **Presets**
## User experience requirements
### Entry points
- A **Transfer** button should appear on **all supported resource detail pages**.
- Clicking **Transfer** opens a transfer form for the currently viewed resource.
### Transfer form fields
The form must allow the user to:
- Select **target category** (destination resource type)
- Edit **new name**
- Edit **new id/slug**
- Optionally choose **Use LLM** to regenerate JSON (recommended default: enabled)
All of these fields should be **pre-populated using the existing resources information**.
### Post-submit behavior
- After transfer completes, redirect to the **target category index page** (e.g. `/looks`).
- **Highlight** the newly created item on the index page.
- Do **not** auto-generate previews/covers.
## Current state (reference implementation)
- Character-only transfer route exists: [`transfer_character()`](../app.py:2216)
- Character detail page shows Transfer in header: [`templates/detail.html`](../templates/detail.html:79)
- Resource detail templates do not currently show Transfer (examples):
- Looks: [`templates/looks/detail.html`](../templates/looks/detail.html:123)
- Outfits: [`templates/outfits/detail.html`](../templates/outfits/detail.html:118)
- Actions: [`templates/actions/detail.html`](../templates/actions/detail.html:129)
## Proposed backend design
### 1) Add a generic resource transfer route
Implement a category-based route:
- `GET|POST /resource/<category>/<slug>/transfer`
Responsibilities:
- Validate `category` is in allowlist: `looks,outfits,actions,styles,scenes,detailers`
- Load the appropriate SQLAlchemy model instance by `slug`
- Render a resource-agnostic transfer form on GET
- On POST:
- Validate new name/id constraints
- Generate a safe/unique slug for the destination category
- Create destination JSON (LLM path or non-LLM fallback)
- Write JSON file to correct `data/<category>/` directory
- Create DB row for the new resource
- Redirect to destination index with highlight
### 2) Shared mapping table
Create a mapping table/dict:
- `category → model_class`
- `category → target_dir`
- `category → id_field` (e.g. `look_id`, `outfit_id`, ...)
- `category → index_route` (e.g. `looks_index`)
This mirrors the `target_type` mapping approach in [`transfer_character()`](../app.py:2251).
## JSON generation strategy
### LLM path (primary)
- Use an explicit transfer prompt (existing code loads `transfer_system.txt` in [`transfer_character()`](../app.py:2289)).
- Provide the source resource JSON as input context.
- Instruct the LLM:
- to output **JSON only** (no markdown)
- to conform to the **target resource schema**
- After parsing:
- enforce required fields: `<target>_id` and `<target>_name` (pattern used in [`transfer_character()`](../app.py:2316))
### Non-LLM fallback
- Create a minimal template JSON for the target type.
- Copy safe common fields where present (e.g. `tags`, `lora`).
- Add a provenance description like “Transferred from <source>”.
## Frontend changes
### 1) Add Transfer button to supported resource detail pages
Add a Transfer button near existing header actions, pointing to `/resource/<category>/<slug>/transfer`.
Targets:
- [`templates/looks/detail.html`](../templates/looks/detail.html:123)
- [`templates/outfits/detail.html`](../templates/outfits/detail.html:118)
- [`templates/actions/detail.html`](../templates/actions/detail.html:129)
- [`templates/styles/detail.html`](../templates/styles/detail.html:1)
- [`templates/scenes/detail.html`](../templates/scenes/detail.html:1)
- [`templates/detailers/detail.html`](../templates/detailers/detail.html:1)
### 2) Highlight the new item on the destination index
Redirect scheme:
- `/looks?highlight=<slug>` (similar for each category)
Index templates to update:
- [`templates/looks/index.html`](../templates/looks/index.html:1)
- [`templates/outfits/index.html`](../templates/outfits/index.html:1)
- [`templates/actions/index.html`](../templates/actions/index.html:1)
- [`templates/styles/index.html`](../templates/styles/index.html:1)
- [`templates/scenes/index.html`](../templates/scenes/index.html:1)
- [`templates/detailers/index.html`](../templates/detailers/index.html:1)
Implementation idea:
- Add `data-slug="..."` to each item card/container and a small script that:
- reads `highlight` query param
- finds the element
- scrolls into view
- applies a temporary CSS class (e.g. `highlight-pulse`)
## Validation & error handling
- Reject invalid/unsupported categories.
- Enforce name length limits (existing character transfer uses `<= 100` in [`transfer_character()`](../app.py:2228)).
- Sanitize id/slug to safe filename characters.
- Ensure destination JSON filename is unique; if exists, auto-suffix `_2`, `_3`, ...
- LLM error handling:
- JSON parse failures should show a user-facing flash message
- preserve form inputs on redirect if possible
## Testing
Add tests covering:
- Happy path transfer for one representative resource (e.g. look → outfit)
- Invalid category rejected
- Duplicate slug resolution works
- LLM returns invalid JSON → proper failure handling
## Deliverables checklist
- New transfer route and supporting helper mapping in app backend
- Resource-agnostic transfer template
- Transfer button on all supported detail pages
- Index page highlight behavior
- Prompt file verified/added for transfer conversions
- Tests + minimal docs update

View File

@@ -0,0 +1,775 @@
# GAZE Gallery Enhancement Plan
## Executive Summary
This plan outlines a comprehensive enhancement to the GAZE Image Gallery, transforming it from a basic grid-and-lightbox viewer into an immersive, multi-modal viewing experience. The enhancement introduces **three major feature categories**: Slideshow Modes, Image Wall Layouts, and Interactive Viewing Tools.
---
## Current State Analysis
### Existing Implementation
| Component | Current State | File Location |
|-----------|--------------|---------------|
| Grid View | Auto-fill grid, 160-210px cards, hover overlays | [`templates/gallery.html`](templates/gallery.html:88) |
| Lightbox | Bootstrap modal, arrow navigation, keyboard support | [`templates/layout.html`](templates/layout.html:123) |
| Filtering | Category, item, sort, pagination | [`templates/gallery.html`](templates/gallery.html:11) |
| Metadata | Prompt modal with LoRA chips, generation params | [`templates/gallery.html`](templates/gallery.html:194) |
### Design System Variables
```css
/* From static/style.css — these will guide our new components */
--bg-base: #07070f;
--bg-card: #0c0c1c;
--accent: #8b7eff;
--accent-glow: rgba(139, 126, 255, 0.14);
--border: #16163a;
--text: #e8e8f5;
--text-muted: #6a6a9a;
--radius: 12px;
--font-display: 'Space Grotesk';
--font-body: 'Inter';
```
---
## Architecture Overview
```mermaid
graph TB
subgraph Gallery Page
A[View Mode Selector] --> B[Grid View]
A --> C[Masonry View]
A --> D[Justified View]
A --> E[Collage View]
end
subgraph Viewer Modes
F[Cinema Slideshow]
G[Classic Slideshow]
H[Showcase Frame]
I[Comparison Mode]
J[Discovery Mode]
K[Ambient Screensaver]
end
subgraph Core Engine
L[GalleryController]
M[ImageLoader - lazy/preload]
N[TransitionEngine]
O[MetadataProvider]
end
B & C & D & E --> L
L --> F & G & H & I & J & K
L --> M
L --> N
L --> O
```
---
## Feature Specifications
### 1. View Mode Selector
A floating toolbar that allows switching between gallery layouts.
**UI Design:**
```
┌─────────────────────────────────────────────────────────────┐
│ [≡ Grid] [◫ Masonry] [▬ Justified] [◱ Collage] │ [▶ Slideshow ▾] │
└─────────────────────────────────────────────────────────────┘
```
**Slideshow Dropdown:**
```
┌──────────────────┐
│ 🎬 Cinema │
│ ⏯ Classic │
│ 🖼 Showcase │
│ ⚡ Discovery │
│ 🌌 Ambient │
│ ⚖ Comparison │
└──────────────────┘
```
---
### 2. Image Wall Layouts
#### 2.1 Masonry Layout (Pinterest-style)
Variable height columns that create a waterfall effect.
```
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ │ │ │ │ │ │ │
│ 1 │ │ 2 │ │ │ │ 4 │
│ │ └──────┘ │ 3 │ │ │
└──────┘ ┌──────┐ │ │ └──────┘
┌──────┐ │ │ │ │ ┌──────┐
│ 5 │ │ 6 │ └──────┘ │ │
└──────┘ │ │ ┌──────┐ │ 8 │
┌──────┐ └──────┘ │ 7 │ │ │
│ 9 │ ┌──────┐ └──────┘ └──────┘
└──────┘ │ 10 │
└──────┘
```
**Technical Approach:**
- CSS columns or CSS Grid with `grid-auto-rows: 1px` and `span` calculation
- JavaScript to calculate optimal column count based on viewport
- Preserve natural aspect ratios of images
- Lazy loading with Intersection Observer
**CSS Variables:**
```css
--masonry-column-width: 280px;
--masonry-gap: 12px;
--masonry-min-columns: 2;
--masonry-max-columns: 6;
```
#### 2.2 Justified Layout (Google Photos-style)
Rows with perfect edge alignment, variable widths.
```
┌────────────┬──────────┬─────────────────┐
│ 1 │ 2 │ 3 │
└────────────┴──────────┴─────────────────┘
┌─────────────────┬────────────┬─────────┐
│ 4 │ 5 │ 6 │
└─────────────────┴────────────┴─────────┘
┌──────────┬─────────────────┬───────────┐
│ 7 │ 8 │ 9 │
└──────────┴─────────────────┴───────────┘
```
**Technical Approach:**
- Linear partition algorithm to distribute images across rows
- Target row height with flex-grow distribution
- Smart breakpoints based on aspect ratios
- Each row is a flex container
**Configuration:**
```javascript
{
targetRowHeight: 240,
minRowHeight: 180,
maxRowHeight: 320,
containerPadding: 16,
imageSpacing: 8
}
```
#### 2.3 Collage Layout (Magazine-style)
Creative mixed-size layout with featured images.
```
┌───────────────────┬─────────┐
│ │ 2 │
│ 1 ├─────────┤
│ FEATURED │ 3 │
│ ├─────────┤
├─────────┬─────────┤ 4 │
│ 5 │ 6 │ │
└─────────┴─────────┴─────────┘
```
**Technical Approach:**
- CSS Grid with named areas
- Template patterns that rotate/cycle
- Special "featured" designation based on:
- User favorites (if implemented)
- Most recent
- Random selection
- Subtle rotation transforms (-2° to +2°) on some tiles
- Optional overlap with z-index layering
**Template Patterns:**
```javascript
const collagePatterns = [
'hero-right', // Large left, stack right
'hero-left', // Stack left, large right
'hero-center', // Center featured, corners fill
'magazine', // Asymmetric editorial style
'scattered' // Random positions with overlap
];
```
---
### 3. Slideshow Modes
#### 3.1 Cinema Mode
Full-screen immersive experience with cinematic effects.
**Features:**
- **Ambient glow**: Extract dominant colors from image, project as soft backdrop glow
- **Ken Burns effect**: Subtle pan/zoom animation on images
- **Vignette overlay**: Soft dark edges for focus
- **Particle system**: Optional floating light particles/orbs
- **Audio reactive**: Optional ambient music visualization (future)
**Visual Design:**
```
┌─────────────────────────────────────────────────────────────────┐
│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
│░░░░░░┌─────────────────────────────────────────────┐░░░░░░░░░░░░│
│░░░░░░│ │░░░░░░░░░░░░│
│░░░░░░│ [Image with Ken Burns] │░░░░░░░░░░░░│
│░░░░░░│ │░░░░░░░░░░░░│
│░░░░░░│ │░░░░░░░░░░░░│
│░░░░░░└─────────────────────────────────────────────┘░░░░░░░░░░░░│
│░░░░░░░░░░░░░░░░░░░ ● ● ● ○ ○ ○ ○ ○ ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
│░░░░░░░░░░░░░░░░░░░░░ 4 / 24 ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
│░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│
│░░░░ = Ambient glow from image colors ░░░░░░░░░░░░░░░░░░░░░░░░░░░│
└─────────────────────────────────────────────────────────────────┘
```
**Controls (appear on hover/touch):**
```
┌─────────────────────────────────────────────────────────────────┐
│ ⏮ ⏯ ⏭ │ ⏱ 5s ▾ │ 🔀 Shuffle │ Info │ ✕ │
└─────────────────────────────────────────────────────────────────┘
```
**Ken Burns Patterns:**
```javascript
const kenBurnsPatterns = [
{ start: 'scale(1) translate(0, 0)', end: 'scale(1.15) translate(-3%, -2%)' },
{ start: 'scale(1.1) translate(-5%, 0)', end: 'scale(1) translate(0, 0)' },
{ start: 'scale(1) translate(0, 5%)', end: 'scale(1.12) translate(2%, -3%)' },
// ... more patterns
];
```
**Ambient Glow Implementation:**
```javascript
// Use canvas to extract dominant colors
async function extractAmbientColors(imageSrc) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = await loadImage(imageSrc);
canvas.width = 50; // Downscale for performance
canvas.height = 50;
ctx.drawImage(img, 0, 0, 50, 50);
const imageData = ctx.getImageData(0, 0, 50, 50);
// Quantize colors and find dominant clusters
return dominantColors(imageData); // Returns [primary, secondary, accent]
}
```
#### 3.2 Classic Slideshow
Clean, professional presentation mode.
**Features:**
- Multiple transition types: fade, slide, zoom, cube
- Progress bar or dot indicators
- Timer controls (3s, 5s, 8s, 15s, manual)
- Loop toggle
- Random/sequential order
**UI:**
```
┌─────────────────────────────────────────────────────────────────┐
│ │
│ │
│ ┌───────────────┐ │
│ ◀ │ │ ▶ │
│ │ IMAGE │ │
│ │ │ │
│ └───────────────┘ │
│ │
│ ● ● ● ○ ○ ○ ○ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░│ │
│ └─────────────────────────────────────────────────────────────┘ │
│ ⏮ ⏯ ⏭ 🔀 Shuffle ⏱ 5s ✕ │
└─────────────────────────────────────────────────────────────────┘
```
**Transitions CSS:**
```css
.slide-transition-fade { transition: opacity 0.8s ease; }
.slide-transition-slide { transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); }
.slide-transition-zoom { transition: transform 0.7s ease, opacity 0.7s ease; }
.slide-transition-cube {
transform-style: preserve-3d;
perspective: 1000px;
transition: transform 0.8s ease;
}
```
#### 3.3 Showcase Frame Mode
Digital picture frame aesthetic with metadata.
**Features:**
- Image displayed with decorative frame/mat
- Metadata panel (character name, category, date)
- Ambient room simulation (optional)
- Clock widget (optional)
- Subtle animations
**Layout:**
```
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ╔══════════════════════════════════════════════╗ │
│ ║ ┌──────────────────────────────────────────┐ ║ │
│ ║ │ │ ║ │
│ ║ │ │ ║ │
│ ║ │ IMAGE │ ║ │
│ ║ │ │ ║ │
│ ║ │ │ ║ │
│ ║ └──────────────────────────────────────────┘ ║ │
│ ╚══════════════════════════════════════════════╝ │
│ │
│ Character: Luna Eclipse │
│ Category: characters • Style: anime │
│ Created: March 8, 2026 │
│ 12:34 │
└─────────────────────────────────────────────────────────────────┘
```
**Frame Styles:**
```javascript
const frameStyles = [
'minimal', // Thin elegant border
'classic', // Traditional wooden frame look
'modern', // Thick black mat, thin metal frame
'gallery', // White mat, thin black frame
'floating', // Shadow box effect
'none' // No frame, edge-to-edge
];
```
#### 3.4 Discovery Mode (Random Shuffle)
Explore your collection with serendipity.
**Features:**
- Random image selection with no repeats until all shown
- "Like" button to save favorites (session-based)
- Quick category jump
- Surprise effects (occasional zoom, pan variations)
- Stats display ("You've discovered 47 of 234 images")
**Controls:**
```
┌─────────────────────────────────────────────────────────────────┐
│ ⚡ DISCOVERY MODE │
│ │
│ ┌───────────────────┐ │
│ │ │ │
│ │ IMAGE │ │
│ │ │ │
│ └───────────────────┘ │
│ │
│ ❤️ Like 🔀 Next Random ⏯ Auto │
│ │
│ Discovered: 47 / 234 ━━━━━━━░░░░░░░░░░░░ │
│ │
│ Quick jump: [Characters] [Actions] [Outfits] [Scenes] [All] │
└─────────────────────────────────────────────────────────────────┘
```
#### 3.5 Ambient Screensaver Mode
Artistic background display with effects.
**Features:**
- Fullscreen with auto-hide UI
- Particle effects (floating light orbs, subtle sparkles)
- Slow crossfade transitions (10-30 seconds)
- Color-matched gradient backgrounds
- Clock overlay option
- Wake on mouse move
**Particle System:**
```javascript
const particleConfig = {
count: 50,
colors: ['#8b7eff', '#c084fc', '#60a5fa', '#ffffff'],
minSize: 2,
maxSize: 8,
speed: 0.3,
opacity: [0.1, 0.6],
blur: [0, 4],
drift: true // Slight random movement
};
```
**Visual Effect:**
```
┌─────────────────────────────────────────────────────────────────┐
│ · · · │
│ · · · │
│ · ┌─────────────────┐ · │
│ · │ │ · │
│ · │ IMAGE │ · · │
│ │ │ · │
│ · └─────────────────┘ · │
│ · · · · · │
│ · · · │
│ 12:34 │
│ · = floating light particles │
└─────────────────────────────────────────────────────────────────┘
```
#### 3.6 Comparison Mode
Side-by-side or overlay comparison.
**Features:**
- Two image selection
- Slider overlay (before/after style)
- Side-by-side split
- Onion skin overlay (opacity blend)
- Difference highlight mode
- Zoom sync (both images zoom together)
**Layouts:**
```
SLIDER MODE: SPLIT MODE:
┌─────────────────────────────┐ ┌───────────┬───────────┐
│ │ │ │ │ │
│ IMAGE A │ IMAGE B │ │ IMAGE A │ IMAGE B │
│ │ │ │ │ │
│ ◄═══|═══► │ │ │ │
│ │ │ │ │ │
└─────────────────────────────┘ └───────────┴───────────┘
ONION SKIN: DIFFERENCE:
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ │ │ │
│ IMAGE A + B BLENDED │ │ PIXEL DIFFERENCE VIEW │
│ │ │ (highlights changes) │
│ Opacity: ●━━━━━━━○ │ │ │
│ │ │ │
└─────────────────────────────┘ └─────────────────────────────┘
```
---
### 4. Enhanced Lightbox Viewer
Upgrade the existing lightbox with professional features.
#### 4.1 Zoom & Pan Controls
**Features:**
- Mouse wheel zoom
- Double-click to fit/fill toggle
- Click-and-drag panning when zoomed
- Pinch-to-zoom on touch devices
- Zoom level indicator
- Reset button
**Implementation:**
```javascript
class ZoomPanController {
constructor(element) {
this.scale = 1;
this.translateX = 0;
this.translateY = 0;
this.minScale = 0.5;
this.maxScale = 5;
}
handleWheel(e) {
const delta = e.deltaY > 0 ? 0.9 : 1.1;
const newScale = Math.min(this.maxScale, Math.max(this.minScale, this.scale * delta));
// Zoom toward cursor position
this.zoomToPoint(newScale, e.clientX, e.clientY);
}
handleDrag(e) {
if (this.scale > 1) {
this.translateX += e.movementX;
this.translateY += e.movementY;
this.applyTransform();
}
}
}
```
#### 4.2 Info Overlay Panel
Slide-out panel with image details.
```
┌─────────────────────────────────────────────────────────────────┐
│ ┌───────────┐│
│ │ℹ INFO ││
│ ├───────────┤│
│ ┌─────────────────────┐ │ ││
│ │ │ │ Luna ││
│ │ IMAGE │ │ Eclipse ││
│ │ │ │ ││
│ │ │ │ Category: ││
│ └─────────────────────┘ │ characters││
│ │ ││
│ │ Actions: ││
│ │ standing ││
│ │ ││
│ │ [Prompt] ││
│ │ [Generate]││
│ └───────────┘│
└─────────────────────────────────────────────────────────────────┘
```
#### 4.3 Thumbnail Strip
Quick navigation with visual preview.
```
┌─────────────────────────────────────────────────────────────────┐
│ │
│ ┌─────────────────┐ │
│ │ │ │
│ │ MAIN │ │
│ │ IMAGE │ │
│ │ │ │
│ └─────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ ▢ ▢ ▢ [▣] ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ ▢ │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
---
### 5. Keyboard Shortcuts
| Key | Action |
|-----|--------|
| `←` / `→` | Previous / Next image |
| `↑` / `↓` | Zoom in / out |
| `Space` | Toggle slideshow play/pause |
| `F` | Toggle fullscreen |
| `I` | Toggle info panel |
| `G` | Open grid view |
| `M` | Toggle masonry view |
| `S` | Start slideshow |
| `C` | Enter comparison mode |
| `R` | Random image |
| `Esc` | Close viewer / exit fullscreen |
| `1-5` | Set slideshow speed preset |
| `+` / `-` | Zoom in / out |
| `0` | Reset zoom |
---
### 6. Mobile & Touch Optimization
**Gestures:**
- **Swipe left/right**: Navigate images
- **Swipe up**: Show info panel
- **Swipe down**: Close viewer
- **Pinch**: Zoom
- **Double-tap**: Toggle fit/fill
- **Long press**: Show context menu
**Responsive Breakpoints:**
```css
/* Mobile - single column, touch optimized */
@media (max-width: 576px) { }
/* Tablet - 2-3 columns */
@media (min-width: 577px) and (max-width: 991px) { }
/* Desktop - 4-6 columns */
@media (min-width: 992px) { }
/* Large displays - up to 8 columns */
@media (min-width: 1400px) { }
```
---
## File Structure
```
static/
├── style.css # Add gallery enhancement styles
├── js/
│ └── gallery/
│ ├── gallery-core.js # Main controller
│ ├── layout-masonry.js # Masonry layout engine
│ ├── layout-justified.js # Justified layout engine
│ ├── layout-collage.js # Collage layout engine
│ ├── slideshow-cinema.js # Cinema mode
│ ├── slideshow-classic.js # Classic slideshow
│ ├── slideshow-showcase.js # Showcase frame
│ ├── mode-discovery.js # Discovery mode
│ ├── mode-ambient.js # Ambient screensaver
│ ├── mode-comparison.js # Comparison mode
│ ├── viewer-zoom.js # Zoom/pan controller
│ ├── effects-particles.js # Particle system
│ ├── effects-ambient.js # Ambient color extraction
│ └── effects-kenburns.js # Ken Burns animations
templates/
├── gallery.html # Enhanced with view mode selector
└── partials/
└── gallery-viewer.html # New standalone viewer partial
```
---
## Implementation Phases
### Phase 1: Foundation
- [ ] Create gallery core controller class
- [ ] Implement view mode selector UI
- [ ] Add masonry layout with lazy loading
- [ ] Enhanced lightbox with zoom/pan
- [ ] Keyboard shortcut system
### Phase 2: Layouts
- [ ] Justified gallery layout
- [ ] Collage layout with templates
- [ ] Thumbnail strip navigation
- [ ] Mobile swipe gestures
### Phase 3: Slideshows
- [ ] Classic slideshow with transitions
- [ ] Cinema mode with ambient glow
- [ ] Ken Burns effect engine
- [ ] Showcase frame mode
### Phase 4: Advanced Features
- [ ] Discovery mode with favorites
- [ ] Comparison mode slider
- [ ] Ambient screensaver with particles
- [ ] Info panel with metadata
### Phase 5: Polish
- [ ] Performance optimization
- [ ] Accessibility improvements
- [ ] Settings persistence
- [ ] Documentation
---
## Technical Considerations
### Performance
- Use `IntersectionObserver` for lazy loading
- Preload next 2 images in slideshow
- Use `requestAnimationFrame` for animations
- Debounce resize handlers
- Use CSS transforms instead of layout properties
- Consider web workers for color extraction
### Browser Support
- Modern browsers (Chrome, Firefox, Safari, Edge)
- CSS Grid and Flexbox required
- Fullscreen API
- Intersection Observer API
- Web Animations API preferred
### Accessibility
- ARIA labels on all controls
- Focus management in modals
- Reduced motion preference respected
- Screen reader announcements for image changes
- Keyboard navigation throughout
---
## CSS Variables for Gallery Enhancement
```css
/* Gallery Enhancement Variables */
:root {
/* Layouts */
--gallery-gap: 8px;
--gallery-card-radius: 12px;
--gallery-masonry-width: 280px;
--gallery-justified-height: 240px;
/* Viewer */
--viewer-bg: rgba(0, 0, 0, 0.97);
--viewer-accent: var(--accent);
--viewer-transition: 0.3s ease;
/* Slideshow */
--slideshow-interval: 5000;
--kenburns-duration: 8000;
--transition-duration: 800;
/* Effects */
--ambient-blur: 100px;
--ambient-opacity: 0.3;
--particle-count: 50;
--vignette-opacity: 0.4;
/* Frames */
--frame-color-light: #f5f5f5;
--frame-color-dark: #1a1a2e;
--mat-width: 40px;
}
```
---
## Mockup: Cinema Mode
```
╔═══════════════════════════════════════════════════════════════════════╗
║▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓║
║▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓▓▓▓▓║
║▓▓▓▓▓░░░░░░ ░░░░░▓▓▓▓▓▓║
║▓▓░░░░░ ░░░░▓▓▓║
║▓░░░░ ╭─────────────────────────────╮ ░░░▓║
║░░░░ │ │ ░░║
║░░ │ │ ░║
║░ │ ★ IMAGE ★ │ ░║
║░ │ │ ░║
║░ │ │ ░║
║░░ │ │ ░░║
║░░░ ╰─────────────────────────────╯ ░░░║
║▓░░░░ ░░▓▓║
║▓▓░░░░░ • • • ○ ○ ░░░░▓▓▓║
║▓▓▓▓░░░░░░░ 4 / 24 ░░░░░▓▓▓▓▓▓║
║▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▓▓▓▓▓▓▓▓▓║
║▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓║
╚═══════════════════════════════════════════════════════════════════════╝
Legend:
▓ = Darkest vignette
░ = Ambient glow (color-matched to image)
★ = Image with Ken Burns animation
• = Active indicator, ○ = Inactive
Controls appear on hover:
┌──────────────────────────────────────────────────────────────────────┐
│ ⏮ │ ⏸ │ ⏭ │ ⏱ 5s ▾ │ 🔀 Shuffle │ Info │ ✕ │
└──────────────────────────────────────────────────────────────────────┘
```
---
## Next Steps
1. **Review and approve** this plan
2. **Prioritize features** for MVP
3. **Begin Phase 1** implementation
4. **Iterate** based on feedback
Would you like me to proceed with implementation, or would you like to adjust any aspects of this plan?