第11环裂变重写:对齐上线版 split.js 一次LLM出N套完整笔记包

架构从"扇出N个GenerationTask各跑完整管道"改为"一次LLM调用直接出N套
完整笔记包(N=1~5)",落 FissionNote 表 + 独立展示页。

后端:
- 018迁移:fission_notes 表(文案JSON+score+passed+imagePlan+images+status)
- fission_prompt:FISSION_SYSTEM+三档参考度(low/mid/high)+normalize_tags+品类兜底
- fission_pipeline:一次LLM出N套→各评分(@80合格线)→排序→落库,不达标标
  needs_optimization 非丢弃;apiports 503 回落 codeproxy gpt-5.5 强档兜底
- fission_images:每套串行调现有生图接口(零改动image_gen/storyboard)
- tasks.py:run_fission_pipeline Celery task,删旧扇出注入
- api/v1/fission:进度聚合FissionNote + GET /fission/{id}/notes(剥内部字段)

前端:FissionProgress对齐状态机 + N套独立展示页 + FissionNoteCard

测试:test_fission_engine(19)+test_fission_pipeline(5) 全过;104 全量回归绿

实测task5(fanout=2,mid)端到端跑通:一次出2套→seq0=85过/seq1=79标优化→
生图codeproxy/edits→1024×1536去AI化→task completed→notes端点返完整数据

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
yangqianqian
2026-06-18 11:17:37 +08:00
parent 7f419f4c8b
commit d85dcd401b
18 changed files with 1772 additions and 106 deletions

View File

@@ -0,0 +1,81 @@
"""
app/services/fission_images.py — 第11环裂变 逐套生图(从 fission_pipeline 拆出)
复用单篇 generate_storyboard_imagescodeproxy edits 带产品参考图),结果存
FissionNote.images_json。拆分原因fission_pipeline.py 超 200 行红线上限。
🔴 生图引擎零改动;串行跑各套(套内 storyboard 已 asyncio.gather 并发)避免打爆中转站。
"""
import logging
from sqlalchemy.orm import Session
logger = logging.getLogger(__name__)
def generate_fission_images(
db: Session, clients, ft, product: dict, image_count: int, note_ids: list[int],
) -> None:
"""逐套生图,结果存 FissionNote.images_json。
每套用该套自己的 note_json含定制 imagePlan当 note 喂生图引擎。
产品参考图沿用源产品同一张,与单篇一致。
"""
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
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
for nid in note_ids:
fn = db.query(FissionNote).filter(FissionNote.id == nid).first()
if not fn:
continue
try:
note = _json.loads(fn.note_json) if fn.note_json else {}
except (ValueError, TypeError):
note = {}
try:
results = asyncio.run(generate_storyboard_images(
client=clients, note=note, product=product,
image_count=image_count, reference_images=reference_images,
))
except Exception as exc: # noqa: BLE001
logger.error("裂变套 seq=%s 生图失败: %s", fn.seq, exc)
fn.status = "failed"; db.commit()
continue
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)
images: list[dict] = []
for i, r in enumerate(results):
if r.get("error") or not r.get("image_bytes"):
images.append({"role": r.get("role", ""), "error": str(r.get("error", "生图失败"))[:64]})
continue
try:
processed = process_image(r["image_bytes"], aspect_ratio="3:4", resample_strength=1)
except Exception: # noqa: BLE001
processed = r["image_bytes"]
fname = f"{i + 1:02d}_{r['role']}.jpg"
with open(os.path.join(img_dir, fname), "wb") as f:
f.write(processed)
images.append({
"role": r["role"], "seq": i + 1,
"url": f"/uploads/{ft.workspace_id}/fission_{ft.id}/{fn.seq}/{fname}",
})
fn.images_json = _json.dumps(images, ensure_ascii=False)
fn.status = "image_done"
db.commit()