增加语音对话功能

This commit is contained in:
yhy
2026-08-29 22:13:55 +08:00
parent 1ffa069151
commit 3a2438f566
51 changed files with 6207 additions and 3 deletions
+680
View File
@@ -0,0 +1,680 @@
# 软件架构设计文档
| 字段 | 内容 |
|------|------|
| 产品名称 | BanDu(伴读)— Zotero 插件 |
| 中文名 | 伴读 |
| 英文名 | BanDu |
| 文档版本 | v0.1 |
| 状态 | 草案 |
| 关联文档 | [PLAN.md](./PLAN.md)、[PRD.md](./PRD.md) |
---
## 1. 架构概述
### 1.1 架构风格
**Local-first 桌面插件架构**:核心计算与缓存均在用户机器完成;仅将段落文本与用户问题发送至用户配置的 LLM / TTS 服务。无自建后端(一期)。
### 1.2 设计原则
| 原则 | 说明 |
|------|------|
| 备课一次、缓存复用 | Lecture 持久化;attachment ID + 文件哈希为缓存键 |
| 增量交付 | 摘要 → beats → 逐段 → TTS 分阶段就绪,缩短 Time-to-Listen |
| UI / 领域 / 集成 分离 | 降低 Zotero API 变动影响 |
| Grounding 优先 | 生成内容必须可追溯到段落 ID |
| 优雅降级 | refs 无效、无 timestamps、无 BBT 均有 fallback |
### 1.3 系统上下文
```
┌─────────────────┐
│ 用户 │
└────────┬────────┘
┌──────────────▼──────────────┐
│ Zotero 7 (宿主) │
│ ┌────────────────────────┐ │
│ │ BanDu 插件 │ │
│ │ UI │ Engine │ Store │ │
│ └─────────┬──────────────┘ │
└────────────┼────────────────┘
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌───────────┐ ┌────────────┐ ┌─────────────┐
│ MinerU │ │ LLM API │ │ TTS API │
│ (本地子进程)│ │ / Ollama │ │ ElevenLabs等 │
└───────────┘ └────────────┘ └─────────────┘
┌───────────┐
│ SQLite + │
│ mp3 文件 │
└───────────┘
```
---
## 2. 逻辑分层
```
┌─────────────────────────────────────────────────────────┐
│ Presentation Layer(表现层) │
│ • BanduPaneItem pane 伴读页) │
│ • LibraryWindow(伴读库) │
│ • SettingsPane(设置) │
│ • PlayerController(播放状态机 + <audio>
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ Application Layer(应用层 / 用例) │
│ • PrepareLectureUseCase(备课编排) │
│ • PlayUseCase(播放 / 跳播 / 断点) │
│ • AskQuestionUseCase(问答 + RAG
│ • GenerateReportUseCase(报告 + Note 回写) │
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ Domain Layer(领域层) │
│ • Paper, Paragraph, Chapter │
│ • Lecture, Beat, ParagraphExplanation │
│ • QARecord, Progress, Highlight │
│ • LecturePreparationState(状态机) │
└───────────────────────────┬─────────────────────────────┘
┌───────────────────────────▼─────────────────────────────┐
│ Infrastructure Layer(基础设施层) │
│ • ZoteroAdapterItem / Attachment / Note
│ • PdfParserMinerU 封装) │
│ • LlmClientOpenAI-compatible / Ollama
│ • TtsClient(多提供商 + timestamps
│ • SqliteRepository │
│ • AudioStoragemp3 路径管理) │
│ • RagIndex(段落检索) │
│ • PromptRegistry(模板版本管理) │
└─────────────────────────────────────────────────────────┘
```
---
## 3. 模块说明
### 3.1 ZoteroAdapter
**职责**:封装所有 Zotero 7 API 调用。
| 接口 | 说明 |
|------|------|
| `getPdfAttachment(itemId)` | 返回主 PDF 路径、attachmentId |
| `computeFileHash(path)` | SHA-256 |
| `openPdfAtPage(itemId, page)` | 跳转 PDF 阅读器 |
| `createBanduNote(itemId, markdown)` | 创建伴读子笔记 |
| `getCitationKey(itemId)` | Better BibTeX 可选集成 |
| `listLibraryItems()` | 伴读库数据源 |
**约束**UI 与用例层不得直接调用 `Zotero.*` 全局对象。
### 3.2 PdfParser
**职责**:调用 MinerU(一期),输出规范化段落树。
**输入**PDF 本地路径
**输出**`ParseResult { chapters[], paragraphs[], metadata }`
**段落字段**
```typescript
interface Paragraph {
id: string; // 稳定 ID,如 "p-0017"
chapterId: string;
orderIndex: number;
text: string;
page: number;
language?: 'en' | 'zh';
bulletPoints?: string[]; // >150 词长段拆分要点
}
```
**失败策略**:抛出 typed error`ScannedPdfError`, `ParseTimeoutError`),UI 映射为用户文案。
### 3.3 PrepareLectureUseCase
**职责**:编排备课流水线,驱动增量就绪事件。
详见 §5 流水线与 §6 状态机。
### 3.4 LlmClient
**职责**:统一 LLM 调用;支持 streaming。
| 方法 | 用途 |
|------|------|
| `generateSummary(paragraphs[])` | 全文摘要 |
| `generateBeats(summary, paragraphIndex[])` | 整体讲解 beats |
| `generateParagraphExplanation(para, summary, chapterTitle)` | 三层讲解 |
| `generateSentenceExplanation(sentence, context)` | 逐句讲解 |
| `answerQuestion(context, question)` | 问答 stream |
**配置**provider、baseUrl、apiKey、model、temperature(默认 0.20.4)。
### 3.5 TtsClient
**职责**:文本 → mp3 + timestamps。
```typescript
interface TtsResult {
audioPath: string;
timestamps: WordTimestamp[]; // 可为空
durationMs: number;
}
interface WordTimestamp {
text: string;
startMs: number;
endMs: number;
}
```
**策略**:优先 provider 原生 boundaries;缺失则 `HighlightGranularity = SENTENCE`
### 3.6 RagIndex
**职责**:段落级检索,供问答组装上下文。
**一期实现**SQLite FTS5 或 BM25MVP 可不引入 embedding。
```typescript
interface RagIndex {
build(paragraphs: Paragraph[]): void;
search(query: string, topK: number): Paragraph[];
}
```
### 3.7 SqliteRepository + AudioStorage
**职责**:结构化数据与二进制音频分离存储。
- SQLite`{ZoteroDataDir}/bandu/bandu.db`
- 音频:`{ZoteroDataDir}/bandu/audio/{cacheKey}/{unitId}.mp3`
---
## 4. 数据模型
### 4.1 ER 关系
```
PaperCache 1──* Paragraph
PaperCache 1──1 Lecture
Lecture 1──* Beat
Lecture 1──* ParagraphExplanation
PaperCache 1──* QARecord
PaperCache 1──1 Progress
PaperCache 1──* Highlight
```
### 4.2 表结构(SQLite
```sql
-- 论文缓存根表
CREATE TABLE paper_cache (
id TEXT PRIMARY KEY, -- hash(attachment_id + file_hash)
attachment_id TEXT NOT NULL,
item_id TEXT NOT NULL,
file_hash TEXT NOT NULL,
file_path TEXT NOT NULL,
title TEXT,
language TEXT, -- 'en' | 'zh'
parse_status TEXT NOT NULL, --见状态机
lecture_status TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
UNIQUE(attachment_id, file_hash)
);
-- 章节
CREATE TABLE chapter (
id TEXT PRIMARY KEY,
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
title TEXT NOT NULL,
order_index INTEGER NOT NULL
);
-- 段落
CREATE TABLE paragraph (
id TEXT PRIMARY KEY,
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
chapter_id TEXT REFERENCES chapter(id),
order_index INTEGER NOT NULL,
text TEXT NOT NULL,
page INTEGER NOT NULL
);
-- 讲义
CREATE TABLE lecture (
paper_id TEXT PRIMARY KEY REFERENCES paper_cache(id),
summary_json TEXT NOT NULL, -- {problem, method, result, limitation}
prompt_version TEXT NOT NULL,
created_at INTEGER NOT NULL
);
-- 整体讲解 beat
CREATE TABLE beat (
id TEXT PRIMARY KEY,
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
order_index INTEGER NOT NULL,
title TEXT,
type TEXT NOT NULL, -- intro|method|experiment|conclusion|extension
script TEXT NOT NULL,
refs_json TEXT NOT NULL, -- ["p-0012","p-0013"]
audio_path TEXT,
timestamps_json TEXT,
tts_status TEXT NOT NULL -- pending|ready|failed
);
-- 逐段三层讲解
CREATE TABLE paragraph_explanation (
paragraph_id TEXT PRIMARY KEY REFERENCES paragraph(id),
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
layer_what TEXT NOT NULL,
layer_how TEXT NOT NULL,
layer_where TEXT NOT NULL,
audio_path TEXT,
timestamps_json TEXT,
gen_status TEXT NOT NULL,
tts_status TEXT NOT NULL
);
-- 播放进度
CREATE TABLE progress (
paper_id TEXT PRIMARY KEY REFERENCES paper_cache(id),
last_mode TEXT, -- overview|paragraph
last_unit_id TEXT,
last_position_ms INTEGER DEFAULT 0,
listened_units_json TEXT, -- 已听 unit id 列表
updated_at INTEGER NOT NULL
);
-- 问答
CREATE TABLE qa_record (
id TEXT PRIMARY KEY,
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
anchor_type TEXT NOT NULL, -- beat|paragraph|sentence|global
anchor_id TEXT,
question TEXT NOT NULL,
answer TEXT NOT NULL,
audio_path TEXT,
created_at INTEGER NOT NULL
);
-- 重点标记
CREATE TABLE highlight (
paper_id TEXT NOT NULL REFERENCES paper_cache(id),
paragraph_id TEXT NOT NULL REFERENCES paragraph(id),
note TEXT,
created_at INTEGER NOT NULL,
PRIMARY KEY (paper_id, paragraph_id)
);
-- 段落 FTSRAG
CREATE VIRTUAL TABLE paragraph_fts USING fts5(
paragraph_id,
paper_id,
text,
tokenize='unicode61'
);
```
### 4.3 缓存键策略
```
cacheKey = SHA256(attachmentId + "|" + fileHash)
失效条件:
• fileHash 变化(PDF 被替换)
• 用户「强制重新备课」
• prompt_version _major 升级(可选策略:仅影响新生成)
```
### 4.4 Lecture JSON 摘要结构
```json
{
"problem": "…",
"method": "…",
"result": "…",
"limitation": "…"
}
```
### 4.5 Beat refs 校验算法
```
输入: beats[], validParagraphIds (Set)
对每个 beat:
beat.refs = beat.refs.filter(id => validParagraphIds.has(id))
if beat.refs.isEmpty:
beat.degraded = true // UI: 不展示原文高亮
输出: beats[]
```
---
## 5. 核心流水线
### 5.1 端到端数据流
```
PDF path
→ [PdfParser] → paragraphs[] → SQLite
→ [LlmClient.generateSummary] → lecture.summary
→ [LlmClient.generateBeats] → beats[] → refs 校验 → SQLite
→ (event: beats_ready) → UI 可播整体模式
→ [LlmClient.generateParagraphExplanation] × N → SQLite
→ (event: paragraph_batch_ready)
→ [TtsClient.synthesize] × units → mp3 → SQLite
→ (event: unit_audio_ready)
```
### 5.2 增量备课时序
```
时间 ──────────────────────────────────────────────►
解析 ████████
摘要 ███
beats ██████ ← beats_ready~1-2min
逐段讲解 ████████████████████
TTS(beats) ████████ ← 整体模式有声音
TTS(段落) █████████████████
```
### 5.3 问答上下文组装
```
AskContext {
question: string
anchor: { type, id, text, explanation? }
summary: SummaryJson
ragParagraphs: Paragraph[] // topK=5
paperTitle: string
}
Prompt 模板顺序:
系统角色 → 全文摘要 → RAG 段落 → 锚点原文+讲解 → 用户问题
```
### 5.4 报告生成
```
ReportBuilder.build(paperId):
summary ← lecture.summary
progress ← progress.listened_units
qas ← qa_record ORDER BY created_at
highlights ← highlight JOIN paragraph
bibtex ← ZoteroAdapter.getCitationKey + export
MarkdownRenderer → ZoteroAdapter.createBanduNote
```
**报告策略(建议 MVP**:同一 Item 只保留一个「伴读报告」笔记;再次生成时**更新**同名 Note(`BanDu 伴读报告: {title}`),避免笔记爆炸。
---
## 6. 状态机
### 6.1 论文 / 备课状态(lecture_status
```
┌─────────┐
│ idle │ 未开始
└────┬────┘
│ start
┌─────────┐
┌────│ parsing │────┐ fail
│ └────┬────┘ │
│ │ success ▼
│ │ ┌─────────┐
│ └───►│ parsed │
│ └────┬────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ summarizing │
│ └──────┬───────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ beats_generating │
│ └──────┬───────┘
│ │ beats_ready
│ ▼
│ ┌────────────────────┐
│ │ paragraphs_generating │◄──┐
│ └──────────┬─────────┘ │ batch loop
│ │ │
│ └─────────────┘
│ │ all para done
│ ▼
│ ┌────────────────────┐
│ │ tts_generating │
│ └──────────┬─────────┘
│ │
│ ▼
│ ┌──────────────┐
└─────────────►│ ready │
error └──────────────┘
┌─────────┐
│ failed │
└─────────┘
```
**可恢复**:每个阶段完成后持久化;重启插件从 `lecture_status` 继续,跳过已完成步骤。
### 6.2 播放器状态
```
PlayerState {
mode: 'overview' | 'paragraph'
currentUnitId: string
isPlaying: boolean
playbackRate: 1 | 1.5 | 2
highlightCursorMs: number
}
```
**跳播**:切换 `currentUnitId` → 加载对应 mp3 → seek(0) → 更新 Progress。
---
## 7. 接口设计(内部事件)
插件内使用 EventBus 解耦 UI 与长时间任务:
| 事件 | 载荷 | 订阅者 |
|------|------|--------|
| `parse:progress` | `{ percent, message }` | BanduPane |
| `lecture:beats_ready` | `{ paperId, beatCount }` | BanduPane, Player |
| `lecture:paragraph_ready` | `{ paperId, paragraphId }` | BanduPane |
| `tts:unit_ready` | `{ unitId, audioPath }` | Player |
| `lecture:failed` | `{ paperId, error }` | BanduPane |
| `qa:chunk` | `{ textDelta }` | QAPanel |
| `qa:complete` | `{ qaRecordId }` | QAPanel, Report |
**长时间任务**:在 Zotero 主线程外使用 Web Worker 或子进程(MinerU);LLM/TTS 使用 async fetch,避免阻塞 UI。
---
## 8. 外部集成
### 8.1 LLM
| 提供商 | 协议 | 用途 |
|--------|------|------|
| OpenAI 兼容 API | HTTPS SSE | 备课 + 问答 |
| Ollama | localhost HTTP | 全本地备选 |
### 8.2 TTS
| 提供商 | 选型维度 |
|--------|----------|
| 火山 / Azure / MiniMax / ElevenLabs | 中文自然度、价格、word timestamps |
**抽象接口**
```typescript
interface TtsProvider {
name: string;
synthesize(text: string, options: TtsOptions): Promise<TtsResult>;
supportsTimestamps: boolean;
}
```
### 8.3 MinerU
- **集成方式(待 Phase 0 确认)**:子进程 CLIstdin/stdout JSON
- **打包**:插件安装包内含 MinerU 二进制或安装脚本;文档说明 Python 环境依赖
### 8.4 Better BibTeX(可选)
- 检测 `Zotero.BetterBibTeX` 是否存在
- 不存在:从 Item 字段生成简易 BibTeX 或留空字段
---
## 9. 安全与隐私
| 项 | 方案 |
|----|------|
| PDF 文件 | 不离开本地 |
| 发往 LLM 的数据 | 段落 text + 用户问题 + 摘要;设置页明示 |
| API Key | Zotero prefs 加密存储或 OS Keychain |
| 日志 | 不记录 Key、不完整记录段落正文(可截断) |
| Ollama | 默认 localhost,无外网 |
---
## 10. 部署与构建
### 10.1 技术栈
| 层 | 选型 |
|----|------|
| 插件框架 | [zotero-plugin-template](https://github.com/windingwind/zotero-plugin-template) |
| 语言 | TypeScript |
| UI | HTML + CSSFluent 风格贴近 Zotero |
| 音频 | HTML5 `<audio>` |
| 数据库 | better-sqlite3 或 Zotero 内置 SQL(待 POC |
| PDF | MinerUPython,本地 CPU |
### 10.2 目录结构(建议)
```
bandu/
├── addon/
│ ├── bootstrap.js
│ ├── manifest.json
│ └── prefs.js
├── src/
│ ├── ui/
│ │ ├── BanduPane.tsx
│ │ ├── LibraryWindow.tsx
│ │ └── SettingsPane.tsx
│ ├── application/
│ │ ├── PrepareLectureUseCase.ts
│ │ ├── PlayUseCase.ts
│ │ ├── AskQuestionUseCase.ts
│ │ └── GenerateReportUseCase.ts
│ ├── domain/
│ ├── infrastructure/
│ │ ├── zotero/
│ │ ├── pdf/
│ │ ├── llm/
│ │ ├── tts/
│ │ └── storage/
│ └── prompts/
├── tests/
└── docs/
```
### 10.3 版本兼容
- `manifest.json` 声明 `applications.zotero.strict_min_version` / `strict_max_version`
- CI 固定 Zotero 7.x 版本做 smoke test
---
## 11. Phase 0 技术验证(Go / No-Go
| # | 验证项 | 通过标准 | 阻塞 MVP |
|---|--------|----------|----------|
| P0-1 | Item pane 嵌入自定义页 | 可展示 UI、读取当前 Item | 是 |
| P0-2 | 读取 PDF attachment 路径 | 路径有效、哈希可算 | 是 |
| P0-3 | `<audio>` 长音频播放 | 10 min+ 无泄漏/崩溃 | 是 |
| P0-4 | MinerU 解析 3 篇论文 | 段落切分可用率 ≥ 80% | 是 |
| P0-5 | TTS 中文样本 | 自然度可接受 | 是 |
| P0-6 | TTS timestamps | 有则记录格式;无则确认句子级降级 | 否(降级) |
| P0-7 | Zotero Note Markdown | 报告渲染可接受 | 否 |
---
## 12. 性能与扩展
### 12.1 长论文策略
| 页数 | 策略 |
|------|------|
| ≤ 30 | 全量逐段备课 |
| 30–80 | 逐段 lazy:按章节或用户点击生成 |
| > 80 | 默认仅整体讲解;逐段需用户确认(成本提示) |
### 12.2 并发控制
- LLM 备课:beats 串行或小批量并行(≤ 3)避免 rate limit
- TTS:队列化,优先 beats 音频
- 同一 paper 不允许重复触发备课(互斥锁)
---
## 13. 可观测性
| 类型 | 内容 |
|------|------|
| 结构化日志 | paperId, stage, durationMs, errorCode |
| 用户可见进度 | 解析 %、beats 数、段落 i/n、TTS i/n |
| Dogfooding 导出 | 单篇 metrics JSON(可选调试菜单) |
---
## 14. 风险与架构对策
| 风险 | 架构对策 |
|------|----------|
| Zotero API 变更 | Adapter 层隔离;pin 版本 |
| MinerU 打包复杂 | Phase 0 验证;文档化外部依赖安装 |
| refs 幻觉 | 校验器 + degraded beat |
| TTS 无 timestamps | HighlightGranularity 降级 |
| SQLite 锁 | 写操作单线程队列 |
| 备课中断 | 状态机 + 阶段持久化 |
---
## 15. 开放问题
1. SQLite 用 `better-sqlite3` 还是 Zotero `Services.storage` — 待插件 POC 验证 WASM/ native 限制
2. MinerU 随插件分发 vs 用户自行安装 Python 环境
3. 问答 TTS MVP 用流式还是整段合成(影响架构复杂度)
4. 伴读库窗口:全库扫描性能(>5000 items 时的索引策略)
---
## 16. 文档修订记录
| 版本 | 日期 | 说明 |
|------|------|------|
| v0.1 | 2026-08-29 | 自 PLAN.md 拆分为架构初稿 |