@mingxy/ocosay 1.0.3 → 1.0.4

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 (59) hide show
  1. package/dist/config.js +2 -2
  2. package/dist/config.js.map +1 -1
  3. package/dist/plugin.js +2 -2
  4. package/dist/plugin.js.map +1 -1
  5. package/package.json +1 -1
  6. package/TECH_PLAN.md +0 -352
  7. package/__mocks__/@opencode-ai/plugin.ts +0 -32
  8. package/jest.config.js +0 -15
  9. package/src/config.ts +0 -183
  10. package/src/core/backends/afplay-backend.ts +0 -162
  11. package/src/core/backends/aplay-backend.ts +0 -160
  12. package/src/core/backends/base.ts +0 -117
  13. package/src/core/backends/index.ts +0 -128
  14. package/src/core/backends/naudiodon-backend.ts +0 -164
  15. package/src/core/backends/powershell-backend.ts +0 -173
  16. package/src/core/player.ts +0 -322
  17. package/src/core/speaker.ts +0 -283
  18. package/src/core/stream-player.ts +0 -326
  19. package/src/core/stream-reader.ts +0 -190
  20. package/src/core/streaming-synthesizer.ts +0 -123
  21. package/src/core/types.ts +0 -185
  22. package/src/index.ts +0 -236
  23. package/src/plugin.ts +0 -178
  24. package/src/providers/base.ts +0 -150
  25. package/src/providers/minimax.ts +0 -515
  26. package/src/tools/tts.ts +0 -277
  27. package/src/types/config.ts +0 -38
  28. package/src/types/naudiodon.d.ts +0 -19
  29. package/tests/__mocks__/@opencode-ai/plugin.ts +0 -32
  30. package/tests/backends.test.ts +0 -831
  31. package/tests/config.test.ts +0 -327
  32. package/tests/index.test.ts +0 -201
  33. package/tests/integration-test.d.ts +0 -6
  34. package/tests/integration-test.d.ts.map +0 -1
  35. package/tests/integration-test.js +0 -84
  36. package/tests/integration-test.js.map +0 -1
  37. package/tests/integration-test.ts +0 -93
  38. package/tests/p1-fixes.test.ts +0 -160
  39. package/tests/plugin.test.ts +0 -312
  40. package/tests/provider.test.d.ts +0 -2
  41. package/tests/provider.test.d.ts.map +0 -1
  42. package/tests/provider.test.js +0 -69
  43. package/tests/provider.test.js.map +0 -1
  44. package/tests/provider.test.ts +0 -87
  45. package/tests/speaker.test.d.ts +0 -2
  46. package/tests/speaker.test.d.ts.map +0 -1
  47. package/tests/speaker.test.js +0 -63
  48. package/tests/speaker.test.js.map +0 -1
  49. package/tests/speaker.test.ts +0 -232
  50. package/tests/stream-player.test.ts +0 -303
  51. package/tests/stream-reader.test.ts +0 -269
  52. package/tests/streaming-synthesizer.test.ts +0 -225
  53. package/tests/tts-tools.test.ts +0 -270
  54. package/tests/types.test.d.ts +0 -2
  55. package/tests/types.test.d.ts.map +0 -1
  56. package/tests/types.test.js +0 -61
  57. package/tests/types.test.js.map +0 -1
  58. package/tests/types.test.ts +0 -63
  59. package/tsconfig.json +0 -22
