Files
life-echo/docs/plans/2026-03-19-image-intent-placeholder-removal-design.md
Kevin 7f57f96c25 重构回忆录为 story-first / markdown-first 架构并整合图片意图与前端 UI 修复
本次 squash merge 将 codex-story-first-image-intent 的整体改动合入 development,核心内容包括:

1. 后端数据与迁移:新增 stories、story_versions、story_image_intents、chapter_cover_intents、assets 等模型与 Alembic 迁移,建立 story-first、markdown-first、asset-first 的主数据链路。

2. 生成与任务链:引入 StoryBuilderOrchestrator、ChapterComposerOrchestrator、story_image_tasks、chapter_cover_tasks,图片生成从正文占位符改为结构化 intent -> asset -> markdown 回填。

3. 并发与一致性:为 story/chapter intent 增加 claim_token、claimed_at、attempt_count,采用数据库原子 claim 为主、Redis 锁为辅,避免重复生成、锁误删和 processing 卡死。

4. Memoir 读写路径:章节 canonical_markdown 成为正文真源,列表/详情接口补齐 markdown、cover_asset、word_count 等字段,PDF 与 asset 解析链路同步升级。

5. Memory / Retrieval:扩展 transcript ingest、chunking、evidence 检索与 story 聚合基础设施,为后续 story-first RAG 与多 agent 编排提供底座。

6. App 端体验:章节页继续走 MarkdownRenderer 阅读链,同时吸收 fix3-19 的跨平台 UI glitch 修复;更新对话页、首页、文案资源与章节列表映射逻辑。

7. 测试与文档:补充 asset resolver、story image task、章节封面派发、markdown 映射等回归测试,并加入图片占位符退役设计文档。
2026-03-20 10:31:51 +08:00

10 KiB
Raw Blame History

Image Intent 化与占位符退役设计

日期2026-03-19 前提:story-first + markdown-first 总体重构已完成。 目标:彻底移除 {{IMAGE:描述}} 这类正文占位符,把图片生成与回填改造成结构化 image intent 流程。

1. 结论

