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

353 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 中只允许最终图片引用:
```md
![奶奶坐在院子里的藤椅上](asset://img_123)
```
### 5.2 禁止形式
以下形式全部退出线上正文:
- `{{IMAGE:描述}}`
- `{{{{IMAGE:描述}}}}`
- `<!-- image-intent: ... -->`
- 任意 HTML 媒体占位标记
### 5.3 解释
正文只服务阅读与导出,不再承载“待生成意图”。
待生成意图只存在于结构化表中。
## 6. 流程设计
### 6.1 Story 主插图流程
```mermaid
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 封面流程
```mermaid
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_pending``image_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:描述}}` 这类过渡协议。