一、角色锁定 Cast Agent:全书外貌一次定稿

多角色接线之后,配角能进 characters[] 了,但实测仍有一个问题:出图 Prompt Agent 每页各自写 characters[].prompt,小松鼠第 3 页和第 7 页毛色、体型对不上。

我在 image_prompt_generation.py 里把出图 LLM 拆成 两步:

【Cast Agent】→ locked_character_prompts(全书角色外貌表)

      ↓

【Page Agent】→ 每页 scene + action(prompt 强制引用 locked 表)

Cast Agent 单独调一次 LLM,输入主角设定 + supporting_cast,输出 ImageCastRegistryLLM:

def _generate_locked_character_prompts(self, *, story_id, payload, supporting_cast):

    prompt, parser, llm = self._build_cast_chain()

    result = chain.invoke({

        "protagonist": payload.protagonist,

        "character_design": payload.character_design or payload.protagonist,

        "supporting_cast_json": cast_json,

        ...

    })

    parsed = ImageCastRegistryLLM.model_validate(result)

    locked = {item.name: item.prompt[:600] for item in parsed.characters if item.name}

    return locked or fallback  # Agent 失败 → build_cast_registry 规则兜底

Page Agent 生成完后,_finalize_payloads 把各页 characters[].prompt 覆盖为 locked 表里的同名字符串,只有 action 允许页间变化:

def _apply_locked_prompts(llm_chars, *, locked_prompts, protagonist):

    for ch in llm_chars:

        key = hero if slot == "主角" or name == hero else name

        if locked := locked_prompts.get(key):

            item["prompt"] = locked[:600]

日志里会先出现 cast 锁定,再出现 image_prompt_agent ok。Qwen 参考图 + VL 一致性检测,终于有稳定的文字锚点。


二、构图多样性:composition_type 从分镜传到出图

8 页插图构图容易雷同——全是中景、主角居中。我在 v6 分镜 LLM 的输出 Schema 里加了 composition_type:

class StoryboardPageLLM(BaseModel):

    composition_type: str = Field(

        description="远景环境镜头/中景叙事镜头/近景表情镜头/特写细节镜头/俯视全景镜头",

    )

storyboard_generation.py 里还有 _normalize_composition:相邻两页禁止重复同一景别,避免「8 页全是中景」。

这条字段一路传到出图 Prompt Agent 的输入 JSON,并在 Prompt 里加了硬性规则:

> 写 scene 时必须体现该景别,禁止与 composition_type 矛盾(近景表情镜头不得写成广阔远景)。

实测一本 8 页森林故事,景别分布类似:远景 → 中景 → 特写 → 近景 → 俯视 → …,翻起来更像绘本而不是 PPT。


三、字数策略:按年龄段控每页文字量

v6 先写完整故事再切片,但 LLM 有时一段写太长,阅读页一页塞满字。新增 page_text_policy.py:

def max_chars_per_page(payload) -> int:

    upper = _parse_age_upper(payload.age_group, payload.age)

    if upper <= 5: return 45

    if upper <= 8: return 75

    return 95

叙事 Prompt 注入 page_text_limit_guidance;reading_layout.py 在落库前调用 enforce_page_reading_text,超长则在句末截断并打日志 page_text_trimmed。

同一套策略也复用到 PDF 字号:pdf_font_sizes 按年龄段返回 body / dialogue / title 字号,3~5 岁 body 18pt,6~8 岁 15pt,更大 14pt。


四、PDF 导出:从「网页截图感」到「纸质绘本感」

左图右文骨架有了之后,我继续打磨 pdf_export.py,主要改动:

做法
左页满幅图 有图时去掉圆角灰框,插图从页面左缘铺满左半 spread
装订 gutter 中缝两侧各留 _INSIDE_MARGIN,文字/人脸不贴书脊
旁白 / 对白分样式 有 narration/dialogue 时分开展示;对白略大 + 浅橙背景块
封面满幅 + 书名条 优先 cover.png 全页铺满,底部半透明黑条显示书名
浅纸色 每页背景 #FFF8F0 代替纯白

导出时从创作参数快照读取 age_group / age,PDF 和在线阅读用同一套字数/字号策略。


五、独立封面 cover.png

之前 PDF 封面往往直接复用第 1 页内页图,风格偏「叙事瞬间」而不是「书封」。

build_cover_payload 单独拼封面 scene:主角居中、无文字、满幅构图,输出文件名固定 cover.png:

return {

    "scene": scene[:800],

    "illustration_style": illustration_style + ",封面级精美构图",

    "negative_prompt": _NEGATIVE + ",画面文字,标题,书名,对话框",

    "_output_filename": "cover.png",

    ...

}

render_story_images_from_payloads 在页循环之外先画封面;PDF _resolve_cover_path 优先读 cover.png,没有再 fallback 到 1.png。


六、出图 ∥ TTS 并行:缩短总墙钟时间

CosyVoice TTS 如果等 imaging 全部完成再跑,8 页又要多等几分钟。

加了 PIPELINE_PARALLEL_TTS=true(默认开):每画完一页,线程池立刻合成该页 MP3,Job 进度同时显示「第 x/y 页插图 · 朗读 a/b 页」:

def on_page_complete(page_index, total):

    if not parallel_tts:

        return

    futures.append(executor.submit(_tts_worker))  # try_synthesize_page(...)

render_story_images_from_payloads(..., on_page_complete=on_page_complete)

narrating 阶段会跳过已合成的页,只补漏。8 页绘本总耗时从「出图 + TTS 线性相加」缩到「大致取较长者」,等待感明显减轻。


七、系列收官

回顾这八篇,系统演进路径大致是:

(一~二)环境 + SD 单角色验证

(三)    Qwen-Image + VL 一致性模块

(四)    FastAPI 后端接入

(五)    双流编剧:故事 LLM + 出图 Prompt LLM

(六)    多角色分镜 + 出图 RAG + 语料冷启动

(七)    v6 叙事/分镜拆分 + 按页进度 + 绘本 PDF

(八)    Cast 锁定 + 构图/字数/PDF/封面/听读并行  ← 本篇

当前完整链路:

用户参数

  → v6 叙事(full_story + paragraphs + supporting_cast)

  → v6 分镜(composition_type + appearing_characters)

  → Cast Agent 锁角色 + Page Agent 写出图 JSON + RAG

  → Qwen 出图(cover + N 页,按页进度)

  → CosyVoice TTS(与出图并行)

  → 在线阅读 / PDF 导出


小结

第八篇补上了「角色一致性」和「成品感」两块拼图:Cast Agent 让多角色外貌全书统一,composition_type 让镜头有节奏,字数策略 + PDF 打磨 + 独立封面 让导出更像儿童绘本,出图∥TTS 缩短等待。

八篇下来,这不只是一个「调 API 拼 Demo」的项目,而是一条 多层 Agent + RAG + 一致性检测 + 异步 Job 的垂直链路。系列完结。

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