本设计的核心决策如下:

  1. {{IMAGE:描述}} 不再是正文协议。
  2. story 正文只保留最终可阅读的 markdown。
  3. 每个 story 必须且仅有一张主插图。
  4. 插图属于 story封面属于 chapter。
  5. 图片“待生成意图”以结构化数据存储,不再嵌入正文。
  6. 正文中的图片只允许最终引用形式,例如 ![caption](asset://image_id)

一句话概括新流程:

story markdown -> extract image intent -> generate asset -> write new story version with asset:// reference

2. 问题定义

旧占位符方案存在以下问题:

  • 生成意图与正文内容耦合,污染 markdown 真源。
  • 占位符兼容双层、四层、多层花括号,协议不稳定。
  • 图片样式模板被直接拼进占位符字符串,数据边界混乱。
  • 旧链路依赖 section 拆分,和 story-first 架构冲突。
  • 占位符错误、残留或格式偏差会直接泄漏到阅读层。
  • “每 3 段 1 图”这类旧 fallback 是技术债,不应继续存在。

因此,新系统必须彻底移除正文占位符,把图片生成升级成 story/chapter 的结构化资产流程。

3. 目标与非目标

3.1 目标

  • 让图片生成链路与 story/chapter 正文解耦。
  • 保证每个 story 恰好一张主插图。
  • 让 chapter 封面从章节内全部 stories 聚合生成。
  • 让 app、PDF、未来运营端消费统一的 markdown 图片协议。
  • 建立可重试、可审计、可回填的图片版本链。

3.2 非目标

  • 不保留 {{IMAGE:描述}} 作为线上兼容格式。
  • 不支持 story 多张正文插图的一期能力。
  • 不在本阶段做通用媒体编辑器。
  • 不把封面也写回 chapter 正文 markdown。

4. 核心模型

4.1 Story 插图意图

建议新增 story_image_intents

字段 类型 说明
id string 主键
story_id string 所属 story
story_version_id string 提取意图时对应的正文版本
intent_role string 固定为 primary
source_span json/null 对应正文中的段落或块位置信息
caption string 最终图注候选
prompt_brief text 供出图使用的结构化场景摘要
style_profile string/null 风格策略键
status string pending / processing / completed / failed
asset_id string/null 生成成功后的资产
error text/null 错误信息
created_at datetime 创建时间
updated_at datetime 更新时间

关键约束:

  • 每个 active story 只能有 1 条 intent_role=primary 的有效 intent。

4.2 Chapter 封面意图

建议新增 chapter_cover_intents

字段 类型 说明
id string 主键
chapter_id string 所属 chapter
chapter_version_id string 封面生成时对应的章节版本
story_ids json 参与聚合的 stories
prompt_brief text 章节封面摘要
status string pending / processing / completed / failed
asset_id string/null 封面资产
error text/null 错误信息
created_at datetime 创建时间
updated_at datetime 更新时间

4.3 统一资源表

建议统一使用 assets 或在现有图片表基础上重构:

字段 类型 说明
id string 主键
asset_type string story_image / chapter_cover
storage_key string 对象存储键
url string/null 可访问地址
provider string 生成 provider
style_profile string/null 风格配置
prompt_final text 最终发送给模型的 prompt
status string completed / failed / deleted
width int/null
height int/null
created_at datetime 创建时间

5. 新的正文协议

5.1 允许形式

正文 markdown 中只允许最终图片引用:

![奶奶坐在院子里的藤椅上](asset://img_123)

5.2 禁止形式

以下形式全部退出线上正文:

  • {{IMAGE:描述}}
  • {{{{IMAGE:描述}}}}
  • <!-- image-intent: ... -->
  • 任意 HTML 媒体占位标记

5.3 解释

正文只服务阅读与导出,不再承载“待生成意图”。

待生成意图只存在于结构化表中。

6. 流程设计

6.1 Story 主插图流程

flowchart LR
    A["StorySynthesisAgent"] --> B["story canonical markdown"]
    B --> C["StoryImageIntentExtractor"]
    C --> D["story_image_intents"]
    D --> E["ImageGenerationTask"]
    E --> F["assets"]
    F --> G["Create new story_version"]
    G --> H["story markdown includes asset:// reference"]

步骤:

  1. StorySynthesisAgent 生成或更新 story canonical markdown。
  2. StoryImageIntentExtractor 从 story markdown 或 AST 中提取唯一主图意图。
  3. 写入 story_image_intents,状态为 pending
  4. 异步图片任务读取 intent生成资产。
  5. 成功后写入 assets
  6. 创建新的 story_version,把 markdown 中对应位置回填成 asset:// 图片引用。
  7. 更新 stories.current_version_id

6.2 Chapter 封面流程

flowchart LR
    A["ChapterComposerOrchestrator"] --> B["chapter markdown"]
    B --> C["Aggregate chapter stories"]
    C --> D["chapter_cover_intent"]
    D --> E["Cover image generation"]
    E --> F["cover asset"]
    F --> G["chapters.cover_asset_id"]

步骤:

  1. ChapterComposerOrchestrator 完成章节编排。
  2. 聚合本章 stories 的人物、地点、时间、情绪、时代背景。
  3. 生成唯一 chapter_cover_intent
  4. 生成封面资源并绑定到 chapters.cover_asset_id

说明:

  • 封面不回写进正文 markdown。
  • 阅读页顶部可单独展示封面 asset。

7. Image Intent 提取策略

7.1 规则

每个 story 必须且仅有一张主插图,因此 extractor 不做多图候选池。

优先级:

  1. 最具画面感的场景段落
  2. 具有人物 + 动作 + 场景 + 时代细节的段落
  3. 故事转折点或记忆锚点段落
  4. 若 story 过于抽象,则退化为“人物/地点/时代感”概括图

7.2 输出

输出结构至少包含:

  • caption
  • prompt_brief
  • source_span
  • style_profile

7.3 失败兜底

如果规则和 agent 都未提取到高质量意图,则使用最小兜底策略:

  • story title
  • story stage
  • time refs
  • place refs
  • people refs
  • story summary

即使降级,也必须生成 1 条 primary intent。

8. 版本回填策略

8.1 原则

图片生成成功后,不能原地覆盖 story 正文。

必须:

  1. 基于当前 story version 创建新版本
  2. 将最终图片引用回填到 markdown
  3. 写入 change_summary
  4. 更新当前生效版本指针

8.2 回填位置

建议由 source_span 或 block id 决定回填位置。

如果定位失败:

  • 退化为在 story 开头或相关段落后插入图片引用
  • 但仍需创建新版本,不可丢图

9. 状态机

9.1 Story 状态建议

  • content_pending
  • content_ready_image_pending
  • content_ready_image_processing
  • published
  • image_failed

9.2 约束

  • published story 必须有 resolved primary image asset
  • content_ready_image_pending 允许正文已就绪但图片仍在处理中
  • image_failed 允许重试,但不允许伪装成已发布完整内容

10. 失败处理

10.1 意图提取失败

  • 走 deterministic fallback
  • 必须产出 intent

10.2 图片生成失败

  • intent 状态置为 failed
  • story 状态置为 content_ready_image_pendingimage_failed
  • 支持后台重试

10.3 回填失败

  • asset 保留
  • intent 状态可为 completed_but_unapplied
  • 创建修复任务重新生成 story version

10.4 Chapter 封面失败

  • 不影响章节正文阅读
  • 允许章节无封面但正文可读

11. 测试计划

11.1 单元测试

  • 每个 story 只能生成一个 primary intent
  • abstract story 走 fallback 也能生成 intent
  • 回填后 markdown 只含 asset://,不含 placeholder

11.2 集成测试

  • story markdown -> image intent -> asset -> new story version
  • chapter stories -> cover intent -> cover asset

11.3 迁移测试

  • {{IMAGE:描述}} 正文可被正确提取为 intent
  • 旧图片记录可被映射为 asset
  • 迁移后正文不再含 placeholder

11.4 渲染测试

  • app 阅读页正确渲染 asset://
  • PDF 正确渲染 story 图片与 chapter 封面
  • 未解析外链或非法资源时安全失败

12. 旧链路退役清单

以下逻辑应退出线上主链路:

  • inject_image_placeholder_template
  • inject_placeholders
  • parse_image_placeholders
  • split_narrative_to_sections
  • parse_narrative_to_sections
  • 基于 placeholder 创建段落配图的逻辑
  • “每 3 段 1 图”的旧 fallback

注意:

  • 这些逻辑可短期保留在离线迁移脚本中读取历史数据
  • 但不允许继续出现在线上写路径和读路径

13. 一次性实施步骤

  1. 新增 story_image_intents
  2. 新增 chapter_cover_intents
  3. 统一资源表为 assets
  4. 删除 prompt 中对 {{IMAGE:描述}} 的输出要求
  5. 重写 story 生成链:正文生成后提取 primary image intent
  6. 重写图片任务:读取 intent不读取正文占位符
  7. 重写 story version 回填逻辑:写入 asset://
  8. 重写 chapter 封面聚合逻辑
  9. app / PDF 渲染只认 asset://
  10. 迁移历史正文与旧图片记录
  11. 删除旧占位符相关线上逻辑与测试
  12. 补齐新 intent / asset / cover 测试

14. 最终判断

这次“修复占位符”本质上不是字符串格式修复,而是把旧的正文 DSL 彻底退役。

正确的长期模型应当是:

  • story 有且只有一张主插图
  • chapter 有一张聚合封面
  • 图片意图是结构化资产流程
  • markdown 只保存最终可阅读结果

只要这四点成立,未来无论是运营润色、重新生成图片、替换封面、审计版本还是导出 PDF都不需要再碰 {{IMAGE:描述}} 这类过渡协议。