@mastra/voice-azure 0.1.0-alpha.1

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/dist/index.js ADDED
@@ -0,0 +1,396 @@
1
+ import { Readable } from 'stream';
2
+ import { MastraVoice } from '@mastra/core/voice';
3
+ import * as Azure from 'microsoft-cognitiveservices-speech-sdk';
4
+
5
+ // src/index.ts
6
+
7
+ // src/voices.ts
8
+ var AZURE_VOICES = [
9
+ "af-ZA-AdriNeural",
10
+ "af-ZA-WillemNeural",
11
+ "am-ET-MekdesNeural",
12
+ "am-ET-AmehaNeural",
13
+ "ar-AE-FatimaNeural",
14
+ "ar-AE-HamdanNeural",
15
+ "ar-BH-LailaNeural",
16
+ "ar-BH-AliNeural",
17
+ "ar-DZ-AminaNeural",
18
+ "ar-DZ-IsmaelNeural",
19
+ "ar-EG-SalmaNeural",
20
+ "ar-EG-ShakirNeural",
21
+ "ar-IQ-RanaNeural",
22
+ "ar-IQ-BasselNeural",
23
+ "ar-JO-SanaNeural",
24
+ "ar-JO-TaimNeural",
25
+ "ar-KW-NouraNeural",
26
+ "ar-KW-FahedNeural",
27
+ "ar-LB-LaylaNeural",
28
+ "ar-LB-RamiNeural",
29
+ "ar-LY-ImanNeural",
30
+ "ar-LY-OmarNeural",
31
+ "ar-MA-MounaNeural",
32
+ "ar-MA-JamalNeural",
33
+ "ar-OM-AyshaNeural",
34
+ "ar-OM-AbdullahNeural",
35
+ "ar-QA-AmalNeural",
36
+ "ar-QA-MoazNeural",
37
+ "ar-SA-ZariyahNeural",
38
+ "ar-SA-HamedNeural",
39
+ "ar-SY-AmanyNeural",
40
+ "ar-SY-LaithNeural",
41
+ "ar-TN-ReemNeural",
42
+ "ar-TN-HediNeural",
43
+ "ar-YE-MaryamNeural",
44
+ "ar-YE-SalehNeural",
45
+ "as-IN-YashicaNeural",
46
+ "as-IN-PriyomNeural",
47
+ "az-AZ-BanuNeural",
48
+ "az-AZ-BabekNeural",
49
+ "bg-BG-KalinaNeural",
50
+ "bg-BG-BorislavNeural",
51
+ "bn-BD-NabanitaNeural",
52
+ "bn-BD-PradeepNeural",
53
+ "bn-IN-TanishaaNeural",
54
+ "bn-IN-BashkarNeural",
55
+ "bs-BA-VesnaNeural",
56
+ "bs-BA-GoranNeural",
57
+ "ca-ES-JoanaNeural",
58
+ "ca-ES-EnricNeural",
59
+ "ca-ES-AlbaNeural",
60
+ "cs-CZ-VlastaNeural",
61
+ "cs-CZ-AntoninNeural",
62
+ "cy-GB-NiaNeural",
63
+ "cy-GB-AledNeural",
64
+ "da-DK-ChristelNeural",
65
+ "da-DK-JeppeNeural",
66
+ "de-AT-IngridNeural",
67
+ "de-AT-JonasNeural",
68
+ "de-CH-LeniNeural",
69
+ "de-CH-JanNeural",
70
+ "de-DE-KatjaNeural",
71
+ "de-DE-ConradNeural",
72
+ "de-DE-SeraphinaMultilingualNeural",
73
+ "de-DE-FlorianMultilingualNeural",
74
+ "de-DE-AmalaNeural",
75
+ "de-DE-BerndNeural",
76
+ "de-DE-ChristophNeural",
77
+ "de-DE-ElkeNeural",
78
+ "de-DE-GiselaNeural",
79
+ "de-DE-KasperNeural",
80
+ "de-DE-KillianNeural",
81
+ "de-DE-KlarissaNeural",
82
+ "de-DE-KlausNeural",
83
+ "de-DE-LouisaNeural",
84
+ "de-DE-MajaNeural",
85
+ "de-DE-RalfNeural",
86
+ "de-DE-TanjaNeural",
87
+ "de-DE-Seraphina:DragonHDLatestNeural",
88
+ "el-GR-AthinaNeural",
89
+ "el-GR-NestorasNeural",
90
+ "en-AU-NatashaNeural",
91
+ "en-AU-WilliamNeural",
92
+ "en-AU-AnnetteNeural",
93
+ "en-AU-CarlyNeural",
94
+ "en-AU-DarrenNeural",
95
+ "en-AU-DuncanNeural",
96
+ "en-AU-ElsieNeural",
97
+ "en-AU-FreyaNeural",
98
+ "en-AU-JoanneNeural",
99
+ "en-AU-KenNeural",
100
+ "en-AU-KimNeural",
101
+ "en-AU-NeilNeural",
102
+ "en-AU-TimNeural",
103
+ "en-AU-TinaNeural",
104
+ "en-CA-ClaraNeural",
105
+ "en-CA-LiamNeural",
106
+ "en-GB-SoniaNeural",
107
+ "en-GB-RyanNeural",
108
+ "en-GB-LibbyNeural",
109
+ "en-GB-AdaMultilingualNeural",
110
+ "en-GB-OllieMultilingualNeural",
111
+ "en-GB-AbbiNeural",
112
+ "en-GB-AlfieNeural",
113
+ "en-GB-BellaNeural",
114
+ "en-GB-ElliotNeural",
115
+ "en-GB-EthanNeural",
116
+ "en-GB-HollieNeural",
117
+ "en-GB-MaisieNeural",
118
+ "en-GB-NoahNeural",
119
+ "en-GB-OliverNeural",
120
+ "en-GB-OliviaNeural",
121
+ "en-GB-ThomasNeural",
122
+ "en-GB-MiaNeural",
123
+ "en-HK-YanNeural",
124
+ "en-HK-SamNeural",
125
+ "en-IE-EmilyNeural",
126
+ "en-IE-ConnorNeural",
127
+ "en-IN-AaravNeural",
128
+ "en-IN-AashiNeural",
129
+ "en-IN-AnanyaNeural",
130
+ "en-IN-KavyaNeural",
131
+ "en-IN-KunalNeural",
132
+ "en-IN-NeerjaNeural",
133
+ "en-IN-PrabhatNeural",
134
+ "en-IN-RehaanNeural",
135
+ "en-IN-AartiNeural",
136
+ "en-IN-ArjunNeural",
137
+ "en-KE-AsiliaNeural",
138
+ "en-KE-ChilembaNeural",
139
+ "en-NG-EzinneNeural",
140
+ "en-NG-AbeoNeural",
141
+ "en-NZ-MollyNeural",
142
+ "en-NZ-MitchellNeural",
143
+ "en-PH-RosaNeural",
144
+ "en-PH-JamesNeural",
145
+ "en-SG-LunaNeural",
146
+ "en-SG-WayneNeural",
147
+ "en-TZ-ImaniNeural",
148
+ "en-TZ-ElimuNeural",
149
+ "en-US-AvaMultilingualNeural",
150
+ "en-US-AndrewMultilingualNeural",
151
+ "en-US-EmmaMultilingualNeural",
152
+ "en-US-BrianMultilingualNeural",
153
+ "en-US-AvaNeural",
154
+ "en-US-AndrewNeural",
155
+ "en-US-EmmaNeural",
156
+ "en-US-BrianNeural",
157
+ "en-US-JennyNeural",
158
+ "en-US-GuyNeural",
159
+ "en-US-AriaNeural",
160
+ "en-US-DavisNeural",
161
+ "en-US-JaneNeural",
162
+ "en-US-JasonNeural",
163
+ "en-US-KaiNeural",
164
+ "en-US-LunaNeural",
165
+ "en-US-SaraNeural",
166
+ "en-US-TonyNeural",
167
+ "en-US-NancyNeural",
168
+ "en-US-CoraMultilingualNeural",
169
+ "en-US-ChristopherMultilingualNeural",
170
+ "en-US-BrandonMultilingualNeural",
171
+ "en-US-AmberNeural",
172
+ "en-US-AnaNeural",
173
+ "en-US-AshleyNeural",
174
+ "en-US-BrandonNeural",
175
+ "en-US-ChristopherNeural",
176
+ "en-US-CoraNeural",
177
+ "en-US-ElizabethNeural",
178
+ "en-US-EricNeural",
179
+ "en-US-JacobNeural",
180
+ "en-US-JennyMultilingualNeural",
181
+ "en-US-MichelleNeural",
182
+ "en-US-MonicaNeural",
183
+ "en-US-RogerNeural",
184
+ "en-US-RyanMultilingualNeural",
185
+ "en-US-SteffanNeural",
186
+ "en-US-AdamMultilingualNeural",
187
+ "en-US-AIGenerate1Neural",
188
+ "en-US-AIGenerate2Neural",
189
+ "en-US-AlloyTurboMultilingualNeural",
190
+ "en-US-AmandaMultilingualNeural",
191
+ "en-US-BlueNeural",
192
+ "en-US-DavisMultilingualNeural",
193
+ "en-US-DerekMultilingualNeural",
194
+ "en-US-DustinMultilingualNeural",
195
+ "en-US-EchoTurboMultilingualNeural",
196
+ "en-US-EvelynMultilingualNeural",
197
+ "en-US-FableTurboMultilingualNeural",
198
+ "en-US-LewisMultilingualNeural",
199
+ "en-US-LolaMultilingualNeural",
200
+ "en-US-NancyMultilingualNeural",
201
+ "en-US-NovaTurboMultilingualNeural",
202
+ "en-US-OnyxTurboMultilingualNeural",
203
+ "en-US-PhoebeMultilingualNeural",
204
+ "en-US-SamuelMultilingualNeural",
205
+ "en-US-SerenaMultilingualNeural",
206
+ "en-US-ShimmerTurboMultilingualNeural",
207
+ "en-US-SteffanMultilingualNeural",
208
+ "en-US-Andrew:DragonHDLatestNeural",
209
+ "en-US-Andrew2:DragonHDLatestNeural",
210
+ "en-US-Aria:DragonHDLatestNeural",
211
+ "en-US-Ava:DragonHDLatestNeural",
212
+ "en-US-Brian:DragonHDLatestNeural",
213
+ "en-US-Davis:DragonHDLatestNeural",
214
+ "en-US-Emma:DragonHDLatestNeural",
215
+ "en-US-Emma2:DragonHDLatestNeural",
216
+ "en-US-Jenny:DragonHDLatestNeural",
217
+ "en-US-Steffan:DragonHDLatestNeural",
218
+ "en-ZA-LeahNeural",
219
+ "en-ZA-LukeNeural"
220
+ ];
221
+
222
+ // src/index.ts
223
+ var AzureVoice = class extends MastraVoice {
224
+ speechConfig;
225
+ listeningConfig;
226
+ speechSynthesizer;
227
+ speechRecognizer;
228
+ /**
229
+ * Creates a new instance of AzureVoice for text-to-speech and speech-to-text services.
230
+ *
231
+ * @param {Object} config - Configuration options
232
+ * @param {AzureVoiceConfig} [config.speechModel] - Configuration for text-to-speech
233
+ * @param {AzureVoiceConfig} [config.listeningModel] - Configuration for speech-to-text
234
+ * @param {VoiceId} [config.speaker] - Default voice ID for speech synthesis
235
+ */
236
+ constructor({
237
+ speechModel,
238
+ listeningModel,
239
+ speaker
240
+ } = {}) {
241
+ super({
242
+ speechModel: {
243
+ name: "",
244
+ apiKey: speechModel?.apiKey ?? process.env.AZURE_API_KEY
245
+ },
246
+ listeningModel: {
247
+ name: "",
248
+ apiKey: listeningModel?.apiKey ?? process.env.AZURE_API_KEY
249
+ },
250
+ speaker
251
+ });
252
+ const envApiKey = process.env.AZURE_API_KEY;
253
+ const envRegion = process.env.AZURE_REGION;
254
+ if (speechModel) {
255
+ const apiKey = speechModel.apiKey ?? envApiKey;
256
+ const region = speechModel.region ?? envRegion;
257
+ if (!apiKey) throw new Error("No Azure API key provided for speech model");
258
+ if (!region) throw new Error("No region provided for speech model");
259
+ this.speechConfig = Azure.SpeechConfig.fromSubscription(apiKey, region);
260
+ this.speechConfig.speechSynthesisVoiceName = speechModel.voiceName || speaker || "en-US-AriaNeural";
261
+ this.speechSynthesizer = new Azure.SpeechSynthesizer(this.speechConfig);
262
+ }
263
+ if (listeningModel) {
264
+ const apiKey = listeningModel.apiKey ?? envApiKey;
265
+ const region = listeningModel.region ?? envRegion;
266
+ if (!apiKey) throw new Error("No Azure API key provided for listening model");
267
+ if (!region) throw new Error("No region provided for listening model");
268
+ this.listeningConfig = Azure.SpeechConfig.fromSubscription(apiKey, region);
269
+ if (listeningModel.language) {
270
+ this.listeningConfig.speechRecognitionLanguage = listeningModel.language;
271
+ }
272
+ this.speechRecognizer = new Azure.SpeechRecognizer(this.listeningConfig);
273
+ }
274
+ }
275
+ /**
276
+ * Gets a list of available voices for speech synthesis.
277
+ *
278
+ * @returns {Promise<Array<{ voiceId: string; language: string; region: string; }>>} List of available voices
279
+ */
280
+ async getSpeakers() {
281
+ return this.traced(async () => {
282
+ return AZURE_VOICES.map((voice) => ({
283
+ voiceId: voice,
284
+ language: voice.split("-")[0],
285
+ region: voice.split("-")[1]
286
+ }));
287
+ }, "voice.azure.voices")();
288
+ }
289
+ /**
290
+ * Converts text to speech using Azure's Text-to-Speech service.
291
+ *
292
+ * @param {string | NodeJS.ReadableStream} input - Text to convert to speech
293
+ * @param {Object} [options] - Optional parameters
294
+ * @param {string} [options.speaker] - Voice ID to use for synthesis
295
+ * @returns {Promise<NodeJS.ReadableStream>} Stream containing the synthesized audio
296
+ * @throws {Error} If speech model is not configured or synthesis fails
297
+ */
298
+ async speak(input, options) {
299
+ if (!this.speechConfig) {
300
+ throw new Error("Speech model (Azure) not configured");
301
+ }
302
+ if (typeof input !== "string") {
303
+ const chunks = [];
304
+ try {
305
+ for await (const chunk of input) {
306
+ chunks.push(chunk);
307
+ }
308
+ input = Buffer.concat(chunks).toString("utf-8");
309
+ } catch (error) {
310
+ throw new Error(`Failed to read input stream: ${error instanceof Error ? error.message : String(error)}`);
311
+ }
312
+ }
313
+ if (!input?.trim()) {
314
+ throw new Error("Input text is empty");
315
+ }
316
+ if (options?.speaker) {
317
+ this.speechConfig.speechSynthesisVoiceName = options.speaker;
318
+ }
319
+ const synthesizer = new Azure.SpeechSynthesizer(this.speechConfig);
320
+ try {
321
+ const timeoutPromise = new Promise((_, reject) => {
322
+ setTimeout(() => reject(new Error("Speech synthesis timed out")), 5e3);
323
+ });
324
+ const synthesisPromise = this.traced(
325
+ () => new Promise((resolve, reject) => {
326
+ synthesizer.speakTextAsync(
327
+ input,
328
+ (result2) => result2.errorDetails ? reject(new Error(`Speech synthesis failed: ${result2.errorDetails}`)) : resolve(result2),
329
+ (error) => reject(new Error(`Speech synthesis error: ${String(error)}`))
330
+ );
331
+ }),
332
+ "voice.azure.speak"
333
+ )();
334
+ const result = await Promise.race([synthesisPromise, timeoutPromise]);
335
+ synthesizer.close();
336
+ if (result.reason !== Azure.ResultReason.SynthesizingAudioCompleted) {
337
+ throw new Error(`Speech synthesis failed: ${result.errorDetails || result.reason}`);
338
+ }
339
+ return Readable.from([Buffer.from(result.audioData)]);
340
+ } catch (error) {
341
+ synthesizer.close();
342
+ throw error instanceof Error ? error : new Error(String(error));
343
+ }
344
+ }
345
+ /**
346
+ * Transcribes audio (STT) from a Node.js stream using Azure.
347
+ *
348
+ * @param {NodeJS.ReadableStream} audioStream - The audio to be transcribed, must be in .wav format.
349
+ * @returns {Promise<string>} - The recognized text.
350
+ */
351
+ async listen(audioStream) {
352
+ if (!this.listeningConfig || !this.speechRecognizer) {
353
+ throw new Error("Listening model (Azure) not configured");
354
+ }
355
+ const chunks = [];
356
+ for await (const chunk of audioStream) {
357
+ chunks.push(chunk);
358
+ }
359
+ const audioData = Buffer.concat(chunks);
360
+ const pushStream = Azure.AudioInputStream.createPushStream();
361
+ const audioConfig = Azure.AudioConfig.fromStreamInput(pushStream);
362
+ const recognizer = new Azure.SpeechRecognizer(this.listeningConfig, audioConfig);
363
+ try {
364
+ const recognitionPromise = new Promise((resolve, reject) => {
365
+ recognizer.recognizeOnceAsync(
366
+ (result) => {
367
+ if (result.reason === Azure.ResultReason.RecognizedSpeech) {
368
+ resolve(result.text);
369
+ } else {
370
+ const reason = Azure.ResultReason[result.reason] || result.reason;
371
+ reject(new Error(`Speech recognition failed: ${reason} - ${result.errorDetails || ""}`));
372
+ }
373
+ },
374
+ (error) => reject(new Error(`Speech recognition error: ${String(error)}`))
375
+ );
376
+ });
377
+ const chunkSize = 4096;
378
+ for (let i = 0; i < audioData.length; i += chunkSize) {
379
+ const chunk = audioData.slice(i, i + chunkSize);
380
+ pushStream.write(chunk);
381
+ }
382
+ pushStream.close();
383
+ const text = await this.traced(() => recognitionPromise, "voice.azure.listen")();
384
+ return text;
385
+ } catch (error) {
386
+ if (error instanceof Error) {
387
+ throw error;
388
+ }
389
+ throw new Error(String(error));
390
+ } finally {
391
+ recognizer.close();
392
+ }
393
+ }
394
+ };
395
+
396
+ export { AzureVoice };
@@ -0,0 +1,6 @@
1
+ import { createConfig } from '@internal/lint/eslint';
2
+
3
+ const config = await createConfig();
4
+
5
+ /** @type {import("eslint").Linter.Config[]} */
6
+ export default [...config];
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@mastra/voice-azure",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Mastra Azure speech integration",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.cts",
16
+ "default": "./dist/index.cjs"
17
+ }
18
+ },
19
+ "./package.json": "./package.json"
20
+ },
21
+ "dependencies": {
22
+ "microsoft-cognitiveservices-speech-sdk": "^1.42.0",
23
+ "@mastra/core": "^0.6.4-alpha.1"
24
+ },
25
+ "devDependencies": {
26
+ "@microsoft/api-extractor": "^7.52.1",
27
+ "@types/node": "^22.13.10",
28
+ "eslint": "^9.22.0",
29
+ "tsup": "^8.4.0",
30
+ "typescript": "^5.8.2",
31
+ "vitest": "^2.1.9",
32
+ "@internal/lint": "0.0.1"
33
+ },
34
+ "scripts": {
35
+ "build": "tsup src/index.ts --format esm,cjs --experimental-dts --clean --treeshake=smallest --splitting",
36
+ "build:watch": "pnpm build --watch",
37
+ "test": "vitest run",
38
+ "lint": "eslint ."
39
+ }
40
+ }
@@ -0,0 +1,167 @@
1
+ import { createReadStream, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { Readable } from 'stream';
4
+ import { describe, it, expect, beforeAll } from 'vitest';
5
+
6
+ import { AzureVoice } from './index';
7
+
8
+ describe('AzureVoice Integration Tests', () => {
9
+ let voice: AzureVoice;
10
+ const outputDir = join(process.cwd(), 'test-outputs');
11
+ const subscriptionKey = process.env.AZURE_API_KEY ?? 'fake-key';
12
+ const region = process.env.AZURE_REGION ?? 'eastus';
13
+
14
+ beforeAll(() => {
15
+ try {
16
+ mkdirSync(outputDir, { recursive: true });
17
+ } catch (err) {
18
+ // Ignore if directory already exists
19
+ console.log('Directory already exists:', err);
20
+ }
21
+
22
+ voice = new AzureVoice({
23
+ speechModel: { apiKey: subscriptionKey, region },
24
+ listeningModel: { apiKey: subscriptionKey, region },
25
+ });
26
+ });
27
+
28
+ describe('getSpeakers', () => {
29
+ it('should list available voices', async () => {
30
+ const voices = await voice.getSpeakers();
31
+ expect(voices.length).toBeGreaterThan(0);
32
+ expect(voices[0]).toHaveProperty('voiceId');
33
+ expect(voices[0]).toHaveProperty('language');
34
+ expect(voices[0]).toHaveProperty('region');
35
+ });
36
+ });
37
+
38
+ it('should initialize with default parameters', async () => {
39
+ const defaultVoice = new AzureVoice();
40
+ const voices = await defaultVoice.getSpeakers();
41
+ expect(voices).toBeInstanceOf(Array);
42
+ expect(voices.length).toBeGreaterThan(0);
43
+ });
44
+
45
+ describe('speak', () => {
46
+ it('should speak with default parameters', async () => {
47
+ const defaultVoice = new AzureVoice({
48
+ speechModel: { apiKey: subscriptionKey, region },
49
+ });
50
+ const audioStream = await defaultVoice.speak('Hello with defaults');
51
+
52
+ const chunks: Buffer[] = [];
53
+ for await (const chunk of audioStream) {
54
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
55
+ }
56
+ const audioBuffer = Buffer.concat(chunks);
57
+ expect(audioBuffer.length).toBeGreaterThan(0);
58
+ });
59
+
60
+ it('should generate audio stream from text', async () => {
61
+ const audioStream = await voice.speak('Hello World', {
62
+ speaker: 'en-US-AriaNeural',
63
+ });
64
+
65
+ const chunks: Buffer[] = [];
66
+ for await (const chunk of audioStream) {
67
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
68
+ }
69
+ const audioBuffer = Buffer.concat(chunks);
70
+ expect(audioBuffer.length).toBeGreaterThan(0);
71
+
72
+ const outputPath = join(outputDir, 'azure-speech-test.wav');
73
+ writeFileSync(outputPath, audioBuffer);
74
+ });
75
+
76
+ it('should work with different parameters', async () => {
77
+ const audioStream = await voice.speak('Test with parameters', {
78
+ speaker: 'en-US-JennyNeural',
79
+ });
80
+
81
+ const chunks: Buffer[] = [];
82
+ for await (const chunk of audioStream) {
83
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
84
+ }
85
+ const audioBuffer = Buffer.concat(chunks);
86
+ expect(audioBuffer.length).toBeGreaterThan(0);
87
+
88
+ const outputPath = join(outputDir, 'azure-speech-params.wav');
89
+ writeFileSync(outputPath, audioBuffer);
90
+ });
91
+
92
+ it('should accept text stream as input', async () => {
93
+ const inputStream = new Readable();
94
+ inputStream.push('Hello from stream');
95
+ inputStream.push(null);
96
+
97
+ const audioStream = await voice.speak(inputStream, {
98
+ speaker: 'en-US-AriaNeural',
99
+ });
100
+
101
+ const chunks: Buffer[] = [];
102
+ for await (const chunk of audioStream) {
103
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
104
+ }
105
+ const audioBuffer = Buffer.concat(chunks);
106
+ expect(audioBuffer.length).toBeGreaterThan(0);
107
+
108
+ const outputPath = join(outputDir, 'azure-speech-stream.wav');
109
+ writeFileSync(outputPath, audioBuffer);
110
+ });
111
+ });
112
+
113
+ describe('listen', () => {
114
+ it('should listen with default parameters', async () => {
115
+ const defaultVoice = new AzureVoice({
116
+ speechModel: { apiKey: subscriptionKey, region },
117
+ listeningModel: { apiKey: subscriptionKey, region },
118
+ });
119
+ const audioStream = await defaultVoice.speak('Listening test with defaults');
120
+
121
+ const text = await defaultVoice.listen(audioStream);
122
+ expect(text).toBeTruthy();
123
+ expect(typeof text).toBe('string');
124
+ expect(text.toLowerCase()).toContain('listening test');
125
+ });
126
+
127
+ it('should transcribe audio from file', async () => {
128
+ const filePath = join(outputDir, 'azure-speech-test.wav');
129
+ const audioStream = createReadStream(filePath);
130
+
131
+ const text = await voice.listen(audioStream);
132
+ expect(text).toBeTruthy();
133
+ expect(typeof text).toBe('string');
134
+ expect(text.length).toBeGreaterThan(0);
135
+ });
136
+
137
+ it('should transcribe audio stream', async () => {
138
+ const audioStream = await voice.speak('This is a test for transcription', {
139
+ speaker: 'en-US-AriaNeural',
140
+ });
141
+
142
+ const text = await voice.listen(audioStream);
143
+ expect(text).toBeTruthy();
144
+ expect(typeof text).toBe('string');
145
+ expect(text.toLowerCase()).toContain('test');
146
+ });
147
+ });
148
+
149
+ describe('error handling', () => {
150
+ it('should handle empty text', async () => {
151
+ await expect(voice.speak('')).rejects.toThrow('Input text is empty');
152
+ });
153
+
154
+ it('should handle missing API key', () => {
155
+ const { AZURE_API_KEY } = process.env;
156
+ delete process.env.AZURE_API_KEY;
157
+
158
+ expect(() => {
159
+ new AzureVoice({
160
+ speechModel: { region: 'eastus' },
161
+ });
162
+ }).toThrow('No Azure API key provided for speech model');
163
+
164
+ process.env.AZURE_API_KEY = AZURE_API_KEY;
165
+ });
166
+ });
167
+ });