feat: add diagnostic logs for generation flows
This commit is contained in:
@@ -6,12 +6,13 @@ import copy
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
from . import appconfig, db
|
||||
from . import appconfig, db, diagnostics
|
||||
from . import prompts as prompt_module
|
||||
from .config import make_slug
|
||||
|
||||
@@ -34,12 +35,15 @@ def gen_title(
|
||||
retry=None,
|
||||
config=None,
|
||||
models_path=appconfig.AI_MODELS_PATH,
|
||||
on_step=None,
|
||||
):
|
||||
"""Generate a new product title from a prompt and the old title."""
|
||||
|
||||
cfg = appconfig.load_config() if config is None else config
|
||||
ai_cfg = appconfig.ai_config(cfg)
|
||||
_notify_step(on_step, "load_text_model")
|
||||
model = _role_model("text", ai_cfg.get("default_text_model"), models_path)
|
||||
_notify_step(on_step, "title_build_request")
|
||||
payload = _chat_payload(
|
||||
model,
|
||||
[
|
||||
@@ -51,6 +55,7 @@ def gen_title(
|
||||
],
|
||||
)
|
||||
attempts = _attempt_count(ai_cfg, retry)
|
||||
_notify_step(on_step, "title_request")
|
||||
data = _call_with_retry(
|
||||
model,
|
||||
payload,
|
||||
@@ -58,6 +63,7 @@ def gen_title(
|
||||
attempts,
|
||||
request_kind="json",
|
||||
)
|
||||
_notify_step(on_step, "title_parse_response")
|
||||
text = _extract_text(data).strip()
|
||||
if not text:
|
||||
raise AIError("AI 返回为空标题")
|
||||
@@ -73,9 +79,11 @@ def gen_cover(
|
||||
retry=None,
|
||||
config=None,
|
||||
models_path=appconfig.AI_MODELS_PATH,
|
||||
on_step=None,
|
||||
):
|
||||
"""Generate a new cover image and save it as a JPEG file."""
|
||||
|
||||
_notify_step(on_step, "cover_validate_input")
|
||||
old_cover_path = os.path.abspath(str(old_cover_path))
|
||||
if not os.path.exists(old_cover_path):
|
||||
raise FileNotFoundError("旧封面图片不存在: %s" % old_cover_path)
|
||||
@@ -84,14 +92,17 @@ def gen_cover(
|
||||
|
||||
cfg = appconfig.load_config() if config is None else config
|
||||
ai_cfg = appconfig.ai_config(cfg)
|
||||
_notify_step(on_step, "load_image_model")
|
||||
model = _role_model("image", ai_cfg.get("default_image_model"), models_path)
|
||||
resolution = str(resolution or ai_cfg.get("resolution", "1k"))
|
||||
quality = _jpg_quality(jpg_quality if jpg_quality is not None else ai_cfg.get("jpg_quality", 90))
|
||||
attempts = _attempt_count(ai_cfg, retry)
|
||||
|
||||
api_type = model.get("api_type", "auto")
|
||||
_notify_step(on_step, "cover_build_request")
|
||||
if api_type == "images_edits":
|
||||
body, content_type = _image_edit_body(model, cover_prompt, old_cover_path, resolution)
|
||||
_notify_step(on_step, "cover_request")
|
||||
data = _call_with_retry(
|
||||
model,
|
||||
body,
|
||||
@@ -102,12 +113,24 @@ def gen_cover(
|
||||
)
|
||||
else:
|
||||
payload = _image_chat_payload(model, cover_prompt, old_cover_path, resolution)
|
||||
_notify_step(on_step, "cover_request")
|
||||
data = _call_with_retry(model, payload, cfg, attempts, request_kind="json")
|
||||
|
||||
_notify_step(on_step, "cover_parse_response")
|
||||
image_bytes = _extract_image_bytes(data, model, cfg)
|
||||
_notify_step(on_step, "cover_save")
|
||||
return _save_jpeg(image_bytes, out_path, resolution, quality)
|
||||
|
||||
|
||||
def _notify_step(callback, step):
|
||||
if callback is None:
|
||||
return
|
||||
try:
|
||||
callback(step)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=None):
|
||||
"""Generate titles first, then covers, and persist each successful task."""
|
||||
|
||||
@@ -132,6 +155,8 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
||||
image_root = runtime.get("image_dir") or appconfig.image_dir(config)
|
||||
account_by_alias = runtime.get("account_by_alias") or {}
|
||||
on_task_update = runtime.get("on_task_update")
|
||||
on_event = runtime.get("on_event")
|
||||
on_error = runtime.get("on_error")
|
||||
title_prompt = _prompt_value(prompts, "title")
|
||||
cover_prompt = _prompt_value(prompts, "cover")
|
||||
should_stop = should_stop or (lambda: False)
|
||||
@@ -149,6 +174,22 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
||||
}
|
||||
_emit_generation_progress(on_progress, summary)
|
||||
title_results = {}
|
||||
step_by_task = {}
|
||||
step_lock = threading.Lock()
|
||||
|
||||
def set_step(task, step):
|
||||
with step_lock:
|
||||
step_by_task[getattr(task, "id", None)] = str(step)
|
||||
|
||||
def get_step(task, fallback):
|
||||
with step_lock:
|
||||
return step_by_task.get(getattr(task, "id", None), fallback)
|
||||
|
||||
def step_callback(task, phase):
|
||||
def callback(step):
|
||||
set_step(task, step)
|
||||
_emit_generation_event(on_event, task, phase, step, "start")
|
||||
return callback
|
||||
|
||||
with ThreadPoolExecutor(
|
||||
max_workers=max(1, int(generation_cfg.get("title_concurrency", 1)))
|
||||
@@ -158,6 +199,8 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
||||
if should_stop():
|
||||
summary["cancelled"] = True
|
||||
break
|
||||
set_step(task, "title_submit")
|
||||
_emit_generation_event(on_event, task, "title", "title_submit", "start")
|
||||
futures[
|
||||
executor.submit(
|
||||
gen_title,
|
||||
@@ -166,6 +209,7 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
||||
retry=generation_cfg.get("retry"),
|
||||
config=config,
|
||||
models_path=models_path,
|
||||
on_step=step_callback(task, "title"),
|
||||
)
|
||||
] = task
|
||||
for future in as_completed(futures):
|
||||
@@ -176,12 +220,18 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
||||
try:
|
||||
title_results[task.id] = future.result()
|
||||
summary["title_done"] += 1
|
||||
set_step(task, "title_done")
|
||||
_emit_generation_event(on_event, task, "title", "title_done", "success")
|
||||
except CancelledError:
|
||||
summary["cancelled"] = True
|
||||
_emit_generation_event(on_event, task, "title", get_step(task, "title_request"), "cancelled", level="warning")
|
||||
except Exception as exc:
|
||||
summary["failed"] += 1
|
||||
summary["ok"] = False
|
||||
_mark_generate_failed(task, exc, db_path, on_task_update)
|
||||
step = get_step(task, "title_request")
|
||||
error = _mark_generate_failed(task, exc, db_path, on_task_update)
|
||||
_emit_generation_event(on_event, task, "title", step, "failed", detail=error, level="error")
|
||||
_emit_generation_error(on_error, task, "title", step, exc, error)
|
||||
_emit_generation_progress(on_progress, summary)
|
||||
|
||||
cover_tasks = [
|
||||
@@ -197,23 +247,38 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
||||
summary["cancelled"] = True
|
||||
break
|
||||
new_title = title_results[task.id]
|
||||
rendered_cover_prompt = prompt_module.render_prompt(
|
||||
cover_prompt,
|
||||
_prompt_context(task, new_title, account_by_alias),
|
||||
)
|
||||
futures[
|
||||
executor.submit(
|
||||
gen_cover,
|
||||
rendered_cover_prompt,
|
||||
getattr(task, "old_cover_path", "") or "",
|
||||
_new_cover_path(task, account_by_alias, image_root),
|
||||
resolution=generation_cfg.get("resolution"),
|
||||
jpg_quality=generation_cfg.get("jpg_quality"),
|
||||
retry=generation_cfg.get("retry"),
|
||||
config=config,
|
||||
models_path=models_path,
|
||||
try:
|
||||
set_step(task, "cover_prompt_render")
|
||||
_emit_generation_event(on_event, task, "cover", "cover_prompt_render", "start")
|
||||
rendered_cover_prompt = prompt_module.render_prompt(
|
||||
cover_prompt,
|
||||
_prompt_context(task, new_title, account_by_alias),
|
||||
)
|
||||
] = (task, new_title)
|
||||
_emit_generation_event(on_event, task, "cover", "cover_prompt_render", "success")
|
||||
set_step(task, "cover_submit")
|
||||
_emit_generation_event(on_event, task, "cover", "cover_submit", "start")
|
||||
futures[
|
||||
executor.submit(
|
||||
gen_cover,
|
||||
rendered_cover_prompt,
|
||||
getattr(task, "old_cover_path", "") or "",
|
||||
_new_cover_path(task, account_by_alias, image_root),
|
||||
resolution=generation_cfg.get("resolution"),
|
||||
jpg_quality=generation_cfg.get("jpg_quality"),
|
||||
retry=generation_cfg.get("retry"),
|
||||
config=config,
|
||||
models_path=models_path,
|
||||
on_step=step_callback(task, "cover"),
|
||||
)
|
||||
] = (task, new_title)
|
||||
except Exception as exc:
|
||||
summary["failed"] += 1
|
||||
summary["ok"] = False
|
||||
step = get_step(task, "cover_prompt_render")
|
||||
error = _mark_generate_failed(task, exc, db_path, on_task_update)
|
||||
_emit_generation_event(on_event, task, "cover", step, "failed", detail=error, level="error")
|
||||
_emit_generation_error(on_error, task, "cover", step, exc, error)
|
||||
_emit_generation_progress(on_progress, summary)
|
||||
for future in as_completed(futures):
|
||||
task, new_title = futures[future]
|
||||
if should_stop():
|
||||
@@ -221,6 +286,8 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
||||
_cancel_pending(futures)
|
||||
try:
|
||||
new_cover_path = future.result()
|
||||
set_step(task, "db_write")
|
||||
_emit_generation_event(on_event, task, "cover", "db_write", "start")
|
||||
db.set_generated(task.id, new_title, new_cover_path, path=db_path)
|
||||
summary["cover_done"] += 1
|
||||
if on_task_update is not None:
|
||||
@@ -233,19 +300,23 @@ def generate_batch(tasks, prompts, ai_cfg=None, on_progress=None, should_stop=No
|
||||
"new_cover_path": new_cover_path,
|
||||
},
|
||||
)
|
||||
_emit_generation_event(on_event, task, "cover", "db_write", "success")
|
||||
except CancelledError:
|
||||
summary["cancelled"] = True
|
||||
_emit_generation_event(on_event, task, "cover", get_step(task, "cover_request"), "cancelled", level="warning")
|
||||
except Exception as exc:
|
||||
summary["failed"] += 1
|
||||
summary["ok"] = False
|
||||
_mark_generate_failed(task, exc, db_path, on_task_update)
|
||||
step = get_step(task, "cover_request")
|
||||
error = _mark_generate_failed(task, exc, db_path, on_task_update)
|
||||
_emit_generation_event(on_event, task, "cover", step, "failed", detail=error, level="error")
|
||||
_emit_generation_error(on_error, task, "cover", step, exc, error)
|
||||
_emit_generation_progress(on_progress, summary)
|
||||
|
||||
if summary["cancelled"]:
|
||||
summary["ok"] = False
|
||||
return summary
|
||||
|
||||
|
||||
def _role_model(category, name, models_path):
|
||||
if not name:
|
||||
raise AIError("未配置默认 %s 模型" % category)
|
||||
@@ -332,10 +403,45 @@ def _new_cover_path(task, account_by_alias, image_root):
|
||||
|
||||
|
||||
def _mark_generate_failed(task, exc, db_path, on_task_update):
|
||||
error = str(exc) or exc.__class__.__name__
|
||||
error = diagnostics.redact_log_text(str(exc) or exc.__class__.__name__)
|
||||
db.mark_failed(task.id, "generate", error, path=db_path)
|
||||
if on_task_update is not None:
|
||||
on_task_update(task.id, {"status": "failed", "last_error": error})
|
||||
return error
|
||||
|
||||
|
||||
def _emit_generation_event(callback, task, phase, step, result, detail=None, level="info"):
|
||||
if callback is None:
|
||||
return
|
||||
payload = {
|
||||
"task": task,
|
||||
"phase": phase,
|
||||
"step": str(step),
|
||||
"result": result,
|
||||
"level": level,
|
||||
}
|
||||
if detail is not None:
|
||||
payload["detail"] = diagnostics.redact_log_text(detail)
|
||||
try:
|
||||
callback(payload)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def _emit_generation_error(callback, task, phase, step, exc, error):
|
||||
if callback is None:
|
||||
return
|
||||
payload = {
|
||||
"task": task,
|
||||
"phase": phase,
|
||||
"step": str(step),
|
||||
"error": diagnostics.redact_log_text(error),
|
||||
"exception": exc,
|
||||
}
|
||||
try:
|
||||
callback(payload)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
def _cancel_pending(futures):
|
||||
@@ -401,7 +507,7 @@ def _call_once(model, body, config, request_kind, content_type=None):
|
||||
else:
|
||||
data = body
|
||||
request = urllib.request.Request(
|
||||
model["url"],
|
||||
appconfig.model_request_url(model),
|
||||
data=data,
|
||||
headers=_headers(model, content_type),
|
||||
method="POST",
|
||||
|
||||
Reference in New Issue
Block a user