419 lines
12 KiB
Markdown
419 lines
12 KiB
Markdown
|
|
# 回忆录自动图片生成与插入设计
|
|||
|
|
|
|||
|
|
日期: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. 封面图生成
|
|||
|
|
|
|||
|
|
本设计已确认,可进入实现计划阶段。
|