Major refactor: deduplicate routes, sync, JS, and fix bugs

- 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>
This commit is contained in:
Aodhan Collins
2026-03-21 23:06:58 +00:00
parent ed9a7b4b11
commit 55ff58aba6
42 changed files with 1493 additions and 3105 deletions

View File

@@ -272,8 +272,7 @@ def _make_finalize(category, slug, db_model_class=None, action=None, metadata=No
logger.debug("=" * 80)
return
logger.warning("FINALIZE - No images found in outputs!")
logger.debug("=" * 80)
raise RuntimeError("No images found in ComfyUI outputs")
return _finalize

View File

@@ -1,12 +1,15 @@
import os
import json
import asyncio
import logging
import requests
from flask import has_request_context, request as flask_request
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from models import Settings
logger = logging.getLogger('gaze')
DANBOORU_TOOLS = [
{
"type": "function",
@@ -73,7 +76,7 @@ def call_mcp_tool(name, arguments):
try:
return asyncio.run(_run_mcp_tool(name, arguments))
except Exception as e:
print(f"MCP Tool Error: {e}")
logger.error("MCP Tool Error: %s", e)
return json.dumps({"error": str(e)})
@@ -95,7 +98,7 @@ def call_character_mcp_tool(name, arguments):
try:
return asyncio.run(_run_character_mcp_tool(name, arguments))
except Exception as e:
print(f"Character MCP Tool Error: {e}")
logger.error("Character MCP Tool Error: %s", e)
return None
@@ -164,7 +167,7 @@ def call_llm(prompt, system_prompt="You are a creative assistant."):
# If 400 Bad Request and we were using tools, try once without tools
if response.status_code == 400 and use_tools:
print(f"LLM Provider {settings.llm_provider} rejected tools. Retrying without tool calling...")
logger.warning("LLM Provider %s rejected tools. Retrying without tool calling...", settings.llm_provider)
use_tools = False
max_turns += 1 # Reset turn for the retry
continue
@@ -186,7 +189,7 @@ def call_llm(prompt, system_prompt="You are a creative assistant."):
for tool_call in message['tool_calls']:
name = tool_call['function']['name']
args = json.loads(tool_call['function']['arguments'])
print(f"Executing MCP tool: {name}({args})")
logger.debug("Executing MCP tool: %s(%s)", name, args)
tool_result = call_mcp_tool(name, args)
messages.append({
"role": "tool",
@@ -195,7 +198,7 @@ def call_llm(prompt, system_prompt="You are a creative assistant."):
"content": tool_result
})
if tool_turns_remaining <= 0:
print("Tool turn limit reached — next request will not offer tools")
logger.warning("Tool turn limit reached — next request will not offer tools")
continue
return message['content']
@@ -209,7 +212,7 @@ def call_llm(prompt, system_prompt="You are a creative assistant."):
raw = ""
try: raw = response.text[:500]
except: pass
print(f"Unexpected LLM response format (key={e}). Raw response: {raw}")
logger.warning("Unexpected LLM response format (key=%s). Raw response: %s", e, raw)
if format_retries > 0:
format_retries -= 1
max_turns += 1 # don't burn a turn on a format error
@@ -222,7 +225,7 @@ def call_llm(prompt, system_prompt="You are a creative assistant."):
"Do not include any explanation or markdown — only the raw JSON object."
)
})
print(f"Retrying after format error ({format_retries} retries left)…")
logger.info("Retrying after format error (%d retries left)…", format_retries)
continue
raise RuntimeError(f"Unexpected LLM response format after retries: {str(e)}") from e

View File

