- 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>
161 lines
7.5 KiB
Python
161 lines
7.5 KiB
Python
"""
|
||
app/models/task.py — generation_tasks / text_candidates / image_candidates / delivery_packages
|
||
Alembic 003 业务主体(2/3)
|
||
任务主键:自增 BIGINT + mongo_trace_id VARCHAR(24)
|
||
eval_score 留 NULL,不接 banana 假评分(基石)
|
||
"""
|
||
|
||
from datetime import datetime
|
||
from typing import Optional
|
||
|
||
from sqlalchemy import (
|
||
BigInteger, Boolean, DateTime, Enum, Float, ForeignKey,
|
||
Index, Integer, String, Text, func,
|
||
)
|
||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||
|
||
from app.constants.enums import (
|
||
BannedWordStatus, CandidateSource, ImageRole,
|
||
PackageStatus, ReviewStatus, TaskStatus,
|
||
)
|
||
from app.core.database import Base
|
||
|
||
|
||
class GenerationTask(Base):
|
||
"""生产任务,审核字段平铺(不建独立审核表)。"""
|
||
__tablename__ = "generation_tasks"
|
||
|
||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||
workspace_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
product_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("products.id"), nullable=False
|
||
)
|
||
operator_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("users.id"), nullable=False
|
||
)
|
||
theme: Mapped[str | None] = mapped_column(String(256))
|
||
text_count: Mapped[int] = mapped_column(Integer, default=5, nullable=False)
|
||
image_count: Mapped[int] = mapped_column(Integer, default=3, nullable=False)
|
||
track: Mapped[str] = mapped_column(String(16), default="ai", nullable=False)
|
||
# 本次产品是否入镜:True=必须用产品参考图(无图禁生成,不降级纯文生图);False=允许纯文生图
|
||
need_product_image: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||
status: Mapped[str] = mapped_column(
|
||
Enum(TaskStatus, values_callable=lambda x: [e.value for e in x]),
|
||
default=TaskStatus.PENDING, nullable=False,
|
||
)
|
||
mongo_trace_id: Mapped[str | None] = mapped_column(String(24)) # MongoDB trace
|
||
# ── 审核字段(平铺)──────────────────────────────
|
||
review_status: Mapped[str | None] = mapped_column(
|
||
Enum(ReviewStatus, values_callable=lambda x: [e.value for e in x])
|
||
)
|
||
reviewer_id: Mapped[int | None] = mapped_column(BigInteger, ForeignKey("users.id"))
|
||
reviewed_at: Mapped[datetime | None] = mapped_column(DateTime)
|
||
reject_reason: Mapped[str | None] = mapped_column(Text)
|
||
approved_at: Mapped[datetime | None] = mapped_column(DateTime)
|
||
archived_at: Mapped[datetime | None] = mapped_column(DateTime)
|
||
# 011: 第2环S12+第11环裂变
|
||
benchmark_ids: Mapped[str | None] = mapped_column(Text, comment="关联标杆笔记ID列表(JSON list),第2环引用")
|
||
source_fission_id: Mapped[int | None] = mapped_column(Integer, comment="裂变来源fission_task ID;NULL=普通任务")
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime, server_default=func.now(), nullable=False
|
||
)
|
||
updated_at: Mapped[datetime] = mapped_column(
|
||
DateTime, server_default=func.now(), onupdate=func.now(), nullable=False
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("idx_generation_tasks_workspace_status", "workspace_id", "status"),
|
||
)
|
||
|
||
|
||
class TextCandidate(Base):
|
||
"""文案候选(source=ai/import 区分双轨)。eval_score 留 NULL。"""
|
||
__tablename__ = "text_candidates"
|
||
|
||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||
workspace_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
task_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("generation_tasks.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
source: Mapped[str] = mapped_column(
|
||
Enum(CandidateSource, values_callable=lambda x: [e.value for e in x]),
|
||
default=CandidateSource.AI, nullable=False,
|
||
)
|
||
angle_label: Mapped[str | None] = mapped_column(String(64))
|
||
strategy: Mapped[str | None] = mapped_column(String(4)) # A/B/C 正交叙事套(A痛点/B场景/C成分),与图片侧同轴
|
||
content: Mapped[str | None] = mapped_column(Text)
|
||
score_json: Mapped[str | None] = mapped_column(Text) # 五维分 JSON
|
||
banned_word_status: Mapped[str] = mapped_column(
|
||
Enum(BannedWordStatus, values_callable=lambda x: [e.value for e in x]),
|
||
default=BannedWordStatus.PASS, nullable=False,
|
||
)
|
||
eval_score: Mapped[float | None] = mapped_column(Float) # 一期留 NULL
|
||
is_selected: Mapped[bool] = mapped_column(default=False, nullable=False)
|
||
edited: Mapped[bool] = mapped_column(default=False, nullable=False) # 改稿标记:用户改过=最强飞轮信号
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime, server_default=func.now(), nullable=False
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("idx_text_candidates_task_id", "task_id"),
|
||
)
|
||
|
||
|
||
class ImageCandidate(Base):
|
||
"""图片候选。eval_score 留 NULL。"""
|
||
__tablename__ = "image_candidates"
|
||
|
||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||
workspace_id: Mapped[int] = mapped_column(BigInteger, nullable=False)
|
||
task_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("generation_tasks.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
# role 用 varchar 不用 enum:storyboard 角色名仍在演进,约束放应用层 ImageRole
|
||
role: Mapped[str] = mapped_column(
|
||
String(32), default=ImageRole.HOOK.value, nullable=False,
|
||
)
|
||
url: Mapped[str | None] = mapped_column(String(512))
|
||
strategy: Mapped[str | None] = mapped_column(String(4)) # A/B/C(二期)
|
||
seq: Mapped[int] = mapped_column(Integer, default=1) # 分镜序号
|
||
is_selected: Mapped[bool] = mapped_column(default=False, nullable=False)
|
||
# R2 重生标记:True=用户手动重生产出(新增不删旧,前端同strategy+role默认只展示最新)
|
||
is_regen: Mapped[bool] = mapped_column(default=False, nullable=False)
|
||
eval_score: Mapped[float | None] = mapped_column(Float) # 一期留 NULL
|
||
# 标题文字校验结果JSON(NULL=通过;有值=曾不过/重生,前端提示运营人工筛)
|
||
text_review: Mapped[str | None] = mapped_column(String(512))
|
||
# AI评图分0-100(展示+生成时筛选+排序用,绝不进飞轮权重;eval_score 另留 NULL)
|
||
ai_visual_score: Mapped[float | None] = mapped_column(Float)
|
||
ai_visual_note: Mapped[str | None] = mapped_column(String(256)) # AI评图一句话评语(前端展示)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime, server_default=func.now(), nullable=False
|
||
)
|
||
|
||
__table_args__ = (
|
||
Index("idx_image_candidates_task_id", "task_id"),
|
||
)
|
||
|
||
|
||
class DeliveryPackage(Base):
|
||
"""达人素材交付包。"""
|
||
__tablename__ = "delivery_packages"
|
||
|
||
id: Mapped[int] = mapped_column(BigInteger, primary_key=True, autoincrement=True)
|
||
workspace_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("workspaces.id", ondelete="CASCADE"), nullable=False
|
||
)
|
||
task_id: Mapped[int] = mapped_column(
|
||
BigInteger, ForeignKey("generation_tasks.id"), nullable=False
|
||
)
|
||
status: Mapped[str] = mapped_column(
|
||
Enum(PackageStatus, values_callable=lambda x: [e.value for e in x]),
|
||
default=PackageStatus.PENDING, nullable=False,
|
||
)
|
||
package_path: Mapped[str | None] = mapped_column(String(512))
|
||
download_url: Mapped[str | None] = mapped_column(String(512))
|
||
expires_at: Mapped[datetime | None] = mapped_column(DateTime)
|
||
created_at: Mapped[datetime] = mapped_column(
|
||
DateTime, server_default=func.now(), nullable=False
|
||
)
|