@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.
- package/README.md +556 -0
- package/TECH_PLAN.md +352 -0
- package/__mocks__/@opencode-ai/plugin.ts +32 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +95 -0
- package/dist/config.js.map +1 -0
- package/dist/core/backends/afplay-backend.d.ts +33 -0
- package/dist/core/backends/afplay-backend.d.ts.map +1 -0
- package/dist/core/backends/afplay-backend.js +144 -0
- package/dist/core/backends/afplay-backend.js.map +1 -0
- package/dist/core/backends/aplay-backend.d.ts +33 -0
- package/dist/core/backends/aplay-backend.d.ts.map +1 -0
- package/dist/core/backends/aplay-backend.js +142 -0
- package/dist/core/backends/aplay-backend.js.map +1 -0
- package/dist/core/backends/base.d.ts +94 -0
- package/dist/core/backends/base.d.ts.map +1 -0
- package/dist/core/backends/base.js +6 -0
- package/dist/core/backends/base.js.map +1 -0
- package/dist/core/backends/index.d.ts +29 -0
- package/dist/core/backends/index.d.ts.map +1 -0
- package/dist/core/backends/index.js +114 -0
- package/dist/core/backends/index.js.map +1 -0
- package/dist/core/backends/naudiodon-backend.d.ts +52 -0
- package/dist/core/backends/naudiodon-backend.d.ts.map +1 -0
- package/dist/core/backends/naudiodon-backend.js +123 -0
- package/dist/core/backends/naudiodon-backend.js.map +1 -0
- package/dist/core/backends/powershell-backend.d.ts +34 -0
- package/dist/core/backends/powershell-backend.d.ts.map +1 -0
- package/dist/core/backends/powershell-backend.js +154 -0
- package/dist/core/backends/powershell-backend.js.map +1 -0
- package/dist/core/player.d.ts +97 -0
- package/dist/core/player.d.ts.map +1 -0
- package/dist/core/player.js +268 -0
- package/dist/core/player.js.map +1 -0
- package/dist/core/speaker.d.ts +97 -0
- package/dist/core/speaker.d.ts.map +1 -0
- package/dist/core/speaker.js +218 -0
- package/dist/core/speaker.js.map +1 -0
- package/dist/core/stream-player.d.ts +107 -0
- package/dist/core/stream-player.d.ts.map +1 -0
- package/dist/core/stream-player.js +272 -0
- package/dist/core/stream-player.js.map +1 -0
- package/dist/core/stream-reader.d.ts +86 -0
- package/dist/core/stream-reader.d.ts.map +1 -0
- package/dist/core/stream-reader.js +172 -0
- package/dist/core/stream-reader.js.map +1 -0
- package/dist/core/streaming-synthesizer.d.ts +51 -0
- package/dist/core/streaming-synthesizer.d.ts.map +1 -0
- package/dist/core/streaming-synthesizer.js +103 -0
- package/dist/core/streaming-synthesizer.js.map +1 -0
- package/dist/core/types.d.ts +141 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +37 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +179 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +151 -0
- package/dist/plugin.js.map +1 -0
- package/dist/providers/base.d.ts +55 -0
- package/dist/providers/base.d.ts.map +1 -0
- package/dist/providers/base.js +95 -0
- package/dist/providers/base.js.map +1 -0
- package/dist/providers/minimax.d.ts +84 -0
- package/dist/providers/minimax.d.ts.map +1 -0
- package/dist/providers/minimax.js +387 -0
- package/dist/providers/minimax.js.map +1 -0
- package/dist/tools/tts.d.ts +147 -0
- package/dist/tools/tts.d.ts.map +1 -0
- package/dist/tools/tts.js +232 -0
- package/dist/tools/tts.js.map +1 -0
- package/jest.config.js +15 -0
- package/package.json +49 -0
- package/src/config.ts +121 -0
- package/src/core/backends/afplay-backend.ts +162 -0
- package/src/core/backends/aplay-backend.ts +160 -0
- package/src/core/backends/base.ts +117 -0
- package/src/core/backends/index.ts +128 -0
- package/src/core/backends/naudiodon-backend.ts +164 -0
- package/src/core/backends/powershell-backend.ts +173 -0
- package/src/core/player.ts +322 -0
- package/src/core/speaker.ts +283 -0
- package/src/core/stream-player.ts +326 -0
- package/src/core/stream-reader.ts +190 -0
- package/src/core/streaming-synthesizer.ts +123 -0
- package/src/core/types.ts +185 -0
- package/src/index.ts +233 -0
- package/src/plugin.ts +166 -0
- package/src/providers/base.ts +150 -0
- package/src/providers/minimax.ts +515 -0
- package/src/tools/tts.ts +277 -0
- package/src/types/naudiodon.d.ts +19 -0
- package/tests/__mocks__/@opencode-ai/plugin.ts +32 -0
- package/tests/backends.test.ts +831 -0
- package/tests/index.test.ts +201 -0
- package/tests/integration-test.d.ts +6 -0
- package/tests/integration-test.d.ts.map +1 -0
- package/tests/integration-test.js +84 -0
- package/tests/integration-test.js.map +1 -0
- package/tests/integration-test.ts +93 -0
- package/tests/p1-fixes.test.ts +160 -0
- package/tests/plugin.test.ts +311 -0
- package/tests/provider.test.d.ts +2 -0
- package/tests/provider.test.d.ts.map +1 -0
- package/tests/provider.test.js +69 -0
- package/tests/provider.test.js.map +1 -0
- package/tests/provider.test.ts +87 -0
- package/tests/speaker.test.d.ts +2 -0
- package/tests/speaker.test.d.ts.map +1 -0
- package/tests/speaker.test.js +63 -0
- package/tests/speaker.test.js.map +1 -0
- package/tests/speaker.test.ts +232 -0
- package/tests/stream-player.test.ts +303 -0
- package/tests/stream-reader.test.ts +269 -0
- package/tests/streaming-synthesizer.test.ts +225 -0
- package/tests/tts-tools.test.ts +270 -0
- package/tests/types.test.d.ts +2 -0
- package/tests/types.test.d.ts.map +1 -0
- package/tests/types.test.js +61 -0
- package/tests/types.test.js.map +1 -0
- package/tests/types.test.ts +63 -0
- package/tsconfig.json +22 -0
package/TECH_PLAN.md
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
# ocosay - OpenCode TTS 播放插件技术方案
|
|
2
|
+
|
|
3
|
+
## 项目概述
|
|
4
|
+
|
|
5
|
+
**ocosay** 是 OpenCode 的 TTS(文本转语音)播放插件,支持:
|
|
6
|
+
- 文本转语音自动播放
|
|
7
|
+
- 暂停/停止/恢复控制
|
|
8
|
+
- 多 TTS 模型可扩展架构
|
|
9
|
+
- 同步/异步/流式 多种合成模式
|
|
10
|
+
|
|
11
|
+
## 架构设计
|
|
12
|
+
|
|
13
|
+
### 核心原则
|
|
14
|
+
- **可扩展架构**:抽象 TTS Provider 接口,MiniMax 作为第一个实现,未来可扩展其他提供商
|
|
15
|
+
- **Provider 隔离**:每个 TTS 模型独立 Provider 实现,核心逻辑与模型无关
|
|
16
|
+
- **合成模式可配置**:通过 `model` 参数指定 `sync` | `async` | `stream`
|
|
17
|
+
|
|
18
|
+
### 目录结构
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
ocosay/
|
|
22
|
+
├── src/
|
|
23
|
+
│ ├── providers/
|
|
24
|
+
│ │ ├── base.ts # TTS Provider 接口定义
|
|
25
|
+
│ │ ├── minimax.ts # MiniMax TTS Provider
|
|
26
|
+
│ │ └── index.ts # Provider 导出
|
|
27
|
+
│ ├── core/
|
|
28
|
+
│ │ ├── player.ts # 音频播放引擎
|
|
29
|
+
│ │ ├── speaker.ts # 统一调用入口
|
|
30
|
+
│ │ └── types.ts # 公共类型定义
|
|
31
|
+
│ ├── tools/
|
|
32
|
+
│ │ └── tts.ts # OpenCode 自定义工具
|
|
33
|
+
│ ├── index.ts # Plugin 入口
|
|
34
|
+
│ └── config.ts # 配置管理
|
|
35
|
+
├── package.json
|
|
36
|
+
├── tsconfig.json
|
|
37
|
+
└── README.md
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 核心接口设计
|
|
43
|
+
|
|
44
|
+
### 错误类型
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// 错误类型定义
|
|
48
|
+
enum TTSErrorCode {
|
|
49
|
+
NETWORK = 'NETWORK',
|
|
50
|
+
AUTH = 'AUTH',
|
|
51
|
+
QUOTA = 'QUOTA',
|
|
52
|
+
INVALID_VOICE = 'INVALID_VOICE',
|
|
53
|
+
INVALID_PARAMS = 'INVALID_PARAMS',
|
|
54
|
+
PLAYER_ERROR = 'PLAYER_ERROR',
|
|
55
|
+
UNKNOWN = 'UNKNOWN'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class TTSError extends Error {
|
|
59
|
+
constructor(
|
|
60
|
+
message: string,
|
|
61
|
+
code: TTSErrorCode,
|
|
62
|
+
provider: string,
|
|
63
|
+
details?: unknown
|
|
64
|
+
) {
|
|
65
|
+
super(message)
|
|
66
|
+
this.name = 'TTSError'
|
|
67
|
+
this.code = code
|
|
68
|
+
this.provider = provider
|
|
69
|
+
this.details = details
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
code: TTSErrorCode
|
|
73
|
+
provider: string
|
|
74
|
+
details?: unknown
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 音色定义
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
interface Voice {
|
|
82
|
+
id: string
|
|
83
|
+
name: string
|
|
84
|
+
language?: string // 'zh-CN' | 'en-US' 等
|
|
85
|
+
gender?: 'male' | 'female' | 'neutral'
|
|
86
|
+
previewUrl?: string // 音色预览 URL
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 能力定义
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
interface TTSCapabilities {
|
|
94
|
+
// 基础能力(必有)
|
|
95
|
+
speak: true
|
|
96
|
+
|
|
97
|
+
// 高级能力
|
|
98
|
+
voiceClone?: boolean // 音色克隆支持
|
|
99
|
+
stream?: boolean // 流式合成支持
|
|
100
|
+
voiceList?: boolean // 音色列表支持
|
|
101
|
+
|
|
102
|
+
// 合成模式支持
|
|
103
|
+
sync?: boolean // 同步合成支持
|
|
104
|
+
async?: boolean // 异步合成支持
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 合成配置
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
// 合成模式枚举
|
|
112
|
+
type SynthesisModel = 'sync' | 'async' | 'stream'
|
|
113
|
+
|
|
114
|
+
// Speak 配置参数
|
|
115
|
+
interface SpeakOptions {
|
|
116
|
+
voice?: string // 音色 ID
|
|
117
|
+
model?: SynthesisModel // 合成模式,默认 stream
|
|
118
|
+
speed?: number // 语速 0.5-2.0
|
|
119
|
+
volume?: number // 音量 0-100
|
|
120
|
+
pitch?: number // 音调 0.5-2.0
|
|
121
|
+
|
|
122
|
+
// 音色克隆参数(可选)
|
|
123
|
+
sourceVoice?: string // 克隆源音色 URL
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface AudioResult {
|
|
127
|
+
audioData: Buffer | ReadableStream
|
|
128
|
+
sampleRate?: number
|
|
129
|
+
channels?: number
|
|
130
|
+
duration?: number
|
|
131
|
+
format: string // 'mp3' | 'wav' | 'flac'
|
|
132
|
+
isStream: boolean
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### TTSProvider 接口
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
interface TTSProvider {
|
|
140
|
+
name: string
|
|
141
|
+
capabilities: TTSCapabilities
|
|
142
|
+
|
|
143
|
+
// 生命周期
|
|
144
|
+
initialize(): Promise<void>
|
|
145
|
+
destroy(): Promise<void>
|
|
146
|
+
|
|
147
|
+
// 核心能力
|
|
148
|
+
speak(text: string, options?: SpeakOptions): Promise<AudioResult>
|
|
149
|
+
pause(): Promise<void>
|
|
150
|
+
resume(): Promise<void>
|
|
151
|
+
stop(): Promise<void>
|
|
152
|
+
listVoices(): Promise<Voice[]>
|
|
153
|
+
|
|
154
|
+
// 查询能力
|
|
155
|
+
getCapabilities(): TTSCapabilities
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Provider 注册机制
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
// 静态注册
|
|
163
|
+
const providers = new Map<string, TTSProvider>()
|
|
164
|
+
|
|
165
|
+
export function registerProvider(name: string, provider: TTSProvider): void {
|
|
166
|
+
providers.set(name, provider)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function getProvider(name: string): TTSProvider {
|
|
170
|
+
const provider = providers.get(name)
|
|
171
|
+
if (!provider) {
|
|
172
|
+
throw new TTSError(
|
|
173
|
+
`TTS Provider "${name}" not found`,
|
|
174
|
+
TTSErrorCode.UNKNOWN,
|
|
175
|
+
'system'
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
return provider
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function listProviders(): string[] {
|
|
182
|
+
return Array.from(providers.keys())
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function hasProvider(name: string): boolean {
|
|
186
|
+
return providers.has(name)
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 事件系统
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
// 事件类型
|
|
196
|
+
type TTSEvent =
|
|
197
|
+
| 'start' // 开始播放
|
|
198
|
+
| 'end' // 播放结束
|
|
199
|
+
| 'error' // 错误发生
|
|
200
|
+
| 'progress' // 播放进度(流式场景)
|
|
201
|
+
| 'pause' // 暂停
|
|
202
|
+
| 'resume' // 恢复
|
|
203
|
+
| 'stop' // 停止
|
|
204
|
+
|
|
205
|
+
// 事件处理器
|
|
206
|
+
interface SpeakerEvents {
|
|
207
|
+
on(event: 'start', handler: (text: string) => void): void
|
|
208
|
+
on(event: 'end', handler: (text: string) => void): void
|
|
209
|
+
on(event: 'error', handler: (error: TTSError) => void): void
|
|
210
|
+
on(event: 'progress', handler: (progress: { current: number; total: number }) => void): void
|
|
211
|
+
on(event: 'pause', handler: () => void): void
|
|
212
|
+
on(event: 'resume', handler: () => void): void
|
|
213
|
+
on(event: 'stop', handler: () => void): void
|
|
214
|
+
|
|
215
|
+
off(event: TTSEvent, handler: Function): void
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## OpenCode Plugin 集成
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
// src/index.ts
|
|
225
|
+
export default {
|
|
226
|
+
name: 'ocosay',
|
|
227
|
+
version: '1.0.0',
|
|
228
|
+
|
|
229
|
+
tools: [
|
|
230
|
+
{
|
|
231
|
+
name: 'tts_speak',
|
|
232
|
+
description: '将文本转换为语音并播放',
|
|
233
|
+
input: {
|
|
234
|
+
text: { type: 'string', required: true },
|
|
235
|
+
provider: { type: 'string', default: 'minimax' },
|
|
236
|
+
voice: { type: 'string', optional: true },
|
|
237
|
+
model: { type: 'string', enum: ['sync', 'async', 'stream'], default: 'stream' },
|
|
238
|
+
speed: { type: 'number', optional: true },
|
|
239
|
+
volume: { type: 'number', optional: true },
|
|
240
|
+
pitch: { type: 'number', optional: true }
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
name: 'tts_stop',
|
|
245
|
+
description: '停止当前播放'
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: 'tts_pause',
|
|
249
|
+
description: '暂停当前播放'
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
name: 'tts_resume',
|
|
253
|
+
description: '恢复暂停的播放'
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
name: 'tts_list_voices',
|
|
257
|
+
description: '列出可用音色',
|
|
258
|
+
input: {
|
|
259
|
+
provider: { type: 'string', default: 'minimax' }
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: 'tts_list_providers',
|
|
264
|
+
description: '列出所有已注册的 TTS 提供商'
|
|
265
|
+
}
|
|
266
|
+
],
|
|
267
|
+
|
|
268
|
+
session: {
|
|
269
|
+
idle: () => { /* 清理资源 - 调用所有 Provider.destroy() */ }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## MiniMax Provider 实现
|
|
277
|
+
|
|
278
|
+
### API 能力映射
|
|
279
|
+
|
|
280
|
+
| MiniMax API | 映射到 ocosay | 说明 |
|
|
281
|
+
|-------------|--------------|------|
|
|
282
|
+
| T2A v2.5 (WebSocket) | `model: 'stream'` | 流式播放 |
|
|
283
|
+
| T2A v3 (同步) | `model: 'sync'` | 同步返回 |
|
|
284
|
+
| T2A v2 (异步) | `model: 'async'` | 异步轮询 |
|
|
285
|
+
| 音色列表 | `listVoices()` | |
|
|
286
|
+
| 音色克隆 | `voiceClone()` (via sourceVoice) | 通过 sourceVoice 参数指定克隆源 |
|
|
287
|
+
|
|
288
|
+
### 配置项
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
interface MiniMaxConfig {
|
|
292
|
+
apiKey: string
|
|
293
|
+
appId?: string
|
|
294
|
+
voiceId?: string // 默认音色
|
|
295
|
+
model?: SynthesisModel // 默认合成模式
|
|
296
|
+
audioFormat?: 'mp3' | 'wav' | 'flac'
|
|
297
|
+
speed?: number // 0.5 - 2.0
|
|
298
|
+
volume?: number // 0 - 100
|
|
299
|
+
pitch?: number // 0.5 - 2.0
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function validateConfig(config: MiniMaxConfig): void {
|
|
303
|
+
if (!config.apiKey) {
|
|
304
|
+
throw new TTSError('API key is required', TTSErrorCode.AUTH, 'minimax')
|
|
305
|
+
}
|
|
306
|
+
if (config.speed !== undefined && (config.speed < 0.5 || config.speed > 2.0)) {
|
|
307
|
+
throw new TTSError('Speed must be between 0.5 and 2.0', TTSErrorCode.INVALID_PARAMS, 'minimax')
|
|
308
|
+
}
|
|
309
|
+
if (config.volume !== undefined && (config.volume < 0 || config.volume > 100)) {
|
|
310
|
+
throw new TTSError('Volume must be between 0 and 100', TTSErrorCode.INVALID_PARAMS, 'minimax')
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
## 评审修复记录
|
|
318
|
+
|
|
319
|
+
| 日期 | 修复内容 | 评审来源 |
|
|
320
|
+
|------|---------|---------|
|
|
321
|
+
| 2026-04-04 | speak() 添加 SpeakOptions 配置参数 | 萍萍 |
|
|
322
|
+
| 2026-04-04 | pause()/stop()/resume() 改为返回 Promise | 凤雏 |
|
|
323
|
+
| 2026-04-04 | 添加 TTSError 类型定义 | 凤雏 |
|
|
324
|
+
| 2026-04-04 | 同步/异步/流式 改为 model 参数可配置 | 老爷确认 |
|
|
325
|
+
| 2026-04-04 | 添加 Speaker 事件系统 | 小猪 |
|
|
326
|
+
| 2026-04-04 | 添加 listProviders() 方法 | 萍萍 |
|
|
327
|
+
| 2026-04-04 | 添加 Provider 生命周期 initialize()/destroy() | 凤雏 |
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 技术栈
|
|
332
|
+
|
|
333
|
+
- **语言**: TypeScript
|
|
334
|
+
- **运行时**: Node.js
|
|
335
|
+
- **音频播放**: node-simplespeaker / node-player / play-sound
|
|
336
|
+
- **HTTP 客户端**: axios
|
|
337
|
+
- **WebSocket**: ws (for MiniMax 流式)
|
|
338
|
+
- **构建**: npm
|
|
339
|
+
|
|
340
|
+
## 下一步计划
|
|
341
|
+
|
|
342
|
+
1. 创建项目结构
|
|
343
|
+
2. 实现 types.ts - 核心类型定义
|
|
344
|
+
3. 实现 base.ts - Provider 接口
|
|
345
|
+
4. 实现 config.ts - 配置管理
|
|
346
|
+
5. 实现 MiniMax Provider
|
|
347
|
+
6. 实现 player.ts - 音频播放引擎
|
|
348
|
+
7. 实现 speaker.ts - 统一调用入口
|
|
349
|
+
8. 实现 tools/tts.ts - OpenCode 工具
|
|
350
|
+
9. 实现 index.ts - Plugin 入口
|
|
351
|
+
10. 单元测试
|
|
352
|
+
11. 文档编写
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const schema = {
|
|
2
|
+
string: () => ({
|
|
3
|
+
describe: (desc: string) => ({ describe: desc }),
|
|
4
|
+
optional: () => ({
|
|
5
|
+
describe: (desc: string) => ({ describe: desc })
|
|
6
|
+
}),
|
|
7
|
+
number: () => ({
|
|
8
|
+
describe: (desc: string) => ({ describe: desc }),
|
|
9
|
+
optional: () => ({
|
|
10
|
+
describe: (desc: string) => ({ describe: desc })
|
|
11
|
+
})
|
|
12
|
+
}),
|
|
13
|
+
enum: (values: string[]) => ({
|
|
14
|
+
describe: (desc: string) => ({ describe: desc }),
|
|
15
|
+
optional: () => ({
|
|
16
|
+
describe: (desc: string) => ({ describe: desc })
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function tool({ description, args, execute }: any) {
|
|
23
|
+
return {
|
|
24
|
+
description,
|
|
25
|
+
args,
|
|
26
|
+
execute,
|
|
27
|
+
schema
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { tool, schema }
|
|
32
|
+
export const Plugin = jest.fn()
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Management
|
|
3
|
+
* 配置管理模块
|
|
4
|
+
*/
|
|
5
|
+
import { GlobalConfig, ProviderConfig, OcosayConfig } from './core/types';
|
|
6
|
+
declare const DEFAULT_CONFIG: OcosayConfig;
|
|
7
|
+
declare class ConfigManager {
|
|
8
|
+
private config;
|
|
9
|
+
private configPath;
|
|
10
|
+
private providers;
|
|
11
|
+
constructor(configPath?: string);
|
|
12
|
+
private loadConfig;
|
|
13
|
+
saveConfig(): void;
|
|
14
|
+
getGlobal(): OcosayConfig;
|
|
15
|
+
getProviderConfig(providerName: string): ProviderConfig | undefined;
|
|
16
|
+
setProviderConfig(providerName: string, config: ProviderConfig): void;
|
|
17
|
+
getDefaultProvider(): string;
|
|
18
|
+
setDefaultProvider(provider: string): void;
|
|
19
|
+
validateApiKey(provider: string, apiKey: string): void;
|
|
20
|
+
validateStreamConfig(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare const configManager: ConfigManager;
|
|
23
|
+
export { ConfigManager, DEFAULT_CONFIG };
|
|
24
|
+
export type { OcosayConfig, GlobalConfig, ProviderConfig };
|
|
25
|
+
export default configManager;
|
|
26
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAKzE,QAAA,MAAM,cAAc,EAAE,YAarB,CAAA;AAED,cAAM,aAAa;IACjB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,UAAU,CAAQ;IAC1B,OAAO,CAAC,SAAS,CAAqC;gBAE1C,UAAU,CAAC,EAAE,MAAM;IAO/B,OAAO,CAAC,UAAU;IAelB,UAAU,IAAI,IAAI;IAalB,SAAS,IAAI,YAAY;IAIzB,iBAAiB,CAAC,YAAY,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAInE,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,GAAG,IAAI;IAKrE,kBAAkB,IAAI,MAAM;IAI5B,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAK1C,cAAc,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAUtD,oBAAoB,IAAI,IAAI;CAgB7B;AAGD,eAAO,MAAM,aAAa,eAAsB,CAAA;AAEhD,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,CAAA;AACxC,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,CAAA;AAC1D,eAAe,aAAa,CAAA"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration Management
|
|
3
|
+
* 配置管理模块
|
|
4
|
+
*/
|
|
5
|
+
import { TTSError, TTSErrorCode } from './core/types';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
const DEFAULT_CONFIG = {
|
|
9
|
+
enabled: true,
|
|
10
|
+
autoPlay: false,
|
|
11
|
+
autoRead: false, // 豆包模式开关
|
|
12
|
+
streamMode: true, // 流式朗读模式
|
|
13
|
+
streamBufferSize: 30, // 缓冲字符数
|
|
14
|
+
streamBufferTimeout: 2000, // 缓冲区超时(ms)
|
|
15
|
+
provider: 'minimax',
|
|
16
|
+
ttsModel: 'speech-02-turbo',
|
|
17
|
+
baseURL: 'https://api.minimax.io',
|
|
18
|
+
speed: 1.0,
|
|
19
|
+
volume: 80,
|
|
20
|
+
pitch: 1.0
|
|
21
|
+
};
|
|
22
|
+
class ConfigManager {
|
|
23
|
+
config;
|
|
24
|
+
configPath;
|
|
25
|
+
providers = {};
|
|
26
|
+
constructor(configPath) {
|
|
27
|
+
this.configPath = configPath || './config.json';
|
|
28
|
+
const loaded = this.loadConfig();
|
|
29
|
+
this.config = loaded;
|
|
30
|
+
this.providers = loaded.providers || {};
|
|
31
|
+
}
|
|
32
|
+
loadConfig() {
|
|
33
|
+
try {
|
|
34
|
+
if (fs.existsSync(this.configPath)) {
|
|
35
|
+
const content = fs.readFileSync(this.configPath, 'utf-8');
|
|
36
|
+
const parsed = JSON.parse(content);
|
|
37
|
+
this.providers = parsed.providers || {};
|
|
38
|
+
const { providers, ...config } = parsed;
|
|
39
|
+
return { ...DEFAULT_CONFIG, ...config };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
console.warn('Failed to load config, using defaults');
|
|
44
|
+
}
|
|
45
|
+
return { ...DEFAULT_CONFIG };
|
|
46
|
+
}
|
|
47
|
+
saveConfig() {
|
|
48
|
+
try {
|
|
49
|
+
const dir = path.dirname(this.configPath);
|
|
50
|
+
if (!fs.existsSync(dir)) {
|
|
51
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
const configWithProviders = { ...this.config, providers: this.providers };
|
|
54
|
+
fs.writeFileSync(this.configPath, JSON.stringify(configWithProviders, null, 2));
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('Failed to save config:', error);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
getGlobal() {
|
|
61
|
+
return this.config;
|
|
62
|
+
}
|
|
63
|
+
getProviderConfig(providerName) {
|
|
64
|
+
return this.providers[providerName];
|
|
65
|
+
}
|
|
66
|
+
setProviderConfig(providerName, config) {
|
|
67
|
+
this.providers[providerName] = { enabled: true, ...config };
|
|
68
|
+
this.saveConfig();
|
|
69
|
+
}
|
|
70
|
+
getDefaultProvider() {
|
|
71
|
+
return this.config.provider || 'minimax';
|
|
72
|
+
}
|
|
73
|
+
setDefaultProvider(provider) {
|
|
74
|
+
this.config.provider = provider;
|
|
75
|
+
this.saveConfig();
|
|
76
|
+
}
|
|
77
|
+
validateApiKey(provider, apiKey) {
|
|
78
|
+
if (!apiKey) {
|
|
79
|
+
throw new TTSError(`API key is required for provider "${provider}"`, TTSErrorCode.AUTH, provider);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
validateStreamConfig() {
|
|
83
|
+
if ((this.config.streamBufferSize ?? 30) < 5) {
|
|
84
|
+
throw new TTSError('streamBufferSize must be at least 5', TTSErrorCode.INVALID_PARAMS, 'config');
|
|
85
|
+
}
|
|
86
|
+
if ((this.config.streamBufferTimeout ?? 2000) < 500) {
|
|
87
|
+
throw new TTSError('streamBufferTimeout must be at least 500ms', TTSErrorCode.INVALID_PARAMS, 'config');
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// 单例导出
|
|
92
|
+
export const configManager = new ConfigManager();
|
|
93
|
+
export { ConfigManager, DEFAULT_CONFIG };
|
|
94
|
+
export default configManager;
|
|
95
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Afplay Backend - macOS 平台音频播放后端
|
|
3
|
+
* 使用系统内置的 afplay 命令
|
|
4
|
+
*/
|
|
5
|
+
import { AudioBackend, BackendOptions } from './base';
|
|
6
|
+
/**
|
|
7
|
+
* AfplayBackend - macOS 原生音频播放后端
|
|
8
|
+
* 不支持真正的流式播放,需要先将数据写入临时文件
|
|
9
|
+
*/
|
|
10
|
+
export declare class AfplayBackend implements AudioBackend {
|
|
11
|
+
readonly name = "afplay";
|
|
12
|
+
readonly supportsStreaming = false;
|
|
13
|
+
private process?;
|
|
14
|
+
private tempFile?;
|
|
15
|
+
private events?;
|
|
16
|
+
private _started;
|
|
17
|
+
private _paused;
|
|
18
|
+
private _stopped;
|
|
19
|
+
private chunks;
|
|
20
|
+
private hasEnded;
|
|
21
|
+
constructor(options?: BackendOptions);
|
|
22
|
+
start(filePath: string): void;
|
|
23
|
+
write(chunk: Buffer): void;
|
|
24
|
+
end(): void;
|
|
25
|
+
pause(): void;
|
|
26
|
+
resume(): void;
|
|
27
|
+
stop(): void;
|
|
28
|
+
setVolume(_volume: number): void;
|
|
29
|
+
destroy(): void;
|
|
30
|
+
private cleanup;
|
|
31
|
+
private handleError;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=afplay-backend.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"afplay-backend.d.ts","sourceRoot":"","sources":["../../../src/core/backends/afplay-backend.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,YAAY,EAAsB,cAAc,EAAE,MAAM,QAAQ,CAAA;AAQzE;;;GAGG;AACH,qBAAa,aAAc,YAAW,YAAY;IAChD,QAAQ,CAAC,IAAI,YAAW;IACxB,QAAQ,CAAC,iBAAiB,SAAQ;IAElC,OAAO,CAAC,OAAO,CAAC,CAAc;IAC9B,OAAO,CAAC,QAAQ,CAAC,CAAQ;IACzB,OAAO,CAAC,MAAM,CAAC,CAAoB;IACnC,OAAO,CAAC,QAAQ,CAAQ;IACxB,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,QAAQ,CAAQ;IAExB,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,QAAQ,CAAQ;gBAEZ,OAAO,GAAE,cAAmB;IAIxC,KAAK,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAgC7B,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAM1B,GAAG,IAAI,IAAI;IAaX,KAAK,IAAI,IAAI;IAcb,MAAM,IAAI,IAAI;IAcd,IAAI,IAAI,IAAI;IAsBZ,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI;IAIhC,OAAO,IAAI,IAAI;IAIf,OAAO,CAAC,OAAO;IAaf,OAAO,CAAC,WAAW;CAGpB"}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Afplay Backend - macOS 平台音频播放后端
|
|
3
|
+
* 使用系统内置的 afplay 命令
|
|
4
|
+
*/
|
|
5
|
+
import { execFile } from 'child_process';
|
|
6
|
+
import { tmpdir } from 'os';
|
|
7
|
+
import { join } from 'path';
|
|
8
|
+
import { writeFileSync, unlinkSync, existsSync } from 'fs';
|
|
9
|
+
// 白名单:只允许特定路径格式(禁止 - 防止命令注入)
|
|
10
|
+
const SAFE_PATH_REGEX = /^[\w\/\.]+$/;
|
|
11
|
+
/**
|
|
12
|
+
* AfplayBackend - macOS 原生音频播放后端
|
|
13
|
+
* 不支持真正的流式播放,需要先将数据写入临时文件
|
|
14
|
+
*/
|
|
15
|
+
export class AfplayBackend {
|
|
16
|
+
name = 'afplay';
|
|
17
|
+
supportsStreaming = false;
|
|
18
|
+
process;
|
|
19
|
+
tempFile;
|
|
20
|
+
events;
|
|
21
|
+
_started = false;
|
|
22
|
+
_paused = false;
|
|
23
|
+
_stopped = false;
|
|
24
|
+
// P0-4: 缓冲所有chunk,等end()时一次性写入文件
|
|
25
|
+
chunks = [];
|
|
26
|
+
hasEnded = false;
|
|
27
|
+
constructor(options = {}) {
|
|
28
|
+
this.events = options.events;
|
|
29
|
+
}
|
|
30
|
+
start(filePath) {
|
|
31
|
+
if (this._started)
|
|
32
|
+
return;
|
|
33
|
+
if (!SAFE_PATH_REGEX.test(filePath)) {
|
|
34
|
+
throw new Error(`Invalid file path: ${filePath}`);
|
|
35
|
+
}
|
|
36
|
+
this.tempFile = filePath;
|
|
37
|
+
this._started = true;
|
|
38
|
+
this._stopped = false;
|
|
39
|
+
this.events?.onStart?.();
|
|
40
|
+
// 启动播放进程
|
|
41
|
+
this.process = execFile('afplay', [filePath], (error) => {
|
|
42
|
+
if (this._stopped)
|
|
43
|
+
return;
|
|
44
|
+
if (error) {
|
|
45
|
+
this.handleError(error);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// 播放正常结束
|
|
49
|
+
this._started = false;
|
|
50
|
+
this.events?.onEnd?.();
|
|
51
|
+
});
|
|
52
|
+
this.process.on('error', (error) => {
|
|
53
|
+
this.handleError(error);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
write(chunk) {
|
|
57
|
+
if (this._stopped)
|
|
58
|
+
return;
|
|
59
|
+
// P0-4: 缓冲所有chunk,等end()时一次性写入
|
|
60
|
+
this.chunks.push(chunk);
|
|
61
|
+
}
|
|
62
|
+
end() {
|
|
63
|
+
if (this._stopped || this.hasEnded)
|
|
64
|
+
return;
|
|
65
|
+
this.hasEnded = true;
|
|
66
|
+
if (this.chunks.length === 0)
|
|
67
|
+
return;
|
|
68
|
+
// P0-4: 所有chunk缓冲完毕后,一次性写入文件并播放
|
|
69
|
+
this.tempFile = join(tmpdir(), `ocosay-${Date.now()}.wav`);
|
|
70
|
+
writeFileSync(this.tempFile, Buffer.concat(this.chunks));
|
|
71
|
+
this.chunks = [];
|
|
72
|
+
this.start(this.tempFile);
|
|
73
|
+
}
|
|
74
|
+
pause() {
|
|
75
|
+
if (!this._started || this._paused || this._stopped)
|
|
76
|
+
return;
|
|
77
|
+
if (this.process) {
|
|
78
|
+
try {
|
|
79
|
+
this.process.kill('SIGSTOP');
|
|
80
|
+
this._paused = true;
|
|
81
|
+
this.events?.onPause?.();
|
|
82
|
+
}
|
|
83
|
+
catch (e) {
|
|
84
|
+
// SIGSTOP 可能失败
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
resume() {
|
|
89
|
+
if (!this._paused || this._stopped)
|
|
90
|
+
return;
|
|
91
|
+
if (this.process) {
|
|
92
|
+
try {
|
|
93
|
+
this.process.kill('SIGCONT');
|
|
94
|
+
this._paused = false;
|
|
95
|
+
this.events?.onResume?.();
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
// SIGCONT 可能失败
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
stop() {
|
|
103
|
+
this._stopped = true;
|
|
104
|
+
this._started = false;
|
|
105
|
+
this._paused = false;
|
|
106
|
+
if (this.process) {
|
|
107
|
+
try {
|
|
108
|
+
this.process.kill('SIGTERM');
|
|
109
|
+
}
|
|
110
|
+
catch (e) {
|
|
111
|
+
// 忽略错误
|
|
112
|
+
}
|
|
113
|
+
this.process = undefined;
|
|
114
|
+
}
|
|
115
|
+
// 清理临时文件
|
|
116
|
+
this.cleanup();
|
|
117
|
+
this.chunks = [];
|
|
118
|
+
this.hasEnded = false;
|
|
119
|
+
this.events?.onStop?.();
|
|
120
|
+
}
|
|
121
|
+
setVolume(_volume) {
|
|
122
|
+
// afplay 不支持命令行设置音量
|
|
123
|
+
}
|
|
124
|
+
destroy() {
|
|
125
|
+
this.stop();
|
|
126
|
+
}
|
|
127
|
+
cleanup() {
|
|
128
|
+
if (this.tempFile && this.tempFile.startsWith(tmpdir())) {
|
|
129
|
+
try {
|
|
130
|
+
if (existsSync(this.tempFile)) {
|
|
131
|
+
unlinkSync(this.tempFile);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (e) {
|
|
135
|
+
// 忽略清理错误
|
|
136
|
+
}
|
|
137
|
+
this.tempFile = undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
handleError(error) {
|
|
141
|
+
this.events?.onError?.(error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=afplay-backend.js.map
|