重构回忆录为 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 映射等回归测试,并加入图片占位符退役设计文档。
This commit is contained in:
Kevin
2026-03-20 10:30:07 +08:00
parent 13e3124b85
commit 7f57f96c25
67 changed files with 4751 additions and 832 deletions

View File

@@ -57,8 +57,8 @@ _IMAGE_PLACEHOLDER_ANY_BRACES_RE = re.compile(
def inject_image_placeholder_template(content: str) -> str:
"""
入库前对章节正文做占位符处理:用正则匹配所有图片占位符位置,拼上固定模板
支持任意层数花括号,输出统一为四层大括号 + 固定模板 + 描述
对正文中的 IMAGE 占位符拼上固定风格模板(四层花括号)
**线上写路径已不使用**;保留供离线迁移脚本处理历史数据
"""
if not content or not content.strip():
return content
@@ -92,7 +92,6 @@ def get_system_prompt() -> str:
4. 将口语化表达改写为书面语,保持原意和情感
5. 生成合适的章节标题和段落结构
6. 提取关键信息,形成连贯的叙述
7. 建议插图位置(在描述场景、人物、地点的地方)
## 内容筛选原则(最重要)
对话中往往夹杂大量与回忆录无关的噪音,你必须严格筛选,只保留有价值的内容:
@@ -171,24 +170,14 @@ def get_text_rewrite_prompt(
请按照以下格式返回 JSON
{{
"title": "章节标题",
"content": "改写后的书面语内容(包含图片占位符)",
"content": "改写后的书面语内容",
"summary": "章节摘要50字以内"
}}
要求:
1. 标题要简洁有力,能概括章节主题
2. 内容要流畅自然,保持原意和情感
3. 如果已有章节内容,请将新内容与已有内容自然融合
4. 在内容中适当位置插入图片占位符
## 图片占位符格式(必须严格遵守)
- **唯一合法格式**:开头恰好四个左花括号、结尾恰好四个右花括号,中间为 IMAGE:具体描述。即:{{{{IMAGE:具体的图片描述}}}}
- 禁止使用两层 {{ }}、六层 {{{{{{ }}}}}} 或任意其它层数,否则会在手机端显示异常。
- 占位符单独占一行,描述要具体、有画面感。系统会在入库前自动拼上统一风格模板,你只需写场景描述即可。
正确示例(仅此格式):
{{{{IMAGE:南方小镇的青石板路,两旁是白墙黑瓦的老房子}}}}
{{{{IMAGE:奶奶坐在院子里的藤椅上,手里摇着蒲扇}}}}"""
3. 如果已有章节内容,请将新内容与已有内容自然融合"""
def get_state_extraction_prompt(
@@ -350,30 +339,11 @@ def get_narrative_prompt(
3. **只输出新内容的改写结果**,不要重复已有内容
4. 如果有衔接上下文,确保新内容与之自然衔接(语气、时间线连贯)
5. 语气自然,有情绪
6. 在适合配图的地方插入图片占位符
7. 如果有用户的基本信息(出生地、成长地等),在叙述中自然融入地域文化和时代背景
6. 如果有用户的基本信息(出生地、成长地等),在叙述中自然融入地域文化和时代背景
8. **不要将对话中的交互性语言(如"我跟你说""你知道吗")写入叙述**
9. **不要在正文中插入章节标题或分类标签**(如"章节:信念与价值观""## 童年与成长背景"等),章节标题由系统单独管理
## 图片占位符格式(必须严格遵守)
- **唯一合法格式**:开头恰好四个左花括号、结尾恰好四个右花括号,即:{{{{IMAGE:具体的图片描述}}}}
- 禁止两层 {{ }}、六层 {{{{{{ }}}}}} 或其它层数,否则会在手机端显示多余花括号。
- 占位符单独占一行,描述要具体、有画面感。系统会在入库前自动拼上统一风格模板,你只需写场景描述即可。
正确示例(仅此格式):
- {{{{IMAGE:南方小镇的青石板路,两旁是白墙黑瓦的老房子}}}}
- {{{{IMAGE:奶奶坐在院子里的藤椅上,手里摇着蒲扇}}}}
- {{{{IMAGE:少年背着书包站在火车站台上,回望身后的小镇}}}}
- {{{{IMAGE:泛黄的大学录取通知书,压在一摞旧课本下}}}}
图片占位符要求:
- 描述要具体、有画面感,便于后续生成或匹配图片
- 每 200-300 字左右可以插入一个
- 单独占一行,不要嵌入段落中
- 不要使用括号或星号等其他格式
- **花括号必须且仅能为四层**{{{{}}}} 各四个,不多不少
只输出新对话内容的改写结果(包含图片占位符)。如果对话中没有值得记录的人生经历内容,输出空字符串。
只输出新对话内容的改写结果。如果对话中没有值得记录的人生经历内容,输出空字符串。
"""
@@ -415,19 +385,18 @@ def get_narrative_json_prompt(
1. 从对话中提炼与人生经历相关的核心内容过滤语气词、寒暄、与AI的交互
2. 使用第一人称,改写为流畅的书面叙述,不要直接引用对话原话
3. 只输出新内容的改写,不要重复已有内容
4. 每 200-300 字左右一个段落,每个段落配一张图
4. 每 200-300 字左右一个段落
5. 如有衔接上下文,确保新内容与之自然衔接
## 输出格式(严格 JSON
{{
"paragraphs": [
{{"content": "段落正文", "image_description": "该段配图的场景描述,具体有画面感"}},
{{"content": "段落正文"}},
...
]
}}
- content: 本段纯正文,不含占位符
- image_description: 该段配图的场景描述,具体、有画面感,便于生成图片。示例:南方小镇的青石板路,两旁是白墙黑瓦的老房子
- content: 本段纯正文
如果对话中没有值得记录的人生经历内容,输出:{{"paragraphs": []}}
"""