@mingxy/ocosay 1.0.0

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.
Files changed (126) hide show
  1. package/README.md +556 -0
  2. package/TECH_PLAN.md +352 -0
  3. package/__mocks__/@opencode-ai/plugin.ts +32 -0
  4. package/dist/config.d.ts +26 -0
  5. package/dist/config.d.ts.map +1 -0
  6. package/dist/config.js +95 -0
  7. package/dist/config.js.map +1 -0
  8. package/dist/core/backends/afplay-backend.d.ts +33 -0
  9. package/dist/core/backends/afplay-backend.d.ts.map +1 -0
  10. package/dist/core/backends/afplay-backend.js +144 -0
  11. package/dist/core/backends/afplay-backend.js.map +1 -0
  12. package/dist/core/backends/aplay-backend.d.ts +33 -0
  13. package/dist/core/backends/aplay-backend.d.ts.map +1 -0
  14. package/dist/core/backends/aplay-backend.js +142 -0
  15. package/dist/core/backends/aplay-backend.js.map +1 -0
  16. package/dist/core/backends/base.d.ts +94 -0
  17. package/dist/core/backends/base.d.ts.map +1 -0
  18. package/dist/core/backends/base.js +6 -0
  19. package/dist/core/backends/base.js.map +1 -0
  20. package/dist/core/backends/index.d.ts +29 -0
  21. package/dist/core/backends/index.d.ts.map +1 -0
  22. package/dist/core/backends/index.js +114 -0
  23. package/dist/core/backends/index.js.map +1 -0
  24. package/dist/core/backends/naudiodon-backend.d.ts +52 -0
  25. package/dist/core/backends/naudiodon-backend.d.ts.map +1 -0
  26. package/dist/core/backends/naudiodon-backend.js +123 -0
  27. package/dist/core/backends/naudiodon-backend.js.map +1 -0
  28. package/dist/core/backends/powershell-backend.d.ts +34 -0
  29. package/dist/core/backends/powershell-backend.d.ts.map +1 -0
  30. package/dist/core/backends/powershell-backend.js +154 -0
  31. package/dist/core/backends/powershell-backend.js.map +1 -0
  32. package/dist/core/player.d.ts +97 -0
  33. package/dist/core/player.d.ts.map +1 -0
  34. package/dist/core/player.js +268 -0
  35. package/dist/core/player.js.map +1 -0
  36. package/dist/core/speaker.d.ts +97 -0
  37. package/dist/core/speaker.d.ts.map +1 -0
  38. package/dist/core/speaker.js +218 -0
  39. package/dist/core/speaker.js.map +1 -0
  40. package/dist/core/stream-player.d.ts +107 -0
  41. package/dist/core/stream-player.d.ts.map +1 -0
  42. package/dist/core/stream-player.js +272 -0
  43. package/dist/core/stream-player.js.map +1 -0
  44. package/dist/core/stream-reader.d.ts +86 -0
  45. package/dist/core/stream-reader.d.ts.map +1 -0
  46. package/dist/core/stream-reader.js +172 -0
  47. package/dist/core/stream-reader.js.map +1 -0
  48. package/dist/core/streaming-synthesizer.d.ts +51 -0
  49. package/dist/core/streaming-synthesizer.d.ts.map +1 -0
  50. package/dist/core/streaming-synthesizer.js +103 -0
  51. package/dist/core/streaming-synthesizer.js.map +1 -0
  52. package/dist/core/types.d.ts +141 -0
  53. package/dist/core/types.d.ts.map +1 -0
  54. package/dist/core/types.js +37 -0
  55. package/dist/core/types.js.map +1 -0
  56. package/dist/index.d.ts +40 -0
  57. package/dist/index.d.ts.map +1 -0
  58. package/dist/index.js +179 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/plugin.d.ts +4 -0
  61. package/dist/plugin.d.ts.map +1 -0
  62. package/dist/plugin.js +151 -0
  63. package/dist/plugin.js.map +1 -0
  64. package/dist/providers/base.d.ts +55 -0
  65. package/dist/providers/base.d.ts.map +1 -0
  66. package/dist/providers/base.js +95 -0
  67. package/dist/providers/base.js.map +1 -0
  68. package/dist/providers/minimax.d.ts +84 -0
  69. package/dist/providers/minimax.d.ts.map +1 -0
  70. package/dist/providers/minimax.js +387 -0
  71. package/dist/providers/minimax.js.map +1 -0
  72. package/dist/tools/tts.d.ts +147 -0
  73. package/dist/tools/tts.d.ts.map +1 -0
  74. package/dist/tools/tts.js +232 -0
  75. package/dist/tools/tts.js.map +1 -0
  76. package/jest.config.js +15 -0
  77. package/package.json +49 -0
  78. package/src/config.ts +121 -0
  79. package/src/core/backends/afplay-backend.ts +162 -0
  80. package/src/core/backends/aplay-backend.ts +160 -0
  81. package/src/core/backends/base.ts +117 -0
  82. package/src/core/backends/index.ts +128 -0
  83. package/src/core/backends/naudiodon-backend.ts +164 -0
  84. package/src/core/backends/powershell-backend.ts +173 -0
  85. package/src/core/player.ts +322 -0
  86. package/src/core/speaker.ts +283 -0
  87. package/src/core/stream-player.ts +326 -0
  88. package/src/core/stream-reader.ts +190 -0
  89. package/src/core/streaming-synthesizer.ts +123 -0
  90. package/src/core/types.ts +185 -0
  91. package/src/index.ts +233 -0
  92. package/src/plugin.ts +166 -0
  93. package/src/providers/base.ts +150 -0
  94. package/src/providers/minimax.ts +515 -0
  95. package/src/tools/tts.ts +277 -0
  96. package/src/types/naudiodon.d.ts +19 -0
  97. package/tests/__mocks__/@opencode-ai/plugin.ts +32 -0
  98. package/tests/backends.test.ts +831 -0
  99. package/tests/index.test.ts +201 -0
  100. package/tests/integration-test.d.ts +6 -0
  101. package/tests/integration-test.d.ts.map +1 -0
  102. package/tests/integration-test.js +84 -0
  103. package/tests/integration-test.js.map +1 -0
  104. package/tests/integration-test.ts +93 -0
  105. package/tests/p1-fixes.test.ts +160 -0
  106. package/tests/plugin.test.ts +311 -0
  107. package/tests/provider.test.d.ts +2 -0
  108. package/tests/provider.test.d.ts.map +1 -0
  109. package/tests/provider.test.js +69 -0
  110. package/tests/provider.test.js.map +1 -0
  111. package/tests/provider.test.ts +87 -0
  112. package/tests/speaker.test.d.ts +2 -0
  113. package/tests/speaker.test.d.ts.map +1 -0
  114. package/tests/speaker.test.js +63 -0
  115. package/tests/speaker.test.js.map +1 -0
  116. package/tests/speaker.test.ts +232 -0
  117. package/tests/stream-player.test.ts +303 -0
  118. package/tests/stream-reader.test.ts +269 -0
  119. package/tests/streaming-synthesizer.test.ts +225 -0
  120. package/tests/tts-tools.test.ts +270 -0
  121. package/tests/types.test.d.ts +2 -0
  122. package/tests/types.test.d.ts.map +1 -0
  123. package/tests/types.test.js +61 -0
  124. package/tests/types.test.js.map +1 -0
  125. package/tests/types.test.ts +63 -0
  126. package/tsconfig.json +22 -0
