基于 AI Agent 的童话编剧与绘本生成器(六)多角色分镜、出图 RAG 与语料冷启动
第五篇结尾我写了下篇计划——出图 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}/。日志里出现 👥 本页角色数量: 2、refs=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_characters,story_characters.py构造多角色 payload -
reconcile_story_cast()修补 LLM 漏登记的角色 -
出图 RAG:每页检索、向量+关键词混合、正负分离、自动收割
-
seed_image_corpus.py支持纯文字批量灌库 + 外部绘本风精选
新增文件:story_characters.py、image_rag/、curated_prompts.py;改造了 generation.py、image_prompt_generation.py。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)