@xdfnet/ispeak 1.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Docs/ARCHITECTURE.md +177 -0
- package/Docs/HTTP Chunked:SSE/345/215/225/345/220/221/346/265/201/345/274/217-V3.md" +896 -0
- package/Docs//345/243/260/351/237/263/345/244/215/345/210/273API-V3.md +873 -0
- package/Docs//351/237/263/350/211/262/345/210/227/350/241/250.md +998 -0
- package/LICENSE +21 -0
- package/README.md +194 -0
- package/configs/com.iSpeak.plist +20 -0
- package/configs/config.example.json +18 -0
- package/configs/hook-speak.sh +76 -0
- package/go.mod +5 -0
- package/go.sum +4 -0
- package/main.go +858 -0
- package/npm/postinstall.js +134 -0
- package/package.json +46 -0
- package/scripts/ispeak +58 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# iSpeak 架构文档
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
|
|
5
|
+
iSpeak 是一个运行在 macOS 上的本地 TTS 播报守护进程,通过 Unix Socket 接收文本,调用火山引擎 TTS 流式 API,边合成边播放。
|
|
6
|
+
|
|
7
|
+
当前版本采用“任务仓库 + 单 speak worker”流式链路:
|
|
8
|
+
- speak worker:领取待合成任务,SSE 每到一段音频就写入播放器 stdin
|
|
9
|
+
- 播放器优先使用 `ffplay -i pipe:0`,没有 `ffplay` 时回退到完整音频 `afplay`
|
|
10
|
+
|
|
11
|
+
## 系统架构
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
15
|
+
│ 客户端 │
|
|
16
|
+
│ ispeak (bash CLI) ──nc -U──> ~/.config/iSpeak/ispeak.sock │
|
|
17
|
+
└─────────────────────────────────────────────────────────────┘
|
|
18
|
+
│
|
|
19
|
+
▼
|
|
20
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
21
|
+
│ ispeakd (Go Daemon) │
|
|
22
|
+
│ │
|
|
23
|
+
│ Socket Acceptor │
|
|
24
|
+
│ - net.Listener.Accept() │
|
|
25
|
+
│ - 每个连接读取文本并提交任务 │
|
|
26
|
+
│ │
|
|
27
|
+
│ Task Engine │
|
|
28
|
+
│ ┌───────────────────────────────────────────────────────┐ │
|
|
29
|
+
│ │ Task Repository (in-memory) │ │
|
|
30
|
+
│ │ - tasks: map[uint64]*Task │ │
|
|
31
|
+
│ │ - pendingSynth: []uint64 (FIFO) │ │
|
|
32
|
+
│ └───────────────────────────────────────────────────────┘ │
|
|
33
|
+
│ │ │
|
|
34
|
+
│ ▼ │
|
|
35
|
+
│ Speak Worker (single) │
|
|
36
|
+
│ - pending_synth -> speaking │
|
|
37
|
+
│ - 调用 TTS 流式接口(失败重试1次) │
|
|
38
|
+
│ - SSE audio chunk -> StreamPlayer.Write │
|
|
39
|
+
│ - 播放完成后删除任务;连续失败删除任务 │
|
|
40
|
+
│ │
|
|
41
|
+
└─────────────────────────────────────────────────────────────┘
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 核心数据结构
|
|
45
|
+
|
|
46
|
+
### Task
|
|
47
|
+
|
|
48
|
+
```go
|
|
49
|
+
type Task struct {
|
|
50
|
+
ID uint64 // 任务 ID(递增)
|
|
51
|
+
Text string // 过滤后的待合成文本
|
|
52
|
+
Status TaskStatus // 当前状态
|
|
53
|
+
Voice VoiceInfo // 任务音色快照
|
|
54
|
+
Cfg Config // 任务配置快照(提交时)
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### TaskStatus
|
|
59
|
+
|
|
60
|
+
```go
|
|
61
|
+
const (
|
|
62
|
+
TaskStatusPendingSynth TaskStatus = iota // 待合成
|
|
63
|
+
TaskStatusSpeaking // 流式合成播放中
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
说明:
|
|
68
|
+
- 终态不持久化。任务成功/失败后都会从仓库删除。
|
|
69
|
+
- 不保留 `failed/canceled/completed` 常驻状态,历史通过日志追踪。
|
|
70
|
+
|
|
71
|
+
### TaskEngine
|
|
72
|
+
|
|
73
|
+
```go
|
|
74
|
+
type TaskEngine struct {
|
|
75
|
+
mu sync.Mutex
|
|
76
|
+
|
|
77
|
+
nextID uint64
|
|
78
|
+
tasks map[uint64]*Task
|
|
79
|
+
pendingSynth []uint64
|
|
80
|
+
|
|
81
|
+
synthWake chan struct{}
|
|
82
|
+
|
|
83
|
+
synthesizeStreamFn func(ctx context.Context, cfg Config, text string, voice *VoiceInfo, onAudio func([]byte) error) error
|
|
84
|
+
newStreamPlayerFn func() (StreamPlayer, error)
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 播放器接口
|
|
89
|
+
|
|
90
|
+
```go
|
|
91
|
+
type StreamPlayer interface {
|
|
92
|
+
Write(audio []byte) error
|
|
93
|
+
CloseAndWait() error
|
|
94
|
+
Abort() error
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 状态机与逻辑
|
|
99
|
+
|
|
100
|
+
### 状态流转
|
|
101
|
+
|
|
102
|
+
```
|
|
103
|
+
pending_synth -> speaking -> delete
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 任务提交(核心规则)
|
|
107
|
+
|
|
108
|
+
`Submit(cleanedText, voice, cfg)` 原子执行:
|
|
109
|
+
1. 删除所有 `pending_synth` 任务
|
|
110
|
+
2. 创建新任务(`pending_synth`)
|
|
111
|
+
3. 唤醒 speak worker
|
|
112
|
+
|
|
113
|
+
策略说明:
|
|
114
|
+
- 只清理“未开始合成”的任务
|
|
115
|
+
- 不打断 `speaking`
|
|
116
|
+
|
|
117
|
+
### Speak worker 规则
|
|
118
|
+
|
|
119
|
+
1. FIFO 领取 `pending_synth` 任务并置 `speaking`
|
|
120
|
+
2. 启动 `StreamPlayer`
|
|
121
|
+
3. 调用 TTS 流式接口,SSE 每解析出一个音频 chunk 就写入播放器
|
|
122
|
+
4. TTS 结束后关闭播放器 stdin 并等待播放结束
|
|
123
|
+
5. 成功:删除任务
|
|
124
|
+
6. 连续失败:删除任务
|
|
125
|
+
|
|
126
|
+
## 消息流程
|
|
127
|
+
|
|
128
|
+
### 1. 接收并清洗消息
|
|
129
|
+
|
|
130
|
+
`handleConnection()`:
|
|
131
|
+
- 读取 socket 文本
|
|
132
|
+
- 解析 `{source:xxx}` 音色前缀
|
|
133
|
+
- `cleanText()` 过滤 Markdown/表格符号
|
|
134
|
+
- 将“过滤后文本”提交给 `TaskEngine.Submit`
|
|
135
|
+
|
|
136
|
+
### 2. 流式合成播放阶段
|
|
137
|
+
|
|
138
|
+
- speak worker 领取任务
|
|
139
|
+
- HTTP POST 火山引擎 TTS 接口
|
|
140
|
+
- 解析 SSE 流并 base64 解码音频 chunk
|
|
141
|
+
- 优先将 chunk 写入 `ffplay` stdin 实时播放
|
|
142
|
+
- 没有 `ffplay` 时缓存完整音频,结束后写临时 MP3 并用 `afplay` 播放
|
|
143
|
+
- 删除任务与临时文件
|
|
144
|
+
|
|
145
|
+
## 并发与一致性
|
|
146
|
+
|
|
147
|
+
- 单引擎锁 `mu` 保护任务仓库与 FIFO 队列
|
|
148
|
+
- 单 speak worker,保证播报顺序稳定
|
|
149
|
+
- `synthWake` 为缓冲 1 的唤醒信号,防止重复唤醒堆积
|
|
150
|
+
- FIFO 保证未开始任务公平顺序
|
|
151
|
+
|
|
152
|
+
## 失败与成本策略
|
|
153
|
+
|
|
154
|
+
- 新任务到达时仅清理 `pending_synth`,避免无效合成
|
|
155
|
+
- 流式合成/播放失败:整条播报重试 1 次后删除
|
|
156
|
+
- 执行中任务不打断,行为稳定、可预期
|
|
157
|
+
|
|
158
|
+
## 文件布局
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
~/.config/iSpeak/
|
|
162
|
+
├── config.json # API Key、音色配置
|
|
163
|
+
├── ispeak.sock # Unix Socket
|
|
164
|
+
├── ispeak.log # 日志(lumberjack 轮转)
|
|
165
|
+
└── hook-speak.sh # Claude/Codex Stop Hook
|
|
166
|
+
|
|
167
|
+
~/Library/LaunchAgents/
|
|
168
|
+
└── com.iSpeak.plist # launchd 服务配置
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## 稳定性设计
|
|
172
|
+
|
|
173
|
+
- 关键 worker 使用 `panic recover`
|
|
174
|
+
- 配置热更新(每次连接重新加载配置)
|
|
175
|
+
- 播放器子进程命令协议,保证“播完再删任务”
|
|
176
|
+
- 日志轮转(10MB/份,保留 3 份)
|
|
177
|
+
- 进程级 temp 目录,退出时自动清理
|