一、imaging 按页进度:告别 80% 黑盒

第六篇之前,imaging 阶段的进度长时间卡在 80%,前端只显示「正在绘制绘本插图…」。8 页 × Qwen + VL 重试,用户等 10~20 分钟完全不知道卡在第几页。

1.1 后端:stage_message + 按页推进

在 generation_tasks 表中新增 stage_message 字段。每画完一页,Job 就更新进度和文案:

# job_sqlite.py
def update_imaging_page(self, *, job_id, page_index, total_pages):
    progress = 80 + int(19 * page_index / total_pages)  # 80% → 99%
    message = f"正在绘制第 {page_index}/{total_pages} 页插图"
    self.update_stage(..., stage_message=message)

image_generator.py 的出图循环里增加回调:

def render_story_images_from_payloads(story_id, page_payloads, *, on_page_progress=None):
    for payload in page_payloads:
        page_num = int(payload["page_num"])
        if on_page_progress:
            on_page_progress(page_num, total)
        result = svc.generate_page(gen_payload)

job_orchestrator 把回调接入 SQLite:

def on_page(page_index, total_pages):
    self._jobs.update_imaging_page(job_id=job_id, page_index=page_index, total_pages=total_pages)

render_story_images_from_payloads(story_id, page_payloads, on_page_progress=on_page)
1.2 前端:轮询 + 文案

create.html 在 imaging 阶段优先读取 job.stage_message

statusText.textContent = stageText(job.status, job.progress, job.stage_message);
// 例如:正在绘制第 3/8 页插图(91%)

二、双角色出图:配角也要有位置描述

问题在哪

双角色页的常见写法是:

  • 主角:action 来自分镜的 character_action,信息够具体;

  • 配角:要么复用同一段 character_action,要么只有「与主角同框互动」这类泛化描述。

结果 Qwen 会优先画信息更满的主角,配角容易被画小、画没、或只留一个模糊轮廓——不是参考图没传,而是 prompt 里配角「该怎么站在画面里」没说清楚。

改法:在 payload 层给每人写「可画的位置 action」

story_characters.py 里做了两件事:

① action_for_character:按角色序号补位置描述

  • 主角:沿用分镜的 character_action

  • 配角:如果原文没写「与…一起」,则补「与{主角}同框互动」

  • 双角色页还会带上左右侧信息(见下)

② apply_dual_character_layout:双角色页强制 left/right

_DUAL_REF_SLOTS = ("left character", "right character")

def apply_dual_character_layout(characters):
    if len(characters) != 2:
        return characters
    for idx, item in enumerate(characters):
        item["slot"] = _DUAL_REF_SLOTS[idx]          # ref 顺序 = 左 = 主角,右 = 配角
        side = "画面左侧" if idx == 0 else "画面右侧"
        if side not in item.get("action", ""):
            item["action"] = f"{side},{item.get('action') or '自然互动'}"
    return characters

效果对比:

角色 优化前 优化后
主角 小狮子踮脚张望远方 画面左侧,小狮子踮脚张望远方,全身入镜
配角 小狮子踮脚张望远方(复制主角) 画面右侧,与勇敢小狮子同框互动,身体完整可见

这样传给 Qwen 的是两个独立、带位置的动作短语,而不是「一个详细 + 一个敷衍」。

generator 侧也同步加了双角色英文构图约束(dual_layout),作为同一思路的第二层加固,但核心的提示词优化发生在 payload 层。


三、绘本 PDF:左图右文,完整故事按镜对齐

旧版 PDF 只有文字(封面 + 每页 page.text),没有插图,不像绘本。

新版 pdf_export.py 采用横版 A4 跨页布局:

部分 内容
封面 标题、作者、故事摘要、第 1 页插图
内页 左侧:data/images/{story_id}/{页码}.png
右侧:该镜 page.text + 章节标题

核心循环逻辑:

for page in detail.pages:
    img_path = data_root() / "images" / story_id / f"{page.index}.png"
    draw_image_in_box(img_path, margin, margin, left_w, content_h)   # 左半幅
    draw_wrapped_text(text=_page_reading_text(page), x=right_x, ...)  # 右半幅

如果某一页文字较长,会自动续页(同一张插图 +「(续)」标题),避免正文被截断。

导出入口不变:done.html → POST /api/v1/stories/{id}/export-pdf → 下载 data/exports/{id}.pdf

全部页的正文拼在一起就是完整故事;每页插图落在对应分镜位置,PDF 和阅读页使用的是同一套 page.text


四、删书时清理磁盘插图

从书库里删除一本绘本,SQLite 记录没了,但 data/images/ 和 data/character_refs/ 里的 PNG 会一直占用磁盘。我加了 story_assets.py,在删书接口里一并清理出图文件:

def purge_story_image_assets(story_id: str) -> dict[str, bool]:
    """删除 images/{story_id}/ 与 character_refs/{story_id}/"""
    ...

触发点:

  • DELETE /api/v1/stories/{story_id} — 删除单本

  • DELETE /api/v1/stories/history/all — 清空历史

保留不动的内容:page_attempts 出图审计 JSON、image_corpus.jsonl RAG 语料(删书不影响以后新书的出图质量)。


五、当前链路

create.html → POST /jobs
      ↓
scripting / storyboarding / imaging(按页更新 stage_message)
      ↓
Qwen 出图(双角色构图 + full-bleed)→ data/images/{story_id}/
      ↓
reader.html 阅读
      ↓
done.html → export-pdf(左图右文 PDF)
      ↓
删书 → purge_story_image_assets() 清磁盘

六、小结

第七篇主要做了四件事:

  • 体验:imaging 按页进度 + 前端长轮询 + 默认关闭热重载;

  • 成图:双角色页为配角补上独立的、带左右位置的 action 描述,告别「配角被画没」;

  • 交付:PDF 左图右文,插图落在分镜对应的故事位置;

  • 运维:删书时自动清理 images/ 与 character_refs/

仍待改进:

  • workflow 分步骤模式尚未接入 stage_message 细粒度进度

更多推荐