@@ -1,6 +1,9 @@
import os
import logging
import subprocess
logger = logging.getLogger('gaze')
# Path to the MCP docker-compose projects, relative to the main app file.
MCP_TOOLS_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'tools')
MCP_COMPOSE_DIR = os.path.join(MCP_TOOLS_DIR, 'danbooru-mcp')
@@ -19,28 +22,28 @@ def _ensure_mcp_repo():
os.makedirs(MCP_TOOLS_DIR, exist_ok=True)
try:
if not os.path.isdir(MCP_COMPOSE_DIR):
print(f'Cloning danbooru-mcp from {MCP_REPO_URL}')
logger.info('Cloning danbooru-mcp from %s', MCP_REPO_URL)
subprocess.run(
['git', 'clone', MCP_REPO_URL, MCP_COMPOSE_DIR],
timeout=120, check=True,
)
print('danbooru-mcp cloned successfully.')
logger.info('danbooru-mcp cloned successfully.')
else:
print('Updating danbooru-mcp via git pull …')
logger.info('Updating danbooru-mcp via git pull …')
subprocess.run(
['git', 'pull'],
cwd=MCP_COMPOSE_DIR,
timeout=60, check=True,
)
print('danbooru-mcp updated.')
logger.info('danbooru-mcp updated.')
except FileNotFoundError:
print('WARNING: git not found on PATH — danbooru-mcp repo will not be cloned/updated.')
logger.warning('git not found on PATH — danbooru-mcp repo will not be cloned/updated.')
except subprocess.CalledProcessError as e:
print(f'WARNING: git operation failed for danbooru-mcp: {e}')
logger.warning('git operation failed for danbooru-mcp: %s', e)
except subprocess.TimeoutExpired:
print('WARNING: git timed out while cloning/updating danbooru-mcp.')
logger.warning('git timed out while cloning/updating danbooru-mcp.')
except Exception as e:
print(f'WARNING: Could not clone/update danbooru-mcp repo: {e}')
logger.warning('Could not clone/update danbooru-mcp repo: %s', e)
def ensure_mcp_server_running():
@@ -55,7 +58,7 @@ def ensure_mcp_server_running():
danbooru-mcp service is managed by compose instead).
"""
if os.environ.get('SKIP_MCP_AUTOSTART', '').lower() == 'true':
print('SKIP_MCP_AUTOSTART set — skipping danbooru-mcp auto-start.')
logger.info('SKIP_MCP_AUTOSTART set — skipping danbooru-mcp auto-start.')
return
_ensure_mcp_repo()
try:
@@ -64,22 +67,22 @@ def ensure_mcp_server_running():
capture_output=True, text=True, timeout=10,
)
if 'danbooru-mcp' in result.stdout:
print('danbooru-mcp container already running.')
logger.info('danbooru-mcp container already running.')
return
# Container not running — start it via docker compose
print('Starting danbooru-mcp container via docker compose …')
logger.info('Starting danbooru-mcp container via docker compose …')
subprocess.run(
['docker', 'compose', 'up', '-d'],
cwd=MCP_COMPOSE_DIR,
timeout=120,
)
print('danbooru-mcp container started.')
logger.info('danbooru-mcp container started.')
except FileNotFoundError:
print('WARNING: docker not found on PATH — danbooru-mcp will not be started automatically.')
logger.warning('docker not found on PATH — danbooru-mcp will not be started automatically.')
except subprocess.TimeoutExpired:
print('WARNING: docker timed out while starting danbooru-mcp.')
logger.warning('docker timed out while starting danbooru-mcp.')
except Exception as e:
print(f'WARNING: Could not ensure danbooru-mcp is running: {e}')
logger.warning('Could not ensure danbooru-mcp is running: %s', e)
def _ensure_character_mcp_repo():
@@ -92,28 +95,28 @@ def _ensure_character_mcp_repo():
os.makedirs(MCP_TOOLS_DIR, exist_ok=True)
try:
if not os.path.isdir(CHAR_MCP_COMPOSE_DIR):
print(f'Cloning character-mcp from {CHAR_MCP_REPO_URL}')
logger.info('Cloning character-mcp from %s', CHAR_MCP_REPO_URL)
subprocess.run(
['git', 'clone', CHAR_MCP_REPO_URL, CHAR_MCP_COMPOSE_DIR],
timeout=120, check=True,
)
print('character-mcp cloned successfully.')
logger.info('character-mcp cloned successfully.')
else:
print('Updating character-mcp via git pull …')
logger.info('Updating character-mcp via git pull …')
subprocess.run(
['git', 'pull'],
cwd=CHAR_MCP_COMPOSE_DIR,
timeout=60, check=True,
)
print('character-mcp updated.')
logger.info('character-mcp updated.')
except FileNotFoundError:
print('WARNING: git not found on PATH — character-mcp repo will not be cloned/updated.')
logger.warning('git not found on PATH — character-mcp repo will not be cloned/updated.')
except subprocess.CalledProcessError as e:
print(f'WARNING: git operation failed for character-mcp: {e}')
logger.warning('git operation failed for character-mcp: %s', e)
except subprocess.TimeoutExpired:
print('WARNING: git timed out while cloning/updating character-mcp.')
logger.warning('git timed out while cloning/updating character-mcp.')
except Exception as e:
print(f'WARNING: Could not clone/update character-mcp repo: {e}')
logger.warning('Could not clone/update character-mcp repo: %s', e)
def ensure_character_mcp_server_running():
@@ -128,7 +131,7 @@ def ensure_character_mcp_server_running():
character-mcp service is managed by compose instead).
"""
if os.environ.get('SKIP_MCP_AUTOSTART', '').lower() == 'true':
print('SKIP_MCP_AUTOSTART set — skipping character-mcp auto-start.')
logger.info('SKIP_MCP_AUTOSTART set — skipping character-mcp auto-start.')
return
_ensure_character_mcp_repo()
try:
@@ -137,19 +140,19 @@ def ensure_character_mcp_server_running():
capture_output=True, text=True, timeout=10,
)
if 'character-mcp' in result.stdout:
print('character-mcp container already running.')
logger.info('character-mcp container already running.')
return
# Container not running — start it via docker compose
print('Starting character-mcp container via docker compose …')
logger.info('Starting character-mcp container via docker compose …')
subprocess.run(
['docker', 'compose', 'up', '-d'],
cwd=CHAR_MCP_COMPOSE_DIR,
timeout=120,
)
print('character-mcp container started.')
logger.info('character-mcp container started.')
except FileNotFoundError:
print('WARNING: docker not found on PATH — character-mcp will not be started automatically.')
logger.warning('docker not found on PATH — character-mcp will not be started automatically.')
except subprocess.TimeoutExpired:
print('WARNING: docker timed out while starting character-mcp.')
logger.warning('docker timed out while starting character-mcp.')
except Exception as e:
print(f'WARNING: Could not ensure character-mcp is running: {e}')
logger.warning('Could not ensure character-mcp is running: %s', e)

