@ssml-builder-js/azure-tts-client 2.12.0 → 2.14.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.
@@ -0,0 +1,110 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import {
4
+ ChunkValidationError,
5
+ UnsupportedMergeFormatError,
6
+ mergeAudioBuffers,
7
+ synthesizeSsmlChunksSafe,
8
+ } from "../src/index.ts";
9
+
10
+ function wav(data: number[], sampleRate = 16_000): ArrayBuffer {
11
+ const pcm = Uint8Array.from(data);
12
+ const output = new Uint8Array(44 + pcm.length + (pcm.length & 1));
13
+ const view = new DataView(output.buffer);
14
+ output.set(new TextEncoder().encode("RIFF"), 0);
15
+ output.set(new TextEncoder().encode("WAVEfmt "), 8);
16
+ view.setUint32(4, output.length - 8, true);
17
+ view.setUint32(16, 16, true);
18
+ view.setUint16(20, 1, true);
19
+ view.setUint16(22, 1, true);
20
+ view.setUint32(24, sampleRate, true);
21
+ view.setUint32(28, sampleRate * 2, true);
22
+ view.setUint16(32, 2, true);
23
+ view.setUint16(34, 16, true);
24
+ output.set(new TextEncoder().encode("data"), 36);
25
+ view.setUint32(40, pcm.length, true);
26
+ output.set(pcm, 44);
27
+ return output.buffer;
28
+ }
29
+
30
+ const validSsml = (text: string) =>
31
+ `<speak version="1.0" xml:lang="en-US"><voice name="en-US-JennyNeural">${text}</voice></speak>`;
32
+
33
+ test("mergeAudioBuffers rebuilds one valid WAV header", () => {
34
+ const merged = mergeAudioBuffers([wav([1, 2]), wav([3, 4, 5])], "riff-16khz-16bit-mono-pcm");
35
+ const bytes = new Uint8Array(merged);
36
+ const view = new DataView(merged);
37
+ assert.equal(new TextDecoder().decode(bytes.slice(0, 4)), "RIFF");
38
+ assert.equal(new TextDecoder().decode(bytes.slice(8, 12)), "WAVE");
39
+ assert.equal(view.getUint32(4, true), merged.byteLength - 8);
40
+ assert.equal(view.getUint32(40, true), 5);
41
+ assert.deepEqual([...bytes.slice(44, 49)], [1, 2, 3, 4, 5]);
42
+ assert.equal(bytes[49], 0);
43
+ });
44
+
45
+ test("mergeAudioBuffers removes per-buffer ID3 tags from MP3 streams", () => {
46
+ const tag = Uint8Array.from([0x49, 0x44, 0x33, 4, 0, 0, 0, 0, 0, 0]);
47
+ const first = new Uint8Array(tag.length + 2);
48
+ first.set(tag);
49
+ first.set([1, 2], tag.length);
50
+ const second = new Uint8Array(tag.length + 1);
51
+ second.set(tag);
52
+ second[tag.length] = 3;
53
+ assert.deepEqual(
54
+ [...new Uint8Array(mergeAudioBuffers([first.buffer, second.buffer], "audio-16khz-128kbitrate-mono-mp3"))],
55
+ [1, 2, 3],
56
+ );
57
+ });
58
+
59
+ test("mergeAudioBuffers rejects container formats that require remultiplexing", () => {
60
+ assert.throws(
61
+ () => mergeAudioBuffers([new ArrayBuffer(1)], "webm-24khz-16bit-mono-opus"),
62
+ (error: unknown) => error instanceof UnsupportedMergeFormatError,
63
+ );
64
+ });
65
+
66
+ test("synthesizeSsmlChunksSafe validates all chunks before calling Azure", async () => {
67
+ let calls = 0;
68
+ const progress: string[] = [];
69
+ const result = await synthesizeSsmlChunksSafe(
70
+ {
71
+ synthesizeSsml: async () => {
72
+ calls += 1;
73
+ return { audioData: new ArrayBuffer(0), durationMs: 0 };
74
+ },
75
+ },
76
+ [validSsml("ok"), "<speak>"],
77
+ { onProgress: (event) => progress.push(`${event.chunkIndex}:${event.status}`) },
78
+ );
79
+ assert.equal(result.ok, false);
80
+ assert.equal(result.status, "validation-error");
81
+ if (!result.ok) {
82
+ assert.ok(result.error instanceof ChunkValidationError);
83
+ assert.equal(result.error.chunkIndex, 1);
84
+ }
85
+ assert.equal(calls, 0);
86
+ assert.deepEqual(progress, ["0:pending", "1:pending", "1:failed"]);
87
+ });
88
+
89
+ test("synthesizeSsmlChunksSafe reports structured lifecycle progress", async () => {
90
+ const progress: Array<{ chunkIndex: number; status: string; durationMs: number }> = [];
91
+ const result = await synthesizeSsmlChunksSafe(
92
+ {
93
+ synthesizeSsml: async () => ({ audioData: Uint8Array.of(1).buffer, durationMs: 20 }),
94
+ },
95
+ [
96
+ { ssml: validSsml("one"), originalTextRange: { start: 0, end: 3 }, sourceNodePath: ["speak", "voice[0]"] },
97
+ { ssml: validSsml("two"), originalTextRange: { start: 3, end: 6 }, sourceNodePath: ["speak", "voice[0]"] },
98
+ ],
99
+ {
100
+ onProgress: (event) =>
101
+ progress.push({ chunkIndex: event.chunkIndex, status: event.status, durationMs: event.durationMs }),
102
+ },
103
+ );
104
+ assert.equal(result.ok, true);
105
+ assert.deepEqual(
106
+ progress.map(({ chunkIndex, status }) => `${chunkIndex}:${status}`),
107
+ ["0:pending", "1:pending", "0:synthesizing", "0:success", "1:synthesizing", "1:success"],
108
+ );
109
+ assert.ok(progress.every(({ durationMs }) => durationMs >= 0));
110
+ });