@mingxy/ocosay 1.0.3 → 1.0.5

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 (62) hide show
  1. package/README.md +12 -0
  2. package/dist/config.js +2 -2
  3. package/dist/config.js.map +1 -1
  4. package/dist/plugin.d.ts +2 -4
  5. package/dist/plugin.d.ts.map +1 -1
  6. package/dist/plugin.js +6 -4
  7. package/dist/plugin.js.map +2 -2
  8. package/package.json +1 -1
  9. package/TECH_PLAN.md +0 -352
  10. package/__mocks__/@opencode-ai/plugin.ts +0 -32
  11. package/jest.config.js +0 -15
  12. package/src/config.ts +0 -183
  13. package/src/core/backends/afplay-backend.ts +0 -162
  14. package/src/core/backends/aplay-backend.ts +0 -160
  15. package/src/core/backends/base.ts +0 -117
  16. package/src/core/backends/index.ts +0 -128
  17. package/src/core/backends/naudiodon-backend.ts +0 -164
  18. package/src/core/backends/powershell-backend.ts +0 -173
  19. package/src/core/player.ts +0 -322
  20. package/src/core/speaker.ts +0 -283
  21. package/src/core/stream-player.ts +0 -326
  22. package/src/core/stream-reader.ts +0 -190
  23. package/src/core/streaming-synthesizer.ts +0 -123
  24. package/src/core/types.ts +0 -185
  25. package/src/index.ts +0 -236
  26. package/src/plugin.ts +0 -178
  27. package/src/providers/base.ts +0 -150
  28. package/src/providers/minimax.ts +0 -515
  29. package/src/tools/tts.ts +0 -277
  30. package/src/types/config.ts +0 -38
  31. package/src/types/naudiodon.d.ts +0 -19
  32. package/tests/__mocks__/@opencode-ai/plugin.ts +0 -32
  33. package/tests/backends.test.ts +0 -831
  34. package/tests/config.test.ts +0 -327
  35. package/tests/index.test.ts +0 -201
  36. package/tests/integration-test.d.ts +0 -6
  37. package/tests/integration-test.d.ts.map +0 -1
  38. package/tests/integration-test.js +0 -84
  39. package/tests/integration-test.js.map +0 -1
  40. package/tests/integration-test.ts +0 -93
  41. package/tests/p1-fixes.test.ts +0 -160
  42. package/tests/plugin.test.ts +0 -312
  43. package/tests/provider.test.d.ts +0 -2
  44. package/tests/provider.test.d.ts.map +0 -1
  45. package/tests/provider.test.js +0 -69
  46. package/tests/provider.test.js.map +0 -1
  47. package/tests/provider.test.ts +0 -87
  48. package/tests/speaker.test.d.ts +0 -2
  49. package/tests/speaker.test.d.ts.map +0 -1
  50. package/tests/speaker.test.js +0 -63
  51. package/tests/speaker.test.js.map +0 -1
  52. package/tests/speaker.test.ts +0 -232
  53. package/tests/stream-player.test.ts +0 -303
  54. package/tests/stream-reader.test.ts +0 -269
  55. package/tests/streaming-synthesizer.test.ts +0 -225
  56. package/tests/tts-tools.test.ts +0 -270
  57. package/tests/types.test.d.ts +0 -2
  58. package/tests/types.test.d.ts.map +0 -1
  59. package/tests/types.test.js +0 -61
  60. package/tests/types.test.js.map +0 -1
  61. package/tests/types.test.ts +0 -63
  62. 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
- }