@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.
- package/README.md +12 -0
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/plugin.d.ts +2 -4
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +6 -4
- package/dist/plugin.js.map +2 -2
- package/package.json +1 -1
- package/TECH_PLAN.md +0 -352
- package/__mocks__/@opencode-ai/plugin.ts +0 -32
- package/jest.config.js +0 -15
- package/src/config.ts +0 -183
- package/src/core/backends/afplay-backend.ts +0 -162
- package/src/core/backends/aplay-backend.ts +0 -160
- package/src/core/backends/base.ts +0 -117
- package/src/core/backends/index.ts +0 -128
- package/src/core/backends/naudiodon-backend.ts +0 -164
- package/src/core/backends/powershell-backend.ts +0 -173
- package/src/core/player.ts +0 -322
- package/src/core/speaker.ts +0 -283
- package/src/core/stream-player.ts +0 -326
- package/src/core/stream-reader.ts +0 -190
- package/src/core/streaming-synthesizer.ts +0 -123
- package/src/core/types.ts +0 -185
- package/src/index.ts +0 -236
- package/src/plugin.ts +0 -178
- package/src/providers/base.ts +0 -150
- package/src/providers/minimax.ts +0 -515
- package/src/tools/tts.ts +0 -277
- package/src/types/config.ts +0 -38
- package/src/types/naudiodon.d.ts +0 -19
- package/tests/__mocks__/@opencode-ai/plugin.ts +0 -32
- package/tests/backends.test.ts +0 -831
- package/tests/config.test.ts +0 -327
- package/tests/index.test.ts +0 -201
- package/tests/integration-test.d.ts +0 -6
- package/tests/integration-test.d.ts.map +0 -1
- package/tests/integration-test.js +0 -84
- package/tests/integration-test.js.map +0 -1
- package/tests/integration-test.ts +0 -93
- package/tests/p1-fixes.test.ts +0 -160
- package/tests/plugin.test.ts +0 -312
- package/tests/provider.test.d.ts +0 -2
- package/tests/provider.test.d.ts.map +0 -1
- package/tests/provider.test.js +0 -69
- package/tests/provider.test.js.map +0 -1
- package/tests/provider.test.ts +0 -87
- package/tests/speaker.test.d.ts +0 -2
- package/tests/speaker.test.d.ts.map +0 -1
- package/tests/speaker.test.js +0 -63
- package/tests/speaker.test.js.map +0 -1
- package/tests/speaker.test.ts +0 -232
- package/tests/stream-player.test.ts +0 -303
- package/tests/stream-reader.test.ts +0 -269
- package/tests/streaming-synthesizer.test.ts +0 -225
- package/tests/tts-tools.test.ts +0 -270
- package/tests/types.test.d.ts +0 -2
- package/tests/types.test.d.ts.map +0 -1
- package/tests/types.test.js +0 -61
- package/tests/types.test.js.map +0 -1
- package/tests/types.test.ts +0 -63
- package/tsconfig.json +0 -22
package/tests/provider.test.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
registerProvider,
|
|
3
|
-
getProvider,
|
|
4
|
-
listProviders,
|
|
5
|
-
hasProvider,
|
|
6
|
-
unregisterProvider,
|
|
7
|
-
BaseTTSProvider
|
|
8
|
-
} from '../src/providers/base'
|
|
9
|
-
import { TTSError, TTSErrorCode, TTSCapabilities } from '../src/core/types'
|
|
10
|
-
|
|
11
|
-
// 创建测试 Provider
|
|
12
|
-
class TestProvider extends BaseTTSProvider {
|
|
13
|
-
name = 'test'
|
|
14
|
-
capabilities: TTSCapabilities = { speak: true }
|
|
15
|
-
|
|
16
|
-
protected async doSpeak(text: string, voice: string | undefined, model: any) {
|
|
17
|
-
return {
|
|
18
|
-
audioData: Buffer.from([]),
|
|
19
|
-
format: 'mp3',
|
|
20
|
-
isStream: model === 'stream'
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
describe('Provider Registry', () => {
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
// 清理
|
|
28
|
-
unregisterProvider('test')
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('should register provider', () => {
|
|
32
|
-
const provider = new TestProvider()
|
|
33
|
-
registerProvider('test', provider)
|
|
34
|
-
expect(hasProvider('test')).toBe(true)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('should get registered provider', () => {
|
|
38
|
-
const provider = new TestProvider()
|
|
39
|
-
registerProvider('test', provider)
|
|
40
|
-
const retrieved = getProvider('test')
|
|
41
|
-
expect(retrieved).toBe(provider)
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('should throw when getting non-existent provider', () => {
|
|
45
|
-
expect(() => getProvider('non-existent')).toThrow(TTSError)
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
it('should list all providers', () => {
|
|
49
|
-
registerProvider('test', new TestProvider())
|
|
50
|
-
const list = listProviders()
|
|
51
|
-
expect(list).toContain('test')
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('should unregister provider', () => {
|
|
55
|
-
registerProvider('test', new TestProvider())
|
|
56
|
-
unregisterProvider('test')
|
|
57
|
-
expect(hasProvider('test')).toBe(false)
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
it('should throw when registering duplicate provider', () => {
|
|
61
|
-
registerProvider('test', new TestProvider())
|
|
62
|
-
expect(() => registerProvider('test', new TestProvider())).toThrow()
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
describe('BaseTTSProvider', () => {
|
|
67
|
-
let provider: TestProvider
|
|
68
|
-
|
|
69
|
-
beforeEach(() => {
|
|
70
|
-
provider = new TestProvider()
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('should speak with options', async () => {
|
|
74
|
-
const result = await provider.speak('Hello', { model: 'sync' })
|
|
75
|
-
expect(result.format).toBe('mp3')
|
|
76
|
-
expect(result.isStream).toBe(false)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('should throw on empty text', async () => {
|
|
80
|
-
await expect(provider.speak('')).rejects.toThrow(TTSError)
|
|
81
|
-
await expect(provider.speak(' ')).rejects.toThrow(TTSError)
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('should stop without error', async () => {
|
|
85
|
-
await expect(provider.stop()).resolves.toBeUndefined()
|
|
86
|
-
})
|
|
87
|
-
})
|
package/tests/speaker.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"speaker.test.d.ts","sourceRoot":"","sources":["speaker.test.ts"],"names":[],"mappings":""}
|
package/tests/speaker.test.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const speaker_1 = require("../src/core/speaker");
|
|
4
|
-
const types_1 = require("../src/core/types");
|
|
5
|
-
const base_1 = require("../src/providers/base");
|
|
6
|
-
// Mock Provider
|
|
7
|
-
class MockProvider extends base_1.BaseTTSProvider {
|
|
8
|
-
name = 'mock';
|
|
9
|
-
capabilities = { speak: true, stream: true };
|
|
10
|
-
async doSpeak(text, voice, model) {
|
|
11
|
-
return {
|
|
12
|
-
audioData: Buffer.from([1, 2, 3]),
|
|
13
|
-
format: 'mp3',
|
|
14
|
-
isStream: model === 'stream',
|
|
15
|
-
duration: 1.0
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
describe('Speaker', () => {
|
|
20
|
-
let speaker;
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
(0, base_1.registerProvider)('mock', new MockProvider());
|
|
23
|
-
speaker = new speaker_1.Speaker({ defaultProvider: 'mock' });
|
|
24
|
-
});
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
(0, base_1.unregisterProvider)('mock');
|
|
27
|
-
});
|
|
28
|
-
describe('initialization', () => {
|
|
29
|
-
it('should create speaker with options', () => {
|
|
30
|
-
const s = new speaker_1.Speaker({ defaultProvider: 'mock', defaultModel: 'sync' });
|
|
31
|
-
expect(s.getProviders()).toContain('mock');
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe('speak', () => {
|
|
35
|
-
it('should throw on empty text', async () => {
|
|
36
|
-
await expect(speaker.speak('')).rejects.toThrow(types_1.TTSError);
|
|
37
|
-
});
|
|
38
|
-
});
|
|
39
|
-
describe('control methods', () => {
|
|
40
|
-
it('should pause without error', () => {
|
|
41
|
-
expect(() => speaker.pause()).not.toThrow();
|
|
42
|
-
});
|
|
43
|
-
it('should resume without error', () => {
|
|
44
|
-
expect(() => speaker.resume()).not.toThrow();
|
|
45
|
-
});
|
|
46
|
-
it('should stop without error', async () => {
|
|
47
|
-
await expect(speaker.stop()).resolves.toBeUndefined();
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
describe('listVoices', () => {
|
|
51
|
-
it('should return voices from provider', async () => {
|
|
52
|
-
const voices = await speaker.listVoices('mock');
|
|
53
|
-
expect(Array.isArray(voices)).toBe(true);
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
describe('getCapabilities', () => {
|
|
57
|
-
it('should return provider capabilities', () => {
|
|
58
|
-
const caps = speaker.getCapabilities('mock');
|
|
59
|
-
expect(caps.speak).toBe(true);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
//# sourceMappingURL=speaker.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"speaker.test.js","sourceRoot":"","sources":["speaker.test.ts"],"names":[],"mappings":";;AAAA,iDAA6C;AAC7C,6CAA6D;AAC7D,gDAA6F;AAE7F,gBAAgB;AAChB,MAAM,YAAa,SAAQ,sBAAe;IACxC,IAAI,GAAG,MAAM,CAAA;IACb,YAAY,GAAoB,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAA;IAEnD,KAAK,CAAC,OAAO,CAAC,IAAY,EAAE,KAAyB,EAAE,KAAU;QACzE,OAAO;YACL,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YACjC,MAAM,EAAE,KAAK;YACb,QAAQ,EAAE,KAAK,KAAK,QAAQ;YAC5B,QAAQ,EAAE,GAAG;SACd,CAAA;IACH,CAAC;CACF;AAED,QAAQ,CAAC,SAAS,EAAE,GAAG,EAAE;IACvB,IAAI,OAAgB,CAAA;IAEpB,UAAU,CAAC,GAAG,EAAE;QACd,IAAA,uBAAgB,EAAC,MAAM,EAAE,IAAI,YAAY,EAAE,CAAC,CAAA;QAC5C,OAAO,GAAG,IAAI,iBAAO,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC,CAAA;IACpD,CAAC,CAAC,CAAA;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,IAAA,yBAAkB,EAAC,MAAM,CAAC,CAAA;IAC5B,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;YAC5C,MAAM,CAAC,GAAG,IAAI,iBAAO,CAAC,EAAE,eAAe,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAA;YACxE,MAAM,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,CAAA;QAC5C,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,OAAO,EAAE,GAAG,EAAE;QACrB,EAAE,CAAC,4BAA4B,EAAE,KAAK,IAAI,EAAE;YAC1C,MAAM,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,gBAAQ,CAAC,CAAA;QAC3D,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;YACpC,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAA;QAC7C,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;YACrC,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,EAAE,CAAA;QAC9C,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,2BAA2B,EAAE,KAAK,IAAI,EAAE;YACzC,MAAM,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,aAAa,EAAE,CAAA;QACvD,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;QAC1B,EAAE,CAAC,oCAAoC,EAAE,KAAK,IAAI,EAAE;YAClD,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,CAAA;YAC/C,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC1C,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,iBAAiB,EAAE,GAAG,EAAE;QAC/B,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;YAC7C,MAAM,IAAI,GAAG,OAAO,CAAC,eAAe,CAAC,MAAM,CAAC,CAAA;YAC5C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC/B,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
package/tests/speaker.test.ts
DELETED
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
import { Speaker, getDefaultSpeaker, speak, stop, pause, resume, listVoices } from '../src/core/speaker'
|
|
2
|
-
import { TTSError, TTSErrorCode, TTSCapabilities, Voice } from '../src/core/types'
|
|
3
|
-
import { registerProvider, unregisterProvider, BaseTTSProvider } from '../src/providers/base'
|
|
4
|
-
import { AudioPlayer } from '../src/core/player'
|
|
5
|
-
|
|
6
|
-
jest.mock('../src/core/player')
|
|
7
|
-
|
|
8
|
-
const MockAudioPlayer = AudioPlayer as jest.MockedClass<typeof AudioPlayer>
|
|
9
|
-
|
|
10
|
-
class MockProvider extends BaseTTSProvider {
|
|
11
|
-
name = 'mock'
|
|
12
|
-
capabilities: TTSCapabilities = { speak: true, stream: true }
|
|
13
|
-
|
|
14
|
-
protected async doSpeak(text: string, voice: string | undefined, model: any) {
|
|
15
|
-
if (text === 'error') {
|
|
16
|
-
throw new TTSError('Provider error', TTSErrorCode.UNKNOWN, this.name)
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
audioData: Buffer.from([1, 2, 3]),
|
|
20
|
-
format: 'mp3',
|
|
21
|
-
isStream: model === 'stream',
|
|
22
|
-
duration: 1.0
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async listVoices(): Promise<Voice[]> {
|
|
27
|
-
return [
|
|
28
|
-
{ id: 'voice1', name: 'Voice 1', language: 'zh-CN' },
|
|
29
|
-
{ id: 'voice2', name: 'Voice 2', language: 'en-US', gender: 'male' as const }
|
|
30
|
-
]
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('Speaker', () => {
|
|
35
|
-
let speaker: Speaker
|
|
36
|
-
let mockPlayer: any
|
|
37
|
-
let capturedEvents: any
|
|
38
|
-
|
|
39
|
-
beforeEach(() => {
|
|
40
|
-
jest.clearAllMocks()
|
|
41
|
-
registerProvider('mock', new MockProvider())
|
|
42
|
-
|
|
43
|
-
capturedEvents = {}
|
|
44
|
-
mockPlayer = {
|
|
45
|
-
play: jest.fn().mockImplementation(() => {
|
|
46
|
-
capturedEvents.onStart?.()
|
|
47
|
-
return Promise.resolve()
|
|
48
|
-
}),
|
|
49
|
-
pause: jest.fn().mockImplementation(() => { capturedEvents.onPause?.() }),
|
|
50
|
-
resume: jest.fn().mockImplementation(() => { capturedEvents.onResume?.() }),
|
|
51
|
-
stop: jest.fn().mockImplementation(() => {
|
|
52
|
-
capturedEvents.onStop?.()
|
|
53
|
-
return Promise.resolve()
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
MockAudioPlayer.mockImplementation((events?: any) => {
|
|
57
|
-
Object.assign(capturedEvents, events)
|
|
58
|
-
return mockPlayer
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
speaker = new Speaker({ defaultProvider: 'mock', defaultModel: 'stream' })
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
afterEach(() => {
|
|
65
|
-
unregisterProvider('mock')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
describe('initialization', () => {
|
|
69
|
-
it('should create speaker with options', () => {
|
|
70
|
-
const s = new Speaker({ defaultProvider: 'mock', defaultModel: 'sync' })
|
|
71
|
-
expect(s.getProviders()).toContain('mock')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('should create speaker with all options', () => {
|
|
75
|
-
const s = new Speaker({
|
|
76
|
-
defaultProvider: 'mock',
|
|
77
|
-
defaultModel: 'stream',
|
|
78
|
-
defaultVoice: 'voice1',
|
|
79
|
-
onEvent: jest.fn()
|
|
80
|
-
})
|
|
81
|
-
expect(s.getProviders()).toContain('mock')
|
|
82
|
-
})
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
describe('speak', () => {
|
|
86
|
-
it('should throw on empty text', async () => {
|
|
87
|
-
await expect(speaker.speak('')).rejects.toThrow(TTSError)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
it('should throw on whitespace-only text', async () => {
|
|
91
|
-
await expect(speaker.speak(' ')).rejects.toThrow(TTSError)
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('should stop current playback before new speak', async () => {
|
|
95
|
-
await speaker.speak('First')
|
|
96
|
-
await speaker.speak('Second')
|
|
97
|
-
expect(mockPlayer.stop).toHaveBeenCalled()
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('should play audio data', async () => {
|
|
101
|
-
await speaker.speak('Hello')
|
|
102
|
-
expect(mockPlayer.play).toHaveBeenCalledWith(Buffer.from([1, 2, 3]), 'mp3')
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
it('should throw on provider error', async () => {
|
|
106
|
-
await expect(speaker.speak('error')).rejects.toThrow()
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
it('should throw when provider not found', async () => {
|
|
110
|
-
await expect(speaker.speak('Hello', { provider: 'non-existent' })).rejects.toThrow()
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
describe('control methods', () => {
|
|
115
|
-
it('should pause without error', () => {
|
|
116
|
-
expect(() => speaker.pause()).not.toThrow()
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
it('should resume without error', () => {
|
|
120
|
-
expect(() => speaker.resume()).not.toThrow()
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('should stop without error', async () => {
|
|
124
|
-
await expect(speaker.stop()).resolves.toBeUndefined()
|
|
125
|
-
})
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
describe('listVoices', () => {
|
|
129
|
-
it('should return voices from provider', async () => {
|
|
130
|
-
const voices = await speaker.listVoices('mock')
|
|
131
|
-
expect(Array.isArray(voices)).toBe(true)
|
|
132
|
-
expect(voices.length).toBe(2)
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('should use default provider when none specified', async () => {
|
|
136
|
-
const voices = await speaker.listVoices()
|
|
137
|
-
expect(Array.isArray(voices)).toBe(true)
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
describe('getCapabilities', () => {
|
|
142
|
-
it('should return provider capabilities', () => {
|
|
143
|
-
const caps = speaker.getCapabilities('mock')
|
|
144
|
-
expect(caps.speak).toBe(true)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('should use default provider when none specified', () => {
|
|
148
|
-
const caps = speaker.getCapabilities()
|
|
149
|
-
expect(caps.speak).toBe(true)
|
|
150
|
-
})
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
describe('getProviders', () => {
|
|
154
|
-
it('should return list of registered providers', () => {
|
|
155
|
-
const providers = speaker.getProviders()
|
|
156
|
-
expect(providers).toContain('mock')
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
describe('isPlaying', () => {
|
|
161
|
-
it('should return false initially', () => {
|
|
162
|
-
expect(speaker.isPlaying()).toBe(false)
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
it('should return true while speaking', async () => {
|
|
166
|
-
const speakPromise = speaker.speak('Hello')
|
|
167
|
-
expect(speaker.isPlaying()).toBe(true)
|
|
168
|
-
await speakPromise
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
it('should return false after stop', async () => {
|
|
172
|
-
await speaker.speak('Hello')
|
|
173
|
-
await speaker.stop()
|
|
174
|
-
expect(speaker.isPlaying()).toBe(false)
|
|
175
|
-
})
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
describe('isPausedState', () => {
|
|
179
|
-
it('should return false initially', () => {
|
|
180
|
-
expect(speaker.isPausedState()).toBe(false)
|
|
181
|
-
})
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
describe('destroy', () => {
|
|
185
|
-
it('should stop player on destroy', async () => {
|
|
186
|
-
await speaker.destroy()
|
|
187
|
-
expect(mockPlayer.stop).toHaveBeenCalled()
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
it('should set isSpeaking to false', async () => {
|
|
191
|
-
await speaker.speak('Hello')
|
|
192
|
-
await speaker.destroy()
|
|
193
|
-
expect(speaker.isPlaying()).toBe(false)
|
|
194
|
-
})
|
|
195
|
-
|
|
196
|
-
it('should be callable multiple times', async () => {
|
|
197
|
-
await speaker.destroy()
|
|
198
|
-
await expect(speaker.destroy()).resolves.not.toThrow()
|
|
199
|
-
})
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
describe('events', () => {
|
|
203
|
-
it('should emit start event', async () => {
|
|
204
|
-
const startCallback = jest.fn()
|
|
205
|
-
speaker.on('start', startCallback)
|
|
206
|
-
await speaker.speak('Hello')
|
|
207
|
-
expect(startCallback).toHaveBeenCalled()
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
it('should emit stop event on stop', async () => {
|
|
211
|
-
const stopCallback = jest.fn()
|
|
212
|
-
speaker.on('stop', stopCallback)
|
|
213
|
-
await speaker.stop()
|
|
214
|
-
expect(stopCallback).toHaveBeenCalled()
|
|
215
|
-
})
|
|
216
|
-
})
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
describe('getDefaultSpeaker', () => {
|
|
220
|
-
beforeEach(() => {
|
|
221
|
-
registerProvider('mock', new MockProvider())
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
afterEach(() => {
|
|
225
|
-
unregisterProvider('mock')
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
it('should return a Speaker instance', () => {
|
|
229
|
-
const speaker = getDefaultSpeaker()
|
|
230
|
-
expect(speaker).toBeInstanceOf(Speaker)
|
|
231
|
-
})
|
|
232
|
-
})
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
import { StreamPlayer } from '../src/core/stream-player'
|
|
2
|
-
import { spawn } from 'child_process'
|
|
3
|
-
import { createWriteStream } from 'fs'
|
|
4
|
-
|
|
5
|
-
jest.mock('child_process')
|
|
6
|
-
jest.mock('fs')
|
|
7
|
-
|
|
8
|
-
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>
|
|
9
|
-
const mockCreateWriteStream = createWriteStream as jest.MockedFunction<typeof createWriteStream>
|
|
10
|
-
|
|
11
|
-
describe('StreamPlayer', () => {
|
|
12
|
-
let mockWriteStream: any
|
|
13
|
-
let mockPlayerProcess: any
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
jest.clearAllMocks()
|
|
17
|
-
|
|
18
|
-
mockWriteStream = {
|
|
19
|
-
write: jest.fn().mockReturnValue(true),
|
|
20
|
-
end: jest.fn(),
|
|
21
|
-
destroy: jest.fn(),
|
|
22
|
-
on: jest.fn(),
|
|
23
|
-
once: jest.fn()
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
mockPlayerProcess = {
|
|
27
|
-
kill: jest.fn(),
|
|
28
|
-
on: jest.fn(),
|
|
29
|
-
stdin: { end: jest.fn() }
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
mockCreateWriteStream.mockReturnValue(mockWriteStream as any)
|
|
33
|
-
mockSpawn.mockImplementation(() => mockPlayerProcess as any)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
jest.restoreAllMocks()
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
function createPlayer(options?: any): StreamPlayer {
|
|
41
|
-
return new StreamPlayer(options)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
describe('initialization', () => {
|
|
45
|
-
it('should create StreamPlayer with default options', () => {
|
|
46
|
-
const player = createPlayer()
|
|
47
|
-
expect(player).toBeInstanceOf(StreamPlayer)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('should create StreamPlayer with custom format', () => {
|
|
51
|
-
const player = createPlayer({ format: 'wav' })
|
|
52
|
-
expect(player).toBeInstanceOf(StreamPlayer)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('should create StreamPlayer with events callback', () => {
|
|
56
|
-
const events = {
|
|
57
|
-
onStart: jest.fn(),
|
|
58
|
-
onEnd: jest.fn(),
|
|
59
|
-
onProgress: jest.fn(),
|
|
60
|
-
onError: jest.fn(),
|
|
61
|
-
onStop: jest.fn()
|
|
62
|
-
}
|
|
63
|
-
const player = createPlayer({ events })
|
|
64
|
-
expect(player).toBeInstanceOf(StreamPlayer)
|
|
65
|
-
})
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
describe('write', () => {
|
|
69
|
-
it('should auto-start if not started', () => {
|
|
70
|
-
const player = createPlayer()
|
|
71
|
-
player.write(Buffer.from([1, 2, 3]))
|
|
72
|
-
expect(player.isStarted()).toBe(true)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('should ignore write if stopped', () => {
|
|
76
|
-
const player = createPlayer()
|
|
77
|
-
player.start()
|
|
78
|
-
player.stop()
|
|
79
|
-
player.write(Buffer.from([1, 2, 3]))
|
|
80
|
-
expect(player.getBytesWritten()).toBe(0)
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('should write chunk to stream', () => {
|
|
84
|
-
const player = createPlayer()
|
|
85
|
-
player.write(Buffer.from([1, 2, 3]))
|
|
86
|
-
expect(mockWriteStream.write).toHaveBeenCalledWith(Buffer.from([1, 2, 3]))
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('should increment bytesWritten counter', () => {
|
|
90
|
-
const player = createPlayer()
|
|
91
|
-
player.write(Buffer.from([1, 2, 3]))
|
|
92
|
-
expect(player.getBytesWritten()).toBe(3)
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
it('should call onProgress callback', () => {
|
|
96
|
-
const onProgress = jest.fn()
|
|
97
|
-
const player = createPlayer({ events: { onProgress } })
|
|
98
|
-
player.write(Buffer.from([1, 2, 3]))
|
|
99
|
-
expect(onProgress).toHaveBeenCalledWith(3)
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('should emit progress event', () => {
|
|
103
|
-
const player = createPlayer()
|
|
104
|
-
const progressCallback = jest.fn()
|
|
105
|
-
player.on('progress', progressCallback)
|
|
106
|
-
player.write(Buffer.from([1, 2, 3]))
|
|
107
|
-
expect(progressCallback).toHaveBeenCalledWith(3)
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
describe('start', () => {
|
|
112
|
-
it('should set started flag to true', () => {
|
|
113
|
-
const player = createPlayer()
|
|
114
|
-
player.start()
|
|
115
|
-
expect(player.isStarted()).toBe(true)
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
it('should not start twice', () => {
|
|
119
|
-
const player = createPlayer()
|
|
120
|
-
player.start()
|
|
121
|
-
player.start()
|
|
122
|
-
expect(mockCreateWriteStream).toHaveBeenCalledTimes(1)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('should call onStart callback', () => {
|
|
126
|
-
const onStart = jest.fn()
|
|
127
|
-
const player = createPlayer({ events: { onStart } })
|
|
128
|
-
player.start()
|
|
129
|
-
expect(onStart).toHaveBeenCalled()
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('should emit start event', () => {
|
|
133
|
-
const player = createPlayer()
|
|
134
|
-
const startCallback = jest.fn()
|
|
135
|
-
player.on('start', startCallback)
|
|
136
|
-
player.start()
|
|
137
|
-
expect(startCallback).toHaveBeenCalled()
|
|
138
|
-
})
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
describe('stop', () => {
|
|
142
|
-
it('should set stopped flag to true', () => {
|
|
143
|
-
const player = createPlayer()
|
|
144
|
-
player.start()
|
|
145
|
-
player.stop()
|
|
146
|
-
expect(player.isStopped()).toBe(true)
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('should kill player process', () => {
|
|
150
|
-
const player = createPlayer()
|
|
151
|
-
player.start()
|
|
152
|
-
player.stop()
|
|
153
|
-
expect(mockPlayerProcess.kill).toHaveBeenCalledWith('SIGTERM')
|
|
154
|
-
})
|
|
155
|
-
|
|
156
|
-
it('should call onStop callback', () => {
|
|
157
|
-
const onStop = jest.fn()
|
|
158
|
-
const player = createPlayer({ events: { onStop } })
|
|
159
|
-
player.start()
|
|
160
|
-
player.stop()
|
|
161
|
-
expect(onStop).toHaveBeenCalled()
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
it('should emit stop event', () => {
|
|
165
|
-
const player = createPlayer()
|
|
166
|
-
player.start()
|
|
167
|
-
const stopCallback = jest.fn()
|
|
168
|
-
player.on('stop', stopCallback)
|
|
169
|
-
player.stop()
|
|
170
|
-
expect(stopCallback).toHaveBeenCalled()
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
describe('pause', () => {
|
|
175
|
-
it('should set paused flag to true', () => {
|
|
176
|
-
const player = createPlayer()
|
|
177
|
-
player.start()
|
|
178
|
-
player.pause()
|
|
179
|
-
expect(player.isPaused()).toBe(true)
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
it('should send SIGSTOP to player process', () => {
|
|
183
|
-
const player = createPlayer()
|
|
184
|
-
player.start()
|
|
185
|
-
player.pause()
|
|
186
|
-
expect(mockPlayerProcess.kill).toHaveBeenCalledWith('SIGSTOP')
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
it('should call onPause callback', () => {
|
|
190
|
-
const onPause = jest.fn()
|
|
191
|
-
const player = createPlayer({ events: { onPause } })
|
|
192
|
-
player.start()
|
|
193
|
-
player.pause()
|
|
194
|
-
expect(onPause).toHaveBeenCalled()
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
it('should emit pause event', () => {
|
|
198
|
-
const player = createPlayer()
|
|
199
|
-
player.start()
|
|
200
|
-
const pauseCallback = jest.fn()
|
|
201
|
-
player.on('pause', pauseCallback)
|
|
202
|
-
player.pause()
|
|
203
|
-
expect(pauseCallback).toHaveBeenCalled()
|
|
204
|
-
})
|
|
205
|
-
|
|
206
|
-
it('should not pause if not started', () => {
|
|
207
|
-
const player = createPlayer()
|
|
208
|
-
player.pause()
|
|
209
|
-
expect(mockPlayerProcess.kill).not.toHaveBeenCalled()
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
it('should not pause if already paused', () => {
|
|
213
|
-
const player = createPlayer()
|
|
214
|
-
player.start()
|
|
215
|
-
player.pause()
|
|
216
|
-
player.pause()
|
|
217
|
-
expect(mockPlayerProcess.kill).toHaveBeenCalledTimes(1)
|
|
218
|
-
})
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
describe('resume', () => {
|
|
222
|
-
it('should set paused flag to false', () => {
|
|
223
|
-
const player = createPlayer()
|
|
224
|
-
player.start()
|
|
225
|
-
player.pause()
|
|
226
|
-
player.resume()
|
|
227
|
-
expect(player.isPaused()).toBe(false)
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('should send SIGCONT to player process', () => {
|
|
231
|
-
const player = createPlayer()
|
|
232
|
-
player.start()
|
|
233
|
-
player.pause()
|
|
234
|
-
player.resume()
|
|
235
|
-
expect(mockPlayerProcess.kill).toHaveBeenCalledWith('SIGCONT')
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
it('should call onResume callback', () => {
|
|
239
|
-
const onResume = jest.fn()
|
|
240
|
-
const player = createPlayer({ events: { onResume } })
|
|
241
|
-
player.start()
|
|
242
|
-
player.pause()
|
|
243
|
-
player.resume()
|
|
244
|
-
expect(onResume).toHaveBeenCalled()
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
it('should emit resume event', () => {
|
|
248
|
-
const player = createPlayer()
|
|
249
|
-
player.start()
|
|
250
|
-
player.pause()
|
|
251
|
-
const resumeCallback = jest.fn()
|
|
252
|
-
player.on('resume', resumeCallback)
|
|
253
|
-
player.resume()
|
|
254
|
-
expect(resumeCallback).toHaveBeenCalled()
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
it('should not resume if not paused', () => {
|
|
258
|
-
const player = createPlayer()
|
|
259
|
-
player.start()
|
|
260
|
-
player.resume()
|
|
261
|
-
expect(mockPlayerProcess.kill).not.toHaveBeenCalledWith('SIGCONT')
|
|
262
|
-
})
|
|
263
|
-
})
|
|
264
|
-
|
|
265
|
-
describe('end', () => {
|
|
266
|
-
it('should end write stream without stopping player', () => {
|
|
267
|
-
const player = createPlayer()
|
|
268
|
-
player.start()
|
|
269
|
-
player.end()
|
|
270
|
-
expect(mockWriteStream.end).toHaveBeenCalled()
|
|
271
|
-
})
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
describe('state queries', () => {
|
|
275
|
-
it('isStarted should return false initially', () => {
|
|
276
|
-
const player = createPlayer()
|
|
277
|
-
expect(player.isStarted()).toBe(false)
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
it('isPaused should return false initially', () => {
|
|
281
|
-
const player = createPlayer()
|
|
282
|
-
expect(player.isPaused()).toBe(false)
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
it('isStopped should return false initially', () => {
|
|
286
|
-
const player = createPlayer()
|
|
287
|
-
expect(player.isStopped()).toBe(false)
|
|
288
|
-
})
|
|
289
|
-
|
|
290
|
-
it('getBytesWritten should return 0 initially', () => {
|
|
291
|
-
const player = createPlayer()
|
|
292
|
-
expect(player.getBytesWritten()).toBe(0)
|
|
293
|
-
})
|
|
294
|
-
})
|
|
295
|
-
|
|
296
|
-
describe('error handling', () => {
|
|
297
|
-
it('should have error handler registered', () => {
|
|
298
|
-
const player = createPlayer()
|
|
299
|
-
player.start()
|
|
300
|
-
expect(mockWriteStream.on).toHaveBeenCalledWith('error', expect.any(Function))
|
|
301
|
-
})
|
|
302
|
-
})
|
|
303
|
-
})
|