Files
beige/backend/app/workers/packaging_task.py
yangqianqian ff21116ff8 A8: 文案按A/B/C三套正交叙事生成,避免套路化重复
- constants: 新增 TEXT_NARRATIVE_BY_STRATEGY(A痛点/B场景/C成分),与图片侧同轴
- build_prompt: 加 strategy_narrative 参数并注入 prompt
- text_variants: 全链路透传(含优化轮)
- run_text_generation: 改循环三套,text_count均摊(divmod余前补),跨套去重,打_strategy标记
- TextCandidate: 加 strategy String(4) 字段 + 迁移021(已upgrade head)
- packaging: 打包按strategy精准配对文图(texts_by_strategy映射+三层兜底)
- SSE text_candidate 事件携带 strategy

独立agent交叉验证7改造点全过,边界(text_count<3/无别名/不截断)无must-fix

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 10:00:47 +08:00

135 lines
5.7 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/workers/packaging_task.py — 交付打包 Celery 任务
build_delivery_package查已选文案+图片 → package_exporter → 存路径
"""
import json
import logging
from app.workers.celery_app import celery_app
logger = logging.getLogger(__name__)
def _get_db():
from app.core.database import SessionLocal
return SessionLocal()
@celery_app.task(
bind=True,
name="app.workers.tasks.build_delivery_package",
max_retries=2,
default_retry_delay=10,
queue="packaging",
)
def build_delivery_package(self, package_id: int) -> dict:
"""打包交付任务。查 delivery_packages → 收集笔记 → package_exporter"""
logger.info("build_delivery_package start: package_id=%s", package_id)
db = _get_db()
try:
from app.models.task import DeliveryPackage, TextCandidate, ImageCandidate
from app.constants.enums import PackageStatus
pkg = db.query(DeliveryPackage).filter(DeliveryPackage.id == package_id).first()
if not pkg:
raise ValueError(f"package_id={package_id} not found")
workspace_id = pkg.workspace_id
task_id = pkg.task_id
from app.core.config import get_settings
settings = get_settings()
upload_base = settings.UPLOAD_BASE_PATH.rstrip("/")
# A8 多套打包:按 strategy(A/B/C 三套正交叙事)分组,每套成一篇独立 note。
# 北哥拿到的交付包含完整 3 套note_01/02/03不再只打第 1 条文案。
selected_texts = db.query(TextCandidate).filter(
TextCandidate.task_id == task_id, TextCandidate.is_selected == True,
).order_by(TextCandidate.id).all()
if not selected_texts:
raise ValueError("无已选文案,请先选择文案")
# 整套全打:一套内全部图按 seq 排序进包,不只打封面。重生场景同 (strategy,seq)
# 可能多条(新增不删旧),去重取最新(id最大),避免包内重复图。
all_images = db.query(ImageCandidate).filter(
ImageCandidate.task_id == task_id,
).order_by(ImageCandidate.strategy, ImageCandidate.seq).all()
def _read_image(ic) -> dict:
img_bytes = b""
if ic.url:
# url 已含 uploads 前缀;工作目录 /applstrip 当相对路径读,勿再拼 base(防 uploads/uploads)
try:
with open(ic.url.lstrip("/"), "rb") as f:
img_bytes = f.read()
except OSError as e:
logger.warning("图片读取失败,跳过:%s %s", ic.url, e)
return {
"seq": ic.seq,
"role": ic.role.value if hasattr(ic.role, "value") else str(ic.role),
"data": img_bytes,
}
# 按 strategy 分组A/B/C老数据 strategy=None 归一套,向后兼容)
from collections import OrderedDict
groups: "OrderedDict[str, dict]" = OrderedDict()
for ic in all_images:
slot = groups.setdefault(ic.strategy or "_", {})
prev = slot.get(ic.seq)
if prev is None or ic.id > prev.id:
slot[ic.seq] = ic # 同 seq 留最新
# 文案按 strategy 建映射供图片组按套精准配对A8文图同套对齐不靠脆弱 idx
# 同套多条选中取第 1 条;老数据 strategy=None 的归入 fallback 列表。
texts_by_strategy: dict = {}
texts_no_strategy: list = []
for tc in selected_texts:
if tc.strategy:
texts_by_strategy.setdefault(tc.strategy, tc)
else:
texts_no_strategy.append(tc)
_text_fallback = iter(texts_no_strategy or selected_texts)
notes = []
for idx, (_strategy, slot) in enumerate(groups.items()):
images_data = [_read_image(slot[k]) for k in sorted(slot)]
# 配对优先级:①同 strategy 文案精准对齐 ②无同套则按顺序取无套文案
# ③再兜底用第 idx 条/第 1 条,确保每组图都有文案不漏。
tc = texts_by_strategy.get(_strategy)
if tc is None:
tc = next(_text_fallback, None)
if tc is None:
tc = selected_texts[idx] if idx < len(selected_texts) else selected_texts[0]
text_data = json.loads(tc.content or "{}")
notes.append({
"title": text_data.get("title", ""),
"content": text_data.get("content", ""),
"tags": text_data.get("tags", []),
"images": images_data,
"banned_word_status": (tc.banned_word_status.value
if hasattr(tc.banned_word_status, "value")
else str(tc.banned_word_status)),
})
if not notes:
raise ValueError("无图片候选,无法打包")
from app.services.ai_engine.package_exporter import build_delivery_package as do_build
# 打包产物放专用目录 uploads/packages/,与图片目录 uploads/{ws}/{task}/ 分开
packages_base = f"{upload_base}/packages"
zip_path = do_build(workspace_id, task_id, notes, base_path=packages_base)
pkg.package_path = zip_path
pkg.download_url = f"/api/v1/delivery-packages/{package_id}/download-file"
pkg.status = PackageStatus.READY
db.commit()
logger.info("delivery package ready: package_id=%s path=%s", package_id, zip_path)
return {"package_id": package_id, "status": "ready", "path": zip_path}
except Exception as exc:
logger.error("build_delivery_package failed: package_id=%s err=%s", package_id, exc)
raise self.retry(exc=exc)
finally:
db.close()