@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/src/index.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { handleToolCall } from './tools/tts'
|
|
2
|
+
import { registerProvider, getProvider, listProviders } from './providers/base'
|
|
3
|
+
import { MiniMaxProvider, MiniMaxConfig } from './providers/minimax'
|
|
4
|
+
import { Speaker, SpeakerOptions } from './core/speaker'
|
|
5
|
+
import { TTSError, TTSErrorCode } from './core/types'
|
|
6
|
+
import { StreamReader } from './core/stream-reader'
|
|
7
|
+
import { StreamingSynthesizer } from './core/streaming-synthesizer'
|
|
8
|
+
import { StreamPlayer } from './core/stream-player'
|
|
9
|
+
|
|
10
|
+
export const pluginInfo = {
|
|
11
|
+
name: 'ocosay',
|
|
12
|
+
version: '1.0.0',
|
|
13
|
+
description: 'OpenCode TTS 播放插件 - 支持 MiniMax TTS',
|
|
14
|
+
author: '',
|
|
15
|
+
license: 'MIT'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let speaker: Speaker | undefined
|
|
19
|
+
let streamReader: StreamReader | undefined
|
|
20
|
+
let streamingSynthesizer: StreamingSynthesizer | undefined
|
|
21
|
+
let streamPlayer: StreamPlayer | undefined
|
|
22
|
+
let initialized = false
|
|
23
|
+
let autoReadEnabled = false
|
|
24
|
+
|
|
25
|
+
export interface InitializeOptions {
|
|
26
|
+
defaultProvider?: string
|
|
27
|
+
defaultModel?: 'sync' | 'async' | 'stream'
|
|
28
|
+
defaultVoice?: string
|
|
29
|
+
providers?: {
|
|
30
|
+
minimax?: MiniMaxConfig
|
|
31
|
+
}
|
|
32
|
+
autoRead?: boolean
|
|
33
|
+
streamBufferSize?: number
|
|
34
|
+
streamBufferTimeout?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function initialize(config?: InitializeOptions): Promise<void> {
|
|
38
|
+
if (initialized) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (config?.providers?.minimax) {
|
|
43
|
+
const minimaxProvider = new MiniMaxProvider(config.providers.minimax)
|
|
44
|
+
registerProvider('minimax', minimaxProvider)
|
|
45
|
+
await minimaxProvider.initialize()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const speakerOptions: SpeakerOptions = {
|
|
49
|
+
defaultProvider: config?.defaultProvider || 'minimax',
|
|
50
|
+
defaultModel: config?.defaultModel || 'stream',
|
|
51
|
+
defaultVoice: config?.defaultVoice
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
speaker = new Speaker(speakerOptions)
|
|
55
|
+
|
|
56
|
+
if (config?.autoRead) {
|
|
57
|
+
autoReadEnabled = true
|
|
58
|
+
initializeStreamComponents(config)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
initialized = true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function initializeStreamComponents(config: InitializeOptions): void {
|
|
65
|
+
const provider = getProvider(config?.defaultProvider || 'minimax')
|
|
66
|
+
|
|
67
|
+
streamReader = new StreamReader(
|
|
68
|
+
config?.streamBufferSize || 30,
|
|
69
|
+
config?.streamBufferTimeout || 2000
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
streamingSynthesizer = new StreamingSynthesizer({
|
|
73
|
+
provider,
|
|
74
|
+
voice: config?.defaultVoice,
|
|
75
|
+
speed: 1.0,
|
|
76
|
+
volume: 1.0,
|
|
77
|
+
pitch: 1.0
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const playerEvents = {
|
|
81
|
+
onStart: () => {},
|
|
82
|
+
onEnd: () => {},
|
|
83
|
+
onProgress: (bytesWritten: number) => {},
|
|
84
|
+
onError: (error: Error) => console.error('Stream player error:', error),
|
|
85
|
+
onStop: () => {}
|
|
86
|
+
}
|
|
87
|
+
streamPlayer = new StreamPlayer({ events: playerEvents })
|
|
88
|
+
|
|
89
|
+
const synthesisQueue: string[] = []
|
|
90
|
+
let isSynthesizing = false
|
|
91
|
+
|
|
92
|
+
async function processQueue(): Promise<void> {
|
|
93
|
+
while (synthesisQueue.length > 0) {
|
|
94
|
+
const text = synthesisQueue.shift()!
|
|
95
|
+
isSynthesizing = true
|
|
96
|
+
try {
|
|
97
|
+
await streamingSynthesizer?.synthesize(text)
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Synthesize error:', error)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
isSynthesizing = false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
streamReader.on('textReady', (text: string) => {
|
|
106
|
+
synthesisQueue.push(text)
|
|
107
|
+
if (!isSynthesizing) {
|
|
108
|
+
processQueue()
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
streamingSynthesizer.on('chunk', (chunk: Buffer) => {
|
|
113
|
+
if (streamPlayer) {
|
|
114
|
+
streamPlayer.write(chunk)
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
streamingSynthesizer.on('done', () => {
|
|
119
|
+
if (streamPlayer) {
|
|
120
|
+
streamPlayer.end()
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const TuiEventBus = (global as any).__opencode_tuieventbus__
|
|
125
|
+
if (TuiEventBus) {
|
|
126
|
+
const eventBus = new TuiEventBus()
|
|
127
|
+
let messagePartDeltaHandler: ((event: any) => void) | undefined
|
|
128
|
+
let messagePartEndHandler: (() => void) | undefined
|
|
129
|
+
|
|
130
|
+
messagePartDeltaHandler = (event: any) => {
|
|
131
|
+
if (event?.properties) {
|
|
132
|
+
streamReader?.handleDelta(
|
|
133
|
+
event.sessionId || '',
|
|
134
|
+
event.messageId || '',
|
|
135
|
+
event.partId || '',
|
|
136
|
+
event.properties.delta || ''
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
messagePartEndHandler = () => {
|
|
141
|
+
streamReader?.handleEnd()
|
|
142
|
+
}
|
|
143
|
+
eventBus.on('message.part.delta', messagePartDeltaHandler)
|
|
144
|
+
eventBus.on('message.part.end', messagePartEndHandler)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getSpeaker(): Speaker {
|
|
149
|
+
if (!speaker) {
|
|
150
|
+
throw new TTSError(
|
|
151
|
+
'Plugin not initialized. Call initialize() first.',
|
|
152
|
+
TTSErrorCode.UNKNOWN,
|
|
153
|
+
'ocosay'
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
return speaker
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function isStreamEnabled(): boolean {
|
|
160
|
+
return streamReader !== undefined && streamingSynthesizer !== undefined && streamPlayer !== undefined
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function isAutoReadEnabled(): boolean {
|
|
164
|
+
return autoReadEnabled
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getStreamStatus(): { isActive: boolean; bytesWritten: number; state: string } {
|
|
168
|
+
if (!streamReader) {
|
|
169
|
+
return { isActive: false, bytesWritten: 0, state: 'not_initialized' }
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
isActive: streamReader.isActive(),
|
|
173
|
+
bytesWritten: streamPlayer?.getBytesWritten() ?? 0,
|
|
174
|
+
state: streamReader.getState()
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getStreamReader(): StreamReader | undefined {
|
|
179
|
+
return streamReader
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function getStreamingSynthesizer(): StreamingSynthesizer | undefined {
|
|
183
|
+
return streamingSynthesizer
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function getStreamPlayer(): StreamPlayer | undefined {
|
|
187
|
+
return streamPlayer
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function destroy(): Promise<void> {
|
|
191
|
+
if (streamReader) {
|
|
192
|
+
streamReader.reset()
|
|
193
|
+
streamReader = undefined
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (streamingSynthesizer) {
|
|
197
|
+
streamingSynthesizer.reset()
|
|
198
|
+
streamingSynthesizer = undefined
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (streamPlayer) {
|
|
202
|
+
await streamPlayer.stop()
|
|
203
|
+
streamPlayer = undefined
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (speaker) {
|
|
207
|
+
await speaker.destroy()
|
|
208
|
+
speaker = undefined
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
for (const providerName of listProviders()) {
|
|
212
|
+
try {
|
|
213
|
+
getProvider(providerName)?.destroy()
|
|
214
|
+
} catch (e) {}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
initialized = false
|
|
218
|
+
autoReadEnabled = false
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export { handleToolCall }
|
|
222
|
+
export const toolNames = [
|
|
223
|
+
'tts_speak',
|
|
224
|
+
'tts_stop',
|
|
225
|
+
'tts_pause',
|
|
226
|
+
'tts_resume',
|
|
227
|
+
'tts_list_voices',
|
|
228
|
+
'tts_list_providers',
|
|
229
|
+
'tts_status',
|
|
230
|
+
'tts_stream_speak',
|
|
231
|
+
'tts_stream_stop',
|
|
232
|
+
'tts_stream_status'
|
|
233
|
+
]
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { tool } from '@opencode-ai/plugin'
|
|
2
|
+
import type { Plugin, PluginInput, PluginOptions } from '@opencode-ai/plugin'
|
|
3
|
+
import { handleToolCall } from './index'
|
|
4
|
+
import { initialize, destroy } from './index'
|
|
5
|
+
|
|
6
|
+
const pluginName = 'ocosay'
|
|
7
|
+
|
|
8
|
+
const ttsSpeakTool = tool({
|
|
9
|
+
description: '将文本转换为语音并播放',
|
|
10
|
+
args: {
|
|
11
|
+
text: tool.schema.string().describe('要转换的文本内容'),
|
|
12
|
+
provider: tool.schema.string().optional().describe('TTS 提供商名称'),
|
|
13
|
+
voice: tool.schema.string().optional().describe('音色 ID'),
|
|
14
|
+
model: tool.schema.enum(['sync', 'async', 'stream']).optional().describe('合成模式'),
|
|
15
|
+
speed: tool.schema.number().optional().describe('语速 0.5-2.0'),
|
|
16
|
+
volume: tool.schema.number().optional().describe('音量 0-100'),
|
|
17
|
+
pitch: tool.schema.number().optional().describe('音调 0.5-2.0')
|
|
18
|
+
},
|
|
19
|
+
execute: async (args) => {
|
|
20
|
+
const result = await handleToolCall('tts_speak', args)
|
|
21
|
+
if (result.success === false) {
|
|
22
|
+
throw new Error(result.error || 'Unknown error')
|
|
23
|
+
}
|
|
24
|
+
return result
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const ttsStopTool = tool({
|
|
29
|
+
description: '停止当前 TTS 播放',
|
|
30
|
+
args: {},
|
|
31
|
+
execute: async () => {
|
|
32
|
+
const result = await handleToolCall('tts_stop')
|
|
33
|
+
if (result.success === false) {
|
|
34
|
+
throw new Error(result.error || 'Unknown error')
|
|
35
|
+
}
|
|
36
|
+
return result
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const ttsPauseTool = tool({
|
|
41
|
+
description: '暂停当前 TTS 播放',
|
|
42
|
+
args: {},
|
|
43
|
+
execute: async () => {
|
|
44
|
+
const result = await handleToolCall('tts_pause')
|
|
45
|
+
if (result.success === false) {
|
|
46
|
+
throw new Error(result.error || 'Unknown error')
|
|
47
|
+
}
|
|
48
|
+
return result
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const ttsResumeTool = tool({
|
|
53
|
+
description: '恢复暂停的 TTS 播放',
|
|
54
|
+
args: {},
|
|
55
|
+
execute: async () => {
|
|
56
|
+
const result = await handleToolCall('tts_resume')
|
|
57
|
+
if (result.success === false) {
|
|
58
|
+
throw new Error(result.error || 'Unknown error')
|
|
59
|
+
}
|
|
60
|
+
return result
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const ttsListVoicesTool = tool({
|
|
65
|
+
description: '列出可用的音色',
|
|
66
|
+
args: {
|
|
67
|
+
provider: tool.schema.string().optional().describe('TTS 提供商名称')
|
|
68
|
+
},
|
|
69
|
+
execute: async (args) => {
|
|
70
|
+
const result = await handleToolCall('tts_list_voices', args)
|
|
71
|
+
if (result.success === false) {
|
|
72
|
+
throw new Error(result.error || 'Unknown error')
|
|
73
|
+
}
|
|
74
|
+
return result
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
const ttsListProvidersTool = tool({
|
|
79
|
+
description: '列出所有已注册的 TTS 提供商',
|
|
80
|
+
args: {},
|
|
81
|
+
execute: async () => {
|
|
82
|
+
const result = await handleToolCall('tts_list_providers')
|
|
83
|
+
if (result.success === false) {
|
|
84
|
+
throw new Error(result.error || 'Unknown error')
|
|
85
|
+
}
|
|
86
|
+
return result
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const ttsStatusTool = tool({
|
|
91
|
+
description: '获取当前 TTS 播放状态',
|
|
92
|
+
args: {},
|
|
93
|
+
execute: async () => {
|
|
94
|
+
const result = await handleToolCall('tts_status')
|
|
95
|
+
if (result.success === false) {
|
|
96
|
+
throw new Error(result.error || 'Unknown error')
|
|
97
|
+
}
|
|
98
|
+
return result
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
const ttsStreamSpeakTool = tool({
|
|
103
|
+
description: '启动流式朗读(豆包模式),订阅AI回复并边生成边朗读',
|
|
104
|
+
args: {
|
|
105
|
+
text: tool.schema.string().optional().describe('初始文本(可选)'),
|
|
106
|
+
voice: tool.schema.string().optional().describe('音色ID'),
|
|
107
|
+
model: tool.schema.enum(['sync', 'async', 'stream']).optional().describe('合成模式')
|
|
108
|
+
},
|
|
109
|
+
execute: async (args) => {
|
|
110
|
+
const result = await handleToolCall('tts_stream_speak', args)
|
|
111
|
+
if (result.success === false) {
|
|
112
|
+
throw new Error(result.error || 'Unknown error')
|
|
113
|
+
}
|
|
114
|
+
return result
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
const ttsStreamStopTool = tool({
|
|
119
|
+
description: '停止当前流式朗读',
|
|
120
|
+
args: {},
|
|
121
|
+
execute: async () => {
|
|
122
|
+
const result = await handleToolCall('tts_stream_stop')
|
|
123
|
+
if (result.success === false) {
|
|
124
|
+
throw new Error(result.error || 'Unknown error')
|
|
125
|
+
}
|
|
126
|
+
return result
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
const ttsStreamStatusTool = tool({
|
|
131
|
+
description: '获取当前流式朗读状态',
|
|
132
|
+
args: {},
|
|
133
|
+
execute: async () => {
|
|
134
|
+
const result = await handleToolCall('tts_stream_status')
|
|
135
|
+
if (result.success === false) {
|
|
136
|
+
throw new Error(result.error || 'Unknown error')
|
|
137
|
+
}
|
|
138
|
+
return typeof result === 'string' ? result : JSON.stringify(result)
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
const OcosayPlugin: Plugin = async (_input: PluginInput, _options?: PluginOptions) => {
|
|
143
|
+
console.info(`${pluginName}: initializing...`)
|
|
144
|
+
|
|
145
|
+
await initialize({
|
|
146
|
+
autoRead: false
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
tool: {
|
|
151
|
+
tts_speak: ttsSpeakTool,
|
|
152
|
+
tts_stop: ttsStopTool,
|
|
153
|
+
tts_pause: ttsPauseTool,
|
|
154
|
+
tts_resume: ttsResumeTool,
|
|
155
|
+
tts_list_voices: ttsListVoicesTool,
|
|
156
|
+
tts_list_providers: ttsListProvidersTool,
|
|
157
|
+
tts_status: ttsStatusTool,
|
|
158
|
+
tts_stream_speak: ttsStreamSpeakTool,
|
|
159
|
+
tts_stream_stop: ttsStreamStopTool,
|
|
160
|
+
tts_stream_status: ttsStreamStatusTool
|
|
161
|
+
},
|
|
162
|
+
config: async () => {}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export default OcosayPlugin
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTS Provider Base Class and Registry
|
|
3
|
+
* Provider 基类和注册机制
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { TTSProvider, TTSError, TTSErrorCode, Voice, TTSCapabilities, SpeakOptions, AudioResult } from '../core/types'
|
|
7
|
+
|
|
8
|
+
// Provider 注册表
|
|
9
|
+
const providers = new Map<string, TTSProvider>()
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 注册 TTS Provider
|
|
13
|
+
*/
|
|
14
|
+
export function registerProvider(name: string, provider: TTSProvider): void {
|
|
15
|
+
if (providers.has(name)) {
|
|
16
|
+
throw new TTSError(
|
|
17
|
+
`Provider "${name}" is already registered`,
|
|
18
|
+
TTSErrorCode.UNKNOWN,
|
|
19
|
+
'system'
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
providers.set(name, provider)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 获取 TTS Provider
|
|
27
|
+
*/
|
|
28
|
+
export function getProvider(name: string): TTSProvider {
|
|
29
|
+
const provider = providers.get(name)
|
|
30
|
+
if (!provider) {
|
|
31
|
+
throw new TTSError(
|
|
32
|
+
`TTS Provider "${name}" not found`,
|
|
33
|
+
TTSErrorCode.UNKNOWN,
|
|
34
|
+
'system'
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
return provider
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 获取所有已注册的 Provider 名称
|
|
42
|
+
*/
|
|
43
|
+
export function listProviders(): string[] {
|
|
44
|
+
return Array.from(providers.keys())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* 检查 Provider 是否已注册
|
|
49
|
+
*/
|
|
50
|
+
export function hasProvider(name: string): boolean {
|
|
51
|
+
return providers.has(name)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 注销 Provider
|
|
56
|
+
*/
|
|
57
|
+
export function unregisterProvider(name: string): boolean {
|
|
58
|
+
return providers.delete(name)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Provider 抽象基类,提供通用实现
|
|
63
|
+
*/
|
|
64
|
+
export abstract class BaseTTSProvider implements TTSProvider {
|
|
65
|
+
abstract name: string
|
|
66
|
+
abstract capabilities: TTSCapabilities
|
|
67
|
+
|
|
68
|
+
protected apiKey?: string
|
|
69
|
+
protected defaultVoice?: string
|
|
70
|
+
protected defaultModel: 'sync' | 'async' | 'stream' = 'stream'
|
|
71
|
+
|
|
72
|
+
async initialize(): Promise<void> {
|
|
73
|
+
// 子类可override
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async destroy(): Promise<void> {
|
|
77
|
+
// 子类可override
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 通用 speak 实现,处理通用逻辑
|
|
82
|
+
*/
|
|
83
|
+
async speak(text: string, options?: SpeakOptions): Promise<AudioResult> {
|
|
84
|
+
if (!text || text.trim().length === 0) {
|
|
85
|
+
throw new TTSError(
|
|
86
|
+
'Text cannot be empty',
|
|
87
|
+
TTSErrorCode.INVALID_PARAMS,
|
|
88
|
+
this.name
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const voice = options?.voice || this.defaultVoice
|
|
93
|
+
const model = options?.model || this.defaultModel
|
|
94
|
+
|
|
95
|
+
return this.doSpeak(text, voice, model, options)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 子类实现的实际 speak 逻辑
|
|
100
|
+
*/
|
|
101
|
+
protected abstract doSpeak(
|
|
102
|
+
text: string,
|
|
103
|
+
voice: string | undefined,
|
|
104
|
+
model: 'sync' | 'async' | 'stream',
|
|
105
|
+
options?: SpeakOptions
|
|
106
|
+
): Promise<AudioResult>
|
|
107
|
+
|
|
108
|
+
pause(): Promise<void> {
|
|
109
|
+
throw new TTSError(
|
|
110
|
+
'Pause is not supported by this provider',
|
|
111
|
+
TTSErrorCode.UNKNOWN,
|
|
112
|
+
this.name
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
resume(): Promise<void> {
|
|
117
|
+
throw new TTSError(
|
|
118
|
+
'Resume is not supported by this provider',
|
|
119
|
+
TTSErrorCode.UNKNOWN,
|
|
120
|
+
this.name
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
stop(): Promise<void> {
|
|
125
|
+
// 默认空实现
|
|
126
|
+
return Promise.resolve()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async listVoices(): Promise<Voice[]> {
|
|
130
|
+
// 默认返回空数组,子类可override
|
|
131
|
+
return []
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getCapabilities(): TTSCapabilities {
|
|
135
|
+
return this.capabilities
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 验证 API Key
|
|
140
|
+
*/
|
|
141
|
+
protected validateApiKey(): void {
|
|
142
|
+
if (!this.apiKey) {
|
|
143
|
+
throw new TTSError(
|
|
144
|
+
`API key is required for provider "${this.name}"`,
|
|
145
|
+
TTSErrorCode.AUTH,
|
|
146
|
+
this.name
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|