View File

@@ -57,7 +57,7 @@ def sync_characters():
if character.image_path:
full_img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], character.image_path)
if not os.path.exists(full_img_path):
print(f"Image missing for {character.name}, clearing path.")
logger.warning("Image missing for %s, clearing path.", character.name)
character.image_path = None
# Explicitly tell SQLAlchemy the JSON field was modified
@@ -73,7 +73,7 @@ def sync_characters():
_sync_nsfw_from_tags(new_char, data)
db.session.add(new_char)
except Exception as e:
print(f"Error importing {filename}: {e}")
logger.error("Error importing %s: %s", filename, e)
# Remove characters that are no longer in the folder
all_characters = Character.query.all()
@@ -83,66 +83,82 @@ def sync_characters():
db.session.commit()
def sync_outfits():
if not os.path.exists(current_app.config['CLOTHING_DIR']):
def _sync_category(config_key, model_class, id_field, name_field,
extra_fn=None, sync_nsfw=True):
"""Generic sync: load JSON files from a data directory into the database.
Args:
config_key: app.config key for the data directory (e.g. 'CLOTHING_DIR')
model_class: SQLAlchemy model class (e.g. Outfit)
id_field: JSON key for the entity ID (e.g. 'outfit_id')
name_field: JSON key for the display name (e.g. 'outfit_name')
extra_fn: optional callable(entity, data) for category-specific field updates
sync_nsfw: if True, call _sync_nsfw_from_tags on create/update
"""
data_dir = current_app.config.get(config_key)
if not data_dir or not os.path.exists(data_dir):
return
current_ids = []
for filename in os.listdir(current_app.config['CLOTHING_DIR']):
for filename in os.listdir(data_dir):
if filename.endswith('.json'):
file_path = os.path.join(current_app.config['CLOTHING_DIR'], filename)
file_path = os.path.join(data_dir, filename)
try:
with open(file_path, 'r') as f:
data = json.load(f)
outfit_id = data.get('outfit_id') or filename.replace('.json', '')
entity_id = data.get(id_field) or filename.replace('.json', '')
current_ids.append(entity_id)
current_ids.append(outfit_id)
slug = re.sub(r'[^a-zA-Z0-9_]', '', entity_id)
entity = model_class.query.filter_by(**{id_field: entity_id}).first()
name = data.get(name_field, entity_id.replace('_', ' ').title())
# Generate URL-safe slug: remove special characters from outfit_id
slug = re.sub(r'[^a-zA-Z0-9_]', '', outfit_id)
if entity:
entity.data = data
entity.name = name
entity.slug = slug
entity.filename = filename
if sync_nsfw:
_sync_nsfw_from_tags(entity, data)
if extra_fn:
extra_fn(entity, data)
# Check if outfit already exists
outfit = Outfit.query.filter_by(outfit_id=outfit_id).first()
name = data.get('outfit_name', outfit_id.replace('_', ' ').title())
if outfit:
outfit.data = data
outfit.name = name
outfit.slug = slug
outfit.filename = filename
_sync_nsfw_from_tags(outfit, data)
# Check if cover image still exists
if outfit.image_path:
full_img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], outfit.image_path)
if entity.image_path:
full_img_path = os.path.join(
current_app.config['UPLOAD_FOLDER'], entity.image_path)
if not os.path.exists(full_img_path):
print(f"Image missing for {outfit.name}, clearing path.")
outfit.image_path = None
logger.warning("Image missing for %s, clearing path.", entity.name)
entity.image_path = None
# Explicitly tell SQLAlchemy the JSON field was modified
flag_modified(outfit, "data")
flag_modified(entity, "data")
else:
new_outfit = Outfit(
outfit_id=outfit_id,
slug=slug,
filename=filename,
name=name,
data=data
)
_sync_nsfw_from_tags(new_outfit, data)
db.session.add(new_outfit)
kwargs = {
id_field: entity_id,
'slug': slug,
'filename': filename,
'name': name,
'data': data,
}
new_entity = model_class(**kwargs)
if sync_nsfw:
_sync_nsfw_from_tags(new_entity, data)
if extra_fn:
extra_fn(new_entity, data)
db.session.add(new_entity)
except Exception as e:
print(f"Error importing outfit {filename}: {e}")
logger.error("Error importing %s: %s", filename, e)
# Remove outfits that are no longer in the folder
all_outfits = Outfit.query.all()
for outfit in all_outfits:
if outfit.outfit_id not in current_ids:
db.session.delete(outfit)
for entity in model_class.query.all():
if getattr(entity, id_field) not in current_ids:
db.session.delete(entity)
db.session.commit()
def sync_outfits():
_sync_category('CLOTHING_DIR', Outfit, 'outfit_id', 'outfit_name')
def ensure_default_outfit():
"""Ensure a default outfit file exists and is registered in the database.
@@ -226,114 +242,16 @@ def ensure_default_outfit():
def _sync_look_extra(entity, data):
entity.character_id = data.get('character_id', None)
def sync_looks():
if not os.path.exists(current_app.config['LOOKS_DIR']):
return
current_ids = []
for filename in os.listdir(current_app.config['LOOKS_DIR']):
if filename.endswith('.json'):
file_path = os.path.join(current_app.config['LOOKS_DIR'], filename)
try:
with open(file_path, 'r') as f:
data = json.load(f)
look_id = data.get('look_id') or filename.replace('.json', '')
current_ids.append(look_id)
slug = re.sub(r'[^a-zA-Z0-9_]', '', look_id)
look = Look.query.filter_by(look_id=look_id).first()
name = data.get('look_name', look_id.replace('_', ' ').title())
character_id = data.get('character_id', None)
if look:
look.data = data
look.name = name
look.slug = slug
look.filename = filename
look.character_id = character_id
_sync_nsfw_from_tags(look, data)
if look.image_path:
full_img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], look.image_path)
if not os.path.exists(full_img_path):
look.image_path = None
flag_modified(look, "data")
else:
new_look = Look(
look_id=look_id,
slug=slug,
filename=filename,
name=name,
character_id=character_id,
data=data
)
_sync_nsfw_from_tags(new_look, data)
db.session.add(new_look)
except Exception as e:
print(f"Error importing look {filename}: {e}")
all_looks = Look.query.all()
for look in all_looks:
if look.look_id not in current_ids:
db.session.delete(look)
db.session.commit()
_sync_category('LOOKS_DIR', Look, 'look_id', 'look_name',
extra_fn=_sync_look_extra)
def sync_presets():
if not os.path.exists(current_app.config['PRESETS_DIR']):
return
current_ids = []
for filename in os.listdir(current_app.config['PRESETS_DIR']):
if filename.endswith('.json'):
file_path = os.path.join(current_app.config['PRESETS_DIR'], filename)
try:
with open(file_path, 'r') as f:
data = json.load(f)
preset_id = data.get('preset_id') or filename.replace('.json', '')
current_ids.append(preset_id)
slug = re.sub(r'[^a-zA-Z0-9_]', '', preset_id)
preset = Preset.query.filter_by(preset_id=preset_id).first()
name = data.get('preset_name', preset_id.replace('_', ' ').title())
if preset:
preset.data = data
preset.name = name
preset.slug = slug
preset.filename = filename
if preset.image_path:
full_img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], preset.image_path)
if not os.path.exists(full_img_path):
preset.image_path = None
flag_modified(preset, "data")
else:
new_preset = Preset(
preset_id=preset_id,
slug=slug,
filename=filename,
name=name,
data=data
)
db.session.add(new_preset)
except Exception as e:
print(f"Error importing preset {filename}: {e}")
all_presets = Preset.query.all()
for preset in all_presets:
if preset.preset_id not in current_ids:
db.session.delete(preset)
db.session.commit()
_sync_category('PRESETS_DIR', Preset, 'preset_id', 'preset_name',
sync_nsfw=False)
# ---------------------------------------------------------------------------
@@ -404,240 +322,16 @@ def _resolve_preset_fields(preset_data):
def sync_actions():
if not os.path.exists(current_app.config['ACTIONS_DIR']):
return
current_ids = []
for filename in os.listdir(current_app.config['ACTIONS_DIR']):
if filename.endswith('.json'):
file_path = os.path.join(current_app.config['ACTIONS_DIR'], filename)
try:
with open(file_path, 'r') as f:
data = json.load(f)
action_id = data.get('action_id') or filename.replace('.json', '')
current_ids.append(action_id)
# Generate URL-safe slug
slug = re.sub(r'[^a-zA-Z0-9_]', '', action_id)
# Check if action already exists
action = Action.query.filter_by(action_id=action_id).first()
name = data.get('action_name', action_id.replace('_', ' ').title())
if action:
action.data = data
action.name = name
action.slug = slug
action.filename = filename
_sync_nsfw_from_tags(action, data)
# Check if cover image still exists
if action.image_path:
full_img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], action.image_path)
if not os.path.exists(full_img_path):
print(f"Image missing for {action.name}, clearing path.")
action.image_path = None
flag_modified(action, "data")
else:
new_action = Action(
action_id=action_id,
slug=slug,
filename=filename,
name=name,
data=data
)
_sync_nsfw_from_tags(new_action, data)
db.session.add(new_action)
except Exception as e:
print(f"Error importing action {filename}: {e}")
# Remove actions that are no longer in the folder
all_actions = Action.query.all()
for action in all_actions:
if action.action_id not in current_ids:
db.session.delete(action)
db.session.commit()
_sync_category('ACTIONS_DIR', Action, 'action_id', 'action_name')
def sync_styles():
if not os.path.exists(current_app.config['STYLES_DIR']):
return
current_ids = []
for filename in os.listdir(current_app.config['STYLES_DIR']):
if filename.endswith('.json'):
file_path = os.path.join(current_app.config['STYLES_DIR'], filename)
try:
with open(file_path, 'r') as f:
data = json.load(f)
style_id = data.get('style_id') or filename.replace('.json', '')
current_ids.append(style_id)
# Generate URL-safe slug
slug = re.sub(r'[^a-zA-Z0-9_]', '', style_id)
# Check if style already exists
style = Style.query.filter_by(style_id=style_id).first()
name = data.get('style_name', style_id.replace('_', ' ').title())
if style:
style.data = data
style.name = name
style.slug = slug
style.filename = filename
_sync_nsfw_from_tags(style, data)
# Check if cover image still exists
if style.image_path:
full_img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], style.image_path)
if not os.path.exists(full_img_path):
print(f"Image missing for {style.name}, clearing path.")
style.image_path = None
flag_modified(style, "data")
else:
new_style = Style(
style_id=style_id,
slug=slug,
filename=filename,
name=name,
data=data
)
_sync_nsfw_from_tags(new_style, data)
db.session.add(new_style)
except Exception as e:
print(f"Error importing style {filename}: {e}")
# Remove styles that are no longer in the folder
all_styles = Style.query.all()
for style in all_styles:
if style.style_id not in current_ids:
db.session.delete(style)
db.session.commit()
_sync_category('STYLES_DIR', Style, 'style_id', 'style_name')
def sync_detailers():
if not os.path.exists(current_app.config['DETAILERS_DIR']):
return
current_ids = []
for filename in os.listdir(current_app.config['DETAILERS_DIR']):
if filename.endswith('.json'):
file_path = os.path.join(current_app.config['DETAILERS_DIR'], filename)
try:
with open(file_path, 'r') as f:
data = json.load(f)
detailer_id = data.get('detailer_id') or filename.replace('.json', '')
current_ids.append(detailer_id)
# Generate URL-safe slug
slug = re.sub(r'[^a-zA-Z0-9_]', '', detailer_id)
# Check if detailer already exists
detailer = Detailer.query.filter_by(detailer_id=detailer_id).first()
name = data.get('detailer_name', detailer_id.replace('_', ' ').title())
if detailer:
detailer.data = data
detailer.name = name
detailer.slug = slug
detailer.filename = filename
_sync_nsfw_from_tags(detailer, data)
# Check if cover image still exists
if detailer.image_path:
full_img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], detailer.image_path)
if not os.path.exists(full_img_path):
print(f"Image missing for {detailer.name}, clearing path.")
detailer.image_path = None
flag_modified(detailer, "data")
else:
new_detailer = Detailer(
detailer_id=detailer_id,
slug=slug,
filename=filename,
name=name,
data=data
)
_sync_nsfw_from_tags(new_detailer, data)
db.session.add(new_detailer)
except Exception as e:
print(f"Error importing detailer {filename}: {e}")
# Remove detailers that are no longer in the folder
all_detailers = Detailer.query.all()
for detailer in all_detailers:
if detailer.detailer_id not in current_ids:
db.session.delete(detailer)
db.session.commit()
_sync_category('DETAILERS_DIR', Detailer, 'detailer_id', 'detailer_name')
def sync_scenes():
if not os.path.exists(current_app.config['SCENES_DIR']):
return
current_ids = []
for filename in os.listdir(current_app.config['SCENES_DIR']):
if filename.endswith('.json'):
file_path = os.path.join(current_app.config['SCENES_DIR'], filename)
try:
with open(file_path, 'r') as f:
data = json.load(f)
scene_id = data.get('scene_id') or filename.replace('.json', '')
current_ids.append(scene_id)
# Generate URL-safe slug
slug = re.sub(r'[^a-zA-Z0-9_]', '', scene_id)
# Check if scene already exists
scene = Scene.query.filter_by(scene_id=scene_id).first()
name = data.get('scene_name', scene_id.replace('_', ' ').title())
if scene:
scene.data = data
scene.name = name
scene.slug = slug
scene.filename = filename
_sync_nsfw_from_tags(scene, data)
# Check if cover image still exists
if scene.image_path:
full_img_path = os.path.join(current_app.config['UPLOAD_FOLDER'], scene.image_path)
if not os.path.exists(full_img_path):
print(f"Image missing for {scene.name}, clearing path.")
scene.image_path = None
flag_modified(scene, "data")
else:
new_scene = Scene(
scene_id=scene_id,
slug=slug,
filename=filename,
name=name,
data=data
)
_sync_nsfw_from_tags(new_scene, data)
db.session.add(new_scene)
except Exception as e:
print(f"Error importing scene {filename}: {e}")
# Remove scenes that are no longer in the folder
all_scenes = Scene.query.all()
for scene in all_scenes:
if scene.scene_id not in current_ids:
db.session.delete(scene)
db.session.commit()
_sync_category('SCENES_DIR', Scene, 'scene_id', 'scene_name')
def _default_checkpoint_data(checkpoint_path, filename):
"""Return template-default data for a checkpoint with no JSON file."""
@@ -650,6 +344,7 @@ def _default_checkpoint_data(checkpoint_path, filename):
"steps": 25,
"cfg": 5,
"sampler_name": "euler_ancestral",
"scheduler": "normal",
"vae": "integrated"
}
@@ -669,7 +364,7 @@ def sync_checkpoints():
if ckpt_path:
json_data_by_path[ckpt_path] = data
except Exception as e:
print(f"Error reading checkpoint JSON {filename}: {e}")
logger.error("Error reading checkpoint JSON %s: %s", filename, e)
current_ids = []
dirs = [