一、第四篇做完,还缺什么?

第四篇我把 Qwen 图像模块接进了 FastAPI:LLM 写剧本 → story_images.py 拼 payload → image_generator.py 调 Qwen → 图片写到 data/images/{story_id}/。脚本能跑、接口能调,但用起来三个问题特别明显。

分镜和出图 prompt 各说各话。 故事 LLM 已经输出了 scene、character_action,适配层却还是用 page.text(旁白+对白)去填 scene 和 action。page.text 是给阅读页朗读用的叙事正文,里面常有对话语气、情节推进,并不是「这一页画面上应该出现什么」。图像模型收到的是讲故事的文字,不是画画面的描述,出图效果就很飘。

只有一层文本 LLM。 分镜字段在第一阶段已经有了,但从分镜到 Qwen 需要的固定 JSON,第四篇仍靠规则拼字符串。规则能跑,但很难稳定产出「纯视觉 scene + 独立 action + 全书一致的角色 prompt」这种结构,也缺少根据每页动作微调构图、氛围的能力。

工程体验问题。 阅读页「重试本页插图」按钮点了跟没点一样——没有 loading、没有换图、有时后端还报 500,很劝退。

第五篇就是围绕这三个问题打的补丁:第二个 LLM(出图编剧)、规则层优化、代码去重、重绘修复。


二、双流架构:在第四篇三层之上加第二个 LLM

第四篇的三层架构我保留了,只是在「LLM 剧本」和「Qwen 出图」之间,明确拆成两个文本阶段。这样职责边界清楚:第一个 LLM 管「发生什么、怎么读」,第二个 LLM 管「画什么、怎么画」,Qwen 只负责执行。

用户创作参数
      ↓
【LLM 1】generation.py              scripting:分镜 + 朗读正文
      ↓
【LLM 2】image_prompt_generation.py  storyboarding:出图固定 JSON
      ↓
【Qwen】ImageGenerator/generator.py  imaging:PNG + VL 重试

job_orchestrator.py 里三阶段一一对应。scripting 调故事编剧,storyboarding 调 build_page_payloads_from_story(内部优先走出图 Agent),imaging 调 render_story_images。

# job_orchestrator.py — 三阶段核心
story, pages = await self._run_with_retry(..., status="scripting", progress=20, ...)
page_payloads = await self._run_with_retry(..., status="storyboarding", progress=50, ...)
await self._run_with_retry(..., status="imaging", progress=80,
    timeout_seconds=self._imaging_timeout_seconds, ...)

前端一键模式会在 storyboarding 显示「正在生成分镜提示词…」,在 imaging 显示「正在绘制绘本插图…」。storyboarding 不再是空转等待,而是真正的出图 Prompt 编剧;Agent 失败时自动退回规则拼接,不会卡死整条链路。

单页重绘 retry-image 也走同一条路:取该页分镜 + 创作参数快照 → build_page_payloads_from_story → 只重画一页。


三、出图 Prompt 编剧:image_prompt_generation.py

3.1 为什么还要一个 LLM?

第四篇里 build_page_payloads_from_story 用规则拼字符串,核心逻辑大致是:

# 第四篇末的问题写法(已废弃)
scene_chunks.append(f"{page.chapter_title}:{page.text or ''}")
action = (page.text or page.chapter_title or "自然站立...")

旁白、对白混进 scene,跟第三篇 Qwen 模块期望的「纯视觉 scene + 独立 action」完全对不上。更麻烦的是,每页的 page.text 长度和侧重点不同,800 字截断后 scene 质量波动很大。

所以我新建了 ImagePromptGenerationService,专门做「分镜 → 出图 dict」。它的输入是分镜 JSON,输出是 Qwen 能直接吃的固定结构;系统字段(seed、id、打分阈值)由代码注入,不让 LLM 碰。

3.2 输出 Schema

LLM 只产出「画什么」相关字段,与第三篇 ImageGeneratorService 对齐:

class ImagePagePromptLLM(BaseModel):
    page_num: int
    scene: str              # 纯视觉,无对白
    illustration_style: str
    negative_prompt: str
    characters: list[ImageCharacterPromptLLM]  # 通常 length=1

seed_base、characters[].id、face_enabled、score_min_percent 由 _finalize_payloads 注入。这样 LLM 专注内容,工程参数保持稳定。

3.3 输入:只传分镜,不传 page.text

Agent 的输入 JSON 只带分镜字段和创作参数,刻意不传 page.text:

"pages": [{
    "page_num": page.index,
    "chapter_title": page.chapter_title,
    "scene": page.scene,
    "character_action": page.character_action,
}]

Prompt 里写死几条硬约束:scene 禁止对白旁白;action 必须来自 character_action;characters[0].prompt 全书一致,可吸收 character_design。温度设成 0.4,比故事编剧的 0.8 更低,减少 JSON 字段漂移和多余说明文字。

3.4 后处理与校验

LLM 返回后不是直接交给 Qwen,而是先走 _finalize_payloads:补系统字段、截断长度、调用 _validate_page_payload 校验。任何一页不合格就抛异常,整条 Agent 路径失败,自动触发规则回退。

out["seed_base"] = int(seed_base)
out["characters"][0]["id"] = f"{story_id}_hero"
err = _validate_page_payload(out)  # 不合格 → fallback