package/src/index.ts DELETED
@@ -1,236 +0,0 @@
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
- if (!config.providers.minimax.apiKey) {
44
- throw new Error('[ocosay] API Key is required. Please set minimax.apiKey in ~/.config/opencode/ocosay.jsonc')
45
- }
46
- const minimaxProvider = new MiniMaxProvider(config.providers.minimax)
47
- registerProvider('minimax', minimaxProvider)
48
- await minimaxProvider.initialize()
49
- }
50
-
51
- const speakerOptions: SpeakerOptions = {
52
- defaultProvider: config?.defaultProvider || 'minimax',
53
- defaultModel: config?.defaultModel || 'stream',
54
- defaultVoice: config?.defaultVoice
55
- }
56
-
57
- speaker = new Speaker(speakerOptions)
58
-
59
- if (config?.autoRead) {
60
- autoReadEnabled = true
61
- initializeStreamComponents(config)
62
- }
63
-
64
- initialized = true
65
- }
66
-
67
- function initializeStreamComponents(config: InitializeOptions): void {
68
- const provider = getProvider(config?.defaultProvider || 'minimax')
69
-
70
- streamReader = new StreamReader(
71
- config?.streamBufferSize || 30,
72
- config?.streamBufferTimeout || 2000
73
- )
74
-
75
- streamingSynthesizer = new StreamingSynthesizer({
76
- provider,
77
- voice: config?.defaultVoice,
78
- speed: 1.0,
79
- volume: 1.0,
80
- pitch: 1.0
81
- })
82
-
83
- const playerEvents = {
84
- onStart: () => {},
85
- onEnd: () => {},
86
- onProgress: (bytesWritten: number) => {},
87
- onError: (error: Error) => console.error('Stream player error:', error),
88
- onStop: () => {}
89
- }
90
- streamPlayer = new StreamPlayer({ events: playerEvents })
91
-
92
- const synthesisQueue: string[] = []
93
- let isSynthesizing = false
94
-
95
- async function processQueue(): Promise<void> {
96
- while (synthesisQueue.length > 0) {
97
- const text = synthesisQueue.shift()!
98
- isSynthesizing = true
99
- try {
100
- await streamingSynthesizer?.synthesize(text)
101
- } catch (error) {
102
- console.error('Synthesize error:', error)
103
- }
104
- }
105
- isSynthesizing = false
106
- }
107
-
108
- streamReader.on('textReady', (text: string) => {
109
- synthesisQueue.push(text)
110
- if (!isSynthesizing) {
111
- processQueue()
112
- }
113
- })
114
-
115
- streamingSynthesizer.on('chunk', (chunk: Buffer) => {
116
- if (streamPlayer) {
117
- streamPlayer.write(chunk)
118
- }
119
- })
120
-
121
- streamingSynthesizer.on('done', () => {
122
- if (streamPlayer) {
123
- streamPlayer.end()
124
- }
125
- })
126
-
127
- const TuiEventBus = (global as any).__opencode_tuieventbus__
128
- if (TuiEventBus) {
129
- const eventBus = new TuiEventBus()
130
- let messagePartDeltaHandler: ((event: any) => void) | undefined
131
- let messagePartEndHandler: (() => void) | undefined
132
-
133
- messagePartDeltaHandler = (event: any) => {
134
- if (event?.properties) {
135
- streamReader?.handleDelta(
136
- event.sessionId || '',
137
- event.messageId || '',
138
- event.partId || '',
139
- event.properties.delta || ''
140
- )
141
- }
142
- }
143
- messagePartEndHandler = () => {
144
- streamReader?.handleEnd()
145
- }
146
- eventBus.on('message.part.delta', messagePartDeltaHandler)
147
- eventBus.on('message.part.end', messagePartEndHandler)
148
- }
149
- }
150
-
151
- export function getSpeaker(): Speaker {
152
- if (!speaker) {
153
- throw new TTSError(
154
- 'Plugin not initialized. Call initialize() first.',
155
- TTSErrorCode.UNKNOWN,
156
- 'ocosay'
157
- )
158
- }
159
- return speaker
160
- }
161
-
162
- export function isStreamEnabled(): boolean {
163
- return streamReader !== undefined && streamingSynthesizer !== undefined && streamPlayer !== undefined
164
- }
165
-
166
- export function isAutoReadEnabled(): boolean {
167
- return autoReadEnabled
168
- }
169
-
170
- export function getStreamStatus(): { isActive: boolean; bytesWritten: number; state: string } {
171
- if (!streamReader) {
172
- return { isActive: false, bytesWritten: 0, state: 'not_initialized' }
173
- }
174
- return {
175
- isActive: streamReader.isActive(),
176
- bytesWritten: streamPlayer?.getBytesWritten() ?? 0,
177
- state: streamReader.getState()
178
- }
179
- }
180
-
181
- export function getStreamReader(): StreamReader | undefined {
182
- return streamReader
183
- }
184
-
185
- export function getStreamingSynthesizer(): StreamingSynthesizer | undefined {
186
- return streamingSynthesizer
187
- }
188
-
189
- export function getStreamPlayer(): StreamPlayer | undefined {
190
- return streamPlayer
191
- }
192
-
193
- export async function destroy(): Promise<void> {
194
- if (streamReader) {
195
- streamReader.reset()
196
- streamReader = undefined
197
- }
198
-
199
- if (streamingSynthesizer) {
200
- streamingSynthesizer.reset()
201
- streamingSynthesizer = undefined
202
- }
203
-
204
- if (streamPlayer) {
205
- await streamPlayer.stop()
206
- streamPlayer = undefined
207
- }
208
-
209
- if (speaker) {
210
- await speaker.destroy()
211
- speaker = undefined
212
- }
213
-
214
- for (const providerName of listProviders()) {
215
- try {
216
- getProvider(providerName)?.destroy()
217
- } catch (e) {}
218
- }
219
-
220
- initialized = false
221
- autoReadEnabled = false
222
- }
223
-
224
- export { handleToolCall }
225
- export const toolNames = [
226
- 'tts_speak',
227
- 'tts_stop',
228
- 'tts_pause',
229
- 'tts_resume',
230
- 'tts_list_voices',
231
- 'tts_list_providers',
232
- 'tts_status',
233
- 'tts_stream_speak',
234
- 'tts_stream_stop',
235
- 'tts_stream_status'
236
- ]
package/src/plugin.ts DELETED
@@ -1,178 +0,0 @@
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
- import { loadOrCreateConfig } from './config'
6
-
7
- const pluginName = 'ocosay'
8
-
9
- const ttsSpeakTool = tool({
10
- description: '将文本转换为语音并播放',
11
- args: {
12
- text: tool.schema.string().describe('要转换的文本内容'),
13
- provider: tool.schema.string().optional().describe('TTS 提供商名称'),
14
- voice: tool.schema.string().optional().describe('音色 ID'),
15
- model: tool.schema.enum(['sync', 'async', 'stream']).optional().describe('合成模式'),
16
- speed: tool.schema.number().optional().describe('语速 0.5-2.0'),
17
- volume: tool.schema.number().optional().describe('音量 0-100'),
18
- pitch: tool.schema.number().optional().describe('音调 0.5-2.0')
19
- },
20
- execute: async (args) => {
21
- const result = await handleToolCall('tts_speak', args)
22
- if (result.success === false) {
23
- throw new Error(result.error || 'Unknown error')
24
- }
25
- return result
26
- }
27
- })
28
-
29
- const ttsStopTool = tool({
30
- description: '停止当前 TTS 播放',
31
- args: {},
32
- execute: async () => {
33
- const result = await handleToolCall('tts_stop')
34
- if (result.success === false) {
35
- throw new Error(result.error || 'Unknown error')
36
- }
37
- return result
38
- }
39
- })
40
-
41
- const ttsPauseTool = tool({
42
- description: '暂停当前 TTS 播放',
43
- args: {},
44
- execute: async () => {
45
- const result = await handleToolCall('tts_pause')
46
- if (result.success === false) {
47
- throw new Error(result.error || 'Unknown error')
48
- }
49
- return result
50
- }
51
- })
52
-
53
- const ttsResumeTool = tool({
54
- description: '恢复暂停的 TTS 播放',
55
- args: {},
56
- execute: async () => {
57
- const result = await handleToolCall('tts_resume')
58
- if (result.success === false) {
59
- throw new Error(result.error || 'Unknown error')
60
- }
61
- return result
62
- }
63
- })
64
-
65
- const ttsListVoicesTool = tool({
66
- description: '列出可用的音色',
67
- args: {
68
- provider: tool.schema.string().optional().describe('TTS 提供商名称')
69
- },
70
- execute: async (args) => {
71
- const result = await handleToolCall('tts_list_voices', args)
72
- if (result.success === false) {
73
- throw new Error(result.error || 'Unknown error')
74
- }
75
- return result
76
- }
77
- })
78
-
79
- const ttsListProvidersTool = tool({
80
- description: '列出所有已注册的 TTS 提供商',
81
- args: {},
82
- execute: async () => {
83
- const result = await handleToolCall('tts_list_providers')
84
- if (result.success === false) {
85
- throw new Error(result.error || 'Unknown error')
86
- }
87
- return result
88
- }
89
- })
90
-
91
- const ttsStatusTool = tool({
92
- description: '获取当前 TTS 播放状态',
93
- args: {},
94
- execute: async () => {
95
- const result = await handleToolCall('tts_status')
96
- if (result.success === false) {
97
- throw new Error(result.error || 'Unknown error')
98
- }
99
- return result
100
- }
101
- })
102
-
103
- const ttsStreamSpeakTool = tool({
104
- description: '启动流式朗读(豆包模式),订阅AI回复并边生成边朗读',
105
- args: {
106
- text: tool.schema.string().optional().describe('初始文本(可选)'),
107
- voice: tool.schema.string().optional().describe('音色ID'),
108
- model: tool.schema.enum(['sync', 'async', 'stream']).optional().describe('合成模式')
109
- },
110
- execute: async (args) => {
111
- const result = await handleToolCall('tts_stream_speak', args)
112
- if (result.success === false) {
113
- throw new Error(result.error || 'Unknown error')
114
- }
115
- return result
116
- }
117
- })
118
-
119
- const ttsStreamStopTool = tool({
120
- description: '停止当前流式朗读',
121
- args: {},
122
- execute: async () => {
123
- const result = await handleToolCall('tts_stream_stop')
124
- if (result.success === false) {
125
- throw new Error(result.error || 'Unknown error')
126
- }
127
- return result
128
- }
129
- })
130
-
131
- const ttsStreamStatusTool = tool({
132
- description: '获取当前流式朗读状态',
133
- args: {},
134
- execute: async () => {
135
- const result = await handleToolCall('tts_stream_status')
136
- if (result.success === false) {
137
- throw new Error(result.error || 'Unknown error')
138
- }
139
- return typeof result === 'string' ? result : JSON.stringify(result)
140
- }
141
- })
142
-
143
- const OcosayPlugin: Plugin = async (_input: PluginInput, _options?: PluginOptions) => {
144
- console.info(`${pluginName}: initializing...`)
145
-
146
- const config = loadOrCreateConfig()
147
-
148
- await initialize({
149
- autoRead: config.autoRead,
150
- providers: {
151
- minimax: {
152
- apiKey: config.providers.minimax.apiKey,
153
- baseURL: config.providers.minimax.baseURL || undefined,
154
- voiceId: config.providers.minimax.voiceId || undefined
155
- }
156
- }
157
- })
158
-
159
- return {
160
- tool: {
161
- tts_speak: ttsSpeakTool,
162
- tts_stop: ttsStopTool,
163
- tts_pause: ttsPauseTool,
164
- tts_resume: ttsResumeTool,
165
- tts_list_voices: ttsListVoicesTool,
166
- tts_list_providers: ttsListProvidersTool,
167
- tts_status: ttsStatusTool,
168
- tts_stream_speak: ttsStreamSpeakTool,
169
- tts_stream_stop: ttsStreamStopTool,
170
- tts_stream_status: ttsStreamStatusTool
171
- },
172
- config: async () => {
173
- return
174
- }
175
- }
176
- }
177
-
178
- export default { server: OcosayPlugin }
@@ -1,150 +0,0 @@
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
- }