@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/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
+ }