@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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { pluginInfo, toolNames, initialize, destroy, isStreamEnabled, getStreamReader } from '../src/index'
|
|
2
|
+
import { registerProvider, unregisterProvider, BaseTTSProvider } from '../src/providers/base'
|
|
3
|
+
import { TTSCapabilities } from '../src/core/types'
|
|
4
|
+
|
|
5
|
+
class MockProvider extends BaseTTSProvider {
|
|
6
|
+
name = 'mock'
|
|
7
|
+
capabilities: TTSCapabilities = { speak: true, stream: true }
|
|
8
|
+
|
|
9
|
+
protected async doSpeak(text: string, voice: string | undefined, model: any) {
|
|
10
|
+
return {
|
|
11
|
+
audioData: Buffer.from([1, 2, 3]),
|
|
12
|
+
format: 'mp3',
|
|
13
|
+
isStream: model === 'stream',
|
|
14
|
+
duration: 1.0
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('index module exports', () => {
|
|
20
|
+
describe('pluginInfo', () => {
|
|
21
|
+
it('should export correct plugin info', () => {
|
|
22
|
+
expect(pluginInfo.name).toBe('ocosay')
|
|
23
|
+
expect(pluginInfo.version).toBe('1.0.0')
|
|
24
|
+
expect(pluginInfo.description).toContain('TTS')
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('toolNames', () => {
|
|
29
|
+
it('should export array of tool names', () => {
|
|
30
|
+
expect(Array.isArray(toolNames)).toBe(true)
|
|
31
|
+
expect(toolNames.length).toBeGreaterThan(0)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('should include tts_speak', () => {
|
|
35
|
+
expect(toolNames).toContain('tts_speak')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should include tts_stop', () => {
|
|
39
|
+
expect(toolNames).toContain('tts_stop')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should include tts_stream_speak', () => {
|
|
43
|
+
expect(toolNames).toContain('tts_stream_speak')
|
|
44
|
+
})
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('TuiEventBus integration', () => {
|
|
49
|
+
const originalGlobal = global as any
|
|
50
|
+
let mockProvider: MockProvider
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
mockProvider = new MockProvider()
|
|
54
|
+
registerProvider('mock', mockProvider)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
afterEach(async () => {
|
|
58
|
+
delete originalGlobal.__opencode_tuieventbus__
|
|
59
|
+
await destroy()
|
|
60
|
+
unregisterProvider('mock')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe('P1-1: eventBus available', () => {
|
|
64
|
+
it('should subscribe to message.part.delta and message.part.end events', async () => {
|
|
65
|
+
const mockOn = jest.fn()
|
|
66
|
+
const mockOff = jest.fn()
|
|
67
|
+
const MockTuiEventBus = jest.fn().mockImplementation(() => ({
|
|
68
|
+
on: mockOn,
|
|
69
|
+
off: mockOff
|
|
70
|
+
}))
|
|
71
|
+
originalGlobal.__opencode_tuieventbus__ = MockTuiEventBus
|
|
72
|
+
|
|
73
|
+
await initialize({
|
|
74
|
+
autoRead: true,
|
|
75
|
+
defaultProvider: 'mock'
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(mockOn).toHaveBeenCalledWith('message.part.delta', expect.any(Function))
|
|
79
|
+
expect(mockOn).toHaveBeenCalledWith('message.part.end', expect.any(Function))
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should handle delta events via streamReader', async () => {
|
|
83
|
+
const mockOn = jest.fn()
|
|
84
|
+
const mockOff = jest.fn()
|
|
85
|
+
let deltaHandler: Function | undefined
|
|
86
|
+
|
|
87
|
+
mockOn.mockImplementation((event: string, handler: Function) => {
|
|
88
|
+
if (event === 'message.part.delta') {
|
|
89
|
+
deltaHandler = handler
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const MockTuiEventBus = jest.fn().mockImplementation(() => ({
|
|
94
|
+
on: mockOn,
|
|
95
|
+
off: mockOff
|
|
96
|
+
}))
|
|
97
|
+
originalGlobal.__opencode_tuieventbus__ = MockTuiEventBus
|
|
98
|
+
|
|
99
|
+
await initialize({
|
|
100
|
+
autoRead: true,
|
|
101
|
+
defaultProvider: 'mock'
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const mockEvent = {
|
|
105
|
+
sessionId: 'session-1',
|
|
106
|
+
messageId: 'msg-1',
|
|
107
|
+
partId: 'part-1',
|
|
108
|
+
properties: { delta: 'Hello' }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
expect(deltaHandler).toBeDefined()
|
|
112
|
+
deltaHandler!(mockEvent)
|
|
113
|
+
})
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('P1-1: eventBus unavailable (graceful fallback)', () => {
|
|
117
|
+
it('should not throw when eventBus is not available', async () => {
|
|
118
|
+
delete originalGlobal.__opencode_tuieventbus__
|
|
119
|
+
|
|
120
|
+
await expect(initialize({
|
|
121
|
+
autoRead: true,
|
|
122
|
+
defaultProvider: 'mock'
|
|
123
|
+
})).resolves.not.toThrow()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should have stream components initialized even without eventBus', async () => {
|
|
127
|
+
delete originalGlobal.__opencode_tuieventbus__
|
|
128
|
+
|
|
129
|
+
await initialize({
|
|
130
|
+
autoRead: true,
|
|
131
|
+
defaultProvider: 'mock'
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
expect(isStreamEnabled()).toBe(true)
|
|
135
|
+
expect(getStreamReader()).toBeDefined()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('initialize() idempotency', () => {
|
|
141
|
+
let mockProvider: MockProvider
|
|
142
|
+
|
|
143
|
+
beforeEach(() => {
|
|
144
|
+
mockProvider = new MockProvider()
|
|
145
|
+
registerProvider('mock', mockProvider)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
afterEach(async () => {
|
|
149
|
+
await destroy()
|
|
150
|
+
unregisterProvider('mock')
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('P1-2: repeated initialize() calls', () => {
|
|
154
|
+
it('should be safe to call initialize() multiple times', async () => {
|
|
155
|
+
await initialize({
|
|
156
|
+
defaultProvider: 'mock'
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
await expect(initialize({
|
|
160
|
+
defaultProvider: 'mock'
|
|
161
|
+
})).resolves.not.toThrow()
|
|
162
|
+
|
|
163
|
+
await expect(initialize({
|
|
164
|
+
defaultProvider: 'mock'
|
|
165
|
+
})).resolves.not.toThrow()
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
it('should not re-initialize components on repeated calls', async () => {
|
|
169
|
+
await initialize({
|
|
170
|
+
autoRead: true,
|
|
171
|
+
defaultProvider: 'mock'
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const firstStreamReader = getStreamReader()
|
|
175
|
+
expect(firstStreamReader).toBeDefined()
|
|
176
|
+
|
|
177
|
+
await initialize({
|
|
178
|
+
defaultProvider: 'mock'
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
const secondStreamReader = getStreamReader()
|
|
182
|
+
expect(secondStreamReader).toBe(firstStreamReader)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should return early if already initialized', async () => {
|
|
186
|
+
const spyInitialize = jest.spyOn(mockProvider, 'initialize')
|
|
187
|
+
|
|
188
|
+
await initialize({
|
|
189
|
+
defaultProvider: 'mock'
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
spyInitialize.mockClear()
|
|
193
|
+
|
|
194
|
+
await initialize({
|
|
195
|
+
defaultProvider: 'mock'
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
expect(spyInitialize).not.toHaveBeenCalled()
|
|
199
|
+
})
|
|
200
|
+
})
|
|
201
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration-test.d.ts","sourceRoot":"","sources":["integration-test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const { initialize } = require('../dist/index')
|
|
2
|
+
const { getProvider } = require('../dist/providers/base')
|
|
3
|
+
|
|
4
|
+
const API_KEY = process.env.MINIMAX_API_KEY
|
|
5
|
+
|
|
6
|
+
if (!API_KEY) {
|
|
7
|
+
console.error('❌ 请设置环境变量 MINIMAX_API_KEY')
|
|
8
|
+
console.log('示例: MINIMAX_API_KEY=your-api-key node tests/integration-test.js')
|
|
9
|
+
process.exit(1)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function test() {
|
|
13
|
+
console.log('🔧 初始化 ocosay...')
|
|
14
|
+
|
|
15
|
+
await initialize({
|
|
16
|
+
providers: {
|
|
17
|
+
minimax: {
|
|
18
|
+
apiKey: API_KEY,
|
|
19
|
+
voiceId: 'male-qn-qingse',
|
|
20
|
+
model: 'stream'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
console.log('✅ 初始化成功!\n')
|
|
26
|
+
|
|
27
|
+
console.log('📝 测试 1: 同步合成 (model: sync)')
|
|
28
|
+
try {
|
|
29
|
+
const provider = getProvider('minimax')
|
|
30
|
+
const result = await provider.speak('你好,这是同步合成测试!', { model: 'sync' })
|
|
31
|
+
const data = Buffer.isBuffer(result.audioData) ? result.audioData : Buffer.from([])
|
|
32
|
+
console.log('✅ 同步合成成功! 音频大小: ' + data.length + ' bytes, 格式: ' + result.format)
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('❌ 同步合成失败:', error.message || error)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('')
|
|
38
|
+
|
|
39
|
+
console.log('📝 测试 2: 流式合成 (model: stream)')
|
|
40
|
+
try {
|
|
41
|
+
const provider = getProvider('minimax')
|
|
42
|
+
const result = await provider.speak('你好,这是流式合成测试!', { model: 'stream' })
|
|
43
|
+
const data = Buffer.isBuffer(result.audioData) ? result.audioData : Buffer.from([])
|
|
44
|
+
console.log('✅ 流式合成成功! 音频大小: ' + data.length + ' bytes, 格式: ' + result.format)
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('❌ 流式合成失败:', error.message || error)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log('')
|
|
50
|
+
|
|
51
|
+
console.log('📝 测试 3: 异步合成 (model: async)')
|
|
52
|
+
try {
|
|
53
|
+
const provider = getProvider('minimax')
|
|
54
|
+
const result = await provider.speak('你好,这是异步合成测试!', { model: 'async' })
|
|
55
|
+
const data = Buffer.isBuffer(result.audioData) ? result.audioData : Buffer.from([])
|
|
56
|
+
console.log('✅ 异步合成成功! 音频大小: ' + data.length + ' bytes, 格式: ' + result.format)
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('❌ 异步合成失败:', error.message || error)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
console.log('')
|
|
62
|
+
|
|
63
|
+
console.log('📝 测试 4: 列出音色')
|
|
64
|
+
try {
|
|
65
|
+
const provider = getProvider('minimax')
|
|
66
|
+
const voices = await provider.listVoices()
|
|
67
|
+
console.log('✅ 获取到 ' + voices.length + ' 个音色:')
|
|
68
|
+
voices.slice(0, 5).forEach(v => {
|
|
69
|
+
console.log(' - ' + v.id + ': ' + v.name + ' (' + (v.language || 'unknown') + ')')
|
|
70
|
+
})
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error('❌ 列出音色失败:', error.message || error)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log('')
|
|
76
|
+
console.log('🎉 集成测试完成!')
|
|
77
|
+
|
|
78
|
+
process.exit(0)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test().catch(err => {
|
|
82
|
+
console.error('❌ 测试失败:', err)
|
|
83
|
+
process.exit(1)
|
|
84
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"integration-test.js","sourceRoot":"","sources":["integration-test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { initialize, speak, stop, listVoices, getSpeaker } from '../src/index'
|
|
2
|
+
import { getDefaultSpeaker } from '../src/core/speaker'
|
|
3
|
+
import { getProvider } from '../src/providers/base'
|
|
4
|
+
|
|
5
|
+
const API_KEY = process.env.MINIMAX_API_KEY
|
|
6
|
+
|
|
7
|
+
if (!API_KEY) {
|
|
8
|
+
console.error('❌ 请设置环境变量 MINIMAX_API_KEY')
|
|
9
|
+
console.log('示例: MINIMAX_API_KEY=your-api-key node tests/integration-test.ts')
|
|
10
|
+
process.exit(1)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function test() {
|
|
14
|
+
console.log('🔧 初始化 ocosay...')
|
|
15
|
+
|
|
16
|
+
await initialize({
|
|
17
|
+
providers: {
|
|
18
|
+
minimax: {
|
|
19
|
+
apiKey: API_KEY,
|
|
20
|
+
voiceId: 'male-qn-qingse',
|
|
21
|
+
model: 'stream'
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
console.log('✅ 初始化成功!\n')
|
|
27
|
+
|
|
28
|
+
console.log('📝 测试 1: 同步合成 (model: sync)')
|
|
29
|
+
try {
|
|
30
|
+
const provider = getProvider('minimax')
|
|
31
|
+
const result = await provider.speak('你好,这是同步合成测试!', { model: 'sync' })
|
|
32
|
+
console.log(`✅ 同步合成成功! 音频大小: ${result.audioData.length} bytes, 格式: ${result.format}`)
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
console.error('❌ 同步合成失败:', error.message || error)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log('')
|
|
38
|
+
|
|
39
|
+
console.log('📝 测试 2: 流式合成 (model: stream)')
|
|
40
|
+
try {
|
|
41
|
+
const provider = getProvider('minimax')
|
|
42
|
+
const result = await provider.speak('你好,这是流式合成测试!', { model: 'stream' })
|
|
43
|
+
console.log(`✅ 流式合成成功! 音频大小: ${result.audioData.length} bytes, 格式: ${result.format}`)
|
|
44
|
+
} catch (error: any) {
|
|
45
|
+
console.error('❌ 流式合成失败:', error.message || error)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.log('')
|
|
49
|
+
|
|
50
|
+
console.log('📝 测试 3: 异步合成 (model: async)')
|
|
51
|
+
try {
|
|
52
|
+
const provider = getProvider('minimax')
|
|
53
|
+
const result = await provider.speak('你好,这是异步合成测试!', { model: 'async' })
|
|
54
|
+
console.log(`✅ 异步合成成功! 音频大小: ${result.audioData.length} bytes, 格式: ${result.format}`)
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
console.error('❌ 异步合成失败:', error.message || error)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log('')
|
|
60
|
+
|
|
61
|
+
console.log('📝 测试 4: 列出音色')
|
|
62
|
+
try {
|
|
63
|
+
const voices = await listVoices('minimax')
|
|
64
|
+
console.log(`✅ 获取到 ${voices.length} 个音色:`)
|
|
65
|
+
voices.slice(0, 5).forEach((v: any) => {
|
|
66
|
+
console.log(` - ${v.id}: ${v.name} (${v.language || 'unknown'})`)
|
|
67
|
+
})
|
|
68
|
+
} catch (error: any) {
|
|
69
|
+
console.error('❌ 列出音色失败:', error.message || error)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('')
|
|
73
|
+
|
|
74
|
+
console.log('📝 测试 5: 便捷函数 speak 调用链验证')
|
|
75
|
+
try {
|
|
76
|
+
const speaker = getDefaultSpeaker()
|
|
77
|
+
speaker.on('end', () => console.log(' 播放结束!'))
|
|
78
|
+
speaker.on('error', (err: any) => console.log(' 播放错误(正常,没有音频设备):', err.message))
|
|
79
|
+
console.log('✅ speak 便捷函数调用链验证通过')
|
|
80
|
+
} catch (error: any) {
|
|
81
|
+
console.error('❌ speak 失败:', error.message || error)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
console.log('')
|
|
85
|
+
console.log('🎉 集成测试完成!')
|
|
86
|
+
|
|
87
|
+
process.exit(0)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test().catch((err: any) => {
|
|
91
|
+
console.error('❌ 测试失败:', err)
|
|
92
|
+
process.exit(1)
|
|
93
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { Speaker } from '../src/core/speaker'
|
|
2
|
+
import { TTSCapabilities } from '../src/core/types'
|
|
3
|
+
import { registerProvider, unregisterProvider, BaseTTSProvider } from '../src/providers/base'
|
|
4
|
+
import { initialize, destroy, isAutoReadEnabled, getStreamReader } from '../src/index'
|
|
5
|
+
|
|
6
|
+
class MockProvider extends BaseTTSProvider {
|
|
7
|
+
name = 'mock'
|
|
8
|
+
capabilities: TTSCapabilities = { speak: true, stream: true }
|
|
9
|
+
private shouldThrow = false
|
|
10
|
+
private destroyCalled = false
|
|
11
|
+
|
|
12
|
+
protected async doSpeak(text: string, voice: string | undefined, model: any) {
|
|
13
|
+
if (this.shouldThrow) {
|
|
14
|
+
throw new Error('Mock synthesize error')
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
audioData: Buffer.from([1, 2, 3]),
|
|
18
|
+
format: 'mp3',
|
|
19
|
+
isStream: model === 'stream',
|
|
20
|
+
duration: 1.0
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async destroy(): Promise<void> {
|
|
25
|
+
this.destroyCalled = true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
setShouldThrow(throwError: boolean): void {
|
|
29
|
+
this.shouldThrow = throwError
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
wasDestroyCalled(): boolean {
|
|
33
|
+
return this.destroyCalled
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
reset(): void {
|
|
37
|
+
this.shouldThrow = false
|
|
38
|
+
this.destroyCalled = false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('P1 Fixes', () => {
|
|
43
|
+
let mockProvider: MockProvider
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
mockProvider = new MockProvider()
|
|
47
|
+
registerProvider('mock', mockProvider)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
afterEach(async () => {
|
|
51
|
+
try {
|
|
52
|
+
await destroy()
|
|
53
|
+
} catch (e) {
|
|
54
|
+
// ignore destroy errors in cleanup
|
|
55
|
+
}
|
|
56
|
+
unregisterProvider('mock')
|
|
57
|
+
mockProvider.reset()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('P1-3: Speaker.destroy() 方法', () => {
|
|
61
|
+
it('should cleanup player when destroy() is called', async () => {
|
|
62
|
+
const speaker = new Speaker({ defaultProvider: 'mock' })
|
|
63
|
+
|
|
64
|
+
await speaker.destroy()
|
|
65
|
+
|
|
66
|
+
expect((speaker as any).player).toBeUndefined()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('should reset isSpeaking and isPaused when destroy() is called', async () => {
|
|
70
|
+
const speaker = new Speaker({ defaultProvider: 'mock' })
|
|
71
|
+
|
|
72
|
+
expect(speaker.isPlaying()).toBe(false)
|
|
73
|
+
expect(speaker.isPausedState()).toBe(false)
|
|
74
|
+
|
|
75
|
+
await speaker.destroy()
|
|
76
|
+
|
|
77
|
+
expect(speaker.isPlaying()).toBe(false)
|
|
78
|
+
expect(speaker.isPausedState()).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('should cleanup currentProvider and currentText', async () => {
|
|
82
|
+
const speaker = new Speaker({ defaultProvider: 'mock' })
|
|
83
|
+
|
|
84
|
+
await speaker.destroy()
|
|
85
|
+
|
|
86
|
+
expect((speaker as any).currentProvider).toBeUndefined()
|
|
87
|
+
expect((speaker as any).currentText).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('P1-4: destroy() 调用 provider.destroy()', () => {
|
|
92
|
+
it('should call provider.destroy() when global destroy() is called', async () => {
|
|
93
|
+
await initialize({
|
|
94
|
+
defaultProvider: 'mock',
|
|
95
|
+
autoRead: false
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
expect(mockProvider.wasDestroyCalled()).toBe(false)
|
|
99
|
+
|
|
100
|
+
await destroy()
|
|
101
|
+
|
|
102
|
+
expect(mockProvider.wasDestroyCalled()).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
describe('P1-1: destroy() 重置 autoReadEnabled', () => {
|
|
107
|
+
it('should reset autoReadEnabled to false after destroy()', async () => {
|
|
108
|
+
expect(isAutoReadEnabled()).toBe(false)
|
|
109
|
+
|
|
110
|
+
await initialize({
|
|
111
|
+
defaultProvider: 'mock',
|
|
112
|
+
autoRead: true
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
expect(isAutoReadEnabled()).toBe(true)
|
|
116
|
+
|
|
117
|
+
await destroy()
|
|
118
|
+
|
|
119
|
+
expect(isAutoReadEnabled()).toBe(false)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('should correctly check autoReadEnabled state', async () => {
|
|
123
|
+
await initialize({
|
|
124
|
+
defaultProvider: 'mock',
|
|
125
|
+
autoRead: false
|
|
126
|
+
})
|
|
127
|
+
expect(isAutoReadEnabled()).toBe(false)
|
|
128
|
+
await destroy()
|
|
129
|
+
|
|
130
|
+
await initialize({
|
|
131
|
+
defaultProvider: 'mock',
|
|
132
|
+
autoRead: true
|
|
133
|
+
})
|
|
134
|
+
expect(isAutoReadEnabled()).toBe(true)
|
|
135
|
+
await destroy()
|
|
136
|
+
expect(isAutoReadEnabled()).toBe(false)
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('P1-2: processQueue() 错误处理', () => {
|
|
141
|
+
it('should not throw when provider synthesize throws', async () => {
|
|
142
|
+
await initialize({
|
|
143
|
+
defaultProvider: 'mock',
|
|
144
|
+
autoRead: true
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const streamReader = getStreamReader()
|
|
148
|
+
expect(streamReader).toBeDefined()
|
|
149
|
+
|
|
150
|
+
mockProvider.setShouldThrow(true)
|
|
151
|
+
|
|
152
|
+
streamReader!.handleDelta('session1', 'msg1', 'part1', 'Error sentence')
|
|
153
|
+
streamReader!.handleEnd()
|
|
154
|
+
|
|
155
|
+
await new Promise(resolve => setTimeout(resolve, 50))
|
|
156
|
+
|
|
157
|
+
expect(() => streamReader!.handleDelta('session2', 'msg2', 'part2', 'Normal sentence')).not.toThrow()
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
})
|