- Extract 8 common route patterns into factory functions in routes/shared.py (favourite, upload, replace cover, save defaults, clone, save JSON, get missing, clear covers) — removes ~1,100 lines across 9 route files - Extract generic _sync_category() in sync.py — 7 sync functions become one-liner wrappers, removing ~350 lines - Extract shared detail page JS into static/js/detail-common.js — all 9 detail templates now call initDetailPage() with minimal config - Extract layout inline JS into static/js/layout-utils.js (~185 lines) - Extract library toolbar JS into static/js/library-toolbar.js - Fix finalize missing-image bug: raise RuntimeError instead of logging warning so job is marked failed - Fix missing scheduler default in _default_checkpoint_data() - Fix N+1 query in Character.get_available_outfits() with batch IN query - Convert all print() to logger across services and routes - Add missing tags display to styles, scenes, detailers, checkpoints detail - Update delete buttons to use trash.png icon with solid red background - Update CLAUDE.md to reflect new architecture Net reduction: ~1,600 lines Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
154 lines
6.6 KiB
Python
154 lines
6.6 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)
|
|
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()
|
|
if 'duplicate column name' not in str(e).lower() and 'already exists' not in str(e).lower():
|
|
logger.debug("Migration note (%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']:
|
|
_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)
|