Files
life-echo/docs/plans/2026-03-10-memoir-image-generation-design.md
2026-03-10 14:33:46 +08:00

419 lines
12 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.
# 回忆录自动图片生成与插入设计
日期2026-03-10
范围:`api` + `app-android` + PDF 导出链路
状态:已完成设计评审(分节确认通过)
## 1. 背景与问题
当前回忆录生成链路已经具备三个前置条件:
- LLM 会在章节正文中插入 `{{{{IMAGE:描述}}}}` 占位符
- `Chapter.images``Book.cover_image_url` 已在数据库模型中预留
- Android 端已具备“无图片则移除占位符”的兜底逻辑
但系统仍缺少完整的图片生产与消费闭环:
- 没有占位符解析与结构化存储
- 没有图片生成 provider 封装
- 没有对象存储持久化
- Android 无法按占位符位置渲染图片
- PDF 导出会原样输出图片占位符
- `memory_agent.py` 中的 `image_suggestions` 未进入正式链路
这导致当前提示词虽然已经产出图片意图,但业务上仍然只能展示纯文本章节。
## 2. 目标与非目标
### 2.1 目标
- 在章节叙事生成后自动提取图片占位符
- 通过图片生成 provider 生成配图,并持久化到腾讯云 COS
- 将图片结果写回 `Chapter.images`
- Android 按正文中的占位符位置渲染图片、加载态与失败态
- PDF 导出时嵌入图片或清理占位符
- 图片生成支持开关、上限、风格、尺寸等配置
- 图片生成失败不影响章节文本生成与展示
### 2.2 非目标
- 本期不做 iOS 适配
- 本期不引入多 provider UI 配置
- 本期不新增独立图片任务表
- 本期不依赖 provider 临时 URL 作为最终展示地址
## 3. 方案决策
### 3.1 已确认的关键决策
- `Chapter.images` 直接升级为对象数组,不保留 `List<String>` 旧契约
- 图片生成采用异步补图模式,不阻塞章节文本返回
- 图片最终持久化到腾讯云 COS
- 当前 provider 定为 Liblib但其调用细节暂未确认后端通过 adapter 抽象同步/异步差异
### 3.2 方案对比
- 方案 A将图片生成并上传全部并入 `process_memoir_segments`
- 优点:代码集中
- 缺点:章节任务变长,失败面扩大,不符合“先出文本、后补图”
- 方案 B章节生成任务 + 章节补图子任务
- 优点:职责清晰,失败隔离好,最贴合当前需求
- 结论:采用
- 方案 C新增图片任务表与完整流水线
- 优点:长期最规范
- 缺点:超出本期范围,改动过重
## 4. 总体架构
```mermaid
flowchart TD
segment["Segment transcripts"] --> memoirTask["process_memoir_segments"]
memoirTask --> narrative["Generate chapter narrative with IMAGE placeholders"]
narrative --> initImages["Initialize Chapter.images from placeholders"]
initImages --> persistChapter["Save chapter text + pending image items"]
persistChapter --> returnText["Chapter text available to client"]
persistChapter --> imageTask["generate_chapter_images(chapter_id)"]
imageTask --> promptOpt["Prompt optimization with LLM"]
promptOpt --> provider["ImageGenerationProvider"]
provider --> download["Download generated image bytes"]
download --> cos["Tencent COS upload"]
cos --> updateChapter["Update Chapter.images item status/url"]
updateChapter --> android["Android chapter rendering"]
updateChapter --> pdf["PDF export rendering"]
```
设计原则:
- 章节文本与图片生成解耦
- `Chapter.images` 作为图片状态与渲染的唯一事实来源
- 图片子任务按 item 状态幂等执行
- provider 与存储能力通过 service abstraction 隔离
## 5. 数据模型设计
`Chapter.images` 升级为对象数组,建议最小结构如下:
```json
[
{
"index": 0,
"placeholder": "{{{{IMAGE:南方小镇的青石板路}}}}",
"description": "南方小镇的青石板路",
"prompt": "A serene old town in southern China, bluestone path, white walls, black tiles...",
"url": "https://cdn.example.com/memoirs/chapters/xxx.png",
"status": "pending",
"provider": "liblib",
"style": "watercolor",
"size": "1024x1024",
"error": null,
"created_at": "2026-03-10T10:00:00Z",
"updated_at": "2026-03-10T10:00:00Z"
}
]
```
字段说明:
- `index`:该图片在章节中的顺序索引
- `placeholder`:原始占位符文本,用于正文位置匹配
- `description`:从占位符中解析出的原始描述
- `prompt`:优化后的生成 prompt便于审计与排障
- `url`:腾讯云 COS 上的持久化访问地址
- `status``pending | processing | completed | failed`
- `provider`:当前为 `liblib`
- `style` / `size`:便于后续风格和尺寸策略落地
- `error`:失败时的摘要信息
- `created_at` / `updated_at`:状态追踪
`Book.cover_image_url` 保留现有结构,作为 P3 交付项。
## 6. 后端处理流程
### 6.1 章节任务
`process_memoir_segments` 保持现有文本生成职责,在章节内容落库前后增加两步:
1. 生成叙事正文,正文中保留 `{{{{IMAGE:...}}}}`
2. 从正文中提取图片占位符
3. 按配置进行去重、排序、截断
4. 初始化 `Chapter.images``pending` item 列表
5. 保存章节文本和图片元数据
6. 派发 `generate_chapter_images.delay(chapter.id)`
这样章节文本可先返回,不被图片生成阻塞。
### 6.2 图片子任务
新增 `generate_chapter_images(chapter_id: str)`
1. 读取最新章节数据
2. 仅选择 `pending` 或允许重试的 `failed` item
3. 逐项将状态更新为 `processing`
4. 生成 prompt
5. 提交 provider 任务并等待结果
6. 下载图片字节
7. 上传 COS
8. 回写 `url``prompt``status=completed`
9. 失败则回写 `status=failed``error`
单张图片失败不影响同章节其他图片。
### 6.3 幂等性
- 已有 `completed` 且存在 `url` 的 item 不重复生成
- 重跑章节时按 `placeholder` 去重
- COS key 使用 `chapter_id + index + prompt_hash` 生成
- 子任务每次读取数据库最新状态,避免并发覆盖旧值
## 7. 占位符解析与 Prompt 优化
### 7.1 占位符解析
新增 `parse_image_placeholders(content: str)`,输出:
```json
[
{
"index": 0,
"description": "南方小镇的青石板路",
"placeholder": "{{{{IMAGE:南方小镇的青石板路}}}}",
"start_offset": 128
}
]
```
规则:
- 只匹配 `{{{{IMAGE:...}}}}` 主格式
- 保留原始顺序
- 去掉空描述
- 可按描述文本去重
- 配置图片上限,默认每章最多 2 至 3 张
`start_offset` 仅服务端内部使用,不暴露给客户端。
### 7.2 Prompt 优化
新增 image prompt optimizer
- 输入:占位符描述、章节标题、章节分类、前后文摘要
- 输出:英文图片生成 prompt、风格、尺寸
优先按章节分类映射默认风格:
- `childhood` / `family`:温暖插画或水彩
- `career_*`:偏写实
- `beliefs` / `summary`:更克制、书籍插画感
如果 LLM 优化失败,则退回“原始中文描述 + 章节上下文”的降级 prompt不阻塞图片生成。
## 8. Provider 与腾讯云 COS 设计
### 8.1 Provider 抽象
新增接口:
```python
class ImageGenerationProvider:
def submit_generation(self, prompt: str, size: str, style: str) -> dict: ...
def poll_generation(self, job: dict) -> dict: ...
def download_image(self, result: dict) -> bytes: ...
```
当前实现 `LiblibImageProvider`,内部可适配两类 provider 行为:
- 同步返回图片结果
- 异步返回任务 ID 后轮询结果
这样即使 Liblib 最终 API 细节变化,任务编排层仍保持稳定。
### 8.2 COS 存储
新增 `TencentCosStorageService`
- 输入图片字节、key、content type
- 输出:可持久访问的 URL
推荐 key 结构:
- `memoirs/{user_id}/{chapter_id}/{index}-{prompt_hash}.png`
存储要求:
- 只保存 COS URL 到数据库
- 不依赖第三方临时 URL
- 上传后可选做图片格式标准化与压缩
## 9. Android 渲染方案
Android 不再只把正文当作一整块 Markdown 字符串处理,而是新增“正文块解析层”:
- `TextBlock`
- `ImageBlock`
流程:
1. 根据正文中的 `placeholder` 将内容拆分为顺序块
2.`placeholder` 匹配 `Chapter.images`
3. 按图片状态渲染对应 UI
渲染规则:
- `completed`:显示图片
- `pending` / `processing`:显示固定比例占位骨架
- `failed`:隐藏图片并保持正文连续
推荐使用自定义 Compose 组件渲染图片,不使用 Markdown 内联图片,原因是:
- 更易处理加载态和失败态
- 更易支持点击放大查看
- 更便于后续统一样式和缓存控制
`TextUtils.removeImagePlaceholders()` 调整为兜底工具,仅在“无可用图片渲染链路”场景下移除占位符。
## 10. PDF 导出方案
`pdf_service.py` 需要从“纯文本分段输出”升级为“文本块 + 图片块”输出:
1. 解析章节内容中的占位符
2. 匹配 `Chapter.images`
3.`completed` 图片下载并嵌入 PDF
4.`pending` / `failed` / 丢失图片移除占位符
约束:
- PDF 中绝不能原样输出 `{{{{IMAGE:...}}}}`
- 图片下载失败时回退为纯文本段落
- 图片尺寸需要统一,避免破坏版式
## 11. 配置与开关
建议新增环境变量:
- `MEMOIR_IMAGE_ENABLED`
- `MEMOIR_IMAGE_MAX_PER_CHAPTER`
- `MEMOIR_IMAGE_PROVIDER`
- `MEMOIR_IMAGE_STYLE_DEFAULT`
- `MEMOIR_IMAGE_SIZE_DEFAULT`
- `LIBLIB_API_KEY`
- `LIBLIB_BASE_URL`
- `TENCENT_COS_SECRET_ID`
- `TENCENT_COS_SECRET_KEY`
- `TENCENT_COS_REGION`
- `TENCENT_COS_BUCKET`
- `TENCENT_COS_BASE_URL`
默认策略:
- 功能默认关闭
- 每章图片默认上限 2
- provider 默认 `liblib`
- 图片生成失败不影响章节成功状态
## 12. 错误处理与重试
重试分层:
- 章节任务:保持现有重试策略
- 图片子任务:单张图片最多重试 2 至 3 次
- COS 上传失败:可在单图级别重试
错误原则:
- 单图失败不阻塞同章节其他图片
- 保留 `error` 字段用于排障
- provider 或 LLM 异常时只影响对应图片项
状态流转:
```text
pending -> processing -> completed
pending -> processing -> failed
failed -> processing -> completed
```
## 13. 测试与验收
### 13.1 后端测试
- 占位符解析单元测试
- 章节初始化图片列表测试
- 图片子任务幂等测试
- provider adapter mock 测试
- COS 上传 service 测试
- PDF 导出占位符清理与图片嵌入测试
### 13.2 Android 测试
- 内容块解析测试
- 图片状态渲染测试
- 图片点击放大交互测试
- 无图片 / 失败图片 / 部分成功图片混合场景测试
### 13.3 验收标准
- 章节生成后自动初始化图片任务
- 生成成功图片的 URL 正确写入 `Chapter.images`
- Android 正确按正文位置展示图片
- 图片失败时正文正常显示,用户不看到原始占位符
- PDF 成功嵌入图片或清理占位符
- 功能可通过配置开关关闭
## 14. 分期落地
### P0
- 占位符解析
- `Chapter.images` 对象数组升级
- 图片 provider adapter
- Prompt 优化
- 腾讯云 COS 上传
- Celery 图片子任务
### P1
- Android 图片渲染与点击放大
- 加载态 / 失败态展示
### P2
- PDF 图片嵌入与占位符清理
### P3
- 基于整本回忆录摘要生成 `Book.cover_image_url`
### P4
- 多 provider 支持
- 风格自定义
- iOS 适配
## 15. 风险与缓解
### 15.1 风险
- `Chapter.images` API 契约升级会影响 Android 反序列化
- Liblib API 细节未最终确认
- 图片生成成本可能随章节数增长
- PDF 图片下载可能导致导出变慢
### 15.2 缓解
- 服务端与 Android 同步升级,空数组兼容历史数据
- 使用 provider adapter 隔离 Liblib 具体协议
- 配置每章图片上限,默认关闭功能
- PDF 对失败图片只做清理,不阻塞导出
## 16. 实施建议
推荐按以下顺序实现:
1. 后端数据契约与占位符解析
2. 图片子任务与 COS 上传
3. Android 内容块渲染
4. PDF 导出改造
5. 封面图生成
本设计已确认,可进入实现计划阶段。