@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
@@ -1,162 +0,0 @@
1
- /**
2
- * Afplay Backend - macOS 平台音频播放后端
3
- * 使用系统内置的 afplay 命令
4
- */
5
-
6
- import { execFile, ChildProcess } from 'child_process'
7
- import { AudioBackend, AudioBackendEvents, BackendOptions } from './base'
8
- import { tmpdir } from 'os'
9
- import { join } from 'path'
10
- import { writeFileSync, unlinkSync, existsSync } from 'fs'
11
-
12
- // 白名单:只允许特定路径格式(禁止 - 防止命令注入)
13
- const SAFE_PATH_REGEX = /^[\w\/\.]+$/
14
-
15
- /**
16
- * AfplayBackend - macOS 原生音频播放后端
17
- * 不支持真正的流式播放,需要先将数据写入临时文件
18
- */
19
- export class AfplayBackend implements AudioBackend {
20
- readonly name = 'afplay'
21
- readonly supportsStreaming = false
22
-
23
- private process?: ChildProcess
24
- private tempFile?: string
25
- private events?: AudioBackendEvents
26
- private _started = false
27
- private _paused = false
28
- private _stopped = false
29
- // P0-4: 缓冲所有chunk,等end()时一次性写入文件
30
- private chunks: Buffer[] = []
31
- private hasEnded = false
32
-
33
- constructor(options: BackendOptions = {}) {
34
- this.events = options.events
35
- }
36
-
37
- start(filePath: string): void {
38
- if (this._started) return
39
-
40
- if (!SAFE_PATH_REGEX.test(filePath)) {
41
- throw new Error(`Invalid file path: ${filePath}`)
42
- }
43
-
44
- this.tempFile = filePath
45
- this._started = true
46
- this._stopped = false
47
-
48
- this.events?.onStart?.()
49
-
50
- // 启动播放进程
51
- this.process = execFile('afplay', [filePath], (error) => {
52
- if (this._stopped) return
53
-
54
- if (error) {
55
- this.handleError(error)
56
- return
57
- }
58
-
59
- // 播放正常结束
60
- this._started = false
61
- this.events?.onEnd?.()
62
- })
63
-
64
- this.process.on('error', (error) => {
65
- this.handleError(error)
66
- })
67
- }
68
-
69
- write(chunk: Buffer): void {
70
- if (this._stopped) return
71
- // P0-4: 缓冲所有chunk,等end()时一次性写入
72
- this.chunks.push(chunk)
73
- }
74
-
75
- end(): void {
76
- if (this._stopped || this.hasEnded) return
77
- this.hasEnded = true
78
-
79
- if (this.chunks.length === 0) return
80
-
81
- // P0-4: 所有chunk缓冲完毕后,一次性写入文件并播放
82
- this.tempFile = join(tmpdir(), `ocosay-${Date.now()}.wav`)
83
- writeFileSync(this.tempFile, Buffer.concat(this.chunks))
84
- this.chunks = []
85
- this.start(this.tempFile)
86
- }
87
-
88
- pause(): void {
89
- if (!this._started || this._paused || this._stopped) return
90
-
91
- if (this.process) {
92
- try {
93
- this.process.kill('SIGSTOP')
94
- this._paused = true
95
- this.events?.onPause?.()
96
- } catch (e) {
97
- // SIGSTOP 可能失败
98
- }
99
- }
100
- }
101
-
102
- resume(): void {
103
- if (!this._paused || this._stopped) return
104
-
105
- if (this.process) {
106
- try {
107
- this.process.kill('SIGCONT')
108
- this._paused = false
109
- this.events?.onResume?.()
110
- } catch (e) {
111
- // SIGCONT 可能失败
112
- }
113
- }
114
- }
115
-
116
- stop(): void {
117
- this._stopped = true
118
- this._started = false
119
- this._paused = false
120
-
121
- if (this.process) {
122
- try {
123
- this.process.kill('SIGTERM')
124
- } catch (e) {
125
- // 忽略错误
126
- }
127
- this.process = undefined
128
- }
129
-
130
- // 清理临时文件
131
- this.cleanup()
132
- this.chunks = []
133
- this.hasEnded = false
134
-
135
- this.events?.onStop?.()
136
- }
137
-
138
- setVolume(_volume: number): void {
139
- // afplay 不支持命令行设置音量
140
- }
141
-
142
- destroy(): void {
143
- this.stop()
144
- }
145
-
146
- private cleanup(): void {
147
- if (this.tempFile && this.tempFile.startsWith(tmpdir())) {
148
- try {
149
- if (existsSync(this.tempFile)) {
150
- unlinkSync(this.tempFile)
151
- }
152
- } catch (e) {
153
- // 忽略清理错误
154
- }
155
- this.tempFile = undefined
156
- }
157
- }
158
-
159
- private handleError(error: Error): void {
160
- this.events?.onError?.(error)
161
- }
162
- }
@@ -1,160 +0,0 @@
1
- /**
2
- * Aplay Backend - Linux 平台音频播放后端
3
- * 使用 ALSA 的 aplay 命令
4
- */
5
-
6
- import { execFile, ChildProcess } from 'child_process'
7
- import { AudioBackend, AudioBackendEvents, BackendOptions } from './base'
8
- import { tmpdir } from 'os'
9
- import { join } from 'path'
10
- import { writeFileSync, unlinkSync, existsSync } from 'fs'
11
-
12
- // 白名单:只允许特定路径格式(禁止 - 防止命令注入)
13
- const SAFE_PATH_REGEX = /^[\w\/\.]+$/
14
-
15
- /**
16
- * AplayBackend - Linux ALSA 音频播放后端
17
- * 不支持真正的流式播放,需要先将数据写入临时文件
18
- */
19
- export class AplayBackend implements AudioBackend {
20
- readonly name = 'aplay'
21
- readonly supportsStreaming = false
22
-
23
- private process?: ChildProcess
24
- private tempFile?: string
25
- private events?: AudioBackendEvents
26
- private _started = false
27
- private _paused = false
28
- private _stopped = false
29
- // P0-4: 缓冲所有chunk,等end()时一次性写入文件
30
- private chunks: Buffer[] = []
31
- private hasEnded = false
32
-
33
- constructor(options: BackendOptions = {}) {
34
- this.events = options.events
35
- }
36
-
37
- start(filePath: string): void {
38
- if (this._started) return
39
-
40
- if (!SAFE_PATH_REGEX.test(filePath)) {
41
- throw new Error(`Invalid file path: ${filePath}`)
42
- }
43
-
44
- this.tempFile = filePath
45
- this._started = true
46
- this._stopped = false
47
-
48
- this.events?.onStart?.()
49
-
50
- // 启动播放进程
51
- this.process = execFile('aplay', [filePath], (error) => {
52
- if (this._stopped) return
53
-
54
- if (error) {
55
- this.handleError(error)
56
- return
57
- }
58
-
59
- this._started = false
60
- this.events?.onEnd?.()
61
- })
62
-
63
- this.process.on('error', (error) => {
64
- this.handleError(error)
65
- })
66
- }
67
-
68
- write(chunk: Buffer): void {
69
- if (this._stopped) return
70
- // P0-4: 缓冲所有chunk,等end()时一次性写入
71
- this.chunks.push(chunk)
72
- }
73
-
74
- end(): void {
75
- if (this._stopped || this.hasEnded) return
76
- this.hasEnded = true
77
-
78
- if (this.chunks.length === 0) return
79
-
80
- // P0-4: 所有chunk缓冲完毕后,一次性写入文件并播放
81
- this.tempFile = join(tmpdir(), `ocosay-${Date.now()}.wav`)
82
- writeFileSync(this.tempFile, Buffer.concat(this.chunks))
83
- this.chunks = []
84
- this.start(this.tempFile)
85
- }
86
-
87
- pause(): void {
88
- if (!this._started || this._paused || this._stopped) return
89
-
90
- if (this.process) {
91
- try {
92
- this.process.kill('SIGSTOP')
93
- this._paused = true
94
- this.events?.onPause?.()
95
- } catch (e) {
96
- // SIGSTOP 可能失败
97
- }
98
- }
99
- }
100
-
101
- resume(): void {
102
- if (!this._paused || this._stopped) return
103
-
104
- if (this.process) {
105
- try {
106
- this.process.kill('SIGCONT')
107
- this._paused = false
108
- this.events?.onResume?.()
109
- } catch (e) {
110
- // SIGCONT 可能失败
111
- }
112
- }
113
- }
114
-
115
- stop(): void {
116
- this._stopped = true
117
- this._started = false
118
- this._paused = false
119
-
120
- if (this.process) {
121
- try {
122
- this.process.kill('SIGTERM')
123
- } catch (e) {
124
- // 忽略错误
125
- }
126
- this.process = undefined
127
- }
128
-
129
- this.cleanup()
130
- this.chunks = []
131
- this.hasEnded = false
132
-
133
- this.events?.onStop?.()
134
- }
135
-
136
- setVolume(_volume: number): void {
137
- // aplay 不支持命令行设置音量
138
- }
139
-
140
- destroy(): void {
141
- this.stop()
142
- }
143
-
144
- private cleanup(): void {
145
- if (this.tempFile && this.tempFile.startsWith(tmpdir())) {
146
- try {
147
- if (existsSync(this.tempFile)) {
148
- unlinkSync(this.tempFile)
149
- }
150
- } catch (e) {
151
- // 忽略清理错误
152
- }
153
- this.tempFile = undefined
154
- }
155
- }
156
-
157
- private handleError(error: Error): void {
158
- this.events?.onError?.(error)
159
- }
160
- }
@@ -1,117 +0,0 @@
1
- /**
2
- * Audio Backend Interface
3
- * 音频后端接口定义 - 统一各平台音频播放实现
4
- */
5
-
6
- /**
7
- * 音频后端接口
8
- * 定义各平台音频后端必须实现的方法
9
- */
10
- export interface AudioBackend {
11
- /** 后端名称 */
12
- readonly name: string
13
-
14
- /** 是否支持真正的流式播放(边收边播) */
15
- readonly supportsStreaming: boolean
16
-
17
- /**
18
- * 开始播放音频文件
19
- * @param filePath 音频文件路径
20
- */
21
- start(filePath: string): void
22
-
23
- /**
24
- * 写入音频数据块(用于流式播放)
25
- * @param chunk 音频数据块
26
- */
27
- write(chunk: Buffer): void
28
-
29
- /**
30
- * 结束写入,关闭流
31
- */
32
- end(): void
33
-
34
- /**
35
- * 暂停播放
36
- */
37
- pause(): void
38
-
39
- /**
40
- * 恢复播放
41
- */
42
- resume(): void
43
-
44
- /**
45
- * 停止播放
46
- */
47
- stop(): void
48
-
49
- /**
50
- * 获取当前播放位置(秒)
51
- * 如果不支持返回 undefined
52
- */
53
- getCurrentTime?(): number | undefined
54
-
55
- /**
56
- * 获取音频总时长(秒)
57
- * 如果不支持返回 undefined
58
- */
59
- getDuration?(): number | undefined
60
-
61
- /**
62
- * 设置音量
63
- * @param volume 音量 0.0 - 1.0
64
- */
65
- setVolume?(volume: number): void
66
-
67
- /**
68
- * 销毁后端,释放资源
69
- */
70
- destroy(): void
71
- }
72
-
73
- /**
74
- * 音频后端事件回调接口
75
- */
76
- export interface AudioBackendEvents {
77
- /** 开始播放回调 */
78
- onStart?: () => void
79
-
80
- /** 播放结束回调 */
81
- onEnd?: () => void
82
-
83
- /** 错误回调 */
84
- onError?: (error: Error) => void
85
-
86
- /** 暂停回调 */
87
- onPause?: () => void
88
-
89
- /** 恢复回调 */
90
- onResume?: () => void
91
-
92
- /** 停止回调 */
93
- onStop?: () => void
94
-
95
- /** 进度回调(已写入字节数) */
96
- onProgress?: (bytesWritten: number) => void
97
- }
98
-
99
- /**
100
- * 后端配置选项
101
- */
102
- export interface BackendOptions {
103
- /** 音频格式 (mp3, wav, flac) */
104
- format?: 'mp3' | 'wav' | 'flac'
105
-
106
- /** 采样率 (如 16000, 44100) */
107
- sampleRate?: number
108
-
109
- /** 声道数 (1 = 单声道, 2 = 立体声) */
110
- channels?: number
111
-
112
- /** 音量 0.0 - 1.0 */
113
- volume?: number
114
-
115
- /** 事件回调 */
116
- events?: AudioBackendEvents
117
- }
@@ -1,128 +0,0 @@
1
- /**
2
- * Audio Backends - 多平台音频后端统一导出
3
- */
4
-
5
- // 接口和类型
6
- export { AudioBackend, AudioBackendEvents, BackendOptions } from './base'
7
-
8
- // 各平台后端实现
9
- export { NaudiodonBackend } from './naudiodon-backend'
10
- export { AfplayBackend } from './afplay-backend'
11
- export { AplayBackend } from './aplay-backend'
12
- export { PowerShellBackend } from './powershell-backend'
13
-
14
- import { AudioBackend, BackendOptions } from './base'
15
- import { NaudiodonBackend } from './naudiodon-backend'
16
- import { AfplayBackend } from './afplay-backend'
17
- import { AplayBackend } from './aplay-backend'
18
- import { PowerShellBackend } from './powershell-backend'
19
-
20
- /**
21
- * 后端类型枚举
22
- */
23
- export enum BackendType {
24
- NAUDIODON = 'naudiodon',
25
- AFPLAY = 'afplay',
26
- APLAY = 'aplay',
27
- POWERSHELL = 'powershell',
28
- AUTO = 'auto'
29
- }
30
-
31
- let naudiodonCache: any = null
32
-
33
- async function tryLoadNaudiodon(): Promise<any> {
34
- if (naudiodonCache !== null) {
35
- return naudiodonCache
36
- }
37
- try {
38
- naudiodonCache = await import('naudiodon')
39
- return naudiodonCache
40
- } catch (e) {
41
- naudiodonCache = false
42
- return null
43
- }
44
- }
45
-
46
- function isNaudiodonAvailable(): boolean {
47
- try {
48
- require.resolve('naudiodon')
49
- return true
50
- } catch (e) {
51
- return false
52
- }
53
- }
54
-
55
- /**
56
- * 创建音频后端
57
- * @param type 后端类型,默认 AUTO(自动选择)
58
- * @param options 后端配置选项
59
- * @returns 音频后端实例
60
- */
61
- export function createBackend(type: BackendType = BackendType.AUTO, options: BackendOptions = {}): AudioBackend {
62
- const platform = process.platform
63
-
64
- if (type !== BackendType.AUTO) {
65
- return createBackendByType(type, options)
66
- }
67
-
68
- if (isNaudiodonAvailable()) {
69
- try {
70
- const naudiodon = require('naudiodon')
71
- if (naudiodon) {
72
- return new NaudiodonBackend(options)
73
- }
74
- } catch (e) {}
75
- }
76
-
77
- switch (platform) {
78
- case 'darwin':
79
- return new AfplayBackend(options)
80
- case 'linux':
81
- return new AplayBackend(options)
82
- case 'win32':
83
- return new PowerShellBackend(options)
84
- default:
85
- throw new Error(`Unsupported platform: ${platform}`)
86
- }
87
- }
88
-
89
- function createBackendByType(type: BackendType, options: BackendOptions): AudioBackend {
90
- switch (type) {
91
- case BackendType.NAUDIODON:
92
- return new NaudiodonBackend(options)
93
- case BackendType.AFPLAY:
94
- return new AfplayBackend(options)
95
- case BackendType.APLAY:
96
- return new AplayBackend(options)
97
- case BackendType.POWERSHELL:
98
- return new PowerShellBackend(options)
99
- default:
100
- throw new Error(`Unknown backend type: ${type}`)
101
- }
102
- }
103
-
104
- export function supportsStreaming(type: BackendType): boolean {
105
- if (type === BackendType.AUTO) {
106
- return isNaudiodonAvailable()
107
- }
108
- return type === BackendType.NAUDIODON
109
- }
110
-
111
- export function getDefaultBackendType(): BackendType {
112
- const platform = process.platform
113
-
114
- if (supportsStreaming(BackendType.AUTO)) {
115
- return BackendType.NAUDIODON
116
- }
117
-
118
- switch (platform) {
119
- case 'darwin':
120
- return BackendType.AFPLAY
121
- case 'linux':
122
- return BackendType.APLAY
123
- case 'win32':
124
- return BackendType.POWERSHELL
125
- default:
126
- return BackendType.NAUDIODON
127
- }
128
- }