基于AI Agent的童话编剧与绘本生成器(三)多角色一致性:从ControlNet到Qwen的迁移
在前两篇博客中,我们基于Stable Diffusion + ControlNet IP-Adapter实现了单角色的稳定生成,并通过分层提示词、解剖学正负向约束以及CLIP+InsightFace的重试闭环,让主角的一致性达到了可用水平。测试结果令人沮丧——第4页的小鸟还算正常,第5页的小鸟要么体型变得和小男孩一样大,要么羽毛从蓝色变成了灰色,甚至有时脸上出现了人类的五官特征。配角权重大,主角特
一、ControlNet的“天花板”:多角色为什么总是翻车?
在前两篇博客中,我们基于Stable Diffusion + ControlNet IP-Adapter实现了单角色的稳定生成,并通过分层提示词、解剖学正负向约束以及CLIP+InsightFace的重试闭环,让主角的一致性达到了可用水平。然而,当故事里出现第二个、第三个角色时,问题就像多米诺骨牌一样倒下了。
以《勇敢的小英雄》为例:第4页是小男孩蹲下,树枝上站着一只蓝色小鸟;第5页是小男孩指向树上的鸟巢,小鸟飞在空中。测试结果令人沮丧——第4页的小鸟还算正常,第5页的小鸟要么体型变得和小男孩一样大,要么羽毛从蓝色变成了灰色,甚至有时脸上出现了人类的五官特征。
我花了整整一周时间调参,最终不得不承认:Stable Diffusion + ControlNet 的架构天生不适合多角色绘本生成。原因有三:
1. 参考图特征污染
IP-Adapter同时注入多个角色的参考图时,所有参考图的特征会被混在一起注入到交叉注意力层。模型无法完美区分“这是小男孩的脸”和“这是小鸟的羽毛”,导致特征互相迁移。
2. 空间位置与尺寸失控
虽然我们在第二篇中通过提示词强化(如“small size, perched on a branch”)试图控制,但SD对“small size”的理解极不稳定——有时候鸟只有拳头大,有时候却和小男孩一样高。
3. 权重平衡像走钢丝
我一开始采用了差异化权重(主角1.2,配角0.8),但多角色场景下这个策略失效了。主角权重大,配角容易变形;配角权重大,主角特征被稀释。最优权重组合随场景变化,无法通用。
在尝试了多角度参考图、构图保护提示词等一系列手段后,我意识到必须换条路走。
二、为什么选择通义万相(Qwen Image)?
转向Qwen API基于四个核心考量:
1. 原生多图参考
Qwen Image 2.0-Pro支持在生成时传入多张参考图,并在底层通过多模态注意力机制分别对齐每个角色的特征。你可以明确告诉模型:“第一张参考图是小男孩,第二张是女孩,生成时请分别保持它们的样子”。这从根本上避免了特征污染——不需要再手动分配权重。
2. 更好的卡通风格理解
前两篇中我们虽然用了ToonYou模型,但SD对“绘本风格”的把握仍然不稳定。Qwen Image在训练时覆盖了大量儿童插画数据,对“cel shading, thick outlines, chibi proportions”等风格词响应更准确。
3. 内置的多模态一致性评分
第二篇我们费尽心思写了CLIP和InsightFace来打分,但两者对卡通风格都不够友好。Qwen-VL-Max直接能判断“这张图里的小男孩和参考图里的是不是同一个人”。不过,评分机制本身已在第二篇详细讲过,本篇不再重复。
三、提示词架构的进化:从“拼接”到“结构化”
在第二篇中,我们设计了分层提示词架构:[角色核心特征] + [动作] + [场景] + [画风] + [解剖学正向约束]。这个架构在单角色下工作良好,但到了多角色场景,它暴露出两个问题:
-
角色特征混在一起:所有角色的描述挤在一段话里,模型难以区分“小男孩的蓝眼睛”和“小女孩的黑头发”分别属于谁。
-
空间信息缺失:没有告诉模型每个角色大致在画面的什么位置,导致角色重叠或位置错乱。
新架构的改进:角色独立描述 + slot标签
在Qwen API的方案中,我们不再把所有角色描述拼成一个长字符串,而是通过characters列表传入结构化数据,每个角色包含独立的prompt(外貌)、slot(位置标签)和action(动作)。生成时,系统会将这些信息组织成如下格式:
"characters": [
{
"id": CHARACTER_ID,
"prompt": CHARACTER_PROMPT,
"slot": "left character",
"action": "looking up, curious expression, hand shading eyes",
"face_enabled": True,
},
{
"id": CHARACTER_FRIEND_ID,
"prompt": CHARACTER_FRIEND_PROMPT,
"slot": "right character",
"action": "pointing at rainbow, joyful expression",
"face_enabled": True,
},
]
slot的作用:left character、right character、above them 这些位置标签是第二篇中没有的。实验表明,加入slot后,模型的空间分配能力大幅提升——两个角色不再挤在一起,动物角色也不会再占据画面中心。
动作描述的细化:第二篇中动作描述较简单(如“standing”),新架构要求每个动作都带上表情和细节(如“standing happily, smiling, arms open”)。这帮助模型区分角色——当两个角色都“站着”时容易混淆,但一个“跳跃欢呼”另一个“旋转大笑”就能清晰区分。
画风锚点的强化:第二篇的画风提示词是children's book illustration style, Pixar style。新架构中我们加入了更具体的风格锚点:
style_anchor = (
"2D cartoon, children picture book style, cel shading, thick clean outlines, "
"big expressive eyes, rounded cute face, simplified shapes, chibi-like proportions, "
"vibrant colors, high saturation, bright cheerful palette, flat color blocks, "
"storybook character turn-around sheet, highly stylized non-photorealistic"
)
这些词直接锁定了“Q版卡通绘本”的风格,避免模型滑向写实或3D渲染。
负向提示词的补充:除了第二篇已有的负向词,新架构额外加入了photorealistic, 3d render, ray tracing,专门压制写实风格。
四、对外接口封装:让后端一键调用
图像生成模块最终要服务于上层的Web服务或AI Agent。因此,我们需要设计一个清晰、易用、解耦的对外接口。架构中的Image_generator.py正是为此而生。
设计思路
-
隐藏内部复杂性:调用方不需要知道Qwen API的细节、不需要处理参考图缓存、不需要关心重试逻辑。
-
固定输入格式:所有参数通过一个结构化的字典传入,便于序列化(JSON)和跨语言调用。
-
服务单例:整个应用只维护一个生成器实例,避免重复初始化。
核心接口:generate_page
该方法接收一个字典payload,必须包含以下字段:
{
"scene": str, # 场景描述,如 "a magical mushroom forest"
"illustration_style": str, # 画风描述
"negative_prompt": str, # 负向提示词
"page_num": int, # 页码(用于种子生成)
"characters": [ # 角色列表
{
"id": str, # 角色唯一标识(用于缓存参考图)
"prompt": str, # 角色外观描述
"slot": str, # 位置标签,如 "left character"
"action": str, # 当前动作描述
"face_enabled": bool, # 是否做人脸一致性检测
"score_min_percent": int # 可选,该角色单独阈值
}
],
"seed_base": int (可选) # 种子基础值
}
返回格式同样结构化:
{
"success": bool,
"image_path": str, # 生成的图片路径
"seed_used": int,
"details": {...}, # 每个角色的一致性分数
"min_score": int, # 所有角色分数的最小值
"warning": str (可选) # 如果重试耗尽但仍有结果
}
批量接口:generate_book
对于整本绘本,可以一次传入多个页面的payload,系统自动顺序生成并返回结果列表。这简化了上层调用方的循环逻辑。
服务实例化
通过create_service()工厂函数获取服务实例,内部采用单例模式。调用方示例:
from Image_generator import create_service
service = create_service()
result = service.generate_page({
"scene": "a magical forest",
"illustration_style": "children's book style",
"negative_prompt": "blurry, ugly",
"page_num": 1,
"characters": [...]
})
为什么这样设计?
-
解耦:后端(如Flask/FastAPI)只需要处理HTTP请求、参数校验,然后调用
generate_page即可,完全不关心图像生成的技术细节。 -
可扩展:未来如果切换图像生成模型,只需修改
generator.py内部实现,对外接口保持不变。
五、测试效果:从翻车到稳定
在test.py中,我们定义了三个角色:hero(小男孩)、friend(小女孩)、pet(蓝色小鸟)。8页绘本包含1-3个角色不等,覆盖了单人、双人、三人场景。
关键测试点对比第二篇:
| 问题场景 | 第二篇(ControlNet) | 第三篇(Qwen API) |
|---|---|---|
| 第4页(小男孩+小鸟) | 小鸟尺寸时大时小,有时像老鹰 | 小鸟始终拳头大小,比例稳定 |
| 第5页(双人,无鸟) | 偶尔串脸,小女孩出现男孩发型 | 两人特征清晰分离 |
| 第7页(三人同框) | 几乎不可用,角色重叠 | 左、右、上方布局合理 |
生成速度:单张图12-15秒,比本地SD(15-20秒)快。
图片生成效果:




可以看到,基本上保证了画风、角色的一致性
六、尚未完美解决的问题
-
API限流:高峰期调用可能遇到429,代码中已加入指数退避重试。
更多推荐




所有评论(0)