这种「Agent 优先、校验兜底」的设计,是为了线上不会因为某一页 JSON 缺字段就把 Qwen 调崩。

3.5 入口:Agent 优先,规则兜底

对外入口仍是 build_page_payloads_from_story,内部转给 build_page_payloads_with_agent:

if service.is_ready():
    try:
        return service.generate_payloads(...)
    except Exception:
        logger.warning("image_prompt_agent failed, fallback to rules")
return build_page_payloads_rule_based(...)

配置复用故事 LLM 的 Key,开关 IMAGE_PROMPT_AGENT_ENABLED=true。日志里 image_prompt_agent ok 表示走了 Agent;fallback to rules 表示 Agent 未启用、报错或校验失败,走了规则层。


四、规则层优化:Agent 失败时的底线

即使 Agent 可用,我也重写了 build_page_payloads_rule_based,作为对照和 fallback。规则层的目标不是替代 Agent,而是在无 Key、Agent 超时、JSON 校验失败时,仍能产出结构正确、字段来源正确的 payload。

场景改用分镜字段,全局信息只放第一页,避免每页重复占满 800 字:

chunks = [f"本页场景:{page.scene or payload.scene}"]
chunks.append(f"构图要求:{_composition_hint(page.index)}")
# 仅首页:故事世界总设定、年龄、兴趣标签

动作直接读 character_action,不再从 page.text 截取。

角色 prompt 加厚,吸收 character_design,并写死「全书服装、体型、辨识度一致」——这行约束对跨页 VL 打分很重要。

画风通过 _STYLE_PRESETS 与前端 art_style(水彩、扁平等)对齐;负面词补充对话框、字幕、路人等,减少画面上突然冒出气泡文字。

规则层和 Agent 共用 _build_illustration_style、_NEGATIVE 等常量,保证 fallback 时画风基调不会突变。


五、代码去重:第四篇遗留的重复封装

接入后端后,出图相关函数叠了好几层:try_render_story_images、render_story_images_from_prompts 本质一样;ImageProvider 写了却几乎没用,Key 判断散落在 image_generator.py 各处。

我收敛后对外只留三个入口:

  • build_page_payloads_from_story() — 构造 payload,Agent 优先
  • render_story_images() — 已有 payload,调 Qwen
  • render_story_images_for_story() — 拼 payload + 出图;quiet=True 时失败只记日志

ImageProvider 统一 DashScope Key 和模型名:

class ImageProvider:
    def api_key(self) -> str:
        return (settings.image_api_key or settings.dashscope_api_key or "").strip()

第三篇的 QwenImageGenerator 和 VL 打分逻辑一点没动。这篇只改了「prompt 从哪来」和工程胶水层,图像生成核心保持独立,方便以后换模型或调参。


六、阅读页「重试本页插图」

6.1 后端

重绘走 POST /api/v1/stories/{id}/pages/{n}/retry-image,同步跑完一整页 Qwen(约 20~60 秒)。它会读取创作参数快照(gen_params),否则无法还原主角设定和画风;然后对该页单独走 build_page_payloads_from_story + render_story_images。

6.2 前端

两个 bug :

一是 URL 没有 story_id 时,页面从 localStorage 缓存读故事能正常浏览,但重绘逻辑写死 if (!storyId) return,静默返回,按钮毫无反馈。

二是重绘成功后只弹 alert「请刷新」,img 的 src 没变,浏览器继续显示旧图缓存。

修复后增加了 effectiveStoryId() 从缓存补全 ID;点击后按钮变「重绘中,请稍候…」并更新提示文案;成功后给图片 URL 加 ?t= 时间戳再 renderPage()。

const sid = effectiveStoryId();
retryImageBtn.textContent = "重绘中,请稍候…";
page._imageCacheBust = Date.now();
renderPage();

七、单页 payload 长什么样(LLM 2 → Qwen)

Agent 产出经 _finalize_payloads 后,交给第三篇 generate_scene 的结构如下。注意 scene 里是环境+构图,action 在 characters 里,角色 prompt 全书共用:

{
  "scene": "本页场景:阳光穿过树梢的森林小径。构图要求:中景,单一焦点…",
  "illustration_style": "儿童绘本插画,柔和水彩质感,全书画风统一",
  "page_num": 1,
  "seed_base": 1847293651,
  "characters": [{
    "id": "{story_id}_hero",
    "prompt": "儿童绘本主角:勇敢小狮子。橙色鬃毛,绿色背带裤…",
    "action": "小狮子踮脚张望远方,表情好奇",
    "face_enabled": false,
    "score_min_percent": 70
  }]
}

Qwen 侧流程不变:先用 characters[0].prompt 生成/复用角色参考图,再按 scene + action 出场景图,VL 打分不达标就重试。VL 一致性评分机制还是第三篇的 Qwen-VL-Max,这篇没有改动它。


八、小结

第五篇的核心是把链路补全:

故事 LLM 写分镜 → 出图 LLM 写 Qwen JSON(新增)→ 规则层兜底(优化)→ Qwen 出图(第三篇)

代码上新增 image_prompt_generation.py,改造 story_images.py 和 job_orchestrator.py,并修了前端重绘体验。两个 LLM 共用同一套 Key 配置,关掉出图编剧只需 IMAGE_PROMPT_AGENT_ENABLED=false。

下一篇计划写出图 RAG 语料建设。

Logo

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

更多推荐