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>
This commit is contained in:
@@ -15,8 +15,8 @@ from services.prompts import build_prompt, _resolve_character, _ensure_character
|
||||
from services.sync import sync_actions
|
||||
from services.file_io import get_available_loras
|
||||
from services.llm import load_prompt, call_llm
|
||||
from utils import allowed_file, _LORA_DEFAULTS
|
||||
from routes.shared import register_common_routes
|
||||
from utils import allowed_file, _LORA_DEFAULTS, clean_html_text
|
||||
from routes.shared import register_common_routes, apply_library_filters
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
@@ -26,17 +26,8 @@ def register_routes(app):
|
||||
|
||||
@app.route('/actions')
|
||||
def actions_index():
|
||||
query = Action.query
|
||||
fav = request.args.get('favourite')
|
||||
nsfw = request.args.get('nsfw', 'all')
|
||||
if fav == 'on':
|
||||
query = query.filter_by(is_favourite=True)
|
||||
if nsfw == 'sfw':
|
||||
query = query.filter_by(is_nsfw=False)
|
||||
elif nsfw == 'nsfw':
|
||||
query = query.filter_by(is_nsfw=True)
|
||||
actions = query.order_by(Action.is_favourite.desc(), Action.name).all()
|
||||
return render_template('actions/index.html', actions=actions, favourite_filter=fav or '', nsfw_filter=nsfw)
|
||||
actions, fav, nsfw = apply_library_filters(Action.query, Action)
|
||||
return render_template('actions/index.html', actions=actions, favourite_filter=fav, nsfw_filter=nsfw)
|
||||
|
||||
@app.route('/actions/rescan', methods=['POST'])
|
||||
def rescan_actions():
|
||||
@@ -228,9 +219,9 @@ def register_routes(app):
|
||||
selected_fields.append(f'identity::{key}')
|
||||
# Add wardrobe fields (unless suppressed)
|
||||
if not suppress_wardrobe:
|
||||
from utils import _WARDROBE_KEYS
|
||||
from utils import _BODY_GROUP_KEYS
|
||||
wardrobe = character.get_active_wardrobe()
|
||||
for key in _WARDROBE_KEYS:
|
||||
for key in _BODY_GROUP_KEYS:
|
||||
if wardrobe.get(key):
|
||||
selected_fields.append(f'wardrobe::{key}')
|
||||
|
||||
@@ -302,9 +293,9 @@ def register_routes(app):
|
||||
|
||||
# Wardrobe (active outfit) — skip if suppressed
|
||||
if not suppress_wardrobe:
|
||||
from utils import _WARDROBE_KEYS
|
||||
from utils import _BODY_GROUP_KEYS
|
||||
wardrobe = extra_char.get_active_wardrobe()
|
||||
for key in _WARDROBE_KEYS:
|
||||
for key in _BODY_GROUP_KEYS:
|
||||
val = wardrobe.get(key)
|
||||
if val:
|
||||
extra_parts.append(val)
|
||||
@@ -389,11 +380,7 @@ def register_routes(app):
|
||||
try:
|
||||
with open(html_path, 'r', encoding='utf-8', errors='ignore') as hf:
|
||||
html_raw = hf.read()
|
||||
clean_html = re.sub(r'<script[^>]*>.*?</script>', '', html_raw, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<style[^>]*>.*?</style>', '', clean_html, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<img[^>]*>', '', clean_html)
|
||||
clean_html = re.sub(r'<[^>]+>', ' ', clean_html)
|
||||
html_content = ' '.join(clean_html.split())
|
||||
html_content = clean_html_text(html_raw)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from services.llm import call_character_mcp_tool, call_llm, load_prompt
|
||||
from services.prompts import build_prompt
|
||||
from services.sync import sync_characters
|
||||
from services.workflow import _get_default_checkpoint, _prepare_workflow
|
||||
from routes.shared import register_common_routes
|
||||
from routes.shared import register_common_routes, apply_library_filters
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
@@ -22,17 +22,8 @@ def register_routes(app):
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
query = Character.query
|
||||
fav = request.args.get('favourite')
|
||||
nsfw = request.args.get('nsfw', 'all')
|
||||
if fav == 'on':
|
||||
query = query.filter_by(is_favourite=True)
|
||||
if nsfw == 'sfw':
|
||||
query = query.filter_by(is_nsfw=False)
|
||||
elif nsfw == 'nsfw':
|
||||
query = query.filter_by(is_nsfw=True)
|
||||
characters = query.order_by(Character.is_favourite.desc(), Character.name).all()
|
||||
return render_template('index.html', characters=characters, favourite_filter=fav or '', nsfw_filter=nsfw)
|
||||
characters, fav, nsfw = apply_library_filters(Character.query, Character)
|
||||
return render_template('index.html', characters=characters, favourite_filter=fav, nsfw_filter=nsfw)
|
||||
|
||||
@app.route('/rescan', methods=['POST'])
|
||||
def rescan():
|
||||
@@ -274,16 +265,16 @@ def register_routes(app):
|
||||
# Fetch reference data from wiki URL if provided
|
||||
wiki_reference = ''
|
||||
if wiki_url:
|
||||
logger.info(f"Fetching character data from URL: {wiki_url}")
|
||||
logger.info("Fetching character data from URL: %s", wiki_url)
|
||||
wiki_data = call_character_mcp_tool('get_character_from_url', {
|
||||
'url': wiki_url,
|
||||
'name': name,
|
||||
})
|
||||
if wiki_data:
|
||||
wiki_reference = f"\n\nReference data from wiki:\n{wiki_data}\n\nUse this reference to accurately describe the character's appearance, outfit, and features."
|
||||
logger.info(f"Got wiki reference data ({len(wiki_data)} chars)")
|
||||
logger.info("Got wiki reference data (%d chars)", len(wiki_data))
|
||||
else:
|
||||
logger.warning(f"Failed to fetch wiki data from {wiki_url}")
|
||||
logger.warning("Failed to fetch wiki data from %s", wiki_url)
|
||||
|
||||
# Step 1: Generate or select outfit first
|
||||
default_outfit_id = 'default'
|
||||
@@ -352,7 +343,7 @@ Create an outfit JSON with wardrobe fields appropriate for this character."""
|
||||
db.session.commit()
|
||||
|
||||
default_outfit_id = outfit_slug
|
||||
logger.info(f"Generated outfit: {outfit_name} for character {name}")
|
||||
logger.info("Generated outfit: %s for character %s", outfit_name, name)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Outfit generation error: %s", e)
|
||||
|
||||
@@ -14,8 +14,8 @@ from services.prompts import build_prompt, _resolve_character, _ensure_character
|
||||
from services.sync import sync_checkpoints, _default_checkpoint_data
|
||||
from services.file_io import get_available_checkpoints
|
||||
from services.llm import load_prompt, call_llm
|
||||
from utils import allowed_file
|
||||
from routes.shared import register_common_routes
|
||||
from utils import allowed_file, clean_html_text
|
||||
from routes.shared import register_common_routes, apply_library_filters
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
@@ -58,17 +58,8 @@ def register_routes(app):
|
||||
|
||||
@app.route('/checkpoints')
|
||||
def checkpoints_index():
|
||||
query = Checkpoint.query
|
||||
fav = request.args.get('favourite')
|
||||
nsfw = request.args.get('nsfw', 'all')
|
||||
if fav == 'on':
|
||||
query = query.filter_by(is_favourite=True)
|
||||
if nsfw == 'sfw':
|
||||
query = query.filter_by(is_nsfw=False)
|
||||
elif nsfw == 'nsfw':
|
||||
query = query.filter_by(is_nsfw=True)
|
||||
checkpoints = query.order_by(Checkpoint.is_favourite.desc(), Checkpoint.name).all()
|
||||
return render_template('checkpoints/index.html', checkpoints=checkpoints, favourite_filter=fav or '', nsfw_filter=nsfw)
|
||||
checkpoints, fav, nsfw = apply_library_filters(Checkpoint.query, Checkpoint)
|
||||
return render_template('checkpoints/index.html', checkpoints=checkpoints, favourite_filter=fav, nsfw_filter=nsfw)
|
||||
|
||||
@app.route('/checkpoints/rescan', methods=['POST'])
|
||||
def rescan_checkpoints():
|
||||
@@ -179,11 +170,7 @@ def register_routes(app):
|
||||
try:
|
||||
with open(html_path, 'r', encoding='utf-8', errors='ignore') as hf:
|
||||
html_raw = hf.read()
|
||||
clean_html = re.sub(r'<script[^>]*>.*?</script>', '', html_raw, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<style[^>]*>.*?</style>', '', clean_html, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<img[^>]*>', '', clean_html)
|
||||
clean_html = re.sub(r'<[^>]+>', ' ', clean_html)
|
||||
html_content = ' '.join(clean_html.split())
|
||||
html_content = clean_html_text(html_raw)
|
||||
except Exception as e:
|
||||
logger.error("Error reading HTML for %s: %s", filename, e)
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ from services.prompts import build_prompt, _resolve_character, _ensure_character
|
||||
from services.sync import sync_detailers
|
||||
from services.file_io import get_available_loras
|
||||
from services.llm import load_prompt, call_llm
|
||||
from utils import _WARDROBE_KEYS
|
||||
from routes.shared import register_common_routes
|
||||
from utils import _BODY_GROUP_KEYS, clean_html_text
|
||||
from routes.shared import register_common_routes, apply_library_filters
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
@@ -47,7 +47,7 @@ def register_routes(app):
|
||||
selected_fields.append(f'identity::{key}')
|
||||
selected_fields.append('special::name')
|
||||
wardrobe = character.get_active_wardrobe()
|
||||
for key in _WARDROBE_KEYS:
|
||||
for key in _BODY_GROUP_KEYS:
|
||||
if wardrobe.get(key):
|
||||
selected_fields.append(f'wardrobe::{key}')
|
||||
selected_fields.extend(['lora::lora_triggers'])
|
||||
@@ -87,17 +87,8 @@ def register_routes(app):
|
||||
|
||||
@app.route('/detailers')
|
||||
def detailers_index():
|
||||
query = Detailer.query
|
||||
fav = request.args.get('favourite')
|
||||
nsfw = request.args.get('nsfw', 'all')
|
||||
if fav == 'on':
|
||||
query = query.filter_by(is_favourite=True)
|
||||
if nsfw == 'sfw':
|
||||
query = query.filter_by(is_nsfw=False)
|
||||
elif nsfw == 'nsfw':
|
||||
query = query.filter_by(is_nsfw=True)
|
||||
detailers = query.order_by(Detailer.is_favourite.desc(), Detailer.name).all()
|
||||
return render_template('detailers/index.html', detailers=detailers, favourite_filter=fav or '', nsfw_filter=nsfw)
|
||||
detailers, fav, nsfw = apply_library_filters(Detailer.query, Detailer)
|
||||
return render_template('detailers/index.html', detailers=detailers, favourite_filter=fav, nsfw_filter=nsfw)
|
||||
|
||||
@app.route('/detailers/rescan', methods=['POST'])
|
||||
def rescan_detailers():
|
||||
@@ -296,11 +287,7 @@ def register_routes(app):
|
||||
try:
|
||||
with open(html_path, 'r', encoding='utf-8', errors='ignore') as hf:
|
||||
html_raw = hf.read()
|
||||
clean_html = re.sub(r'<script[^>]*>.*?</script>', '', html_raw, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<style[^>]*>.*?</style>', '', clean_html, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<img[^>]*>', '', clean_html)
|
||||
clean_html = re.sub(r'<[^>]+>', ' ', clean_html)
|
||||
html_content = ' '.join(clean_html.split())
|
||||
html_content = clean_html_text(html_raw)
|
||||
except Exception as e:
|
||||
logger.error("Error reading HTML %s: %s", html_filename, e)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import json
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from utils import clean_html_text
|
||||
|
||||
from flask import render_template, request, redirect, url_for, flash, session
|
||||
from sqlalchemy.orm.attributes import flag_modified
|
||||
@@ -13,7 +14,7 @@ from services.prompts import build_prompt, _resolve_character, _ensure_character
|
||||
from services.sync import sync_looks
|
||||
from services.file_io import get_available_loras, _count_look_assignments
|
||||
from services.llm import load_prompt, call_llm
|
||||
from routes.shared import register_common_routes
|
||||
from routes.shared import register_common_routes, apply_library_filters
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
@@ -57,18 +58,9 @@ def register_routes(app):
|
||||
|
||||
@app.route('/looks')
|
||||
def looks_index():
|
||||
query = Look.query
|
||||
fav = request.args.get('favourite')
|
||||
nsfw = request.args.get('nsfw', 'all')
|
||||
if fav == 'on':
|
||||
query = query.filter_by(is_favourite=True)
|
||||
if nsfw == 'sfw':
|
||||
query = query.filter_by(is_nsfw=False)
|
||||
elif nsfw == 'nsfw':
|
||||
query = query.filter_by(is_nsfw=True)
|
||||
looks = query.order_by(Look.is_favourite.desc(), Look.name).all()
|
||||
looks, fav, nsfw = apply_library_filters(Look.query, Look)
|
||||
look_assignments = _count_look_assignments()
|
||||
return render_template('looks/index.html', looks=looks, look_assignments=look_assignments, favourite_filter=fav or '', nsfw_filter=nsfw)
|
||||
return render_template('looks/index.html', looks=looks, look_assignments=look_assignments, favourite_filter=fav, nsfw_filter=nsfw)
|
||||
|
||||
@app.route('/looks/rescan', methods=['POST'])
|
||||
def rescan_looks():
|
||||
@@ -320,7 +312,7 @@ Character ID: {character_slug}"""
|
||||
character_data['lora'] = lora_data
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"LLM character generation error: {e}")
|
||||
logger.exception("LLM character generation error: %s", e)
|
||||
flash(f'Failed to generate character with AI: {e}', 'error')
|
||||
return redirect(url_for('look_detail', slug=slug))
|
||||
else:
|
||||
@@ -494,11 +486,7 @@ Character ID: {character_slug}"""
|
||||
try:
|
||||
with open(html_path, 'r', encoding='utf-8', errors='ignore') as hf:
|
||||
html_raw = hf.read()
|
||||
clean_html = re.sub(r'<script[^>]*>.*?</script>', '', html_raw, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<style[^>]*>.*?</style>', '', clean_html, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<img[^>]*>', '', clean_html)
|
||||
clean_html = re.sub(r'<[^>]+>', ' ', clean_html)
|
||||
html_content = ' '.join(clean_html.split())
|
||||
html_content = clean_html_text(html_raw)
|
||||
except Exception as e:
|
||||
logger.error("Error reading HTML %s: %s", html_filename, e)
|
||||
|
||||
|
||||
@@ -13,9 +13,9 @@ from services.job_queue import _enqueue_job, _make_finalize, _enqueue_task
|
||||
from services.prompts import build_prompt, _resolve_character, _ensure_character_fields, _append_background
|
||||
from services.sync import sync_outfits
|
||||
from services.file_io import get_available_loras, _count_outfit_lora_assignments
|
||||
from utils import allowed_file, _LORA_DEFAULTS
|
||||
from utils import allowed_file, _LORA_DEFAULTS, clean_html_text
|
||||
from services.llm import load_prompt, call_llm
|
||||
from routes.shared import register_common_routes
|
||||
from routes.shared import register_common_routes, apply_library_filters
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
@@ -25,18 +25,9 @@ def register_routes(app):
|
||||
|
||||
@app.route('/outfits')
|
||||
def outfits_index():
|
||||
query = Outfit.query
|
||||
fav = request.args.get('favourite')
|
||||
nsfw = request.args.get('nsfw', 'all')
|
||||
if fav == 'on':
|
||||
query = query.filter_by(is_favourite=True)
|
||||
if nsfw == 'sfw':
|
||||
query = query.filter_by(is_nsfw=False)
|
||||
elif nsfw == 'nsfw':
|
||||
query = query.filter_by(is_nsfw=True)
|
||||
outfits = query.order_by(Outfit.is_favourite.desc(), Outfit.name).all()
|
||||
outfits, fav, nsfw = apply_library_filters(Outfit.query, Outfit)
|
||||
lora_assignments = _count_outfit_lora_assignments()
|
||||
return render_template('outfits/index.html', outfits=outfits, lora_assignments=lora_assignments, favourite_filter=fav or '', nsfw_filter=nsfw)
|
||||
return render_template('outfits/index.html', outfits=outfits, lora_assignments=lora_assignments, favourite_filter=fav, nsfw_filter=nsfw)
|
||||
|
||||
@app.route('/outfits/rescan', methods=['POST'])
|
||||
def rescan_outfits():
|
||||
@@ -90,11 +81,7 @@ def register_routes(app):
|
||||
try:
|
||||
with open(html_path, 'r', encoding='utf-8', errors='ignore') as hf:
|
||||
html_raw = hf.read()
|
||||
clean_html = re.sub(r'<script[^>]*>.*?</script>', '', html_raw, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<style[^>]*>.*?</style>', '', clean_html, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<img[^>]*>', '', clean_html)
|
||||
clean_html = re.sub(r'<[^>]+>', ' ', clean_html)
|
||||
html_content = ' '.join(clean_html.split())
|
||||
html_content = clean_html_text(html_raw)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
@@ -313,12 +300,12 @@ def register_routes(app):
|
||||
# No explicit field selection (e.g. batch generation) — build a selection
|
||||
# that includes identity + wardrobe + name + lora triggers, but NOT character
|
||||
# defaults (expression, pose, scene), so outfit covers stay generic.
|
||||
from utils import _IDENTITY_KEYS, _WARDROBE_KEYS
|
||||
for key in _IDENTITY_KEYS:
|
||||
from utils import _BODY_GROUP_KEYS
|
||||
for key in _BODY_GROUP_KEYS:
|
||||
if character.data.get('identity', {}).get(key):
|
||||
selected_fields.append(f'identity::{key}')
|
||||
outfit_wardrobe = outfit.data.get('wardrobe', {})
|
||||
for key in _WARDROBE_KEYS:
|
||||
for key in _BODY_GROUP_KEYS:
|
||||
if outfit_wardrobe.get(key):
|
||||
selected_fields.append(f'wardrobe::{key}')
|
||||
selected_fields.append('special::name')
|
||||
|
||||
@@ -8,7 +8,7 @@ from sqlalchemy.orm.attributes import flag_modified
|
||||
from services.sync import sync_presets
|
||||
from services.generation import generate_from_preset
|
||||
from services.llm import load_prompt, call_llm
|
||||
from routes.shared import register_common_routes
|
||||
from routes.shared import register_common_routes, apply_library_filters
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
@@ -18,8 +18,9 @@ def register_routes(app):
|
||||
|
||||
@app.route('/presets')
|
||||
def presets_index():
|
||||
presets = Preset.query.order_by(Preset.filename).all()
|
||||
return render_template('presets/index.html', presets=presets)
|
||||
presets, fav, nsfw = apply_library_filters(Preset.query, Preset)
|
||||
return render_template('presets/index.html', presets=presets,
|
||||
favourite_filter=fav, nsfw_filter=nsfw)
|
||||
|
||||
@app.route('/preset/<path:slug>')
|
||||
def preset_detail(slug):
|
||||
|
||||
@@ -63,7 +63,7 @@ def register_routes(app):
|
||||
clean_json = llm_response.replace('```json', '').replace('```', '').strip()
|
||||
new_data = json.loads(clean_json)
|
||||
except Exception as e:
|
||||
logger.exception(f"Regenerate tags LLM error for {category}/{slug}")
|
||||
logger.exception("Regenerate tags LLM error for %s/%s", category, slug)
|
||||
return {'error': f'LLM error: {str(e)}'}, 500
|
||||
|
||||
# Preserve protected fields from original
|
||||
@@ -106,7 +106,7 @@ def register_routes(app):
|
||||
with open(file_path, 'w') as f:
|
||||
json.dump(new_data, f, indent=2)
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not write {file_path}: {e}")
|
||||
logger.warning("Could not write %s: %s", file_path, e)
|
||||
|
||||
migrated += 1
|
||||
|
||||
@@ -122,7 +122,7 @@ def register_routes(app):
|
||||
migrated += 1
|
||||
|
||||
db.session.commit()
|
||||
logger.info(f"Migrated {migrated} resources from list tags to dict tags")
|
||||
logger.info("Migrated %d resources from list tags to dict tags", migrated)
|
||||
return {'success': True, 'migrated': migrated}
|
||||
|
||||
def _make_regen_task(category, slug, name, system_prompt):
|
||||
|
||||
@@ -13,8 +13,8 @@ from services.prompts import build_prompt, _resolve_character, _ensure_character
|
||||
from services.sync import sync_scenes
|
||||
from services.file_io import get_available_loras
|
||||
from services.llm import load_prompt, call_llm
|
||||
from routes.shared import register_common_routes
|
||||
from utils import _WARDROBE_KEYS
|
||||
from routes.shared import register_common_routes, apply_library_filters
|
||||
from utils import _BODY_GROUP_KEYS, clean_html_text
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
@@ -24,17 +24,8 @@ def register_routes(app):
|
||||
|
||||
@app.route('/scenes')
|
||||
def scenes_index():
|
||||
query = Scene.query
|
||||
fav = request.args.get('favourite')
|
||||
nsfw = request.args.get('nsfw', 'all')
|
||||
if fav == 'on':
|
||||
query = query.filter_by(is_favourite=True)
|
||||
if nsfw == 'sfw':
|
||||
query = query.filter_by(is_nsfw=False)
|
||||
elif nsfw == 'nsfw':
|
||||
query = query.filter_by(is_nsfw=True)
|
||||
scenes = query.order_by(Scene.is_favourite.desc(), Scene.name).all()
|
||||
return render_template('scenes/index.html', scenes=scenes, favourite_filter=fav or '', nsfw_filter=nsfw)
|
||||
scenes, fav, nsfw = apply_library_filters(Scene.query, Scene)
|
||||
return render_template('scenes/index.html', scenes=scenes, favourite_filter=fav, nsfw_filter=nsfw)
|
||||
|
||||
@app.route('/scenes/rescan', methods=['POST'])
|
||||
def rescan_scenes():
|
||||
@@ -177,7 +168,7 @@ def register_routes(app):
|
||||
selected_fields.append(f'identity::{key}')
|
||||
selected_fields.append('special::name')
|
||||
wardrobe = character.get_active_wardrobe()
|
||||
for key in _WARDROBE_KEYS:
|
||||
for key in _BODY_GROUP_KEYS:
|
||||
if wardrobe.get(key):
|
||||
selected_fields.append(f'wardrobe::{key}')
|
||||
selected_fields.extend(['defaults::scene', 'lora::lora_triggers'])
|
||||
@@ -312,11 +303,7 @@ def register_routes(app):
|
||||
try:
|
||||
with open(html_path, 'r', encoding='utf-8', errors='ignore') as hf:
|
||||
html_raw = hf.read()
|
||||
clean_html = re.sub(r'<script[^>]*>.*?</script>', '', html_raw, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<style[^>]*>.*?</style>', '', clean_html, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<img[^>]*>', '', clean_html)
|
||||
clean_html = re.sub(r'<[^>]+>', ' ', clean_html)
|
||||
html_content = ' '.join(clean_html.split())
|
||||
html_content = clean_html_text(html_raw)
|
||||
except Exception as e:
|
||||
logger.error("Error reading HTML %s: %s", html_filename, e)
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ def register_routes(app):
|
||||
db.session.commit()
|
||||
logger.info("Default checkpoint saved to database: %s", checkpoint_path)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to persist checkpoint to database: {e}")
|
||||
logger.error("Failed to persist checkpoint to database: %s", e)
|
||||
db.session.rollback()
|
||||
|
||||
# Also persist to comfy_workflow.json for backwards compatibility
|
||||
@@ -78,7 +78,7 @@ def register_routes(app):
|
||||
with open(workflow_path, 'w') as f:
|
||||
json.dump(workflow, f, indent=2)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to persist checkpoint to workflow file: {e}")
|
||||
logger.error("Failed to persist checkpoint to workflow file: %s", e)
|
||||
|
||||
return {'status': 'ok'}
|
||||
|
||||
|
||||
@@ -20,6 +20,23 @@ from utils import allowed_file
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
|
||||
def apply_library_filters(query, model_class):
|
||||
"""Apply standard favourite/NSFW filters and sorting to a library query.
|
||||
|
||||
Returns (items, favourite_filter, nsfw_filter) tuple.
|
||||
"""
|
||||
fav = request.args.get('favourite')
|
||||
nsfw = request.args.get('nsfw', 'all')
|
||||
if fav == 'on':
|
||||
query = query.filter_by(is_favourite=True)
|
||||
if nsfw == 'sfw':
|
||||
query = query.filter_by(is_nsfw=False)
|
||||
elif nsfw == 'nsfw':
|
||||
query = query.filter_by(is_nsfw=True)
|
||||
items = query.order_by(model_class.is_favourite.desc(), model_class.name).all()
|
||||
return items, fav or '', nsfw
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Category configuration registry
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -237,11 +254,16 @@ def _register_replace_cover_route(app, cfg):
|
||||
def replace_cover(slug):
|
||||
entity = Model.query.filter_by(slug=slug).first_or_404()
|
||||
preview_path = request.form.get('preview_path')
|
||||
if preview_path and os.path.exists(
|
||||
os.path.join(current_app.config['UPLOAD_FOLDER'], preview_path)):
|
||||
entity.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
if preview_path:
|
||||
full_path = os.path.realpath(
|
||||
os.path.join(current_app.config['UPLOAD_FOLDER'], preview_path))
|
||||
upload_root = os.path.realpath(current_app.config['UPLOAD_FOLDER'])
|
||||
if full_path.startswith(upload_root + os.sep) and os.path.exists(full_path):
|
||||
entity.image_path = preview_path
|
||||
db.session.commit()
|
||||
flash('Cover image updated!')
|
||||
else:
|
||||
flash('Invalid preview path.', 'error')
|
||||
else:
|
||||
flash('No valid preview image selected.', 'error')
|
||||
return redirect(url_for(detail_ep, slug=slug))
|
||||
|
||||
@@ -14,8 +14,8 @@ from services.prompts import build_prompt, _resolve_character, _ensure_character
|
||||
from services.sync import sync_styles
|
||||
from services.file_io import get_available_loras
|
||||
from services.llm import load_prompt, call_llm
|
||||
from routes.shared import register_common_routes
|
||||
from utils import _WARDROBE_KEYS
|
||||
from routes.shared import register_common_routes, apply_library_filters
|
||||
from utils import _BODY_GROUP_KEYS, clean_html_text
|
||||
|
||||
logger = logging.getLogger('gaze')
|
||||
|
||||
@@ -47,7 +47,7 @@ def register_routes(app):
|
||||
selected_fields.append(f'identity::{key}')
|
||||
selected_fields.append('special::name')
|
||||
wardrobe = character.get_active_wardrobe()
|
||||
for key in _WARDROBE_KEYS:
|
||||
for key in _BODY_GROUP_KEYS:
|
||||
if wardrobe.get(key):
|
||||
selected_fields.append(f'wardrobe::{key}')
|
||||
selected_fields.extend(['style::artist_name', 'style::artistic_style', 'lora::lora_triggers'])
|
||||
@@ -82,17 +82,8 @@ def register_routes(app):
|
||||
|
||||
@app.route('/styles')
|
||||
def styles_index():
|
||||
query = Style.query
|
||||
fav = request.args.get('favourite')
|
||||
nsfw = request.args.get('nsfw', 'all')
|
||||
if fav == 'on':
|
||||
query = query.filter_by(is_favourite=True)
|
||||
if nsfw == 'sfw':
|
||||
query = query.filter_by(is_nsfw=False)
|
||||
elif nsfw == 'nsfw':
|
||||
query = query.filter_by(is_nsfw=True)
|
||||
styles = query.order_by(Style.is_favourite.desc(), Style.name).all()
|
||||
return render_template('styles/index.html', styles=styles, favourite_filter=fav or '', nsfw_filter=nsfw)
|
||||
styles, fav, nsfw = apply_library_filters(Style.query, Style)
|
||||
return render_template('styles/index.html', styles=styles, favourite_filter=fav, nsfw_filter=nsfw)
|
||||
|
||||
@app.route('/styles/rescan', methods=['POST'])
|
||||
def rescan_styles():
|
||||
@@ -323,11 +314,7 @@ def register_routes(app):
|
||||
try:
|
||||
with open(html_path, 'r', encoding='utf-8', errors='ignore') as hf:
|
||||
html_raw = hf.read()
|
||||
clean_html = re.sub(r'<script[^>]*>.*?</script>', '', html_raw, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<style[^>]*>.*?</style>', '', clean_html, flags=re.DOTALL)
|
||||
clean_html = re.sub(r'<img[^>]*>', '', clean_html)
|
||||
clean_html = re.sub(r'<[^>]+>', ' ', clean_html)
|
||||
html_content = ' '.join(clean_html.split())
|
||||
html_content = clean_html_text(html_raw)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ Generate a complete {target_category.rstrip('s')} profile with all required fiel
|
||||
new_data[target_name_key] = new_name
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"LLM transfer error: {e}")
|
||||
logger.exception("LLM transfer error: %s", e)
|
||||
flash(f'Failed to generate {target_category.rstrip("s")} with AI: {e}')
|
||||
return redirect(url_for('transfer_resource', category=category, slug=slug))
|
||||
else:
|
||||
@@ -290,7 +290,7 @@ Generate a complete {target_category.rstrip('s')} profile with all required fiel
|
||||
lora_moved = True
|
||||
flash(f'Moved LoRA file to {target_lora_dir}')
|
||||
except Exception as lora_e:
|
||||
logger.exception(f"LoRA move error: {lora_e}")
|
||||
logger.exception("LoRA move error: %s", lora_e)
|
||||
flash(f'Warning: Failed to move LoRA file: {lora_e}', 'warning')
|
||||
else:
|
||||
flash(f'Warning: Source LoRA file not found at {abs_source_path}', 'warning')
|
||||
@@ -317,7 +317,7 @@ Generate a complete {target_category.rstrip('s')} profile with all required fiel
|
||||
db.session.delete(resource)
|
||||
flash(f'Removed original {category.rstrip("s")}: {resource_name}')
|
||||
except Exception as rm_e:
|
||||
logger.exception(f"Error removing original: {rm_e}")
|
||||
logger.exception("Error removing original: %s", rm_e)
|
||||
flash(f'Warning: Failed to remove original: {rm_e}', 'warning')
|
||||
|
||||
db.session.commit()
|
||||
@@ -325,7 +325,7 @@ Generate a complete {target_category.rstrip('s')} profile with all required fiel
|
||||
return redirect(url_for(target_config['index_route'], highlight=safe_slug))
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Transfer save error: {e}")
|
||||
logger.exception("Transfer save error: %s", e)
|
||||
flash(f'Failed to save transferred {target_category.rstrip("s")}: {e}')
|
||||
return redirect(url_for('transfer_resource', category=category, slug=slug))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user