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

12 KiB
Raw Blame History

回忆录自动图片生成与插入设计

日期2026-03-10
范围:api + app-android + PDF 导出链路
状态:已完成设计评审(分节确认通过)

1. 背景与问题

当前回忆录生成链路已经具备三个前置条件:

  • LLM 会在章节正文中插入 {{{{IMAGE:描述}}}} 占位符
  • Chapter.imagesBook.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. 总体架构

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 升级为对象数组,建议最小结构如下:

[
  {
    "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 上的持久化访问地址
  • statuspending | 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.imagespending 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. 回写 urlpromptstatus=completed
  9. 失败则回写 status=failederror

单张图片失败不影响同章节其他图片。

6.3 幂等性

  • 已有 completed 且存在 url 的 item 不重复生成
  • 重跑章节时按 placeholder 去重
  • COS key 使用 chapter_id + index + prompt_hash 生成
  • 子任务每次读取数据库最新状态,避免并发覆盖旧值

7. 占位符解析与 Prompt 优化

7.1 占位符解析

新增 parse_image_placeholders(content: str),输出:

[
  {
    "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 抽象

新增接口:

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 异常时只影响对应图片项

状态流转:

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. 封面图生成

本设计已确认,可进入实现计划阶段。