A8 多套交付包(packaging_task.py): - 修复交付包只打第1条混乱note的bug,按ImageCandidate.strategy分A/B/C组 - 每组生独立note_0N夹(6图+文案.txt),同seq留最新去重,老数据兼容 - task74端到端验:3套各6图,独立agent7项交叉验证全过 M4 归档(tasks.py/exports.py/前端): - list_tasks加date_from/date_to/product_id筛选+product_name批量填(防N+1) - 新增exports.py:产品JSON导出+标杆CSV导出(UTF-8 BOM) - 前端HistoryFilters日期/产品筛选+产品列+打回原因红banner - response.py加raise_param_error;独立agent验A1/A2/A9通过 R5 产品多图(product_images.py/020迁移/前端): - product_images表+5端点(上传/列/改场景/设主图/删图) - 生图按ROLE_SCENE_PREFERENCE选对应场景图,回落primary - 前端ProductImageManager多图画廊 R6 账号config拆页(settings/): - 配置页按角色拆/settings(运营+组长+admin)+/config(仅admin) - Key只显末4位不显余额(守红线) 核销表对齐真实代码状态:D1改稿框/M7裂变/E12评图分纠偏为已完成(曾漏回写) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
4.0 KiB
Python
96 lines
4.0 KiB
Python
"""
|
||
app/services/fission_images.py — 第11环裂变 逐套生图(从 fission_pipeline 拆出)
|
||
|
||
复用单篇 generate_storyboard_images(codeproxy 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)
|
||
|
||
# R5多图:裂变同样按场景分组,生图按分镜 role 选对应图
|
||
images_by_scene: dict[str, list[bytes]] = {}
|
||
for _im in (product.get("images") or []):
|
||
_p = _resolve_image_path(_im.get("path", ""))
|
||
if _p and os.path.isfile(_p):
|
||
try:
|
||
with open(_p, "rb") as _f:
|
||
images_by_scene.setdefault(_im.get("scene") or "primary", []).append(_f.read())
|
||
except Exception: # noqa: BLE001
|
||
pass
|
||
if reference_images and not images_by_scene.get("primary"):
|
||
images_by_scene.setdefault("primary", []).extend(reference_images)
|
||
|
||
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,
|
||
images_by_scene=images_by_scene or None,
|
||
))
|
||
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()
|