init
This commit is contained in:
410
docs/Story-Extractor-产品设计文档.md
Normal file
410
docs/Story-Extractor-产品设计文档.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# Story Extractor — 产品设计文档
|
||||
|
||||
> 版本:v1.1 · 2026-04-21
|
||||
|
||||
---
|
||||
|
||||
## 一、产品概述
|
||||
|
||||
### 1.1 产品定位
|
||||
|
||||
Story Extractor 是一个 **局域网部署的 Web 工具**,用于从直播回放文字稿中自动提取学员个人故事,与讲述者个人信息匹配后,为每位讲述者生成独立的 DOCX 文档。
|
||||
|
||||
### 1.2 核心价值
|
||||
|
||||
- **自动化**:AI 自动从上万行口语化文字稿中提取学员故事,替代人工阅读筛选
|
||||
- **结构化**:将散乱的直播内容转化为结构化的"人物 + 故事"文档
|
||||
- **可复用**:工具化交付,支持后续扩展(LLM 升级、搜索引擎等)
|
||||
|
||||
### 1.3 使用场景
|
||||
|
||||
| 维度 | 描述 |
|
||||
|------|------|
|
||||
| **部署环境** | 局域网,多人可访问 |
|
||||
| **数据规模** | 20+ 份文字稿(每份上万行),8+ 位讲述者 |
|
||||
| **用户角色** | 内容运营人员 |
|
||||
|
||||
---
|
||||
|
||||
## 二、文字稿特征分析
|
||||
|
||||
### 2.1 格式特征
|
||||
|
||||
每段格式为:**`发言者(时间戳): 内容`**,例如:
|
||||
```
|
||||
卢慧老师(00:05:38): 这个我们在我们的启蒙课里面...
|
||||
14班+HCM(02:39:31): 就是要要打要骂,呵呵...
|
||||
```
|
||||
|
||||
- 有明确的**说话人标记**(老师 / 学员昵称)
|
||||
- 有**时间戳**,可用于判断发言时长
|
||||
- 主讲老师占 60-70% 内容,学员发言散落其中
|
||||
|
||||
### 2.2 内容构成
|
||||
|
||||
| 内容类型 | 占比 | 说明 |
|
||||
|----------|------|------|
|
||||
| **课程讲授** | ~55% | 老师讲理论、概念、方法论 |
|
||||
| **课堂管理** | ~10% | 改昵称、纪律提醒、进组讨论等技术性内容 |
|
||||
| **疗愈练习** | ~15% | "我愿意看见且释放..." 引导式重复 |
|
||||
| **学员故事** | ~15% | 学员讲述个人经历,老师追问 |
|
||||
| **学员短回应** | ~5% | "好的""收到"等 |
|
||||
|
||||
### 2.3 故事的实际形态
|
||||
|
||||
故事**不是一段完整的自述**,而是**对话式展开**的:
|
||||
- 老师提问/引导 → 学员回答 → 老师追问 → 学员深入讲述 → 可能穿插疗愈练习
|
||||
- 一个故事可能跨 **20-30 段对话**,持续 15-30 分钟
|
||||
- 故事之间没有明显分界,可能上一段还在讲理论,下一段就切入学员故事
|
||||
|
||||
### 2.4 典型故事示例
|
||||
|
||||
| 讲述者 | 故事概要 |
|
||||
|--------|----------|
|
||||
| **14班+HCM** | 从小被妈妈体罚(跪凳子),长大后用同样方式对待女儿,女儿出现躯体化症状 |
|
||||
| **13-阿纳** | 姥姥因特殊年代藏首饰恐惧自杀,形成"不能有钱"的家族限制性信念 |
|
||||
| **06班Rena** | 妈妈在怀自己前打掉一个男孩,爸爸重男轻女,自己被当男孩养,对亲密关系有破坏倾向 |
|
||||
| **11班四维** | 女儿出柜说喜欢同性,追溯到自己出生时妈妈因生女孩而悲伤的家族重男轻女 |
|
||||
|
||||
---
|
||||
|
||||
## 三、用户流程
|
||||
|
||||
### 3.1 流程总览
|
||||
|
||||
```
|
||||
欢迎页 → 上传文件 → 提取故事 → 匹配确认 → 预览导出
|
||||
```
|
||||
|
||||
采用 **线性向导式** 交互,用户依次完成四个步骤。
|
||||
|
||||
### 3.2 步骤指示器
|
||||
|
||||
- 顶部居中展示 4 个步骤节点(上传文件 → 提取故事 → 匹配确认 → 预览导出)
|
||||
- 当前步骤高亮(紫色),已完成步骤显示绿色 ✓
|
||||
- 步骤之间用连线表示进度
|
||||
- 欢迎页不显示步骤指示器
|
||||
- 步骤节点可点击跳转(已完成的步骤)
|
||||
|
||||
### 3.3 底部导航栏
|
||||
|
||||
- 固定在页面底部
|
||||
- 左侧:上一步按钮(第一步时隐藏)
|
||||
- 中间:当前步骤提示文案
|
||||
- 右侧:下一步按钮(最后一步变为"全部导出")
|
||||
- 欢迎页不显示底部导航栏
|
||||
|
||||
---
|
||||
|
||||
## 四、页面详细设计
|
||||
|
||||
### 4.0 欢迎页
|
||||
|
||||
**目的**:产品介绍 + 引导用户开始
|
||||
|
||||
**布局**:页面垂直居中
|
||||
|
||||
**内容**:
|
||||
- 产品 Logo(紫色渐变圆角方块 + 书本图标)
|
||||
- 产品名称 "Story Extractor"
|
||||
- 一句话介绍:"从直播回放文字稿中,AI 自动提取个人故事,与讲述者信息匹配,一键生成精美文档。"
|
||||
- 四步流程预览(图标 + 标题 + 副标题):
|
||||
- 📤 上传文件 — 文字稿 + 个人信息
|
||||
- 🪄 提取故事 — AI 智能识别
|
||||
- 🔗 匹配确认 — 人工核对
|
||||
- 📄 预览导出 — 生成 DOCX
|
||||
- 大号 CTA 按钮:"开始使用"
|
||||
|
||||
**交互**:
|
||||
- 点击"开始使用"进入第一步(上传文件)
|
||||
- 不显示步骤指示器和底部导航栏
|
||||
|
||||
---
|
||||
|
||||
### 4.1 第一步:上传文件
|
||||
|
||||
**目的**:上传直播文字稿和讲述者个人信息
|
||||
|
||||
**布局**:左右两栏
|
||||
|
||||
**左栏 — 直播文字稿**:
|
||||
- 标题:直播文字稿(蓝色图标)+ 右侧标注 "DOCX 格式"
|
||||
- 上传区域(虚线边框拖拽区):
|
||||
- 图标 + "拖拽文件到此处,或点击上传"
|
||||
- 提示:支持批量上传 · 仅限 .docx 格式
|
||||
- 已上传文件列表:
|
||||
- 每个文件显示:文件图标(蓝色 DOCX)+ 文件名 + 元信息(行数、大小)
|
||||
- 右侧删除按钮
|
||||
|
||||
**右栏 — 讲述者信息**:
|
||||
- 标题:讲述者信息(粉色图标)+ 右侧标注 "一人一个 DOCX"
|
||||
- 上传区域(虚线边框拖拽区):
|
||||
- 图标 + "拖拽文件到此处,或点击上传"
|
||||
- 提示:每个讲述者一个 DOCX · 包含个人信息和照片
|
||||
- 已上传文件列表:
|
||||
- 每个文件显示:文件图标(粉色)+ 文件名 + 元信息(含几张照片)
|
||||
- 右侧预览按钮 + 删除按钮
|
||||
|
||||
**交互**:
|
||||
- 底部导航提示:"上传文字稿和讲述者信息文件后进入下一步"
|
||||
- 点击"下一步"进入第二步
|
||||
|
||||
---
|
||||
|
||||
### 4.2 第二步:提取故事
|
||||
|
||||
**目的**:AI 自动从所有文字稿中提取学员个人故事
|
||||
|
||||
**进入方式**:从第一步点击"下一步"后自动进入,**自动开始提取**,无需手动触发
|
||||
|
||||
#### 阶段 A:提取进度(自动展示)
|
||||
|
||||
**布局**:单卡片
|
||||
|
||||
**内容**:
|
||||
- 标题:"AI 正在工作中" + 右侧百分比(大号加粗)
|
||||
- 进度条(粗圆角,紫色渐变填充)
|
||||
- 当前处理状态:
|
||||
- 运行状态指示灯(紫色脉冲动画)
|
||||
- 当前文件名:"正在读取:xxx.docx"
|
||||
- 当前动作:
|
||||
- "预处理:过滤课堂管理内容..."
|
||||
- "预处理:过滤疗愈练习..."
|
||||
- "智能分段:识别话题边界..."
|
||||
- "故事识别:检测学员发言..."
|
||||
- "故事提取:整理故事内容..."
|
||||
- 三列状态卡片:
|
||||
- 文件读取:`X / 23`(已读文件数 / 总文件数)
|
||||
- 话题分段:`XX 段`(已分段数量)
|
||||
- 故事发现:`X 个`(已发现故事数量,发现后变绿色)
|
||||
|
||||
**进度阶段**:
|
||||
1. 0-15%:读取文件,预处理(过滤课堂管理、短回应)
|
||||
2. 15-40%:智能话题分段
|
||||
3. 40-70%:识别学员故事片段
|
||||
4. 70-95%:提取故事内容,生成标题和摘要
|
||||
5. 95-100%:汇总结果
|
||||
|
||||
**完成时**:
|
||||
- 进度条变绿色
|
||||
- 状态灯变绿色
|
||||
- 显示"全部处理完成!共处理 X 份文字稿,发现 X 个故事"
|
||||
- 1.2 秒后自动切换到故事列表
|
||||
|
||||
#### 阶段 B:故事列表(提取完成后展示)
|
||||
|
||||
**布局**:单卡片
|
||||
|
||||
**内容**:
|
||||
- 标题:"已提取的故事" + 右侧 "共 X 个故事"
|
||||
- 故事卡片列表,每个卡片包含:
|
||||
- 故事标题(加粗)
|
||||
- 讲述者昵称(从文字稿提取,如"14班+HCM")
|
||||
- 故事摘要(2-3 句话)
|
||||
- 来源信息(文件名 + 行号范围 + 时长)
|
||||
- 置信度标签(高/中/低)
|
||||
- 操作按钮:查看原文、编辑、删除
|
||||
|
||||
**交互**:
|
||||
- 底部导航提示:"AI 已提取故事,下一步将故事与讲述者匹配"
|
||||
- 点击"下一步"进入第三步(**无需全部匹配,可直接进入**)
|
||||
|
||||
---
|
||||
|
||||
### 4.3 第三步:匹配确认
|
||||
|
||||
**目的**:将提取的故事与讲述者一一对应(可选操作)
|
||||
|
||||
**布局**:三栏(故事列表 | 匹配操作 | 讲述者列表)
|
||||
|
||||
**左栏 — 故事列表**:
|
||||
- 标题:"故事列表" + 右侧 "X 个故事"
|
||||
- Tab 切换:全部 / 已匹配 / 未匹配
|
||||
- 故事卡片列表:
|
||||
- 故事标题
|
||||
- 讲述者昵称(文字稿中的昵称,如"14班+HCM")
|
||||
- 故事摘要(简短版)
|
||||
- 匹配状态标签(已匹配显示讲述者姓名,未匹配显示"待匹配")
|
||||
- 点击选中(高亮为橙色背景)
|
||||
- 操作:删除(不满意的故事可直接删除)
|
||||
|
||||
**中间 — 匹配操作**:
|
||||
- 紫色圆形箭头按钮
|
||||
- 提示文字:"选中故事和讲述者后点击匹配"
|
||||
|
||||
**右栏 — 讲述者列表**:
|
||||
- 标题:"讲述者" + 右侧 "共 X 人"
|
||||
- 讲述者卡片列表:
|
||||
- 头像(姓氏首字,紫色渐变背景)
|
||||
- 姓名
|
||||
- 文字稿昵称映射(如:张小花 → 14班+HCM)
|
||||
- 匹配状态(已匹配 X 个故事 / 未匹配)
|
||||
- 点击选中(紫色边框高亮)
|
||||
|
||||
**交互**:
|
||||
- 选中左侧故事 + 选中右侧讲述者 → 点击中间箭头确认匹配
|
||||
- 匹配完成后故事状态变为"已匹配"
|
||||
- **可删除不满意的故事**(AI 可能提取出无效故事)
|
||||
- **无需全部匹配即可进入下一步**(未匹配的故事不会导出)
|
||||
- 底部导航提示:"将需要的故事与讲述者匹配,未匹配的故事不会导出"
|
||||
|
||||
---
|
||||
|
||||
### 4.4 第四步:预览导出
|
||||
|
||||
**目的**:预览最终文档效果并导出
|
||||
|
||||
**布局**:左右两栏
|
||||
|
||||
**左栏 — 导出列表**:
|
||||
- 标题:"导出列表" + 右侧 "仅显示已匹配的讲述者"
|
||||
- 导出项列表,每项包含:
|
||||
- 状态图标(绿色 ✓ = 已匹配可导出)
|
||||
- 文件名:"张小花 - 故事集.docx"
|
||||
- 元信息:"X 个故事 · 含个人信息和照片"
|
||||
- 预览按钮、下载按钮
|
||||
- 未匹配的故事不会出现在导出列表中
|
||||
|
||||
**右栏 — 文档预览**:
|
||||
- 标题:"文档预览" + 上一个/下一个切换按钮
|
||||
- 文档预览卡片(模拟 DOCX 效果):
|
||||
- 顶部:紫色渐变头部
|
||||
- 圆形头像占位
|
||||
- 讲述者姓名
|
||||
- 个人信息(性别 · 年龄 · 城市 · 职业)
|
||||
- 正文区域:
|
||||
- "她的故事" 标题(紫色下划线)
|
||||
- 故事正文(多段落,如有多个故事依次排列)
|
||||
|
||||
**交互**:
|
||||
- 点击"上一个/下一个"切换预览不同讲述者的文档
|
||||
- 点击单个下载按钮导出单个 DOCX
|
||||
- 底部"全部导出"按钮(绿色)批量导出所有已匹配讲述者的文档
|
||||
|
||||
---
|
||||
|
||||
### 4.5 系统设置(弹窗)
|
||||
|
||||
**触发**:右上角齿轮图标
|
||||
|
||||
**布局**:居中弹窗 Modal
|
||||
|
||||
**内容**:
|
||||
|
||||
**LLM 配置**:
|
||||
- API 提供商(下拉选择):OpenAI (GPT-4) / DeepSeek / 通义千问 / 本地 Ollama / 自定义兼容 API
|
||||
- 模型名称(文本输入)
|
||||
- API Base URL(文本输入)
|
||||
- API Key(密码输入)
|
||||
|
||||
**提取参数**:
|
||||
- 话题分段最大长度(行数)
|
||||
- 故事最小长度(行数)
|
||||
- 置信度阈值(低于此值的故事标记为"低置信度")
|
||||
- 并发请求数
|
||||
- Temperature
|
||||
|
||||
**操作按钮**:
|
||||
- 取消、测试连接、保存
|
||||
|
||||
---
|
||||
|
||||
## 五、数据模型
|
||||
|
||||
### 5.1 文字稿(Transcript)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 唯一标识 |
|
||||
| filename | string | 原始文件名 |
|
||||
| file_path | string | 服务器存储路径 |
|
||||
| line_count | int | 总行数 |
|
||||
| file_size | int | 文件大小(字节) |
|
||||
| status | enum | pending / processing / done / error |
|
||||
| uploaded_at | datetime | 上传时间 |
|
||||
|
||||
### 5.2 讲述者(Person)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 唯一标识 |
|
||||
| name | string | 真实姓名 |
|
||||
| nickname | string | 文字稿中的昵称(如"14班+HCM") |
|
||||
| filename | string | 原始 DOCX 文件名 |
|
||||
| file_path | string | 服务器存储路径 |
|
||||
| photo_path | string | 提取的照片路径 |
|
||||
| info | json | 从 DOCX 提取的个人信息(年龄、性别、城市、职业等) |
|
||||
| uploaded_at | datetime | 上传时间 |
|
||||
|
||||
### 5.3 故事(Story)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | string | 唯一标识 |
|
||||
| title | string | AI 生成的故事标题 |
|
||||
| summary | string | AI 生成的故事摘要 |
|
||||
| content | text | 完整故事内容(仅学员讲述部分,已过滤老师对话) |
|
||||
| speaker_nickname | string | 文字稿中的讲述者昵称(如"14班+HCM") |
|
||||
| source_transcript_id | string | 来源文字稿 ID |
|
||||
| source_lines | json | 在原文中的行号范围 [{start, end}] |
|
||||
| duration_minutes | float | 预估时长(分钟) |
|
||||
| confidence | float | 置信度(0.0-1.0) |
|
||||
| person_id | string | 匹配的讲述者 ID(可为空) |
|
||||
| match_status | enum | pending / matched / deleted |
|
||||
| created_at | datetime | 提取时间 |
|
||||
|
||||
### 5.4 系统配置(Config)
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| llm_provider | string | API 提供商 |
|
||||
| llm_model | string | 模型名称 |
|
||||
| llm_base_url | string | API Base URL |
|
||||
| llm_api_key | string | API Key(加密存储) |
|
||||
| segment_max_lines | int | 话题分段最大行数 |
|
||||
| story_min_lines | int | 故事最小行数 |
|
||||
| confidence_threshold | float | 置信度阈值 |
|
||||
| concurrency | int | 并发请求数 |
|
||||
| temperature | float | Temperature |
|
||||
|
||||
---
|
||||
|
||||
## 六、输出文档模板
|
||||
|
||||
每个讲述者生成一份 DOCX,结构如下:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────┐
|
||||
│ [照片] │
|
||||
│ 姓名:张小花 │
|
||||
│ 性别:女 年龄:35岁 │
|
||||
│ 城市:北京 职业:全职妈妈 │
|
||||
├─────────────────────────────────┤
|
||||
│ │
|
||||
│ 她的故事 │
|
||||
│ ───── │
|
||||
│ 故事正文第一段... │
|
||||
│ │
|
||||
│ 故事正文第二段... │
|
||||
│ │
|
||||
│ (如有多个故事,依次排列) │
|
||||
│ │
|
||||
└─────────────────────────────────┘
|
||||
```
|
||||
|
||||
- 简洁模板,不花哨
|
||||
- 个人信息在顶部,照片居中
|
||||
- 故事正文在下方(仅学员讲述内容,已过滤老师对话)
|
||||
- 一位讲述者可能有多个故事,依次排列
|
||||
|
||||
---
|
||||
|
||||
## 七、非功能性需求
|
||||
|
||||
| 需求 | 说明 |
|
||||
|------|------|
|
||||
| **局域网访问** | 部署在局域网内,多人可通过浏览器访问 |
|
||||
| **扩展性** | LLM 可替换,后续可扩展文章搜索引擎 |
|
||||
| **数据安全** | API Key 加密存储,文件仅在服务器本地处理 |
|
||||
| **并发处理** | 支持多文件并发提取,避免前端长时间等待 |
|
||||
| **容错性** | 单个文件处理失败不影响其他文件 |
|
||||
757
docs/Story-Extractor-后端架构设计文档.md
Normal file
757
docs/Story-Extractor-后端架构设计文档.md
Normal file
@@ -0,0 +1,757 @@
|
||||
# Story Extractor — 后端架构设计文档
|
||||
|
||||
> 版本:v1.3 · 2026-04-21
|
||||
|
||||
---
|
||||
|
||||
## 一、架构总览
|
||||
|
||||
### 1.1 技术栈
|
||||
|
||||
| 层级 | 技术 | 选型理由 |
|
||||
|------|------|----------|
|
||||
| **Web 框架** | FastAPI | 异步支持、自动 API 文档、类型提示、高性能 |
|
||||
| **前端** | Vue 3 + Element Plus | 组件丰富、中文生态好、适合管理类交互 |
|
||||
| **数据库** | SQLite | 轻量零安装、局域网场景够用、后续可迁移 |
|
||||
| **LLM 接口** | OpenAI Python SDK(兼容协议) | 一套代码适配 OpenAI / DeepSeek / 通义千问 / Ollama |
|
||||
| **DOCX 处理** | python-docx | 读写 DOCX 的标准 Python 库 |
|
||||
| **部署** | Docker Compose | 一键启动,局域网分发方便 |
|
||||
|
||||
> **设计原则:极简架构**,不引入 Redis、Celery、Nginx 等外部依赖,全部基于 FastAPI 原生能力。
|
||||
|
||||
### 1.2 系统架构图
|
||||
|
||||
```
|
||||
┌──────────────────┐
|
||||
│ 浏览器 (Vue 3) │
|
||||
└────────┬─────────┘
|
||||
│ HTTP / SSE
|
||||
┌────────┴─────────┐
|
||||
│ │
|
||||
┌──────┴──────┐ ┌────────┴──┐
|
||||
│ FastAPI │ │ SQLite │
|
||||
│ (Web + API │ │ (数据库) │
|
||||
│ + 静态托管 │ │ │
|
||||
│ + 后台任务) │ │ │
|
||||
└──────┬──────┘ └───────────┘
|
||||
│
|
||||
┌───────────┼───────────┐
|
||||
│ │ │
|
||||
┌─────┴─────┐ ┌──┴───┐ ┌────┴────┐
|
||||
│ 预处理器 │ │ LLM │ │ DOCX │
|
||||
│ + 提取器 │ │ 服务 │ │ 生成器 │
|
||||
└───────────┘ └──────┘ └─────────┘
|
||||
```
|
||||
|
||||
**极简设计**:
|
||||
- ❌ 无 Redis(进度追踪用内存)
|
||||
- ❌ 无 Celery(后台任务用 asyncio)
|
||||
- ❌ 无 Nginx(FastAPI 托管静态文件)
|
||||
- ✅ **单容器部署**,一键启动
|
||||
|
||||
### 1.3 项目目录结构
|
||||
|
||||
```
|
||||
story-extractor/
|
||||
├── docker-compose.yml
|
||||
├── Dockerfile
|
||||
├── requirements.txt
|
||||
├── .env.example
|
||||
│
|
||||
├── backend/
|
||||
│ ├── app/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── main.py # FastAPI 入口,路由注册,SSE
|
||||
│ │ ├── config.py # 配置管理(读取 .env)
|
||||
│ │ ├── database.py # SQLite 连接与 ORM
|
||||
│ │ ├── state.py # 内存状态管理(进度追踪)
|
||||
│ │ │
|
||||
│ │ ├── api/ # API 路由层
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── files.py # 文件上传/删除/列表
|
||||
│ │ │ ├── extraction.py # 提取任务管理
|
||||
│ │ │ ├── stories.py # 故事 CRUD(含删除)
|
||||
│ │ │ ├── matching.py # 匹配关系管理
|
||||
│ │ │ ├── export.py # 预览与导出
|
||||
│ │ │ └── settings.py # 系统配置
|
||||
│ │ │
|
||||
│ │ ├── models/ # 数据模型(SQLAlchemy)
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── transcript.py
|
||||
│ │ │ ├── person.py
|
||||
│ │ │ ├── story.py
|
||||
│ │ │ └── config.py
|
||||
│ │ │
|
||||
│ │ ├── schemas/ # Pydantic 请求/响应模型
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── file.py
|
||||
│ │ │ ├── story.py
|
||||
│ │ │ ├── match.py
|
||||
│ │ │ └── config.py
|
||||
│ │ │
|
||||
│ │ └── services/ # 业务逻辑层
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── file_service.py # 文件存储与管理
|
||||
│ │ ├── docx_parser.py # DOCX 解析
|
||||
│ │ ├── preprocessor.py # 预处理(过滤非故事内容)
|
||||
│ │ ├── extractor.py # 故事提取核心(三阶段 Pipeline)
|
||||
│ │ ├── llm_client.py # LLM 抽象层
|
||||
│ │ ├── match_service.py # 匹配关系管理
|
||||
│ │ └── docx_generator.py # 最终 DOCX 生成
|
||||
│ │
|
||||
│ ├── uploads/ # 上传文件存储
|
||||
│ │ ├── transcripts/
|
||||
│ │ └── persons/
|
||||
│ │
|
||||
│ └── exports/ # 导出文件存储
|
||||
│
|
||||
├── frontend/ # Vue 3 前端项目
|
||||
│ ├── src/
|
||||
│ │ ├── views/
|
||||
│ │ │ ├── WelcomeView.vue
|
||||
│ │ │ ├── UploadView.vue
|
||||
│ │ │ ├── ExtractView.vue
|
||||
│ │ │ ├── MatchView.vue
|
||||
│ │ │ └── ExportView.vue
|
||||
│ │ ├── components/
|
||||
│ │ │ ├── Stepper.vue
|
||||
│ │ │ ├── BottomNav.vue
|
||||
│ │ │ ├── UploadZone.vue
|
||||
│ │ │ ├── StoryCard.vue
|
||||
│ │ │ ├── PersonCard.vue
|
||||
│ │ │ ├── MatchPanel.vue
|
||||
│ │ │ ├── DocPreview.vue
|
||||
│ │ │ ├── SettingsModal.vue
|
||||
│ │ │ └── Toast.vue
|
||||
│ │ ├── api/
|
||||
│ │ │ └── index.js
|
||||
│ │ ├── stores/
|
||||
│ │ │ └── app.js
|
||||
│ │ ├── App.vue
|
||||
│ │ └── main.js
|
||||
│ ├── package.json
|
||||
│ └── vite.config.js
|
||||
│
|
||||
├── Dockerfile
|
||||
├── docker-compose.yml
|
||||
├── requirements.txt
|
||||
└── .env.example
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、API 设计
|
||||
|
||||
### 2.1 API 总览
|
||||
|
||||
所有 API 以 `/api/v1` 为前缀。
|
||||
|
||||
| 模块 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| **文件** | POST | `/api/v1/files/transcripts` | 上传文字稿(支持批量) |
|
||||
| | POST | `/api/v1/files/persons` | 上传讲述者信息(支持批量) |
|
||||
| | GET | `/api/v1/files/transcripts` | 获取文字稿列表 |
|
||||
| | GET | `/api/v1/files/persons` | 获取讲述者列表 |
|
||||
| | DELETE | `/api/v1/files/transcripts/{id}` | 删除文字稿 |
|
||||
| | DELETE | `/api/v1/files/persons/{id}` | 删除讲述者信息 |
|
||||
| | GET | `/api/v1/files/persons/{id}/preview` | 预览讲述者信息 |
|
||||
| **提取** | POST | `/api/v1/extraction/start` | 启动提取任务(后台异步) |
|
||||
| | GET | `/api/v1/extraction/status` | 获取提取进度 |
|
||||
| | GET | `/api/v1/extraction/status/stream` | SSE 实时进度推送 |
|
||||
| | POST | `/api/v1/extraction/stop` | 停止提取任务 |
|
||||
| **故事** | GET | `/api/v1/stories` | 获取故事列表(支持筛选) |
|
||||
| | GET | `/api/v1/stories/{id}` | 获取故事详情 |
|
||||
| | PUT | `/api/v1/stories/{id}` | 编辑故事 |
|
||||
| | DELETE | `/api/v1/stories/{id}` | 删除故事 |
|
||||
| **匹配** | POST | `/api/v1/matches` | 创建匹配关系 |
|
||||
| | DELETE | `/api/v1/matches/{story_id}` | 取消匹配 |
|
||||
| | GET | `/api/v1/matches` | 获取所有匹配关系 |
|
||||
| **导出** | GET | `/api/v1/export/preview/{person_id}` | 预览文档 |
|
||||
| | GET | `/api/v1/export/download/{person_id}` | 下载单个 DOCX |
|
||||
| | GET | `/api/v1/export/download-all` | 批量下载 ZIP |
|
||||
| | GET | `/api/v1/export/list` | 获取导出列表 |
|
||||
| **设置** | GET | `/api/v1/settings` | 获取系统配置 |
|
||||
| | PUT | `/api/v1/settings` | 更新系统配置 |
|
||||
| | POST | `/api/v1/settings/test-llm` | 测试 LLM 连接 |
|
||||
|
||||
### 2.2 关键 API 详细设计
|
||||
|
||||
#### 启动提取任务
|
||||
|
||||
```
|
||||
POST /api/v1/extraction/start
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"task_id": "uuid",
|
||||
"status": "started",
|
||||
"total_files": 10
|
||||
}
|
||||
```
|
||||
|
||||
#### SSE 实时进度推送
|
||||
|
||||
```
|
||||
GET /api/v1/extraction/status/stream
|
||||
```
|
||||
|
||||
**实现方式**:FastAPI `StreamingResponse` + `asyncio.Queue`
|
||||
|
||||
```python
|
||||
@router.get("/extraction/status/stream")
|
||||
async def stream_progress():
|
||||
async def event_generator():
|
||||
while True:
|
||||
data = await extraction_state.get_update()
|
||||
yield f"event: progress\ndata: {json.dumps(data)}\n\n"
|
||||
if data.get("status") == "completed":
|
||||
break
|
||||
return StreamingResponse(event_generator(), media_type="text/event-stream")
|
||||
```
|
||||
|
||||
**SSE 事件格式**:
|
||||
```
|
||||
event: progress
|
||||
data: {
|
||||
"percent": 35,
|
||||
"current_file": "2026年1月21日 第10营训练营.docx",
|
||||
"action": "故事识别:检测学员发言...",
|
||||
"files_read": 3,
|
||||
"files_total": 10,
|
||||
"segments_found": 42,
|
||||
"stories_found": 2
|
||||
}
|
||||
|
||||
event: story_found
|
||||
data: {
|
||||
"id": "uuid",
|
||||
"title": "代际传承的体罚创伤",
|
||||
"speaker_nickname": "14班+HCM",
|
||||
"summary": "从小被妈妈体罚,长大后用同样方式对待女儿...",
|
||||
"confidence": 0.85,
|
||||
"source": "2026年1月21日 第10营训练营.docx"
|
||||
}
|
||||
|
||||
event: completed
|
||||
data: {
|
||||
"total_files": 10,
|
||||
"total_stories": 15,
|
||||
"duration_seconds": 186
|
||||
}
|
||||
```
|
||||
|
||||
#### 停止提取任务
|
||||
|
||||
```
|
||||
POST /api/v1/extraction/stop
|
||||
```
|
||||
|
||||
**说明**:通过 `asyncio.Event` 取消后台任务。
|
||||
|
||||
---
|
||||
|
||||
## 三、核心服务设计
|
||||
|
||||
### 3.1 内存状态管理(state.py)
|
||||
|
||||
**替代 Redis 的方案**:用 Python 模块级变量 + `asyncio.Queue` 管理进度。
|
||||
|
||||
```python
|
||||
class ExtractionState:
|
||||
"""提取任务状态管理(内存中,无需 Redis)"""
|
||||
|
||||
def __init__(self):
|
||||
self.is_running: bool = False
|
||||
self.cancel_event: asyncio.Event = asyncio.Event()
|
||||
self.progress: dict = {
|
||||
"percent": 0,
|
||||
"current_file": "",
|
||||
"action": "",
|
||||
"files_read": 0,
|
||||
"files_total": 0,
|
||||
"segments_found": 0,
|
||||
"stories_found": 0,
|
||||
}
|
||||
self.queue: asyncio.Queue = asyncio.Queue()
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""更新进度,同时推送到 SSE 队列"""
|
||||
self.progress.update(kwargs)
|
||||
self.queue.put_nowait(self.progress.copy())
|
||||
|
||||
async def get_update(self) -> dict:
|
||||
"""SSE 消费端:等待新的进度更新"""
|
||||
return await self.queue.get()
|
||||
|
||||
def reset(self):
|
||||
"""重置状态"""
|
||||
self.is_running = False
|
||||
self.cancel_event.clear()
|
||||
self.progress = {k: 0 if k != "" else "" for k in self.progress}
|
||||
self.queue = asyncio.Queue()
|
||||
|
||||
# 全局单例
|
||||
extraction_state = ExtractionState()
|
||||
```
|
||||
|
||||
**局限性**:
|
||||
- 服务重启后进度丢失(可接受,重新提取即可)
|
||||
- 单进程内有效(FastAPI 单进程部署,无问题)
|
||||
|
||||
### 3.2 后台任务管理
|
||||
|
||||
**替代 Celery 的方案**:FastAPI `asyncio.create_task()`
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from app.services.extractor import run_extraction
|
||||
|
||||
@router.post("/extraction/start")
|
||||
async def start_extraction():
|
||||
if extraction_state.is_running:
|
||||
return {"error": "提取任务正在进行中"}
|
||||
extraction_state.reset()
|
||||
extraction_state.is_running = True
|
||||
asyncio.create_task(run_extraction(extraction_state))
|
||||
return {"task_id": str(uuid4()), "status": "started"}
|
||||
|
||||
@router.post("/extraction/stop")
|
||||
async def stop_extraction():
|
||||
extraction_state.cancel_event.set()
|
||||
return {"status": "stopping"}
|
||||
```
|
||||
|
||||
```python
|
||||
# extractor.py
|
||||
async def run_extraction(state: ExtractionState):
|
||||
"""后台提取任务主循环"""
|
||||
try:
|
||||
transcripts = get_all_transcripts()
|
||||
state.update(files_total=len(transcripts))
|
||||
|
||||
for i, transcript in enumerate(transcripts):
|
||||
if state.cancel_event.is_set():
|
||||
break
|
||||
|
||||
state.update(
|
||||
current_file=transcript.filename,
|
||||
action="预处理:过滤课堂管理内容...",
|
||||
files_read=i + 1,
|
||||
)
|
||||
|
||||
paragraphs = parse_transcript(transcript.file_path)
|
||||
filtered = preprocess(paragraphs)
|
||||
segments = await segment_text(filtered, state)
|
||||
stories = await identify_and_extract(segments, state)
|
||||
|
||||
save_stories(stories, transcript.id)
|
||||
|
||||
state.update(
|
||||
percent=round((i + 1) / len(transcripts) * 100),
|
||||
action=f"已完成:{transcript.filename}",
|
||||
)
|
||||
|
||||
state.update(status="completed", percent=100)
|
||||
except Exception as e:
|
||||
state.update(status="error", action=f"错误:{str(e)}")
|
||||
finally:
|
||||
state.is_running = False
|
||||
```
|
||||
|
||||
### 3.3 文字稿解析器(docx_parser.py)
|
||||
|
||||
```python
|
||||
def parse_transcript(file_path: str) -> TranscriptData:
|
||||
"""
|
||||
解析直播回放文字稿 DOCX
|
||||
格式:发言者(时间戳): 内容
|
||||
|
||||
返回:
|
||||
- paragraphs: [{index, speaker, timestamp, content, raw_text}]
|
||||
- total_lines: 总行数
|
||||
"""
|
||||
```
|
||||
|
||||
**关键处理**:
|
||||
- 解析每段的发言者、时间戳、内容
|
||||
- 识别发言者类型:老师(卢慧老师、静静、督导)vs 学员
|
||||
- 建立行号映射关系
|
||||
|
||||
### 3.4 预处理器(preprocessor.py)
|
||||
|
||||
```python
|
||||
class Preprocessor:
|
||||
"""预处理:过滤非故事内容"""
|
||||
|
||||
def filter_classroom_management(self, paragraphs: list) -> list:
|
||||
"""过滤课堂管理内容(改昵称、纪律提醒、进组讨论)"""
|
||||
|
||||
def filter_short_responses(self, paragraphs: list) -> list:
|
||||
"""过滤学员短回应("好的""收到"等,长度<10字)"""
|
||||
|
||||
def filter_healing_exercises(self, paragraphs: list) -> list:
|
||||
"""过滤疗愈练习重复内容(连续"我愿意看见且释放"模式)"""
|
||||
|
||||
def extract_student_segments(self, paragraphs: list) -> list:
|
||||
"""提取学员发言段落,保留上下文"""
|
||||
```
|
||||
|
||||
**过滤规则**:
|
||||
- 课堂管理关键词:昵称、纪律、进组、会议室、扣三个一
|
||||
- 短回应:长度 < 10 字,且不含情感词
|
||||
- 疗愈练习:连续 3 段以上匹配"我愿意看见且释放"模式
|
||||
|
||||
### 3.5 故事提取器(extractor.py)
|
||||
|
||||
**核心算法:三阶段 Pipeline**
|
||||
|
||||
```
|
||||
阶段1: 预处理 → 阶段2: 故事识别 → 阶段3: 故事提取
|
||||
```
|
||||
|
||||
#### 阶段一:预处理(规则过滤)
|
||||
|
||||
```python
|
||||
def preprocess(self, paragraphs: list) -> list:
|
||||
"""
|
||||
1. 过滤课堂管理内容
|
||||
2. 过滤短回应
|
||||
3. 过滤疗愈练习重复
|
||||
4. 提取学员发言段落
|
||||
"""
|
||||
```
|
||||
|
||||
#### 阶段二:故事识别(LLM)
|
||||
|
||||
**分块策略**:
|
||||
- 按 300-500 行分块,块间重叠 30 行
|
||||
- 每块送入 LLM 判断是否包含学员故事
|
||||
|
||||
**Prompt 模板**:
|
||||
```
|
||||
你是一个文本分析助手。以下是某心理成长训练营直播的文字稿片段。
|
||||
|
||||
每段格式为:发言者(时间戳): 内容
|
||||
|
||||
请判断该片段是否包含"学员个人故事"。
|
||||
|
||||
【学员个人故事】的定义:
|
||||
1. 学员(非老师)讲述的自己或家人的真实经历
|
||||
2. 涉及具体的事件、人物、时间线
|
||||
3. 有情感色彩(痛苦、挣扎、觉醒等)
|
||||
4. 主题通常围绕:原生家庭、亲子关系、婚姻情感、财富限制、身体健康、职业发展
|
||||
|
||||
【不属于学员故事】的内容:
|
||||
1. 老师的理论讲解和方法论
|
||||
2. 课堂管理(改昵称、纪律提醒)
|
||||
3. 疗愈练习引导语("我愿意看见且释放...")
|
||||
4. 学员的简短回应("好的""明白了")
|
||||
5. 老师引用的案例(非现场学员讲述)
|
||||
|
||||
请返回 JSON:
|
||||
{
|
||||
"has_story": true/false,
|
||||
"story_speaker": "学员昵称(如14班+HCM)",
|
||||
"story_topic": "一句话概括主题(15字以内)",
|
||||
"story_start_line": 起始行号,
|
||||
"story_end_line": 结束行号,
|
||||
"confidence": 0.0-1.0
|
||||
}
|
||||
```
|
||||
|
||||
#### 阶段三:故事提取(LLM)
|
||||
|
||||
**Prompt 模板**:
|
||||
```
|
||||
以下是某学员在训练营中讲述的个人故事(对话片段)。
|
||||
|
||||
请完成以下任务:
|
||||
1. 整理出完整的故事叙述(去掉老师的提问和引导语,只保留学员的讲述)
|
||||
2. 为故事起一个简洁的标题(10字以内)
|
||||
3. 写一段故事摘要(80-150字,第三人称)
|
||||
4. 识别故事的核心主题标签
|
||||
|
||||
【重要】只提取学员讲述的内容,过滤掉:
|
||||
- 老师的提问、引导、追问
|
||||
- 疗愈练习引导语
|
||||
- 其他学员的发言
|
||||
|
||||
【输出格式】JSON:
|
||||
{
|
||||
"title": "故事标题",
|
||||
"summary": "故事摘要",
|
||||
"content": "整理后的完整故事叙述(第一人称,仅学员讲述部分)",
|
||||
"themes": ["原生家庭", "体罚", "代际传承"],
|
||||
"emotion_tone": "痛苦/挣扎/觉醒/释然/..."
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 LLM 客户端(llm_client.py)
|
||||
|
||||
```python
|
||||
class LLMClient:
|
||||
"""LLM 统一调用接口"""
|
||||
|
||||
def __init__(self, config: LLMConfig):
|
||||
self.client = AsyncOpenAI(
|
||||
api_key=config.api_key,
|
||||
base_url=config.base_url,
|
||||
)
|
||||
self.model = config.model
|
||||
|
||||
async def chat(self, messages: list[dict], temperature: float = 0.3) -> str:
|
||||
"""发送对话请求"""
|
||||
|
||||
async def chat_json(self, messages: list[dict], temperature: float = 0.3) -> dict:
|
||||
"""发送对话请求,期望返回 JSON"""
|
||||
```
|
||||
|
||||
**支持的提供商**(通过 base_url 切换):
|
||||
|
||||
| 提供商 | Base URL |
|
||||
|--------|----------|
|
||||
| OpenAI | `https://api.openai.com/v1` |
|
||||
| DeepSeek | `https://api.deepseek.com/v1` |
|
||||
| 通义千问 | `https://dashscope.aliyuncs.com/compatible-mode/v1` |
|
||||
| Ollama (本地) | `http://localhost:11434/v1` |
|
||||
|
||||
### 3.7 匹配服务(match_service.py)
|
||||
|
||||
```python
|
||||
class MatchService:
|
||||
"""匹配关系管理"""
|
||||
|
||||
def create_match(self, story_id: str, person_id: str) -> Match:
|
||||
"""创建匹配关系"""
|
||||
|
||||
def remove_match(self, story_id: str) -> bool:
|
||||
"""取消匹配"""
|
||||
|
||||
def get_matched_persons(self) -> list[Person]:
|
||||
"""获取已匹配的讲述者列表(用于导出)"""
|
||||
```
|
||||
|
||||
### 3.8 DOCX 生成器(docx_generator.py)
|
||||
|
||||
```python
|
||||
def generate_person_doc(person: Person, stories: list[Story], output_path: str):
|
||||
"""
|
||||
为一位讲述者生成最终 DOCX 文档
|
||||
|
||||
结构:
|
||||
┌─ 照片(居中)
|
||||
├─ 个人信息(姓名、性别、年龄、城市、职业)
|
||||
├─ 分隔线
|
||||
└─ 故事正文(仅学员讲述内容,多个故事依次排列)
|
||||
"""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、错误处理
|
||||
|
||||
| 错误类型 | 处理方式 |
|
||||
|----------|----------|
|
||||
| 单个文字稿解析失败 | 记录错误,跳过,继续处理其他文件 |
|
||||
| LLM API 调用失败 | 自动重试 3 次(指数退避),仍失败则标记该段为 error |
|
||||
| LLM 返回格式异常 | 尝试修复 JSON,无法修复则丢弃该段结果 |
|
||||
| DOCX 文件损坏 | 提示用户文件异常,跳过 |
|
||||
| 提取任务中断(服务重启) | 进度丢失,用户需重新启动提取 |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据库设计
|
||||
|
||||
### 5.1 ER 关系
|
||||
|
||||
```
|
||||
Transcript 1───* Story *───0..1 Person
|
||||
|
||||
Config (单例)
|
||||
```
|
||||
|
||||
### 5.2 表结构
|
||||
|
||||
#### transcripts 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE transcripts (
|
||||
id TEXT PRIMARY KEY,
|
||||
filename TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
line_count INTEGER DEFAULT 0,
|
||||
file_size INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'pending',
|
||||
error_message TEXT,
|
||||
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
#### persons 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE persons (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
nickname TEXT,
|
||||
filename TEXT NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
photo_path TEXT,
|
||||
info TEXT,
|
||||
uploaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
#### stories 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE stories (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
summary TEXT,
|
||||
content TEXT,
|
||||
speaker_nickname TEXT,
|
||||
source_transcript_id TEXT NOT NULL,
|
||||
source_lines TEXT,
|
||||
duration_minutes REAL,
|
||||
confidence REAL,
|
||||
confidence_level TEXT,
|
||||
person_id TEXT,
|
||||
match_status TEXT DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (source_transcript_id) REFERENCES transcripts(id),
|
||||
FOREIGN KEY (person_id) REFERENCES persons(id)
|
||||
);
|
||||
```
|
||||
|
||||
#### config 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、部署方案
|
||||
|
||||
### 6.1 Docker Compose(单容器)
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
services:
|
||||
app:
|
||||
build: .
|
||||
ports:
|
||||
- "80:8000"
|
||||
volumes:
|
||||
- ./data/uploads:/app/uploads
|
||||
- ./data/exports:/app/exports
|
||||
- ./data/db:/app/data
|
||||
env_file: .env
|
||||
```
|
||||
|
||||
### 6.2 FastAPI 托管静态文件
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# API 路由(先注册)
|
||||
app.include_router(files_router, prefix="/api/v1")
|
||||
app.include_router(extraction_router, prefix="/api/v1")
|
||||
app.include_router(stories_router, prefix="/api/v1")
|
||||
app.include_router(matching_router, prefix="/api/v1")
|
||||
app.include_router(export_router, prefix="/api/v1")
|
||||
app.include_router(settings_router, prefix="/api/v1")
|
||||
|
||||
# 托管前端静态文件(放在最后,作为 fallback)
|
||||
app.mount("/", StaticFiles(directory="frontend/dist", html=True), name="static")
|
||||
```
|
||||
|
||||
### 6.3 环境变量(.env)
|
||||
|
||||
```env
|
||||
# LLM 配置
|
||||
LLM_PROVIDER=openai
|
||||
LLM_MODEL=gpt-4
|
||||
LLM_BASE_URL=https://api.openai.com/v1
|
||||
LLM_API_KEY=sk-xxx
|
||||
|
||||
# 提取参数
|
||||
SEGMENT_MAX_LINES=500
|
||||
STORY_MIN_LINES=50
|
||||
CONFIDENCE_THRESHOLD=0.5
|
||||
TEMPERATURE=0.3
|
||||
|
||||
# 应用
|
||||
APP_HOST=0.0.0.0
|
||||
APP_PORT=8000
|
||||
UPLOAD_DIR=/app/uploads
|
||||
EXPORT_DIR=/app/exports
|
||||
```
|
||||
|
||||
### 6.4 启动流程
|
||||
|
||||
```bash
|
||||
# 1. 配置环境变量
|
||||
cp .env.example .env
|
||||
vim .env
|
||||
|
||||
# 2. 构建前端
|
||||
cd frontend && npm install && npm run build && cd ..
|
||||
|
||||
# 3. 一键启动(单容器)
|
||||
docker-compose up -d
|
||||
|
||||
# 4. 访问
|
||||
# http://<局域网IP>
|
||||
```
|
||||
|
||||
### 6.5 依赖清单(requirements.txt)
|
||||
|
||||
```
|
||||
fastapi>=0.110.0
|
||||
uvicorn>=0.27.0
|
||||
python-multipart>=0.0.9
|
||||
sqlalchemy>=2.0
|
||||
aiosqlite>=0.19.0
|
||||
python-docx>=1.1.0
|
||||
openai>=1.12.0
|
||||
pydantic>=2.0
|
||||
```
|
||||
|
||||
> 仅 8 个依赖,无 Redis、无 Celery。
|
||||
|
||||
---
|
||||
|
||||
## 七、扩展性设计
|
||||
|
||||
### 7.1 LLM 升级
|
||||
|
||||
- 所有 LLM 调用通过 `LLMClient` 抽象层
|
||||
- 切换模型只需修改配置(base_url + model + api_key)
|
||||
|
||||
### 7.2 文章搜索引擎(预留)
|
||||
|
||||
- stories 表已存储完整内容 + 结构化元数据
|
||||
- 后续可接入 SQLite FTS5 全文搜索或 Meilisearch
|
||||
|
||||
### 7.3 多用户支持(预留)
|
||||
|
||||
- 当前为单用户模式(局域网共享)
|
||||
- 后续可加入用户认证(JWT)
|
||||
|
||||
### 7.4 从极简架构升级
|
||||
|
||||
如果后续需要更强的任务管理能力,可以平滑升级:
|
||||
- `ExtractionState` → Redis
|
||||
- `asyncio.create_task()` → Celery
|
||||
- 架构设计已预留接口,业务逻辑层不需要改动
|
||||
295
docs/extraction-design.md
Normal file
295
docs/extraction-design.md
Normal file
@@ -0,0 +1,295 @@
|
||||
# 故事提取工具 - 提取逻辑设计文档 v4
|
||||
|
||||
## 一、目标
|
||||
|
||||
**输入**:录像转文字稿(回溯疗愈课堂的对话记录,一个或多个 DOCX 文件)
|
||||
|
||||
**输出**:每个学员一个结构化故事,包含 5 个部分:
|
||||
1. 讲述者简介
|
||||
2. 当下的困境
|
||||
3. 过往的故事
|
||||
4. 转变
|
||||
5. 原文金句
|
||||
|
||||
**核心原则**:把案主回溯的内容和面临的议题提取整理成第三人称故事。
|
||||
|
||||
---
|
||||
|
||||
## 二、整体流程
|
||||
|
||||
```
|
||||
多个文档 → 按顺序逐个处理 → 每个文档独立输出故事 → 汇总展示
|
||||
```
|
||||
|
||||
- 多个文档按顺序处理,一个文档处理完再处理下一个(避免 API 限流)
|
||||
- 每个文档内部:代码预处理 → 并行判断/提取 → 并行切片提取 → 重组 → 并行生成故事
|
||||
- **核心优化**:所有 LLM 调用均使用 `asyncio.gather` 并行执行,总耗时 ≈ 最慢的单次调用耗时,而非所有调用耗时之和
|
||||
|
||||
---
|
||||
|
||||
## 三、单文档处理流程(核心)
|
||||
|
||||
### 第一步:代码预处理(不用 LLM)
|
||||
|
||||
**目的**:把原始对话记录拆分成"以学员为单位"的段落,每个段落包含该学员从第一次发言到最后一次发言之间的**所有内容**。
|
||||
|
||||
**不做的事情**:
|
||||
- 不过滤课堂管理内容(实测过滤掉的行很少,且关键词硬编码不通用,不值得做)
|
||||
|
||||
**具体逻辑**:
|
||||
|
||||
#### 1.1 统计说话人
|
||||
|
||||
遍历所有行,统计每个说话人的:
|
||||
- 发言行数
|
||||
- 总字数
|
||||
- 第一次出现的行号(first)
|
||||
- 最后一次出现的行号(last)
|
||||
|
||||
#### 1.2 识别老师
|
||||
|
||||
- 发言行数最多的人就是老师
|
||||
- 不依赖硬编码名字,适用于任何课程
|
||||
|
||||
#### 1.3 过滤杂音
|
||||
|
||||
- 老师本身不参与拆分
|
||||
- 发言 < 20 句的说话人视为杂音,直接跳过(通常是主持人、助教、偶尔插话的人)
|
||||
|
||||
#### 1.4 按时间范围截取(核心逻辑)
|
||||
|
||||
对每个有效学员,从该学员**第一次发言的行**到**最后一次发言的行**,中间**所有行全部截取**,形成一个完整段落。
|
||||
|
||||
**关键特性:不同学员的段落之间允许重叠。**
|
||||
|
||||
举例说明:
|
||||
```
|
||||
原文行号: 131 132 ... 167 168 ... 321 322 ... 429 430 ... 599 600 ... 800
|
||||
说话人: 04 02 ... 05 02 ... 06 02 ... 04 02 ... 05 02 ... 07
|
||||
|
||||
说话人04段落:截取行[131-429],包含04、02(老师)、05、06的所有对话
|
||||
说话人05段落:截取行[167-599],包含05、02(老师)、06、04的部分对话
|
||||
说话人07段落:截取行[603-800],包含07、02(老师)的对话
|
||||
```
|
||||
|
||||
**为什么允许重叠?**
|
||||
- 课堂场景中,多个学员可能围绕同一个主题互动(比如说话人04和说话人05都在讨论高考漏题创伤)
|
||||
- 老师在不同学员之间的引导是连贯的,不应该被割裂
|
||||
- 重叠部分的老师对话对两个学员的故事都有价值,应该分别保留
|
||||
|
||||
#### 1.5 过滤
|
||||
|
||||
对截取后的段落进行两轮过滤:
|
||||
|
||||
**过滤条件 A:短段落**
|
||||
- 学员发言 < 15 句 → 丢弃(不太可能有故事)
|
||||
- 学员总字数 < 80 字 → 丢弃
|
||||
|
||||
**过滤条件 B:非主讲述人**
|
||||
- 当事人发言行数 / 段落总行数 < 10% → 丢弃
|
||||
- 含义:截取了很大一段范围,但里面绝大部分话不是该学员说的,说明该学员只是旁观者或偶尔插话,不是主讲述人
|
||||
- 举例:说话人03截取了 658 行,但其中只有 21 行是03说的(占比 3.2%),说明03在整个课堂期间只是偶尔插话,不是主讲述人
|
||||
|
||||
#### 1.6 输出
|
||||
|
||||
- 每个通过过滤的学员一个段落
|
||||
- 段落包含该学员时间范围内的所有对话(老师、其他学员、本人)
|
||||
- 段落按学员第一次出现的时间排序
|
||||
|
||||
---
|
||||
|
||||
### 第二步:判断故事 + 提取事实(并行执行)
|
||||
|
||||
**目的**:根据段落长度走不同路径,短段落省一次 LLM 调用。**所有段落并行处理,互不阻塞。**
|
||||
|
||||
**并行策略**:使用 `asyncio.gather` 同时处理所有段落,总耗时 ≈ 最慢那个段落的处理时间。
|
||||
|
||||
**短段落(≤ 4000 字)**:
|
||||
- 跳过"判断故事"步骤,直接让 LLM 提取事实
|
||||
- 理由:短段落内容少,提取失败的成本低,省一次判断调用更划算
|
||||
|
||||
**长段落(> 4000 字)**:
|
||||
- 先让 LLM 判断"是否包含个人故事"
|
||||
- 输出:`{is_story: true/false, confidence: 0.0~1.0, story_hints: "一句话描述"}`
|
||||
- confidence < 0.5 或 is_story = false → 跳过,不浪费后续切片提取的调用
|
||||
- 有故事 → 进入第三步切片提取
|
||||
|
||||
---
|
||||
|
||||
### 第三步:切片提取事实(Map 阶段,仅长段落执行)
|
||||
|
||||
**目的**:对确认有故事的长段落,切成小块并行提取关键信息,避免输出截断。
|
||||
|
||||
**为什么拆成"提取事实"和"生成故事"两步?**
|
||||
- 直接让 LLM 从长文本写故事 → 输出太长,必然截断
|
||||
- 先提取事实(输出短,不会截断)→ 再基于短素材写故事(输入短,输出可控)
|
||||
|
||||
**切片策略**:
|
||||
- 以自然段为最小单位,累加到约 3000 字时,在最近的段落结尾处切断
|
||||
- 块之间重叠 **5 个自然段**(避免信息断裂)
|
||||
- 短段落(≤ 4000 字)在第二步已直接提取,不会进入此步
|
||||
|
||||
**每块的提取任务**(并行执行):
|
||||
- 输入:一块对话文本 + 该学员的故事主题(来自第二步的 hints)
|
||||
- 输出:结构化事实
|
||||
```json
|
||||
{
|
||||
"events": ["事件1(尽量详细)", "事件2"],
|
||||
"emotions": ["情感1", "情感2"],
|
||||
"quotes": ["讲述者原话1", "讲述者原话2", "讲述者原话3"],
|
||||
"key_people": ["相关人物1", "相关人物2"]
|
||||
}
|
||||
```
|
||||
- **关键约束**:每块只提取事实,不做写作。尽量详细、尽量多提取,保留具体细节
|
||||
|
||||
---
|
||||
|
||||
### 第四步:重组(Reduce 阶段,纯代码)
|
||||
|
||||
**目的**:把多个切片的提取结果合并成完整的素材。
|
||||
|
||||
- 输入:同一学员所有切片的提取结果
|
||||
- 处理:
|
||||
- 事件:按原文出现顺序排列,去除重复
|
||||
- 情感:去重
|
||||
- 原话:去重(完全相同的去掉),不限制数量,不限制长度
|
||||
- 人物:去重
|
||||
- 输出:完整素材
|
||||
```json
|
||||
{
|
||||
"events": ["按时间排序的事件列表"],
|
||||
"emotions": ["去重后的情感列表"],
|
||||
"quotes": ["去重后的原话列表(全部保留)"],
|
||||
"key_people": ["去重后的相关人物列表"],
|
||||
"story_hints": "故事主题"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 第五步:生成最终故事(并行执行)
|
||||
|
||||
**目的**:基于重组后的素材,生成结构化的第三人称故事。**所有故事并行生成,互不阻塞。**
|
||||
|
||||
**并行策略**:使用 `asyncio.gather` 同时生成所有故事,总耗时 ≈ 最慢那个故事的生成时间。
|
||||
|
||||
- 输入:重组后的完整素材(短文本,几百字)
|
||||
- 输出:最终故事 JSON
|
||||
```json
|
||||
{
|
||||
"title": "标题",
|
||||
"summary": "80字摘要",
|
||||
"tags": ["标签1", "标签2"],
|
||||
"speaker_nickname": "讲述者昵称",
|
||||
"content": {
|
||||
"讲述者": "讲述者介绍(不限制长度,充分展开)",
|
||||
"当下的困境": "当前面临的困境(不限制长度,充分展开)",
|
||||
"过往的故事": "过去的经历故事(不限制长度,充分展开)",
|
||||
"转变": "疗愈过程中的转变(不限制长度,充分展开)",
|
||||
"原文金句": ["原话1", "原话2", "原话3"]
|
||||
}
|
||||
}
|
||||
```
|
||||
- **关键**:LLM 的输入是"已提取好的素材"(几百字),不是原始长文本
|
||||
- LLM 的任务是"基于素材写作",不需要再从原文中提取信息
|
||||
- **不限制输出长度**,要求素材中的具体经历、事件细节、情感变化尽量保留,让故事丰满
|
||||
- **原文金句为数组格式**,避免字符串中的引号导致 JSON 解析失败
|
||||
|
||||
---
|
||||
|
||||
## 四、数据流图
|
||||
|
||||
```
|
||||
原始 DOCX 文件
|
||||
│
|
||||
▼
|
||||
[代码预处理]
|
||||
│ 1. 统计说话人(行数、字数、首末行号)
|
||||
│ 2. 识别老师(发言最多的人)
|
||||
│ 3. 过滤杂音(< 20句)
|
||||
│ 4. 按时间范围截取(第一句到最后一句,允许重叠)
|
||||
│ 5. 过滤短段落(<15句 / <80字)
|
||||
│ 6. 过滤非主讲述人(当事人占比 < 10%)
|
||||
▼
|
||||
学员段落列表: [学员A段落, 学员B段落, 学员C段落, ...]
|
||||
│
|
||||
▼
|
||||
[第二步:并行判断 + 提取] ── asyncio.gather 并行处理所有段落
|
||||
│ ├─ 短段落(≤4000字):直接提取事实(省1次调用)
|
||||
│ └─ 长段落(>4000字):先判断是否有故事 → 有故事则进入第三步
|
||||
│ ⏱ 总耗时 ≈ 最慢那个段落的处理时间
|
||||
│
|
||||
▼ (仅长段落且有故事)
|
||||
[第三步:并行切片提取] ── asyncio.gather 并行处理所有切片
|
||||
│ 按3000字切块,以自然段为单位,重叠5个自然段
|
||||
│ ⏱ 总耗时 ≈ 最慢那个切片的提取时间
|
||||
│
|
||||
▼
|
||||
[第四步:代码重组] ── 去重、排序、合并(纯代码,无需 LLM)
|
||||
│
|
||||
▼
|
||||
[第五步:并行生成故事] ── asyncio.gather 并行生成所有故事
|
||||
│ 基于素材生成5部分结构化故事
|
||||
│ ⏱ 总耗时 ≈ 最慢那个故事的生成时间
|
||||
│
|
||||
▼
|
||||
最终故事列表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、泛化性设计
|
||||
|
||||
| 环节 | 设计 | 为什么通用 |
|
||||
|------|------|-----------|
|
||||
| 识别老师 | 统计发言行数,最多的就是老师 | 不依赖硬编码名字 |
|
||||
| 过滤杂音 | 发言 < 20句直接跳过 | 适应各种偶尔插话的人 |
|
||||
| 按时间范围截取 | 从第一句到最后一句,中间所有内容 | 保留完整上下文,不丢失老师对话 |
|
||||
| 允许重叠 | 不同学员段落可共享内容 | 课堂中多学员讨论同一主题时,各自保留完整对话 |
|
||||
| 过滤非主讲述人 | 当事人占比 < 10% 丢弃 | 自动识别旁观者/插话者,只保留主讲述人 |
|
||||
| 切片 | 3000字一块,5个自然段重叠 | 适应任意长度 |
|
||||
|
||||
---
|
||||
|
||||
## 六、LLM 调用清单
|
||||
|
||||
| 步骤 | 调用次数 | 并行方式 | 输入大小 | 输出大小 | 用途 |
|
||||
|------|---------|---------|---------|---------|------|
|
||||
| 第二步:短段落提取 | 短段落各 1 次 | **并行**(asyncio.gather) | ≤4000 字 | JSON | 直接提取事实 |
|
||||
| 第二步:长段落判断 | 长段落各 1 次 | **并行**(asyncio.gather) | ≤4000 字 | ~100 字 JSON | 过滤非故事段落 |
|
||||
| 第三步:切片提取 | 每块 1 次 | **并行**(asyncio.gather) | ≤3000 字 | JSON | 提取事件/情感/原话 |
|
||||
| 第五步:生成故事 | 每个故事 1 次 | **并行**(asyncio.gather) | 素材 ≤1000 字 | JSON | 生成最终故事 |
|
||||
|
||||
**典型场景**:一个文档有 4 个学员段落(2短2长),其中 3 个有故事:
|
||||
- 第二步:2次短段落提取 + 2次长段落判断 = 4 次(**并行,耗时 ≈ 1次调用时间**)
|
||||
- 第三步:假设2个长段落都有故事,各切2块 = 4 次(**并行,耗时 ≈ 1次调用时间**)
|
||||
- 第五步:3 次(**并行,耗时 ≈ 1次调用时间**)
|
||||
- **总计:11 次 LLM 调用**
|
||||
|
||||
### 耗时对比
|
||||
|
||||
| 方案 | 第二步 | 第三步 | 第五步 | 总耗时估算 |
|
||||
|------|--------|--------|--------|-----------|
|
||||
| 串行(v3) | ~4次 × 30s = 120s | ~4次 × 30s = 120s | ~3次 × 60s = 180s | **~7 分钟** |
|
||||
| 并行(v4) | ~30s(并行) | ~30s(并行) | ~60s(并行) | **~2 分钟** |
|
||||
|
||||
> 注:以上为单次调用约 30-60s 的粗略估算,实际耗时取决于模型响应速度和网络延迟。并行化后总耗时从分钟级降至约 2-3 分钟。
|
||||
|
||||
---
|
||||
|
||||
## 七、变更记录
|
||||
|
||||
| 变更 | v2 | v3 | v4 | 原因 |
|
||||
|------|----|----|----|------|
|
||||
| 拆分方式 | 按学员拆分 + 同名合并 | 按时间范围截取(第一句到最后一句) | 不变 | v2 会丢失段落间的老师对话;v3 保留完整上下文 |
|
||||
| 段落互斥性 | 互斥(每个对话行只属于一个段落) | 允许重叠(同一行可出现在多个段落) | 不变 | 课堂中多学员讨论同一主题,老师对话对双方都有价值 |
|
||||
| 杂音过滤 | 无(依赖短段落过滤) | 发言 < 20句直接跳过 | 不变 | 提前排除主持人、助教等偶尔插话的人 |
|
||||
| 非主讲述人过滤 | 无 | 当事人占比 < 10% 丢弃 | 不变 | 自动识别旁观者,只保留真正有故事的学员 |
|
||||
| 判断故事 | 所有段落都先判断 | 短段落(≤4000字)跳过判断直接提取 | 不变 | 省一次 LLM 调用 |
|
||||
| 切片重叠 | 200字重叠 | 5个自然段重叠 | 不变 | 按自然段重叠更合理,不会在句子中间断开 |
|
||||
| 过滤管理行 | 不做 | 不做 | 不变 | 过滤掉的行很少,关键词不通用 |
|
||||
| 识别老师 | 统计发言行数 | 统计发言行数(不变) | 不变 | 通用性 |
|
||||
| 提取故事 | 先提取事实,再基于素材写故事 | 先提取事实,再基于素材写故事(不变) | 不变 | 避免输出截断 |
|
||||
| **LLM 调用方式** | 串行 | 串行 | **全并行(asyncio.gather)** | 总耗时从 ~7 分钟降至 ~2 分钟 |
|
||||
| **输出长度限制** | 有字数限制 | 有字数限制 | **不限制** | 故事更丰满,保留更多细节 |
|
||||
| **原文金句格式** | 字符串 | 字符串 | **数组** | 避免引号导致 JSON 解析失败 |
|
||||
167
docs/故事提取工具-方案设计.md
Normal file
167
docs/故事提取工具-方案设计.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# 直播回放文字稿故事提取工具 — 方案设计文档
|
||||
|
||||
## 一、项目概述
|
||||
|
||||
### 1.1 背景
|
||||
|
||||
手头有大量直播回放导出的文字稿(DOCX 格式),其中散落着一些个人生活故事(家长里短类),需要用 AI 自动提取整理,并与讲述者个人信息匹配,最终输出为独立的 DOCX 文档。
|
||||
|
||||
### 1.2 目标
|
||||
|
||||
构建一个 **Web 端可复用工具**,支持:
|
||||
- 批量上传和管理文字稿文件
|
||||
- AI 自动提取故事(过滤讲课/知识点内容)
|
||||
- 人工确认故事与讲述者的匹配关系
|
||||
- 为每个讲述者生成独立 DOCX(个人信息 + 照片 + 故事正文)
|
||||
|
||||
### 1.3 使用场景
|
||||
|
||||
- **部署环境:** 局域网
|
||||
- **用户规模:** 多人使用
|
||||
- **数据规模:** 20+ 份文字稿,每份上万行
|
||||
|
||||
---
|
||||
|
||||
## 二、需求分析
|
||||
|
||||
### 2.1 输入
|
||||
|
||||
| 输入类型 | 格式 | 说明 |
|
||||
|----------|------|------|
|
||||
| 直播回放文字稿 | DOCX | 每份上万行,口语化,混合讲课/知识点内容,无明确说话人标记 |
|
||||
| 讲述者个人信息 | DOCX(一人一个) | 包含姓名、照片等个人信息 |
|
||||
|
||||
### 2.2 处理要求
|
||||
|
||||
1. **故事提取:** 从文字稿中识别并提取个人生活故事(如家庭、情感、生活等),过滤掉纯讲课/知识点内容
|
||||
2. **长文本切分:** 上万行文字稿需智能切分,确保不截断完整故事(每个故事可能跨半小时到一小时,稀疏分布)
|
||||
3. **匹配确认:** AI 提取故事后,由人工在界面上确认每个故事对应的讲述者
|
||||
4. **文档生成:** 按简洁模板生成最终 DOCX
|
||||
|
||||
### 2.3 输出
|
||||
|
||||
每个讲述者一份 DOCX 文档,结构为:
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ 个人信息(含照片) │
|
||||
├─────────────────────────┤
|
||||
│ │
|
||||
│ 故事正文 │
|
||||
│ │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.4 扩展性要求
|
||||
|
||||
- LLM 可替换(支持升级/切换不同模型)
|
||||
- 后续可扩展文章搜索引擎等功能
|
||||
|
||||
---
|
||||
|
||||
## 三、系统架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ 浏览器前端 │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
||||
│ │ 文件管理 │ │ 故事提取 │ │ 匹配确认 & 导出 │ │
|
||||
│ └──────────┘ └──────────┘ └──────────────────┘ │
|
||||
└────────────────────┬────────────────────────────┘
|
||||
│ HTTP API
|
||||
┌────────────────────┴────────────────────────────┐
|
||||
│ Python 后端 (FastAPI) │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
|
||||
│ │ 文件处理 │ │ LLM 服务 │ │ DOCX 生成 │ │
|
||||
│ └──────────┘ └──────────┘ └──────────────────┘ │
|
||||
│ ┌──────────┐ ┌──────────┐ │
|
||||
│ │ 任务队列 │ │ 数据存储 │ │
|
||||
│ └──────────┘ └──────────┘ │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、用户交互流程
|
||||
|
||||
### Step 1:上传文件
|
||||
- 上传文字稿 DOCX(支持批量,显示上传列表)
|
||||
- 上传讲述者信息 DOCX(支持批量,显示上传列表 + 预览)
|
||||
|
||||
### Step 2:提取故事
|
||||
- 选择要处理的文字稿(可多选)
|
||||
- 点击"开始提取",显示进度
|
||||
- LLM 实时返回提取结果
|
||||
- 展示故事列表(摘要 + 原文定位)
|
||||
|
||||
### Step 3:匹配讲述者
|
||||
- 左侧:故事列表
|
||||
- 右侧:讲述者列表
|
||||
- 拖拽/下拉匹配(或 AI 推荐候选)
|
||||
- 确认匹配关系
|
||||
|
||||
### Step 4:预览 & 导出
|
||||
- 预览最终 DOCX 效果
|
||||
- 单个导出 / 批量导出
|
||||
- 下载 ZIP 包
|
||||
|
||||
---
|
||||
|
||||
## 五、技术栈
|
||||
|
||||
| 层级 | 技术 | 理由 |
|
||||
|------|------|------|
|
||||
| **前端** | Vue 3 + Element Plus | 组件丰富、中文生态好、拖拽匹配等交互容易实现 |
|
||||
| **后端** | FastAPI | 异步支持好、自带 API 文档、适合 AI 任务 |
|
||||
| **LLM** | 抽象接口层,先支持 OpenAI 兼容 API | 后续可无缝切换国产模型/本地模型 |
|
||||
| **DOCX 处理** | python-docx | 读写 DOCX 的标准库 |
|
||||
| **任务队列** | Celery + Redis(或内置 asyncio) | 20+ 文件处理需要异步,避免前端卡死 |
|
||||
| **数据存储** | SQLite | 轻量、无需额外安装、局域网够用 |
|
||||
| **部署** | Docker Compose | 一键启动,局域网分发方便 |
|
||||
|
||||
---
|
||||
|
||||
## 六、核心技术难点与解决方案
|
||||
|
||||
| 难点 | 解决方案 |
|
||||
|------|----------|
|
||||
| **长文本切分不截断故事** | 两阶段策略:先用 LLM 做"话题分段"(识别话题边界),再对每个话题段判断是否包含故事 |
|
||||
| **故事 vs 知识点区分** | Prompt 工程 + Few-shot 示例,明确"个人生活故事"的定义和排除规则 |
|
||||
| **无说话人标记** | 提取故事后展示摘要,由人工在 GUI 上匹配讲述者;后续可加入 AI 推荐候选 |
|
||||
| **上万行文本的 LLM 上下文** | 话题分段后,每个段落在上下文窗口内处理;超长段落再按语义子段切分 |
|
||||
|
||||
---
|
||||
|
||||
## 七、项目结构
|
||||
|
||||
```
|
||||
story-extractor/
|
||||
├── frontend/ # Vue 3 前端
|
||||
│ ├── src/
|
||||
│ │ ├── views/ # 页面:文件管理、故事提取、匹配确认、导出
|
||||
│ │ ├── components/ # 通用组件
|
||||
│ │ └── api/ # 后端 API 调用
|
||||
│ └── ...
|
||||
├── backend/ # FastAPI 后端
|
||||
│ ├── app/
|
||||
│ │ ├── api/ # API 路由
|
||||
│ │ ├── services/ # 业务逻辑
|
||||
│ │ │ ├── extractor.py # 故事提取核心
|
||||
│ │ │ ├── llm.py # LLM 抽象层
|
||||
│ │ │ ├── docx_parser.py # DOCX 解析
|
||||
│ │ │ └── docx_generator.py # DOCX 生成
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ └── main.py
|
||||
│ └── ...
|
||||
├── docker-compose.yml
|
||||
└── README.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、待确认事项
|
||||
|
||||
- [ ] LLM API 选型(OpenAI / 国产模型 / 本地模型)
|
||||
- [ ] 技术栈最终确认
|
||||
- [ ] 开发节奏(先跑通核心流程 vs 逐步完善)
|
||||
- [ ] 讲述者信息 DOCX 的具体字段结构(需看样例)
|
||||
- [ ] 文字稿样例(需看实际内容以优化切分和提取策略)
|
||||
133
docs/说话人04_对比分析报告.txt
Normal file
133
docs/说话人04_对比分析报告.txt
Normal file
@@ -0,0 +1,133 @@
|
||||
说话人04截取文件 vs 原文对比分析报告
|
||||
==========================================
|
||||
|
||||
一、数据汇总
|
||||
-----------
|
||||
|
||||
1. 原始 DOCX 文件总行数: 824行
|
||||
2. 说话人04在原文中总行数: 84行 (原文行131-429)
|
||||
3. 截取文件总行数: 167行
|
||||
- 说话人04: 84行
|
||||
- 说话人02(老师): 83行
|
||||
|
||||
4. 原文说话人04范围内(行131-429)的所有说话人:
|
||||
- 说话人02(老师): 147行
|
||||
- 说话人04: 84行
|
||||
- 说话人05: 54行
|
||||
- 说话人03: 8行
|
||||
- 说话人06: 6行
|
||||
- 总计: 299行
|
||||
|
||||
5. 截取文件中缺失的行: 121行
|
||||
- 老师行: 61行
|
||||
- 其他说话人行: 60行 (说话人05: 54行, 说话人03: 8行, 说话人06: 6行)
|
||||
- 说话人04行: 0行 (全部保留)
|
||||
|
||||
|
||||
二、说话人04的内容是否完整?
|
||||
-----------------------------
|
||||
|
||||
说话人04的84行内容在截取文件中 **全部保留**,没有任何丢失。
|
||||
merge 逻辑对学员本人的内容没有造成丢失。
|
||||
|
||||
|
||||
三、老师在说话人04发言期间的内容是否完整?
|
||||
-------------------------------------------
|
||||
|
||||
老师在说话人04范围内共有147行,截取文件中只保留了83行。
|
||||
**丢失了61行老师的对话。**
|
||||
|
||||
丢失的老师对话主要分布在说话人04的12个段落之间的"中间段落"中。
|
||||
这些中间段落是其他学员(说话人03、说话人05、说话人06)发言时,
|
||||
老师与他们的互动对话。
|
||||
|
||||
|
||||
四、merge 逻辑丢失内容的根本原因
|
||||
---------------------------------
|
||||
|
||||
_merge_by_speaker 的工作流程:
|
||||
1. _split_by_student 按学员拆分: 遇到不同学员就断开
|
||||
2. 每个段落只包含: 当前学员的行 + 老师的行
|
||||
3. _merge_by_speaker 把同一个学员的多段 extend 合并
|
||||
|
||||
关键问题:
|
||||
当说话人04的段落A和段落B之间夹了其他学员(如说话人05)的段落时:
|
||||
- 说话人05的段落中包含老师与说话人05的互动对话
|
||||
- 这些老师对话被归入说话人05的段落(而不是说话人04的段落)
|
||||
- merge时只合并说话人04的段落,说话人05段落中的老师对话就丢失了
|
||||
|
||||
但更关键的是: 说话人05在说话人04的发言期间插话,
|
||||
老师回应说话人05的这些对话,实际上可能是在延续与说话人04的话题
|
||||
(比如说话人05也在讨论高考漏题事件,与说话人04的故事直接相关)。
|
||||
这些内容在merge后完全丢失了。
|
||||
|
||||
|
||||
五、具体丢失的重要内容
|
||||
----------------------
|
||||
|
||||
以下是说话人04段落之间夹的中间段落中丢失的老师对话摘要:
|
||||
|
||||
1. 段落2和段落4之间(说话人03段落):
|
||||
- 老师安排下一位连线(行140)
|
||||
|
||||
2. 段落4和段落7之间(说话人03+说话人05段落):
|
||||
- 老师解释"且释放"的深层含义(行166)
|
||||
- 老师提醒"且释放"有暗语(行168)
|
||||
|
||||
3. 段落7和段落9之间(说话人03段落):
|
||||
- 老师引导关于孩子高考的觉察(行173-174)
|
||||
*** 重要: 这是老师对说话人04议题的深入引导,直接丢失!***
|
||||
|
||||
4. 段落9和段落11之间(说话人03段落):
|
||||
- 老师讲解教育系统的不公平和"漏中漏"概念(行224, 226)
|
||||
*** 重要: 这是对说话人04故事的深化分析!***
|
||||
|
||||
5. 段落11和段落14之间(说话人03+说话人05段落):
|
||||
- 老师引导向内探索(行233, 235)
|
||||
- 老师安排有觉知的呼吸(行237)
|
||||
|
||||
6. 段落14和段落16之间(说话人05段落) -- **最大丢失**:
|
||||
- 19行老师对话,包括:
|
||||
- 老师引导说话人05表达"这不公平"的情绪(行282-296)
|
||||
- 老师引导说话人05做愤怒释放(行298-318)
|
||||
- 老师揭示"陪跑"真相(行318)
|
||||
*** 非常重要: 说话人05也在处理高考漏题创伤,与说话人04同主题!***
|
||||
|
||||
7. 段落16和段落18之间(说话人06段落):
|
||||
- 老师解释旁观者角度(行322)
|
||||
|
||||
8. 段落18和段落20之间(说话人05段落):
|
||||
- 老师讲解下海捞明珠的比喻(行335)
|
||||
|
||||
9. 段落20和段落22之间(说话人05段落):
|
||||
- 老师引导表达情绪(行349)
|
||||
|
||||
10. 段落24和段落30之间(说话人05+说话人03+说话人06段落) -- **第二大丢失**:
|
||||
- 33行老师对话,包括:
|
||||
- 老师引导对父亲的指责释放(行359-387)
|
||||
- 老师引导"我没考过"信念释放(行400-426)
|
||||
- 老师揭示"考的是权力不是成绩"的真相(行408, 422, 424, 426)
|
||||
*** 极其重要: 这是整个高考创伤疗愈的高潮部分!***
|
||||
|
||||
|
||||
六、结论
|
||||
-------
|
||||
|
||||
1. 说话人04本人的84行内容 **没有丢失**,全部保留在截取文件中。
|
||||
|
||||
2. 老师在说话人04发言期间的147行中,只保留了83行(56%),
|
||||
丢失了61行(44%)。
|
||||
|
||||
3. 丢失的61行老师对话中,大部分是在说话人04的段落之间,
|
||||
老师与其他学员(主要是说话人05)互动时产生的。
|
||||
|
||||
4. **最严重的问题**: 说话人05与说话人04讨论的是同一个主题
|
||||
(高考漏题创伤),老师在说话人05段落中的引导
|
||||
(特别是"我没考过"信念释放和"考的是权力不是成绩"的真相揭示)
|
||||
实际上是说话人04故事的延续和深化,但这些内容全部丢失了。
|
||||
|
||||
5. merge 逻辑的设计缺陷:
|
||||
- _split_by_student 严格按学员拆分,导致同一主题的对话被割裂
|
||||
- _merge_by_speaker 只合并同一学员的段落,无法恢复跨学员的主题连贯性
|
||||
- 建议: 在 merge 时,应该考虑主题连贯性,将与当前学员主题相关的
|
||||
其他学员段落中的老师对话也纳入合并范围。
|
||||
Reference in New Issue
Block a user