Files
character-browser/app.py
Aodhan Collins 29a6723b25 Code review fixes: wardrobe migration, response validation, path traversal guard, deduplication
- Migrate 11 character JSONs from old wardrobe keys to _BODY_GROUP_KEYS format
- Add is_favourite/is_nsfw columns to Preset model
- Add HTTP response validation and timeouts to ComfyUI client
- Add path traversal protection on replace cover route
- Deduplicate services/mcp.py (4 functions → 2 generic + 2 wrappers)
- Extract apply_library_filters() and clean_html_text() shared helpers
- Add named constants for 17 ComfyUI workflow node IDs
- Fix bare except clauses in services/llm.py
- Fix tags schema in ensure_default_outfit() (list → dict)
- Convert f-string logging to lazy % formatting
- Add 5-minute polling timeout to frontend waitForJob()
- Improve migration error handling (non-duplicate errors log at WARNING)
- Update CLAUDE.md to reflect all changes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-22 00:31:27 +00:00

158 lines
6.8 KiB
Python

import os
import logging
from flask import Flask
from flask_session import Session
from models import db, Settings, Look
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['UPLOAD_FOLDER'] = 'static/uploads'
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-key-123')
app.config['CHARACTERS_DIR'] = 'data/characters'
app.config['CLOTHING_DIR'] = 'data/clothing'
app.config['ACTIONS_DIR'] = 'data/actions'
app.config['STYLES_DIR'] = 'data/styles'
app.config['SCENES_DIR'] = 'data/scenes'
app.config['DETAILERS_DIR'] = 'data/detailers'
app.config['CHECKPOINTS_DIR'] = 'data/checkpoints'
app.config['LOOKS_DIR'] = 'data/looks'
app.config['PRESETS_DIR'] = 'data/presets'
app.config['COMFYUI_URL'] = os.environ.get('COMFYUI_URL', 'http://127.0.0.1:8188')
app.config['ILLUSTRIOUS_MODELS_DIR'] = '/ImageModels/Stable-diffusion/Illustrious/'
app.config['NOOB_MODELS_DIR'] = '/ImageModels/Stable-diffusion/Noob/'
app.config['LORA_DIR'] = '/ImageModels/lora/Illustrious/Looks/'
# Server-side session configuration to avoid cookie size limits
app.config['SESSION_TYPE'] = 'filesystem'
app.config['SESSION_FILE_DIR'] = os.path.join(app.config['UPLOAD_FOLDER'], '../flask_session')
app.config['SESSION_PERMANENT'] = False
db.init_app(app)
Session(app)
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
log_level_str = os.environ.get('LOG_LEVEL', 'INFO').upper()
log_level = getattr(logging, log_level_str, logging.INFO)
logging.basicConfig(
level=log_level,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
logger = logging.getLogger('gaze')
logger.setLevel(log_level)
# ---------------------------------------------------------------------------
# Register all routes
# ---------------------------------------------------------------------------
from routes import register_routes
register_routes(app)
# ---------------------------------------------------------------------------
# Startup
# ---------------------------------------------------------------------------
if __name__ == '__main__':
from services.mcp import ensure_mcp_server_running, ensure_character_mcp_server_running
from services.job_queue import init_queue_worker
from services.sync import (
sync_characters, sync_outfits, sync_actions, sync_styles,
sync_detailers, sync_scenes, sync_looks, sync_checkpoints, sync_presets,
)
ensure_mcp_server_running()
ensure_character_mcp_server_running()
init_queue_worker(app)
with app.app_context():
from sqlalchemy import text
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
os.makedirs(app.config['SESSION_FILE_DIR'], exist_ok=True)
db.create_all()
# --- Helper for safe column additions ---
def _add_column(table, column, col_type):
try:
db.session.execute(text(f'ALTER TABLE {table} ADD COLUMN {column} {col_type}'))
db.session.commit()
logger.info("Added %s.%s column", table, column)
except Exception as e:
db.session.rollback()
err_str = str(e).lower()
if 'duplicate column name' in err_str or 'already exists' in err_str:
pass # Column already exists, expected
else:
logger.warning("Migration failed (%s.%s): %s", table, column, e)
# --- All migrations (grouped before syncs) ---
_add_column('character', 'active_outfit', "VARCHAR(100) DEFAULT 'default'")
_add_column('action', 'default_fields', 'JSON')
_add_column('checkpoint', 'data', 'JSON')
_add_column('look', 'character_ids', 'JSON')
# Settings columns
for col_name, col_type in [
('llm_provider', "VARCHAR(50) DEFAULT 'openrouter'"),
('local_base_url', 'VARCHAR(255)'),
('local_model', 'VARCHAR(100)'),
('lora_dir_characters', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Looks'"),
('lora_dir_outfits', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Clothing'"),
('lora_dir_actions', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Poses'"),
('lora_dir_styles', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Styles'"),
('lora_dir_scenes', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Backgrounds'"),
('lora_dir_detailers', "VARCHAR(500) DEFAULT '/ImageModels/lora/Illustrious/Detailers'"),
('checkpoint_dirs', "VARCHAR(1000) DEFAULT '/ImageModels/Stable-diffusion/Illustrious,/ImageModels/Stable-diffusion/Noob'"),
('default_checkpoint', 'VARCHAR(500)'),
('api_key', 'VARCHAR(255)'),
]:
_add_column('settings', col_name, col_type)
# is_favourite / is_nsfw on all resource tables
for tbl in ['character', 'look', 'outfit', 'action', 'style', 'scene', 'detailer', 'checkpoint', 'preset']:
_add_column(tbl, 'is_favourite', 'BOOLEAN DEFAULT 0')
_add_column(tbl, 'is_nsfw', 'BOOLEAN DEFAULT 0')
# Ensure settings exist
if not Settings.query.first():
db.session.add(Settings())
db.session.commit()
logger.info("Created default settings")
# Log default checkpoint
settings = Settings.query.first()
if settings and settings.default_checkpoint:
logger.info("Default checkpoint: %s", settings.default_checkpoint)
else:
logger.info("No default checkpoint set in database")
# --- Sync all categories ---
sync_characters()
sync_outfits()
sync_actions()
sync_styles()
sync_detailers()
sync_scenes()
sync_looks()
sync_checkpoints()
sync_presets()
# --- Post-sync data migration: character_id → character_ids ---
try:
looks_with_old_field = Look.query.filter(Look.character_id.isnot(None)).all()
migrated_count = 0
for look in looks_with_old_field:
if not look.character_ids:
look.character_ids = []
if look.character_id and look.character_id not in look.character_ids:
look.character_ids.append(look.character_id)
migrated_count += 1
if migrated_count > 0:
db.session.commit()
logger.info("Migrated %d looks from character_id to character_ids", migrated_count)
except Exception as e:
logger.debug("Migration note (character_ids data): %s", e)
app.run(debug=True, host='0.0.0.0', port=5000)