第五篇结尾我写了下篇计划——出图 RAG 语料建设。这篇就把这件事落地,同时把多角色分镜从「只有主角」扩展到「全书多名配角、每页选 1~2 人出镜」。


一、多角色分镜:配角表 + 每页出镜名单

第五篇的出图 payload 里,characters 通常只有 {story_id}_hero 一条。第三篇的 ImageGenerator 本身支持多角色(每角色一张参考图 + VL 打分),瓶颈在应用层:故事侧没有「配角表」和「本页谁出镜」这两层结构。

于是我在故事 LLM 的输出 Schema 里补了两块:

class StoryPageLLM(BaseModel):
    scene: str
    character_action: str
    appearing_characters: list[str] = Field(
        default_factory=list,
        description="本页出镜角色名,须来自 supporting_cast 或主角名,最多 2 个",
    )
    # dialogue / narration ...

class StoryResultLLM(BaseModel):
    supporting_cast: list[SupportingCharacterLLM] = Field(
        default_factory=list,
        description="全书重要配角(不含主角),须给出 visual_design;人数不限",
    )
    pages: list[StoryPageLLM]

设计边界分成两层:

层级 规则
supporting_cast 全书配角登记,人数不限
appearing_characters 本页实际要画的人,最多 2 个

不是每页把所有人画进去,而是按页分配。例如:

  • 全书:小狮子 + 小刺猬团团 + 小蓝鸟

  • 第 3 页:小狮子 + 小刺猬团团

  • 第 5 页:小狮子 + 小蓝鸟

  • 第 1 页:只有小狮子

出图侧新增 story_characters.py,把名字解析成 Qwen 需要的 characters[] 结构:

def build_page_character_payloads(*, story_id, page, payload, supporting_cast, llm_characters=None):
    registry = build_cast_registry(story_id=story_id, payload=payload, supporting_cast=supporting_cast)
    names = resolve_page_character_names(page, protagonist=payload.protagonist, supporting_cast=supporting_cast)
    # 每个角色独立 id / prompt / face_enabled,最多 2 人
    return out[:2]

配角参考图落在 data/character_refs/{story_id}/。日志里出现 👥 本页角色数量: 2refs=2,说明多角色 payload 已成功送进 Qwen。

face_enabled 由角色类型决定:human 做人脸 VL 一致性;animal / fantasy 走物种造型参考,不做真人脸一致性检测。


二、角色登记兜底:旁白写了,就要进 payload

实测里遇到过一个问题:旁白第 5~6 页写了「小蓝鸟被藤蔓缠住」,但 supporting_cast 只登记了「小刺猬团团」,第 5 页的 appearing_characters 也只有主角——出图自然画不出小蓝鸟。这不是 Qwen 漏画,是 pipeline 没把第三角色送进去。

我在 generation.py 解析 LLM 结果后加了 reconcile_story_cast() 做后处理:

supporting_cast, page_appearances = reconcile_story_cast(
    llm_pages, supporting_cast, protagonist=payload.protagonist
)
for idx, llm_page in enumerate(llm_pages):
    llm_page.appearing_characters = page_appearances[idx]

核心逻辑在 story_characters.py 中:

def reconcile_story_cast(pages, supporting_cast, *, protagonist):
    cast = list(supporting_cast)
    # 1) 扫描旁白/对白,发现出现 ≥2 页的具名配角(如「小蓝鸟」)→ 补进 cast
    for extra in discover_missing_cast_members(pages, protagonist=protagonist, existing=cast):
        cast.append(extra)
    # 2) 当页文案提到谁,就把谁写进 appearing_characters
    appearances = [
        repair_page_appearing_characters(page, protagonist=protagonist, supporting_cast=cast, ...)
        for page in pages
    ]
    return cast, appearances

同一本森林故事,修补前后的对比:

场景 修复前 修复后
第 5 页 ['勇敢小狮子'] ['勇敢小狮子', '小蓝鸟']
supporting_cast ['小刺猬团团'] ['小刺猬团团', '小蓝鸟']

Prompt 里已经加了「旁白出现的具名角色必须登记」的规则,但 LLM 仍会漏;后处理兜底才是硬保障。


三、出图 RAG:给第二个 LLM 配「参考书」

第五篇的出图编剧是「裸调 LLM」——水彩、双角色、近景/远景全靠 prompt 硬写。故事侧早就有 corpus.jsonl 做 RAG,出图侧也需要自己的语料库。

3.1 RAG总览
维度 出图 RAG(本篇)
语料文件 image_corpus.jsonl
用在哪 image_prompt_generation.py
内容 scene、画风、角色 prompt
检索方式 关键词 + DashScope 向量
3.2 每页单独检索,正负例分开

构造分镜输入时,每一页独立查 RAG,不再全书共用第一页的参考:

