增加语音对话功能

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
+944
View File
@@ -0,0 +1,944 @@
# ChatPapers 双模式集成架构设计
| 字段 | 内容 |
|------|------|
| 产品名称 | ChatPapers for Zotero |
| 文档名称 | 双模式集成架构(方案 A) |
| 文档版本 | v0.1 |
| 状态 | 草案 |
| 目标平台 | Zotero 9macOS / Windows |
| 关联文档 | [双模式 PRD](./DUAL_MODE_PRD.md)、[开发文档](../development.md)、[BanDu 架构参考](./ARCHITECTURE.md) |
> **本文档定位**:描述 ChatPapers 插件在保留**文字对话**能力的基础上,扩展**语音伴读**(备课 → 听课 → 语音问答)的架构设计。
> 产品统一品牌为 **ChatPapers**;两种模式是同一条「与论文对话」产品线的不同交互形态——文字即时聊 vs 语音系统讲。
---
## 1. 架构概述
### 1.1 架构演进
```
v0.2(当前) v1.0(目标)
───────────────── ─────────────────────────────────
ChatPapers 插件 ChatPapers 插件(单一 .xpi
└── 文字对话 pane ├── 文字对话 pane(已有)
PDFWorker 纯文本 │ 即时问答 / 总结 / JSON 会话
LLM 流式 │
sessions/*.json └── 语音伴读 pane(新增)
MinerU 段落树
Lecture 备课 + TTS + 播放器
SQLite + mp3 缓存
共享:LLM · PDF 附件 · 笔记 · BibTeX
```
### 1.2 架构风格
**Local-first 双模式插件架构**:核心计算与缓存均在用户机器完成;仅将段落文本与用户问题发送至用户配置的 LLM / TTS 服务。无自建后端。
### 1.3 设计原则
| 原则 | 说明 |
|------|------|
| 单一插件、两种对话形态 | 文字对话与语音伴读并存,共享 LLM 与 Zotero 集成 |
| 模块隔离 | 语音模块不侵入文字对话代码路径 |
| 备课一次、缓存复用 | Lecture 持久化;attachment ID + 文件哈希为缓存键 |
| 增量交付 | 摘要 → beats → 逐段 → TTS 分阶段就绪 |
| UI / 领域 / 集成 分离 | 降低 Zotero API 变动影响 |
| Grounding 优先 | 生成内容必须可追溯到段落 ID |
| 优雅降级 | refs 无效、无 timestamps、无 BBT 均有 fallback |
### 1.4 系统上下文
```
┌─────────────────┐
│ 用户 │
└────────┬────────┘
┌──────────────▼──────────────┐
│ Zotero 9(宿主) │
│ ┌────────────────────────┐ │
│ │ ChatPapers 插件 │ │
│ │ ┌──────────┬─────────┐ │ │
│ │ │文字对话 │语音伴读 │ │ │
│ │ │ ChatView │LectureUI│ │ │
│ │ └────┬─────┴────┬────┘ │ │
│ │ │ Shared │ │ │
│ │ └─────┬─────┘ │ │
│ └────────────┼────────────┘ │
└───────────────┼──────────────┘
┌─────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Zotero │ │ LLM API │ │ TTS API │
│ PDFWorker │ │ / Ollama │ │ 火山/Azure等 │
│ MinerU │ │ (共享) │ │(语音模式) │
└─────────────┘ └─────────────┘ └─────────────┘
┌─────────────────────────────────────┐
│ {Profile}/chatpapers/ │
│ sessions/*.json ← 文字对话 │
│ lecture/lecture.db ← 语音伴读 │
│ lecture/audio/ │
└─────────────────────────────────────┘
```
### 1.5 两种对话形态对比
| 维度 | 文字对话(已有) | 语音伴读(新增) |
|------|------------------|------------------|
| 用户意图 | 「我现在想问什么」 | 「请 AI 先读完再给我讲」 |
| 交互 | 打字 → 文字流式回复 | 听课 → 打字提问 → 文字 + 语音回复 |
| PDF 输入 | PDFWorker 整篇纯文本 | MinerU 结构化段落树 |
| LLM 用法 | 每轮即时组装 context | 一次性备课 + 按需问答 |
| 持久化 | `sessions/{itemKey}.json` | `lecture/lecture.db` + mp3 |
| TTS | 无 | 讲解 + 问答回答均支持 |
| 笔记 | `ChatPapers:` 前缀 | `ChatPapers 伴读报告:` 前缀 |
---
## 2. 整体分层架构
```
┌──────────────────────────────────────────────────────────────────┐
│ Presentation Layer(表现层) │
│ 【文字】chatView.ts · multiChatView.ts · readerPane (Chat sec) │
│ 【语音】lecturePane.ts · player/ · libraryWindow.tsP1
│ 【共享】prefs.ts · preferences.xhtml │
└───────────────────────────────┬──────────────────────────────────┘
┌───────────────────────────────▼──────────────────────────────────┐
│ Application Layer(应用层 / 用例) │
│ 【文字】buildChatMessages · buildMultiChatMessages(已有) │
│ 【语音】PrepareLectureUseCase · PlayUseCase │
│ AskQuestionUseCase · GenerateReportUseCase │
└───────────────────────────────┬──────────────────────────────────┘
┌───────────────────────────────▼──────────────────────────────────┐
│ Domain Layer(领域层) │
│ 【文字】ChatMessage · ExtractResult(已有) │
│ 【语音】PaperCache · Paragraph · Chapter · Lecture · Beat │
│ ParagraphExplanation · QARecord · Progress · Highlight │
│ LecturePreparationState · PlayerState │
└───────────────────────────────┬──────────────────────────────────┘
┌───────────────────────────────▼──────────────────────────────────┐
│ Infrastructure Layer(基础设施层) │
│ ┌─ 共享 ─────────────────────────────────────────────────────┐ │
│ │ llm/client · llm/providers · zotero/notes · zotero/citeExport│ │
│ │ pdf/extractor (附件定位) · pdf/selection · utils/* │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ┌─ 文字专属 ─────────────────────────────────────────────────┐ │
│ │ pdf/context · pdf/multiContext · storage/sessions │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ┌─ 语音专属 ─────────────────────────────────────────────────┐ │
│ │ lecture/infrastructure/pdf (MinerU) · tts/ · storage/sqlite │ │
│ │ lecture/infrastructure/rag · audioStorage · promptRegistry │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
```
---
## 3. 现有代码映射
### 3.1 共享层(已实现,语音模式直接复用)
| 模块路径 | 职责 | 文字对话 | 语音伴读 |
|----------|------|:--------:|:--------:|
| `src/hooks.ts` | 生命周期、注册入口 | ✓ | 扩展注册 |
| `src/modules/ui/readerPane.ts` | Item pane section 注册 | ✓ | 扩展第二 section |
| `src/modules/llm/client.ts` | LLM 流式请求 | ✓ | ✓ 备课 + 问答 |
| `src/modules/llm/providers.ts` | Provider 预设 | ✓ | ✓ 只读共享 prefs |
| `src/modules/llm/types.ts` | ChatMessage 等类型 | ✓ | 问答层可复用类型 |
| `src/modules/pdf/extractor.ts` | PDF 附件定位 | ✓ | ✓ 路径 + 扩展 hash |
| `src/modules/pdf/selection.ts` | Reader 选区 | ✓ | ✓ 「解释这句」 |
| `src/modules/zotero/notes.ts` | 子笔记创建 | ✓ | ✓ 伴读报告 |
| `src/modules/zotero/citeExport.ts` | BibTeX 导出 | ✓ | ✓ 参考文献卡片 |
| `src/utils/markdown.ts` | Markdown 渲染 | ✓ | ✓ 报告预览 |
| `src/utils/prefs.ts` | Preference 读写 | ✓ | ✓ 扩展 TTS prefs |
| `src/utils/abort.ts` | 请求取消 | ✓ | ✓ |
### 3.2 文字对话专属(已实现,语音模式不得引用)
| 模块路径 | 职责 |
|----------|------|
| `src/modules/ui/chatView.ts` | 文字对话 UI |
| `src/modules/ui/multiChatView.ts` | 多 PDF 联合对话 |
| `src/modules/pdf/context.ts` | 单篇 context 组装 |
| `src/modules/pdf/multiContext.ts` | 多篇 context 组装 |
| `src/modules/llm/prompts.ts` | 文字对话 system / summary prompt |
| `src/modules/llm/history.ts` | 论文材料持久化到消息历史 |
| `src/modules/storage/sessions.ts` | JSON 会话文件 |
### 3.3 语音伴读新建(`src/modules/lecture/`
| 模块 | 职责 |
|------|------|
| `lecture/ui/lecturePane.ts` | Item pane「语音伴读」section |
| `lecture/ui/playerController.ts` | 播放状态机 + `<audio>` |
| `lecture/ui/unitList.ts` | beats / 段落列表 |
| `lecture/ui/qaPanel.ts` | 锚定提问 + 流式 + 重播 |
| `lecture/application/prepareLecture.ts` | 备课流水线编排 |
| `lecture/application/playLecture.ts` | 播放 / 跳播 / 断点 |
| `lecture/application/askQuestion.ts` | 锚定问答 + RAG |
| `lecture/application/generateReport.ts` | 报告组装 + Note 回写 |
| `lecture/domain/*` | 领域实体与状态机 |
| `lecture/infrastructure/pdf/parser.ts` | MinerU 封装 |
| `lecture/infrastructure/pdf/fileHash.ts` | SHA-256 |
| `lecture/infrastructure/tts/client.ts` | TTS 多提供商 |
| `lecture/infrastructure/tts/providers.ts` | TTS 预设 |
| `lecture/infrastructure/storage/repository.ts` | SQLite CRUD |
| `lecture/infrastructure/storage/audioStorage.ts` | mp3 路径管理 |
| `lecture/infrastructure/rag/index.ts` | FTS5 段落检索 |
| `lecture/infrastructure/llm/lecturePrompts.ts` | 备课 / beats / 三层讲解 prompt |
| `lecture/infrastructure/events.ts` | EventBus |
---
## 4. 依赖规则
```
┌─────────────┐
│ utils/ │
└──────┬──────┘
┌────────────────┼────────────────┐
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ llm/ │ │ pdf/ext │ │ zotero/ │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
┌────┴───────┐ │ ┌────┴─────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
chatView multiChat selection notes citeExport
(文字 UI) (文字 UI) (共享)
lecture/* ──可引用──► llm/, pdf/extractor, pdf/selection, zotero/, utils/
chatView / multiChat / context / sessions ──禁止引用──► lecture/*
lecture/* ──禁止引用──► chatView, context, sessions, multiContext
```
**运行时隔离**:两个 pane 的 `onAsyncRender` 各自实例化 View,异常在 View 边界捕获,不向上冒泡至 `hooks.ts` 导致整个插件失效。
---
## 5. 启动与注册流程
```typescript
// hooks.ts(目标形态,伪代码)
async function onStartup() {
await Promise.all([Zotero.initializationPromise, ...]);
initLocale();
registerPrefs(); // 扩展 TTS / 教师风格分组
registerChatPane(); // 已有:文字对话 section
registerLecturePane(); // 新增:语音伴读 section
registerReaderSelectionHook();
addon.data.initialized = true;
}
```
```typescript
// readerPane.ts 扩展策略
export function registerChatPane() { /* 现有,不动 */ }
export function registerLecturePane() {
Zotero.ItemPaneManager.registerSection({
paneID: "chatpapers-lecture",
pluginID: config.addonID,
header: { l10nID: "chatpapers-item-section-lecture-head", ... },
onItemChange: ({ item, setEnabled }) => {
setEnabled(Boolean(item && findPdfAttachment(item)));
},
onAsyncRender: async ({ body, item }) => {
const view = new LecturePaneView(doc, body, item);
await view.mount();
},
// ...
});
}
```
---
## 6. 语音伴读模块详细设计
### 6.1 Zotero 集成适配(复用 + 薄封装)
语音模块不新建独立 Adapter 文件树,而是在 `lecture/infrastructure/zotero/` 做薄封装,内部调用已有模块:
| 接口 | 实现 |
|------|------|
| `getPdfAttachment(item)` | 委托 `findPdfAttachment` |
| `computeFileHash(path)` | 新增:`IOUtils.read` + SubtleCrypto / nsICryptoHash |
| `openPdfAtPage(itemId, page)` | 新增:Zotero.Reader API |
| `createLectureNote(item, markdown)` | 委托 `createChildNote`,标题 `ChatPapers 伴读报告: {title}` |
| `exportBibtex(item)` | 委托 `citeExport.formatCiteText` |
### 6.2 PdfParserMinerU
**与文字对话 PDF 路径分离**:文字对话继续 `PDFWorker.getFullText`;语音伴读走 MinerU。
**输入**PDF 本地路径(来自 attachment
**输出**
```typescript
interface ParseResult {
chapters: Chapter[];
paragraphs: Paragraph[];
metadata: { pageCount: number; detectedLanguage?: "en" | "zh" };
}
interface Paragraph {
id: string; // "p-0017"
chapterId: string;
orderIndex: number;
text: string;
page: number;
language?: "en" | "zh";
bulletPoints?: string[];
}
```
**失败策略**
| 错误类型 | UI 文案 |
|----------|---------|
| `ScannedPdfError` | 扫描版 PDF,请先 OCR |
| `ParseTimeoutError` | 解析超时,请重试 |
| `MinerUNotFoundError` | 未检测到 MinerU,见设置页安装说明 |
### 6.3 LLM 扩展(共享 client,专属 prompt
`llm/client.ts` 保持通用 `chatStream`;语音备课在 `lecture/infrastructure/llm/lecturePrompts.ts` 定义模板,通过 client 调用:
| 方法 | 位置 | 用途 |
|------|------|------|
| `chatStream` | `llm/client.ts`(已有) | 文字对话 + 语音问答 |
| `generateSummary` | `lecture/.../lectureLlm.ts` | 全文摘要四字段 |
| `generateBeats` | 同上 | 815 beats + refs[] |
| `generateParagraphExplanation` | 同上 | 三层讲解 |
| `generateSentenceExplanation` | 同上 | 逐句讲解 |
| `answerWithRag` | `lecture/application/askQuestion.ts` | 锚定问答 |
**配置**:语音备课 temperature 0.20.4;与文字对话共用 `getRuntimeConfig()`,不新增 LLM prefs 键。
### 6.4 TtsClient
```typescript
interface TtsResult {
audioPath: string;
timestamps: WordTimestamp[];
durationMs: number;
}
interface WordTimestamp {
text: string;
startMs: number;
endMs: number;
}
interface TtsProvider {
name: string;
synthesize(text: string, options: TtsOptions): Promise<TtsResult>;
supportsTimestamps: boolean;
}
```
**高亮降级链**`WORD``SENTENCE``PARAGRAPH``NONE`(仅进度条)
**Prefs 键(建议)**`extensions.zotero.chatpapers.ttsProvider` / `ttsApiKey` / `ttsVoice`
### 6.5 RagIndex
```typescript
interface RagIndex {
build(paperId: string, paragraphs: Paragraph[]): Promise<void>;
search(paperId: string, query: string, topK: number): Paragraph[];
}
```
一期 SQLite FTS5,不引入 embedding。索引在解析完成后构建,与 `paragraph_fts` 表同步。
### 6.6 PrepareLectureUseCase
**职责**:编排备课流水线;发出增量就绪事件;保证同一 paper 互斥(备课锁)。
**伪流程**
```
start(paperId):
if lecture_status != idle && != failed: return // 防重入
set status → parsing
paragraphs ← PdfParser.parse(path)
persist paragraphs + FTS
set status → summarizing
summary ← Llm.generateSummary(paragraphs)
persist lecture.summary
set status → beats_generating
beats ← Llm.generateBeats(summary, paragraphIndex)
beats ← validateRefs(beats, paragraphIds)
persist beats
emit lecture:beats_ready // UI 可开整体讲解
set status → paragraphs_generating
for batch in paragraphs.chunk(5):
explanations ← Llm.generateParagraphExplanation(batch)
persist + emit lecture:paragraph_ready
set status → tts_generating
for unit in beats + explanations:
audio ← Tts.synthesize(unit.script)
persist audio_path + emit tts:unit_ready
set status → ready
```
### 6.7 PlayUseCase + PlayerController
```typescript
interface PlayerState {
mode: "overview" | "paragraph";
currentUnitId: string;
isPlaying: boolean;
playbackRate: 1 | 1.5 | 2;
highlightCursorMs: number;
}
```
**跳播**`currentUnitId` 变更 → 加载 mp3 → `audio.currentTime = 0` → 更新 `progress` 表。
**断点续播**pane 打开时读 `progress.last_unit_id` + `last_position_ms`
### 6.8 AskQuestionUseCase
与文字对话的差异:
| 项 | 文字对话 | 语音伴读问答 |
|----|----------|--------------|
| 上下文 | 整篇 PDF 文本(可能截断) | 锚点 + 摘要 + RAG topK=5 |
| 存储 | sessions JSON | qa_record 表 |
| 输出 | 仅文字流 | 文字流 + TTS mp3 |
| 锚点 | 选区字符串 | beat / paragraph / sentence / global |
```typescript
interface AskContext {
question: string;
anchor: {
type: "beat" | "paragraph" | "sentence" | "global";
id?: string;
text?: string;
explanation?: string;
};
summary: SummaryJson;
ragParagraphs: Paragraph[];
paperTitle: string;
}
```
### 6.9 GenerateReportUseCase
```
ReportBuilder.build(paperId):
summary ← lecture.summary_json
progress ← progress.listened_units_json
qas ← qa_record ORDER BY created_at
highlights ← highlight JOIN paragraph
bibtex ← citeExport
MarkdownRenderer → createLectureNote(parentItem, markdown)
```
**Note 策略**:同名 `ChatPapers 伴读报告: {title}` 存在则更新,避免笔记爆炸。
---
## 7. 数据模型
### 7.1 存储目录
```
{ZoteroProfile}/chatpapers/
├── sessions/ # 文字对话(已有)
│ └── {itemKey}-{attachmentKey}.json
└── lecture/ # 语音伴读(新增)
├── lecture.db # SQLite
└── audio/
└── {cacheKey}/
├── beat-{id}.mp3
├── para-{id}.mp3
└── qa-{id}.mp3
```
### 7.2 缓存键
```
cacheKey = SHA256(attachmentId + "|" + fileHash)
失效条件:
• fileHash 变化
• 用户强制重新备课
• lecture prompt_version major 升级(可选)
```
### 7.3 ER 关系
```
PaperCache 1──* Paragraph
PaperCache 1──1 Lecture
Lecture 1──* Beat
Lecture 1──* ParagraphExplanation
PaperCache 1──* QARecord
PaperCache 1──1 Progress
PaperCache 1──* Highlight
```
### 7.4 SQLite 表结构
```sql
-- 论文缓存根表
CREATE TABLE paper_cache (
id TEXT PRIMARY KEY,
attachment_id TEXT NOT NULL,
item_id TEXT NOT NULL,
file_hash TEXT NOT NULL,
file_path TEXT NOT NULL,
title TEXT,
language TEXT,
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,
prompt_version TEXT NOT NULL,
created_at INTEGER NOT NULL
);
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,
script TEXT NOT NULL,
refs_json TEXT NOT NULL,
audio_path TEXT,
timestamps_json TEXT,
tts_status TEXT NOT NULL
);
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,
last_unit_id TEXT,
last_position_ms INTEGER DEFAULT 0,
listened_units_json TEXT,
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,
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)
);
CREATE VIRTUAL TABLE paragraph_fts USING fts5(
paragraph_id,
paper_id,
text,
tokenize='unicode61'
);
```
### 7.5 Beat refs 校验
```
输入: beats[], validParagraphIds (Set)
对每个 beat:
beat.refs = beat.refs.filter(id => validParagraphIds.has(id))
if beat.refs.isEmpty:
beat.degraded = true
输出: beats[]
```
### 7.6 摘要 JSON
```json
{
"problem": "…",
"method": "…",
"result": "…",
"limitation": "…"
}
```
---
## 8. 核心流水线
### 8.1 语音伴读端到端
```
PDF attachment path
→ findPdfAttachment + computeFileHash
→ [MinerU PdfParser] → paragraphs[] → SQLite + FTS
→ [generateSummary] → lecture.summary
→ [generateBeats] → refs 校验 → SQLite
→ emit lecture:beats_ready ──► 整体讲解可播(文本)
→ [generateParagraphExplanation] × N → SQLite
→ emit lecture:paragraph_ready
→ [Tts.synthesize] × units → mp3 → SQLite
→ emit tts:unit_ready ──► 播放器加载音频
→ 用户提问 → [AskQuestion + RAG] → 文字流 + TTS
→ [GenerateReport] → Zotero Note
```
### 8.2 增量备课时序
```
时间 ──────────────────────────────────────────────►
解析 ████████
摘要 ███
beats ██████ ← beats_ready~1-2min,可先听文本/TTS排队)
逐段讲解 ████████████████████
TTS(beats) ████████
TTS(段落) █████████████████
```
### 8.3 文字对话路径(不变)
```
Item → findPdfAttachment → PDFWorker.getFullText
→ buildChatMessages → chatStream → ChatView 渲染
→ saveSession (JSON)
→ createChildNote(可选)
```
---
## 9. 状态机
### 9.1 备课状态(lecture_status
```
idle → parsing → parsed → summarizing → beats_generating
→ paragraphs_generating (batch loop) → tts_generating → ready
任何阶段 ──error──► failed(可从 failed 或任意已完成阶段 resume
```
**可恢复**:每阶段结束写库;重启后读 `lecture_status`,跳过已完成步骤。
### 9.2 播放器状态
见 §6.7。pane `onDestroy` 时持久化 progress,释放 `<audio>` 引用。
---
## 10. 内部事件(EventBus
| 事件 | 载荷 | 订阅者 |
|------|------|--------|
| `parse:progress` | `{ paperId, percent, message }` | LecturePane |
| `lecture:beats_ready` | `{ paperId, beatCount }` | LecturePane, Player |
| `lecture:paragraph_ready` | `{ paperId, paragraphId }` | LecturePane |
| `tts:unit_ready` | `{ paperId, unitId, audioPath }` | Player |
| `lecture:failed` | `{ paperId, error }` | LecturePane |
| `qa:chunk` | `{ textDelta }` | QAPanel |
| `qa:complete` | `{ qaRecordId, audioPath? }` | QAPanel |
EventBus 实例按 `paperId` 或全局单例均可;MVP 用模块级 `EventTarget` 即可。
**长时间任务**
- MinerU:子进程(不阻塞 UI)
- LLM / TTS`async fetch` + AbortController
- SQLite 写:单线程队列,避免并发锁
---
## 11. 设置与 Preference 设计
### 11.1 分组结构(preferences.xhtml
```
ChatPapers 设置
├── 通用 · LLM ← 两模式共享
│ Provider / Base URL / API Key / Model / Temperature
├── 文字对话 ← 已有
│ System Prompt / Summary Prompt / Answer Language
└── 语音伴读 ← 新增
TTS Provider / API Key / Voice
教师风格(简洁 / 详细)
MinerU 路径(若需)
数据流向说明(静态文案)
```
### 11.2 Prefs 键命名
沿用 `extensions.zotero.chatpapers.*` 前缀,不新增 addon ID
| 键 | 模式 | 说明 |
|----|------|------|
| `provider` | 共享 | 已有 |
| `apiKey` | 共享 | 已有 |
| `systemPrompt` | 文字 | 已有 |
| `ttsProvider` | 语音 | 新增 |
| `ttsApiKey` | 语音 | 新增 |
| `ttsVoice` | 语音 | 新增 |
| `lectureTeacherStyle` | 语音 | 新增 |
| `mineruPath` | 语音 | 新增,可选 |
---
## 12. 目标工程结构
在 [development.md](../development.md) 现有结构基础上扩展:
```text
src/
├── hooks.ts
├── modules/
│ ├── ui/
│ │ ├── readerPane.ts # 注册 Chat + Lecture 两个 section
│ │ ├── chatView.ts # 文字对话(不动)
│ │ ├── multiChatView.ts # 文字多 PDF(不动)
│ │ └── prefs.ts
│ ├── pdf/
│ │ ├── extractor.ts # 共享:附件定位
│ │ ├── context.ts # 文字专属
│ │ ├── multiContext.ts # 文字专属
│ │ └── selection.ts # 共享
│ ├── llm/ # 共享 client + providers
│ ├── storage/
│ │ └── sessions.ts # 文字专属
│ ├── zotero/ # 共享
│ └── lecture/ # ★ 语音伴读(新建)
│ ├── ui/
│ │ ├── lecturePane.ts
│ │ ├── playerController.ts
│ │ ├── unitList.ts
│ │ └── qaPanel.ts
│ ├── application/
│ │ ├── prepareLecture.ts
│ │ ├── playLecture.ts
│ │ ├── askQuestion.ts
│ │ └── generateReport.ts
│ ├── domain/
│ │ ├── types.ts
│ │ ├── lectureState.ts
│ │ └── playerState.ts
│ └── infrastructure/
│ ├── pdf/
│ │ ├── parser.ts
│ │ └── fileHash.ts
│ ├── tts/
│ │ ├── client.ts
│ │ └── providers.ts
│ ├── storage/
│ │ ├── repository.ts
│ │ ├── schema.ts
│ │ └── audioStorage.ts
│ ├── rag/
│ │ └── index.ts
│ ├── llm/
│ │ ├── lectureLlm.ts
│ │ └── lecturePrompts.ts
│ ├── zotero/
│ │ └── adapter.ts
│ └── events.ts
```
**样式**`addon/content/chatpapers.css` 增加 `.chatpapers-lecture-*` 前缀类,与文字对话样式并列。
**i18n**`addon/locale/*/chatpapers.ftl` 增加 `item-section-lecture-*` 等键。
---
## 13. 外部集成
### 13.1 LLM(共享)
| 提供商 | 协议 | 文字对话 | 语音伴读 |
|--------|------|:--------:|:--------:|
| OpenAI 兼容 | HTTPS SSE | ✓ | ✓ 备课 + 问答 |
| Ollama | localhost | ✓ | ✓ |
### 13.2 TTS(语音专属)
| 提供商 | 选型维度 |
|--------|----------|
| 火山 / Azure / MiniMax / ElevenLabs | 中文自然度、价格、word timestamps |
### 13.3 MinerU(语音专属)
- 集成:子进程 CLIstdout JSON
- 分发:Phase 0 决定「随插件文档安装」vs「用户自装 Python 环境」
### 13.4 Better BibTeX(共享,可选)
语音报告参考文献卡片复用 `citeExport`;逻辑已在 Chat 多 PDF 模式验证。
---
## 14. 安全与隐私
| 项 | 文字对话 | 语音伴读 |
|----|----------|----------|
| PDF 文件 | 不上传 | 不上传 |
| 发往 LLM | 正文 text(可能截断) | 段落 text + 摘要 + 问题 |
| 发往 TTS | — | 讲解/回答中文文本 |
| API Key | Zotero prefs | LLM 共享;TTS 独立 pref |
| 日志 | 不记录 Key | 不记录 Key;段落正文可截断 |
设置页「数据流向」分组须同时覆盖两种模式。
---
## 15. Phase 0 技术验证(Go / No-Go
| # | 验证项 | 通过标准 | 阻塞 MVP |
|---|--------|----------|----------|
| P0-1 | 第二 Item pane 与 Chat 并存 | 切换 tab 互不影响 | 是 |
| P0-2 | MinerU 解析 3 篇论文 | 段落可用率 ≥ 80% | 是 |
| P0-3 | Zotero 9 插件内 SQLite CRUD | lecture.db 读写正常 | 是 |
| P0-4 | `<audio>` 10 min+ 播放 | 无泄漏 / 崩溃 | 是 |
| P0-5 | TTS 中文样本 | 自然度可接受 | 是 |
| P0-6 | TTS timestamps | 有则记录;无则句子级降级 | 否 |
| P0-7 | Chat 模式回归 | 对话 / 总结 / 存笔记不受影响 | 是 |
P0-1、P0-7 验证**双模式隔离**P0-2 ~ P0-6 验证**语音链路**。
---
## 16. 性能与扩展
### 16.1 长论文策略
| 页数 | 策略 |
|------|------|
| ≤ 30 | 全量逐段备课 |
| 30–80 | 逐段 lazy:按章节或用户触发 |
| > 80 | 默认仅整体讲解;逐段需用户确认 |
### 16.2 并发控制
- 备课 LLMbeats 小批量并行 ≤ 3
- TTS:队列化,beats 优先
- 同一 paper 备课互斥锁
- SQLite 写串行队列
### 16.3 伴读库窗口(P1
`libraryWindow.ts` 扫描 `paper_cache` 表 + Zotero Items>5000 items 时仅索引「有过备课记录」的 attachment。
---
## 17. 可观测性
| 类型 | 内容 |
|------|------|
| 结构化日志 | `mode: "lecture"`, paperId, stage, durationMs, errorCode |
| 用户可见进度 | 解析 %、beats n/total、TTS n/total |
| 调试导出 | 单篇 metrics JSON(开发者菜单,可选) |
日志前缀建议 `[ChatPapers:Lecture]`,与 `[ChatPapers:Chat]` 区分。
---
## 18. 风险与对策
| 风险 | 对策 |
|------|------|
| 语音模块影响文字对话稳定性 | 依赖隔离 + pane 级 try/catch + P0-7 回归 |
| MinerU 打包复杂 | Phase 0 验证;文档化安装 |
| refs 幻觉 | 校验器 + degraded beat |
| TTS 无 timestamps | 高亮降级链 |
| SQLite 在插件环境不可用 | Phase 0 备选 sql.js / JSON 分文件 |
| 双 pane 内存占用 | onDestroy 释放 audio、取消进行中的 fetch |
---
## 19. 开放问题
| # | 问题 | 影响 |
|---|------|------|
| 1 | SQLitebetter-sqlite3 vs sql.js vs 纯 JSON | 存储层选型 |
| 2 | MinerU 分发方式 | 安装体验 |
| 3 | 问答 TTS:流式 vs 整段合成 | AskQuestion 复杂度 |
| 4 | 文字对话是否未来支持 TTS 朗读回复 | v1.1+ 可统一 TtsClient |
| 5 | Chat 会话 → 语音问答上下文导入 | v1.1 产品决策 |
---
## 20. 文档关系
```
docs/requirements.md ChatPapers 文字对话需求(不变)
docs/development.md 现有工程与工具链(不变)
docs/newidea/
├── PLAN.md / PRD.md BanDu 原始策划(参考)
├── ARCHITECTURE.md 独立插件架构(参考)
├── DUAL_MODE_PRD.md ★ 双模式集成需求
└── DUAL_MODE_ARCHITECTURE.md ★ 本文:双模式集成架构
```
**实施顺序**:需求看 DUAL_MODE_PRD.md → 架构看本文 → 领域细节参考 ARCHITECTURE.md §4–§6。
---
## 21. 文档修订记录
| 版本 | 日期 | 说明 |
|------|------|------|
| v0.1 | 2026-08-29 | 初稿:ChatPapers 双模式集成架构;语音模块命名 `lecture/` |