Files
beige/backend/app/services/fission_image_retry.py
yangqianqian 285791c12f 裂变补图:中转站波动导致单张失败时只重生失败分镜
实测发现 codeproxy 偶发不稳,单套中某张图重试3次仍失败会记 error。
本次加按需补图:只重生标 error 的分镜,成功的图与 token 不浪费。

- 019迁移+模型:fission_tasks 加 source_task_id(新架构不扇出GenerationTask,
  补图需复刻首轮上下文:产品/operator key/张数,故落库回查)
- fission_image_retry:只补失败 role(引擎 target_role 单张重生,R2零改动),
  merge 回 images_json,全恢复则 status=image_done
- tasks.py:retry_fission_images Celery task,key归属源任务operator(基石B),
  幂等锁防重复补图
- api:POST /fission/{id}/notes/{seq}/retry-images,无失败分镜守卫 queued=false
- 前端:FissionNoteCard 失败提示改"重试补图"按钮+loading,父页轮询刷新

实测task5 seq1(hook图首轮codeproxy重试3次失败)补图跑通:只补hook(1024×1536
去AI化)、applied_proof原图不动、recovered=1 still_failed=0、status=image_done;
apiports503回落codeproxy gpt-5.5强档兜底;seq0无失败图守卫queued=false

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 11:30:41 +08:00

106 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
app/services/fission_image_retry.py — 第11环裂变 失败分镜补图(中转站波动兜底)
实测发现 codeproxy 偶发不稳单套中某张图重试3次仍失败会记 error 落库。
本模块对某一套(FissionNote)只补 images_json 里标了 error 的分镜:成功的不动,
失败的用引擎 target_role 单张重生(R2 零改动)merge 回 images_json。
🔴 生图引擎零改动;只补失败 role避免整套重生浪费已成功的图与 token。
"""
import logging
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
def _failed_roles(images: list[dict]) -> list[str]:
"""从 images_json 解析出标了 error 的分镜 role 列表。"""
return [img.get("role", "") for img in images if img.get("error") and img.get("role")]
def retry_fission_note_images(
db: Session, clients, ft, product: dict, image_count: int, note_id: int,
) -> dict:
"""对单套 FissionNote 只补失败分镜。返回 {seq, retried, recovered, still_failed}。"""
import asyncio
import json as _json
import os
from app.models.fission import FissionNote
from app.core.config import get_settings
from app.services.ai_engine.image_gen import generate_storyboard_images
from app.services.ai_engine.image_postprocessor import process_image
from app.workers.pipeline_io import _resolve_image_path
fn = db.query(FissionNote).filter(FissionNote.id == note_id).first()
if not fn:
return {"seq": None, "retried": 0, "recovered": 0, "still_failed": 0}
try:
images = _json.loads(fn.images_json) if fn.images_json else []
except (ValueError, TypeError):
images = []
try:
note = _json.loads(fn.note_json) if fn.note_json else {}
except (ValueError, TypeError):
note = {}
failed = _failed_roles(images)
if not failed:
return {"seq": fn.seq, "retried": 0, "recovered": 0, "still_failed": 0}
# 参考图沿用源产品同一张(与首轮生图一致)
reference_images = None
_img_path = _resolve_image_path(product.get("image_path", ""))
if _img_path and os.path.isfile(_img_path):
try:
with open(_img_path, "rb") as f:
reference_images = [f.read()]
except Exception as e: # noqa: BLE001
logger.warning("裂变补图参考图读取失败,无参考图模式: %s", e)
upload_base = get_settings().UPLOAD_ABS_ROOT
img_dir = os.path.join(upload_base, str(ft.workspace_id), f"fission_{ft.id}", str(fn.seq))
os.makedirs(img_dir, exist_ok=True)
recovered = 0
# role → 在 images 列表里的位置(保持原顺序/seq 不乱)
role_idx = {img.get("role"): i for i, img in enumerate(images)}
for role in failed:
try:
results = asyncio.run(generate_storyboard_images(
client=clients, note=note, product=product,
image_count=image_count, reference_images=reference_images,
target_role=role,
))
except Exception as exc: # noqa: BLE001
logger.error("裂变补图 seq=%s role=%s 仍失败: %s", fn.seq, role, exc)
continue
r = next((x for x in results if x.get("role") == role), None)
if not r or r.get("error") or not r.get("image_bytes"):
logger.warning("裂变补图 seq=%s role=%s 引擎返回空/error", fn.seq, role)
continue
try:
processed = process_image(r["image_bytes"], aspect_ratio="3:4", resample_strength=1)
except Exception: # noqa: BLE001
processed = r["image_bytes"]
idx = role_idx.get(role, len(images))
seq_no = idx + 1
fname = f"{seq_no:02d}_{role}.jpg"
with open(os.path.join(img_dir, fname), "wb") as f:
f.write(processed)
entry = {"role": role, "seq": seq_no,
"url": f"/uploads/{ft.workspace_id}/fission_{ft.id}/{fn.seq}/{fname}"}
if idx < len(images):
images[idx] = entry
else:
images.append(entry)
recovered += 1
fn.images_json = _json.dumps(images, ensure_ascii=False)
still = len(_failed_roles(images))
fn.status = "image_done" if still == 0 else fn.status
db.commit()
return {"seq": fn.seq, "retried": len(failed), "recovered": recovered, "still_failed": still}