def _page_rag_references(page, payload, supporting_cast):
    pos = retrieve_for_image_prompt(payload, page, supporting_cast=supporting_cast, mode="positive")
    neg = retrieve_for_image_prompt(payload, page, supporting_cast=supporting_cast, mode="negative")
    return pos.reference_block, neg.reference_block

# 写入每页 storyboard
page_items.append({
    "page_num": page.index,
    "scene": page.scene,
    "character_action": page.character_action,
    "appearing_characters": page.appearing_characters,
    "rag_positive_examples": rag_pos,   # 成功范例
    "rag_avoid_examples": rag_avoid,    # 失败反例
})

出图 LLM 的 Prompt 里增加了硬约束:参考 rag_positive_examples 的写法,避开 rag_avoid_examples 里的坑。

检索还会按画风、角色数、年龄段、VL 分加权。语料入库时做指纹去重,相似条目只保留分数更高的那条。

3.3 全自动维护,不用手跑脚本
启动后端 → 语料为空则 seed 4 条模板
        ↓
出图成功 + VL 分 ≥ 阈值 → auto_harvest 写入 image_corpus.jsonl
        ↓
重试「先失败后成功」→ bad/good 成对入库

.env(配置) 关键开关:

IMAGE_RAG_ENABLED=true
IMAGE_RAG_AUTO_SEED=true
IMAGE_RAG_AUTO_HARVEST=true
IMAGE_RAG_EMBEDDING_ENABLED=true

正常创书、出图成功,语料就会自己生长。每次出图尝试还会记录到 data/knowledge/page_attempts/{story_id}/,方便审计。


四、语料冷启动:不出图,纯文字填充语料库

自动收割依赖「成功出图」。项目刚跑起来时语料很少——实测只有十几条,RAG 基本靠模板硬撑。

于是加了 seed_image_corpus.py,支持批量灌入纯文字 payload(格式与真实出图一致,不调用 Qwen):

python backend/scripts/seed_image_corpus.py bulk      # 组合模板,约 400+ 条
python backend/scripts/seed_image_corpus.py curated   # 外部精选绘本风
python backend/scripts/seed_image_corpus.py stats

从本地「绘本插画」提示词合集里筛了 12 条儿童绘本风条目(水彩、蜡笔、扁平马卡龙、Q 版动物等),改写后标记为 curated_external 写入语料。暗黑哥特、多格漫画、信息图海报等与单页儿童绘本场景不匹配的条目没有收录。

填充语料库后规模大致为:

总条数: 465
├── template_seed:      445
├── curated_external:    12
└── agent_winner:         8  (真实出图收割)

五、完整链路与双角色 payload 示例

链路在第五篇双流架构基础上扩展:

用户创作参数
      ↓
【LLM 1】generation.py
      → 分镜 + supporting_cast + appearing_characters
      → reconcile_story_cast() 兜底
      ↓
【出图 RAG】每页检索 image_corpus.jsonl
      ↓
【LLM 2】image_prompt_generation.py
      → 读 rag_positive / rag_avoid → 写出图 JSON
      → Agent 失败才 fallback 规则层(不再每次都先跑规则)
      ↓
【Qwen】generator.py → PNG + VL 重试
      ↓
【RAG 回流】auto_harvest → 语料自增长

双角色页的 payload 大致长这样(LLM 2 → Qwen):

{
  "scene": "大树下林间空地,阳光透过树叶,柔和自然光,中景构图",
  "illustration_style": "儿童绘本插画,柔和水彩质感,全书画风统一",
  "page_num": 3,
  "seed_base": 1384023765,
  "characters": [
    {
      "id": "{story_id}_hero",
      "prompt": "儿童绘本主角:勇敢小狮子。金色鬃毛,圆润体型,全书一致",
      "slot": "主角",
      "action": "微微低头,目光惊喜地看向草丛方向",
      "face_enabled": false,
      "score_min_percent": 70
    },
    {
      "id": "{story_id}_小刺猬团团",
      "prompt": "儿童绘本配角:小刺猬团团。棕色短刺,浅蓝围巾,全书一致",
      "slot": "配角",
      "action": "在草丛里慢悠悠滚动,用鼻子顶着小果子",
      "face_enabled": false,
      "score_min_percent": 70
    }
  ]
}

Qwen 侧流程不变:先为每个 characters[].id 生成或复用参考图,再按 scene + 各角色 action 生成场景图,VL 不达标就重试。


六、小结

第六篇把第五篇「能出图」推进到「能出多角色、有参考书、语料会自己长」:

  • 故事 LLM 输出 supporting_cast + appearing_charactersstory_characters.py 构造多角色 payload

  • reconcile_story_cast() 修补 LLM 漏登记的角色

  • 出图 RAG:每页检索、向量+关键词混合、正负分离、自动收割

  • seed_image_corpus.py 支持纯文字批量灌库 + 外部绘本风精选

新增文件:story_characters.pyimage_rag/curated_prompts.py;改造了 generation.pyimage_prompt_generation.py

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