4 Commits

Author SHA1 Message Date
Aodhan Collins
2c1c3a7ed7 Merge branch 'presets'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 23:09:00 +00:00
Aodhan Collins
ee36caccd6 Sort all batch generation queues by JSON filename
All get_missing_* routes and generate_missing routes now order results
by filename (alphabetical) instead of display name or undefined order.
Checkpoint uses checkpoint_path as the equivalent sort key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 23:08:37 +00:00
Aodhan Collins
e226548471 Merge branch 'logging'
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 23:02:41 +00:00
Aodhan Collins
b9196ef5f5 Add structured logging for job queue and workflow prompts
Replace bare print() calls with Python logging module. Job lifecycle
(enqueue, start, ComfyUI acceptance, completion, failure) now emits
timestamped INFO logs to stdout, captured by Docker. Failures use
logger.exception() for full tracebacks. Workflow prompt block logs as
a single INFO entry; LoRA chain details moved to DEBUG level.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-05 22:55:53 +00:00

79
app.py
View File

@@ -1,5 +1,6 @@
import os import os
import json import json
import logging
import time import time
import re import re
import requests import requests
@@ -42,6 +43,16 @@ app.config['SESSION_PERMANENT'] = False
db.init_app(app) db.init_app(app)
Session(app) Session(app)
# ---------------------------------------------------------------------------
# Logging
# ---------------------------------------------------------------------------
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(name)s: %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
)
logger = logging.getLogger('gaze')
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Generation Job Queue # Generation Job Queue
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -78,6 +89,7 @@ def _enqueue_job(label, workflow, finalize_fn):
with _job_queue_lock: with _job_queue_lock:
_job_queue.append(job) _job_queue.append(job)
_job_history[job['id']] = job _job_history[job['id']] = job
logger.info("Job queued: [%s] %s", job['id'][:8], label)
_queue_worker_event.set() _queue_worker_event.set()
return job return job
@@ -104,6 +116,8 @@ def _queue_worker():
with _job_queue_lock: with _job_queue_lock:
job['status'] = 'processing' job['status'] = 'processing'
logger.info("Job started: [%s] %s", job['id'][:8], job['label'])
try: try:
with app.app_context(): with app.app_context():
# Send workflow to ComfyUI # Send workflow to ComfyUI
@@ -114,6 +128,7 @@ def _queue_worker():
comfy_id = prompt_response['prompt_id'] comfy_id = prompt_response['prompt_id']
with _job_queue_lock: with _job_queue_lock:
job['comfy_prompt_id'] = comfy_id job['comfy_prompt_id'] = comfy_id
logger.info("Job [%s] queued in ComfyUI as %s", job['id'][:8], comfy_id)
# Poll until done (max ~10 minutes) # Poll until done (max ~10 minutes)
max_retries = 300 max_retries = 300
@@ -129,15 +144,17 @@ def _queue_worker():
if not finished: if not finished:
raise Exception("ComfyUI generation timed out") raise Exception("ComfyUI generation timed out")
logger.info("Job [%s] generation complete, finalizing...", job['id'][:8])
# Run the finalize callback (saves image to disk / DB) # Run the finalize callback (saves image to disk / DB)
# finalize_fn(comfy_prompt_id, job) — job is passed so callback can store result # finalize_fn(comfy_prompt_id, job) — job is passed so callback can store result
job['finalize_fn'](comfy_id, job) job['finalize_fn'](comfy_id, job)
with _job_queue_lock: with _job_queue_lock:
job['status'] = 'done' job['status'] = 'done'
logger.info("Job done: [%s] %s", job['id'][:8], job['label'])
except Exception as e: except Exception as e:
print(f"[Queue] Job {job['id']} failed: {e}") logger.exception("Job failed: [%s] %s%s", job['id'][:8], job['label'], e)
with _job_queue_lock: with _job_queue_lock:
job['status'] = 'failed' job['status'] = 'failed'
job['error'] = str(e) job['error'] = str(e)
@@ -2078,16 +2095,8 @@ def replace_cover_from_preview(slug):
return redirect(url_for('detail', slug=slug)) return redirect(url_for('detail', slug=slug))
def _log_workflow_prompts(label, workflow): def _log_workflow_prompts(label, workflow):
"""Print the final assembled ComfyUI prompts in a consistent, readable block.""" """Log the final assembled ComfyUI prompts in a consistent, readable block."""
sep = "=" * 72 sep = "=" * 72
print(f"\n{sep}")
print(f" WORKFLOW PROMPTS [{label}]")
print(sep)
print(f" Checkpoint : {workflow['4']['inputs'].get('ckpt_name', '(not set)')}")
print(f" Seed : {workflow['3']['inputs'].get('seed', '(not set)')}")
print(f" Resolution : {workflow['5']['inputs'].get('width', '?')} x {workflow['5']['inputs'].get('height', '?')}")
print(f" Sampler : {workflow['3']['inputs'].get('sampler_name', '?')} / {workflow['3']['inputs'].get('scheduler', '?')} steps={workflow['3']['inputs'].get('steps', '?')} cfg={workflow['3']['inputs'].get('cfg', '?')}")
# LoRA chain summary
active_loras = [] active_loras = []
for node_id, label_str in [("16", "char/look"), ("17", "outfit"), ("18", "action"), ("19", "style/detail/scene")]: for node_id, label_str in [("16", "char/look"), ("17", "outfit"), ("18", "action"), ("19", "style/detail/scene")]:
if node_id in workflow: if node_id in workflow:
@@ -2095,16 +2104,26 @@ def _log_workflow_prompts(label, workflow):
if name: if name:
w = workflow[node_id]["inputs"].get("strength_model", "?") w = workflow[node_id]["inputs"].get("strength_model", "?")
active_loras.append(f"{label_str}:{name.split('/')[-1]}@{w:.3f}" if isinstance(w, float) else f"{label_str}:{name.split('/')[-1]}@{w}") active_loras.append(f"{label_str}:{name.split('/')[-1]}@{w:.3f}" if isinstance(w, float) else f"{label_str}:{name.split('/')[-1]}@{w}")
print(f" LoRAs : {' | '.join(active_loras) if active_loras else '(none)'}")
print(f" [+] Positive : {workflow['6']['inputs'].get('text', '')}")
print(f" [-] Negative : {workflow['7']['inputs'].get('text', '')}")
face_text = workflow.get('14', {}).get('inputs', {}).get('text', '') face_text = workflow.get('14', {}).get('inputs', {}).get('text', '')
hand_text = workflow.get('15', {}).get('inputs', {}).get('text', '') hand_text = workflow.get('15', {}).get('inputs', {}).get('text', '')
lines = [
sep,
f" WORKFLOW PROMPTS [{label}]",
sep,
f" Checkpoint : {workflow['4']['inputs'].get('ckpt_name', '(not set)')}",
f" Seed : {workflow['3']['inputs'].get('seed', '(not set)')}",
f" Resolution : {workflow['5']['inputs'].get('width', '?')} x {workflow['5']['inputs'].get('height', '?')}",
f" Sampler : {workflow['3']['inputs'].get('sampler_name', '?')} / {workflow['3']['inputs'].get('scheduler', '?')} steps={workflow['3']['inputs'].get('steps', '?')} cfg={workflow['3']['inputs'].get('cfg', '?')}",
f" LoRAs : {' | '.join(active_loras) if active_loras else '(none)'}",
f" [+] Positive : {workflow['6']['inputs'].get('text', '')}",
f" [-] Negative : {workflow['7']['inputs'].get('text', '')}",
]
if face_text: if face_text:
print(f" [F] Face : {face_text}") lines.append(f" [F] Face : {face_text}")
if hand_text: if hand_text:
print(f" [H] Hand : {hand_text}") lines.append(f" [H] Hand : {hand_text}")
print(f"{sep}\n") lines.append(sep)
logger.info("\n%s", "\n".join(lines))
def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_negative=None, outfit=None, action=None, style=None, detailer=None, scene=None, width=None, height=None, checkpoint_data=None, look=None, fixed_seed=None): def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_negative=None, outfit=None, action=None, style=None, detailer=None, scene=None, width=None, height=None, checkpoint_data=None, look=None, fixed_seed=None):
@@ -2150,7 +2169,7 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
workflow["16"]["inputs"]["clip"] = ["4", 1] # From checkpoint workflow["16"]["inputs"]["clip"] = ["4", 1] # From checkpoint
model_source = ["16", 0] model_source = ["16", 0]
clip_source = ["16", 1] clip_source = ["16", 1]
print(f"Character LoRA: {char_lora_name} @ {_w16}") logger.debug("Character LoRA: %s @ %s", char_lora_name, _w16)
# Outfit LoRA (Node 17) - chains from character LoRA or checkpoint # Outfit LoRA (Node 17) - chains from character LoRA or checkpoint
outfit_lora_data = outfit.data.get('lora', {}) if outfit else {} outfit_lora_data = outfit.data.get('lora', {}) if outfit else {}
@@ -2166,7 +2185,7 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
workflow["17"]["inputs"]["clip"] = clip_source workflow["17"]["inputs"]["clip"] = clip_source
model_source = ["17", 0] model_source = ["17", 0]
clip_source = ["17", 1] clip_source = ["17", 1]
print(f"Outfit LoRA: {outfit_lora_name} @ {_w17}") logger.debug("Outfit LoRA: %s @ %s", outfit_lora_name, _w17)
# Action LoRA (Node 18) - chains from previous LoRA or checkpoint # Action LoRA (Node 18) - chains from previous LoRA or checkpoint
action_lora_data = action.data.get('lora', {}) if action else {} action_lora_data = action.data.get('lora', {}) if action else {}
@@ -2182,7 +2201,7 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
workflow["18"]["inputs"]["clip"] = clip_source workflow["18"]["inputs"]["clip"] = clip_source
model_source = ["18", 0] model_source = ["18", 0]
clip_source = ["18", 1] clip_source = ["18", 1]
print(f"Action LoRA: {action_lora_name} @ {_w18}") logger.debug("Action LoRA: %s @ %s", action_lora_name, _w18)
# Style/Detailer/Scene LoRA (Node 19) - chains from previous LoRA or checkpoint # Style/Detailer/Scene LoRA (Node 19) - chains from previous LoRA or checkpoint
# Priority: Style > Detailer > Scene (Scene LoRAs are rare but supported) # Priority: Style > Detailer > Scene (Scene LoRAs are rare but supported)
@@ -2200,7 +2219,7 @@ def _prepare_workflow(workflow, character, prompts, checkpoint=None, custom_nega
workflow["19"]["inputs"]["clip"] = clip_source workflow["19"]["inputs"]["clip"] = clip_source
model_source = ["19", 0] model_source = ["19", 0]
clip_source = ["19", 1] clip_source = ["19", 1]
print(f"Style/Detailer LoRA: {style_lora_name} @ {_w19}") logger.debug("Style/Detailer LoRA: %s @ %s", style_lora_name, _w19)
# Apply connections to all model/clip consumers # Apply connections to all model/clip consumers
workflow["3"]["inputs"]["model"] = model_source workflow["3"]["inputs"]["model"] = model_source
@@ -2266,7 +2285,7 @@ def _get_default_checkpoint():
@app.route('/get_missing_characters') @app.route('/get_missing_characters')
def get_missing_characters(): def get_missing_characters():
missing = Character.query.filter((Character.image_path == None) | (Character.image_path == '')).all() missing = Character.query.filter((Character.image_path == None) | (Character.image_path == '')).order_by(Character.filename).all()
return {'missing': [{'slug': c.slug, 'name': c.name} for c in missing]} return {'missing': [{'slug': c.slug, 'name': c.name} for c in missing]}
@app.route('/clear_all_covers', methods=['POST']) @app.route('/clear_all_covers', methods=['POST'])
@@ -2281,7 +2300,7 @@ def clear_all_covers():
def generate_missing(): def generate_missing():
missing = Character.query.filter( missing = Character.query.filter(
(Character.image_path == None) | (Character.image_path == '') (Character.image_path == None) | (Character.image_path == '')
).order_by(Character.name).all() ).order_by(Character.filename).all()
if not missing: if not missing:
flash("No characters missing cover images.") flash("No characters missing cover images.")
@@ -2351,7 +2370,7 @@ def save_defaults(slug):
@app.route('/get_missing_outfits') @app.route('/get_missing_outfits')
def get_missing_outfits(): def get_missing_outfits():
missing = Outfit.query.filter((Outfit.image_path == None) | (Outfit.image_path == '')).all() missing = Outfit.query.filter((Outfit.image_path == None) | (Outfit.image_path == '')).order_by(Outfit.filename).all()
return {'missing': [{'slug': o.slug, 'name': o.name} for o in missing]} return {'missing': [{'slug': o.slug, 'name': o.name} for o in missing]}
@app.route('/clear_all_outfit_covers', methods=['POST']) @app.route('/clear_all_outfit_covers', methods=['POST'])
@@ -2364,7 +2383,7 @@ def clear_all_outfit_covers():
@app.route('/get_missing_actions') @app.route('/get_missing_actions')
def get_missing_actions(): def get_missing_actions():
missing = Action.query.filter((Action.image_path == None) | (Action.image_path == '')).all() missing = Action.query.filter((Action.image_path == None) | (Action.image_path == '')).order_by(Action.filename).all()
return {'missing': [{'slug': a.slug, 'name': a.name} for a in missing]} return {'missing': [{'slug': a.slug, 'name': a.name} for a in missing]}
@app.route('/clear_all_action_covers', methods=['POST']) @app.route('/clear_all_action_covers', methods=['POST'])
@@ -2377,7 +2396,7 @@ def clear_all_action_covers():
@app.route('/get_missing_scenes') @app.route('/get_missing_scenes')
def get_missing_scenes(): def get_missing_scenes():
missing = Scene.query.filter((Scene.image_path == None) | (Scene.image_path == '')).all() missing = Scene.query.filter((Scene.image_path == None) | (Scene.image_path == '')).order_by(Scene.filename).all()
return {'missing': [{'slug': s.slug, 'name': s.name} for s in missing]} return {'missing': [{'slug': s.slug, 'name': s.name} for s in missing]}
@app.route('/clear_all_scene_covers', methods=['POST']) @app.route('/clear_all_scene_covers', methods=['POST'])
@@ -3739,12 +3758,12 @@ def replace_style_cover_from_preview(slug):
@app.route('/get_missing_styles') @app.route('/get_missing_styles')
def get_missing_styles(): def get_missing_styles():
missing = Style.query.filter((Style.image_path == None) | (Style.image_path == '')).all() missing = Style.query.filter((Style.image_path == None) | (Style.image_path == '')).order_by(Style.filename).all()
return {'missing': [{'slug': s.slug, 'name': s.name} for s in missing]} return {'missing': [{'slug': s.slug, 'name': s.name} for s in missing]}
@app.route('/get_missing_detailers') @app.route('/get_missing_detailers')
def get_missing_detailers(): def get_missing_detailers():
missing = Detailer.query.filter((Detailer.image_path == None) | (Detailer.image_path == '')).all() missing = Detailer.query.filter((Detailer.image_path == None) | (Detailer.image_path == '')).order_by(Detailer.filename).all()
return {'missing': [{'slug': d.slug, 'name': d.name} for d in missing]} return {'missing': [{'slug': d.slug, 'name': d.name} for d in missing]}
@app.route('/clear_all_detailer_covers', methods=['POST']) @app.route('/clear_all_detailer_covers', methods=['POST'])
@@ -3767,7 +3786,7 @@ def clear_all_style_covers():
def generate_missing_styles(): def generate_missing_styles():
missing = Style.query.filter( missing = Style.query.filter(
(Style.image_path == None) | (Style.image_path == '') (Style.image_path == None) | (Style.image_path == '')
).order_by(Style.name).all() ).order_by(Style.filename).all()
if not missing: if not missing:
flash("No styles missing cover images.") flash("No styles missing cover images.")
@@ -5119,7 +5138,7 @@ def save_checkpoint_json(slug):
@app.route('/get_missing_checkpoints') @app.route('/get_missing_checkpoints')
def get_missing_checkpoints(): def get_missing_checkpoints():
missing = Checkpoint.query.filter((Checkpoint.image_path == None) | (Checkpoint.image_path == '')).all() missing = Checkpoint.query.filter((Checkpoint.image_path == None) | (Checkpoint.image_path == '')).order_by(Checkpoint.checkpoint_path).all()
return {'missing': [{'slug': c.slug, 'name': c.name} for c in missing]} return {'missing': [{'slug': c.slug, 'name': c.name} for c in missing]}
@app.route('/clear_all_checkpoint_covers', methods=['POST']) @app.route('/clear_all_checkpoint_covers', methods=['POST'])
@@ -5482,7 +5501,7 @@ def create_look():
@app.route('/get_missing_looks') @app.route('/get_missing_looks')
def get_missing_looks(): def get_missing_looks():
missing = Look.query.filter((Look.image_path == None) | (Look.image_path == '')).all() missing = Look.query.filter((Look.image_path == None) | (Look.image_path == '')).order_by(Look.filename).all()
return {'missing': [{'slug': l.slug, 'name': l.name} for l in missing]} return {'missing': [{'slug': l.slug, 'name': l.name} for l in missing]}
@app.route('/clear_all_look_covers', methods=['POST']) @app.route('/clear_all_look_covers', methods=['POST'])