package/README.md ADDED
@@ -0,0 +1,556 @@
1
+ # ocosay - OpenCode TTS 播放插件
2
+
3
+ > 🎙️ 让 AI 开口说话 — 支持豆包模式边接收边朗读的 TTS 插件
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@mengxy/ocosay.svg)](https://www.npmjs.com/package/@mengxy/ocosay)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
7
+
8
+ ## Why This Exists
9
+
10
+ 在 AI 助手对话场景中,文字回复往往显得生硬。**ocosay** 让 AI 能够"开口说话",通过流式 TTS 技术实现边接收边朗读的豆包模式,极大提升对话交互体验。
11
+
12
+ **解决的问题:**
13
+ - 长文本等待焦虑 — 流式播放,首包延迟低
14
+ - 缺乏情感反馈 — 多种音色选择,支持语速/音调调节
15
+ - 交互体验单一 — 从"看"升级到"听"
16
+
17
+ ## Features
18
+
19
+ - 🎙️ **多 TTS 模型支持** - MiniMax 作为第一个实现,可扩展其他提供商
20
+ - 🔊 **多种合成模式** - 同步、异步、流式三种模式可选
21
+ - 🎭 **音色克隆** - 支持音色快速复刻
22
+ - 🎛️ **播放控制** - 暂停、恢复、停止
23
+ - 📋 **音色列表** - 查询可用音色
24
+ - 🔌 **OpenCode Plugin** - 无缝集成 OpenCode
25
+ - 📡 **豆包模式** - autoRead + TuiEventBus,边接收边朗读
26
+
27
+ ## Platform Support
28
+
29
+ | 平台 | 支持状态 | 说明 |
30
+ |------|----------|------|
31
+ | macOS | ✅ 完全支持 | Darwin 架构,native 播放 |
32
+ | Linux | ✅ 完全支持 | 需安装 mpv/libuv |
33
+ | Windows | ⚠️ 部分支持 | WSL 环境推荐 |
34
+
35
+ ## 安装方式
36
+
37
+ ### 方式一:ocx 安装(推荐)
38
+
39
+ ```bash
40
+ ocx install @mengxy/ocosay
41
+ ```
42
+
43
+ ### 方式二:手动配置
44
+
45
+ ```bash
46
+ npm install @mengxy/ocosay
47
+ ```
48
+
49
+ 在 OpenCode 配置文件中添加:
50
+
51
+ ```json
52
+ {
53
+ "plugins": [
54
+ "@mengxy/ocosay"
55
+ ],
56
+ "minimax": {
57
+ "apiKey": "your-api-key",
58
+ "voiceId": "male-qn-qingse"
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## 快速开始
64
+
65
+ ### 初始化
66
+
67
+ ```typescript
68
+ import { initialize } from 'ocosay'
69
+
70
+ await initialize({
71
+ providers: {
72
+ minimax: {
73
+ apiKey: 'your-api-key',
74
+ voiceId: 'male-qn-qingse',
75
+ model: 'stream'
76
+ }
77
+ }
78
+ })
79
+ ```
80
+
81
+ ### 基本使用
82
+
83
+ ```typescript
84
+ import { speak, stop, pause, resume } from 'ocosay'
85
+
86
+ // 说话
87
+ await speak('你好,世界!')
88
+
89
+ // 暂停
90
+ pause()
91
+
92
+ // 恢复
93
+ resume()
94
+
95
+ // 停止
96
+ await stop()
97
+ ```
98
+
99
+ ### 指定音色和模式
100
+
101
+ ```typescript
102
+ await speak('你好,世界!', {
103
+ provider: 'minimax',
104
+ voice: 'female-shaonv',
105
+ model: 'sync',
106
+ speed: 1.0,
107
+ volume: 80
108
+ })
109
+ ```
110
+
111
+ ## 豆包模式
112
+
113
+ 豆包模式是 ocosay 的核心特性 — 开启后,AI 助手的回复会**边接收边朗读**,实现类似豆包 App 的流畅播报体验。
114
+
115
+ ### 核心组件
116
+
117
+ | 组件 | 说明 |
118
+ |------|------|
119
+ | `StreamReader` | 文本缓冲器,收集并缓冲流式文本 |
120
+ | `StreamingSynthesizer` | 流式合成器,边接收边合成音频 |
121
+ | `StreamPlayer` | 流式播放器,边收边播 |
122
+ | `TuiEventBus` | 事件总线,监听 OpenCode 事件流 |
123
+
124
+ > **注意**:`autoRead` 是 `initialize()` 的配置选项(`autoRead: true`),而非独立组件。
125
+
126
+ ### 启用豆包模式
127
+
128
+ 豆包模式通过 `initialize()` 的 `autoRead: true` 配置选项启用:
129
+
130
+ ```typescript
131
+ import { initialize } from 'ocosay'
132
+
133
+ // 启用豆包模式
134
+ await initialize({
135
+ providers: {
136
+ minimax: {
137
+ apiKey: 'your-api-key',
138
+ voiceId: 'male-qn-qingse'
139
+ }
140
+ },
141
+ autoRead: true // 开启边接收边朗读
142
+ })
143
+ ```
144
+
145
+ ### 数据流与事件
146
+
147
+ 豆包模式的数据流如下:
148
+
149
+ ```
150
+ TuiEventBus (message.part.delta)
151
+ → StreamReader (缓冲文本)
152
+ → StreamingSynthesizer (流式合成)
153
+ → StreamPlayer (边收边播)
154
+ → TuiEventBus (message.part.end)
155
+ ```
156
+
157
+ | 事件 | 说明 |
158
+ |------|------|
159
+ | `message.part.delta` | AI回复文本增量事件,携带 `delta` 字段 |
160
+ | `message.part.end` | AI回复片段结束事件 |
161
+
162
+ ### TuiEventBus 事件监听
163
+
164
+ TuiEventBus 负责监听 OpenCode 的事件流,实现边接收边朗读:
165
+
166
+ ```typescript
167
+ import { TuiEventBus } from 'ocosay'
168
+
169
+ const bus = new TuiEventBus()
170
+
171
+ // 监听文本增量事件
172
+ bus.on('message.part.delta', (event) => {
173
+ // event.properties.delta 包含新增的文本
174
+ console.log('收到文本:', event.properties.delta)
175
+ })
176
+
177
+ // 监听回复片段结束
178
+ bus.on('message.part.end', () => {
179
+ console.log('回复片段结束')
180
+ })
181
+ ```
182
+
183
+ ## 工具列表
184
+
185
+ ocosay 提供 10 个工具用于 OpenCode 集成:
186
+
187
+ | 工具名 | 描述 | 参数 |
188
+ |--------|------|------|
189
+ | `tts_speak` | 将文本转换为语音并播放 | `text` (必填), `provider`, `voice`, `model`, `speed`, `volume`, `pitch` |
190
+ | `tts_stop` | 停止当前 TTS 播放 | - |
191
+ | `tts_pause` | 暂停当前 TTS 播放 | - |
192
+ | `tts_resume` | 恢复暂停的 TTS 播放 | - |
193
+ | `tts_list_voices` | 列出可用的音色 | `provider` |
194
+ | `tts_list_providers` | 列出所有已注册的 TTS 提供商 | - |
195
+ | `tts_status` | 获取当前 TTS 播放状态 | - |
196
+ | `tts_stream_speak` | 启动流式朗读(豆包模式) | `text`, `voice`, `model` |
197
+ | `tts_stream_stop` | 停止当前流式朗读 | - |
198
+ | `tts_stream_status` | 获取当前流式朗读状态 | - |
199
+
200
+ ### tts_speak
201
+
202
+ 将文本转换为语音并播放。
203
+
204
+ ```typescript
205
+ // 工具调用
206
+ await tts_speak({
207
+ text: '这是要播放的文本内容',
208
+ provider: 'minimax',
209
+ voice: 'male-qn-qingse',
210
+ speed: 1.0
211
+ })
212
+ ```
213
+
214
+ ### tts_stop
215
+
216
+ 停止当前 TTS 播放。
217
+
218
+ ```typescript
219
+ // 工具调用
220
+ await tts_stop()
221
+ ```
222
+
223
+ ### tts_pause
224
+
225
+ 暂停当前 TTS 播放。
226
+
227
+ ```typescript
228
+ // 工具调用
229
+ await tts_pause()
230
+ ```
231
+
232
+ ### tts_resume
233
+
234
+ 恢复暂停的 TTS 播放。
235
+
236
+ ```typescript
237
+ // 工具调用
238
+ await tts_resume()
239
+ ```
240
+
241
+ ### tts_list_voices
242
+
243
+ 列出可用的音色。
244
+
245
+ ```typescript
246
+ // 工具调用
247
+ const result = await tts_list_voices({ provider: 'minimax' })
248
+ // 返回: { success: true, voices: [...] }
249
+ ```
250
+
251
+ ### tts_list_providers
252
+
253
+ 列出所有已注册的 TTS 提供商。
254
+
255
+ ```typescript
256
+ // 工具调用
257
+ const result = await tts_list_providers()
258
+ // 返回: { success: true, providers: ['minimax'] }
259
+ ```
260
+
261
+ ### tts_status
262
+
263
+ 获取当前 TTS 播放状态。
264
+
265
+ ```typescript
266
+ // 工具调用
267
+ const status = await tts_status()
268
+ // 返回: { success: true, isPlaying: boolean, isPaused: boolean }
269
+ ```
270
+
271
+ ### tts_stream_speak
272
+
273
+ 启动流式朗读(豆包模式),订阅AI回复并边生成边朗读。
274
+
275
+ ```typescript
276
+ // 工具调用
277
+ await tts_stream_speak({
278
+ text: '初始文本(可选)',
279
+ voice: 'female-shaonv',
280
+ model: 'stream'
281
+ })
282
+ ```
283
+
284
+ ### tts_stream_stop
285
+
286
+ 停止当前流式朗读。
287
+
288
+ ```typescript
289
+ // 工具调用
290
+ await tts_stream_stop()
291
+ ```
292
+
293
+ ### tts_stream_status
294
+
295
+ 获取当前流式朗读状态。
296
+
297
+ ```typescript
298
+ // 工具调用
299
+ const status = await tts_stream_status()
300
+ // 返回: { success: true, isActive: boolean, bytesWritten: number, state: string }
301
+ ```
302
+
303
+ ## API 参考
304
+
305
+ ### initialize(config?)
306
+
307
+ 初始化 ocosay 插件。**必须首先调用此方法。**
308
+
309
+ | 参数 | 类型 | 说明 |
310
+ |------|------|------|
311
+ | config.defaultProvider | string | 默认 TTS 提供商,默认 minimax |
312
+ | config.defaultModel | 'sync' \| 'async' \| 'stream' | 默认合成模式,默认 stream |
313
+ | config.defaultVoice | string | 默认音色 ID |
314
+ | config.providers.minimax | MiniMaxConfig | MiniMax 提供商配置 |
315
+ | config.autoRead | boolean | 启用豆包模式(边接收边朗读) |
316
+ | config.streamBufferSize | number | 流式缓冲大小,默认 30 |
317
+ | config.streamBufferTimeout | number | 流式缓冲超时(ms),默认 2000 |
318
+
319
+ ```typescript
320
+ import { initialize } from 'ocosay'
321
+
322
+ await initialize({
323
+ providers: {
324
+ minimax: {
325
+ apiKey: 'your-api-key',
326
+ voiceId: 'male-qn-qingse',
327
+ model: 'stream'
328
+ }
329
+ },
330
+ autoRead: true // 启用豆包模式
331
+ })
332
+ ```
333
+
334
+ ### destroy()
335
+
336
+ 释放所有资源,清理插件状态。应在插件卸载或会话结束时调用。
337
+
338
+ ```typescript
339
+ import { destroy } from 'ocosay'
340
+
341
+ await destroy()
342
+ ```
343
+
344
+ ### TuiEventBus
345
+
346
+ 事件总线类,用于监听 OpenCode 事件流。
347
+
348
+ | 方法 | 说明 |
349
+ |------|------|
350
+ | `on(event, handler)` | 注册事件监听器 |
351
+ | `off(event, handler)` | 注销事件监听器 |
352
+
353
+ **可用事件:**
354
+
355
+ | 事件名 | 说明 | 事件属性 |
356
+ |--------|------|----------|
357
+ | `message.part.delta` | AI回复文本增量 | `sessionId`, `messageId`, `partId`, `properties.delta` |
358
+ | `message.part.end` | AI回复片段结束 | - |
359
+
360
+ ### speak(text, options?)
361
+
362
+ 将文本转换为语音并播放。
363
+
364
+ | 参数 | 类型 | 说明 |
365
+ |------|------|------|
366
+ | text | string | 要转换的文本 |
367
+ | options.provider | string | TTS 提供商,默认 minimax |
368
+ | options.voice | string | 音色 ID |
369
+ | options.model | 'sync' \| 'async' \| 'stream' | 合成模式,默认 stream |
370
+ | options.speed | number | 语速 0.5-2.0 |
371
+ | options.volume | number | 音量 0-100 |
372
+ | options.pitch | number | 音调 0.5-2.0 |
373
+
374
+ ### stop()
375
+
376
+ 停止当前播放。
377
+
378
+ ### pause()
379
+
380
+ 暂停当前播放。
381
+
382
+ ### resume()
383
+
384
+ 恢复暂停的播放。
385
+
386
+ ### listVoices(provider?)
387
+
388
+ 列出可用的音色。
389
+
390
+ ```typescript
391
+ const voices = await listVoices('minimax')
392
+ console.log(voices)
393
+ // [{ id: 'male-qn-qingse', name: '青年清澈', ... }]
394
+ ```
395
+
396
+ ### isAutoReadEnabled()
397
+
398
+ 检查豆包模式是否已启用。
399
+
400
+ ```typescript
401
+ import { isAutoReadEnabled } from 'ocosay'
402
+
403
+ const enabled = isAutoReadEnabled() // 返回: boolean
404
+ ```
405
+
406
+ ### isStreamEnabled()
407
+
408
+ 检查流式组件是否已初始化。
409
+
410
+ ```typescript
411
+ import { isStreamEnabled } from 'ocosay'
412
+
413
+ const enabled = isStreamEnabled() // 返回: boolean
414
+ ```
415
+
416
+ ### getStreamStatus()
417
+
418
+ 获取流式播放状态。
419
+
420
+ ```typescript
421
+ import { getStreamStatus } from 'ocosay'
422
+
423
+ const status = getStreamStatus()
424
+ // 返回: { isActive: boolean, bytesWritten: number, state: string }
425
+ ```
426
+
427
+ ## MiniMax 音色列表
428
+
429
+ | ID | 名称 | 语言 | 性别 |
430
+ |----|------|------|------|
431
+ | male-qn-qingse | 青年清澈 | zh-CN | male |
432
+ | male-qn-qingse_2 | 青年清澈v2 | zh-CN | male |
433
+ | female-shaonv | 少女 | zh-CN | female |
434
+ | male-baiming | 成熟男声 | zh-CN | male |
435
+ | female-tianmei | 甜美女声 | zh-CN | female |
436
+
437
+ ## 合成模式
438
+
439
+ | 模式 | 说明 | 适用场景 |
440
+ |------|----------|----------|
441
+ | stream | 流式合成,边生成边播放 | 长文本,首包延迟低 |
442
+ | sync | 同步合成,等待完整音频 | 短文本,一体化返回 |
443
+ | async | 异步合成,轮询获取结果 | 长文本,需要任务队列 |
444
+
445
+ ## 架构设计
446
+
447
+ ```
448
+ ┌─────────────────────────────────────────────────────────────────────┐
449
+ │ OpenCode │
450
+ │ ┌─────────────────────────────────────────────────────────────────┐ │
451
+ │ │ Plugin (ocosay) │ │
452
+ │ │ ┌───────────────────────────────────────────────────────────┐ │ │
453
+ │ │ │ Speaker │ │ │
454
+ │ │ │ ┌───────────────────────┐ ┌───────────────────────────┐ │ │ │
455
+ │ │ │ │ StreamReader │ │ StreamingSynthesizer │ │ │ │
456
+ │ │ │ │ (文本缓冲) │ │ (流式合成) │ │ │ │
457
+ │ │ │ └───────────────────────┘ └───────────────────────────┘ │ │ │
458
+ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │
459
+ │ │ │ │ StreamPlayer (边收边播) │ │ │ │
460
+ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │
461
+ │ │ │ │ │ │ │
462
+ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │
463
+ │ │ │ │ TuiEventBus │ │ │ │
464
+ │ │ │ │ (监听 OpenCode 事件流) │ │ │ │
465
+ │ │ │ └─────────────────────────────────────────────────────┘ │ │ │
466
+ │ │ └───────────────────────────────────────────────────────────┘ │ │
467
+ │ └─────────────────────────────────────────────────────────────────┘ │
468
+ │ │ │
469
+ │ ┌─────────────────────────────────────────────────────────────────┐ │
470
+ │ │ TTS Providers (可扩展) │ │
471
+ │ │ ┌──────────────────┐ ┌────────────────────────────────────┐ │ │
472
+ │ │ │ MiniMax │ │ (Future) │ │ │
473
+ │ │ └──────────────────┘ └────────────────────────────────────┘ │ │
474
+ │ └─────────────────────────────────────────────────────────────────┘ │
475
+ └─────────────────────────────────────────────────────────────────────┘
476
+ ```
477
+
478
+ ## 错误处理
479
+
480
+ ```typescript
481
+ import { speak } from 'ocosay'
482
+ import { TTSError, TTSErrorCode } from 'ocosay'
483
+
484
+ try {
485
+ await speak('你好')
486
+ } catch (error) {
487
+ if (error instanceof TTSError) {
488
+ console.error(`[${error.code}] ${error.message}`)
489
+ console.error(`Provider: ${error.provider}`)
490
+ }
491
+ }
492
+ ```
493
+
494
+ ### 错误码
495
+
496
+ | 错误码 | 说明 |
497
+ |--------|------|
498
+ | NETWORK | 网络错误 |
499
+ | AUTH | 认证失败 |
500
+ | QUOTA | 配额超限 |
501
+ | INVALID_VOICE | 无效音色 |
502
+ | INVALID_PARAMS | 无效参数 |
503
+ | PLAYER_ERROR | 播放错误 |
504
+ | UNKNOWN | 未知错误 |
505
+
506
+ ## Troubleshooting / FAQ
507
+
508
+ ### Q: 首包延迟太长怎么办?
509
+
510
+ **A:** 切换到 `stream` 模式,并选择较短的音色如 `female-shaonv`。
511
+
512
+ ### Q: 播放出现卡顿?
513
+
514
+ **A:** 检查网络状况,或降低语速 `speed: 0.8`。
515
+
516
+ ### Q: 如何切换不同音色?
517
+
518
+ **A:** 使用 `listVoices()` 查看可用音色,通过 `speak(text, { voice: 'voice-id' })` 指定。
519
+
520
+ ### Q: 豆包模式不工作?
521
+
522
+ **A:** 确认 TuiEventBus 已正确初始化,并检查 `autoRead` 开关状态。
523
+
524
+ ### Q: 报错 AUTH?
525
+
526
+ **A:** 检查 API Key 是否正确配置,或密钥是否过期。
527
+
528
+ ## 开发
529
+
530
+ ```bash
531
+ # 安装依赖
532
+ npm install
533
+
534
+ # 构建
535
+ npm run build
536
+
537
+ # 运行测试
538
+ npm test
539
+
540
+ # 监听模式
541
+ npm run watch
542
+ ```
543
+
544
+ ## Contributing
545
+
546
+ 欢迎提交 Issue 和 PR!
547
+
548
+ 1. Fork 本仓库
549
+ 2. 创建特性分支 (`git checkout -b feature/amazing`)
550
+ 3. 提交更改 (`git commit -m 'Add amazing feature'`)
551
+ 4. 推送分支 (`git push origin feature/amazing`)
552
+ 5. 提交 Pull Request
553
+
554
+ ## License
555
+
556
+ MIT