@ssml-builder-js/azure-tts-client 2.15.0 → 2.16.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/CHANGELOG.md +11 -0
- package/dist/index.d.mts +52 -5
- package/dist/index.d.ts +52 -5
- package/dist/index.js +510 -154
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +512 -155
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +4 -0
- package/src/errors.ts +23 -1
- package/src/index.ts +12 -1
- package/src/safe.ts +238 -111
- package/src/synthesis.ts +361 -66
- package/src/types.ts +31 -0
- package/test/v216-pipeline.test.ts +110 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
AudioFormatMismatchError,
|
|
5
|
+
AzureTtsError,
|
|
6
|
+
mergeAudioBuffers,
|
|
7
|
+
mergeSynthesisResults,
|
|
8
|
+
synthesizeSsmlChunksSafe,
|
|
9
|
+
} from "../src/index.ts";
|
|
10
|
+
|
|
11
|
+
const validSsml = (text: string) =>
|
|
12
|
+
`<speak version="1.0" xml:lang="en-US"><voice name="en-US-JennyNeural">${text}</voice></speak>`;
|
|
13
|
+
|
|
14
|
+
function wav(data: number[], sampleRate: number): ArrayBuffer {
|
|
15
|
+
const output = new Uint8Array(44 + data.length + (data.length & 1));
|
|
16
|
+
const view = new DataView(output.buffer);
|
|
17
|
+
output.set(new TextEncoder().encode("RIFF"), 0);
|
|
18
|
+
output.set(new TextEncoder().encode("WAVEfmt "), 8);
|
|
19
|
+
view.setUint32(4, output.length - 8, true);
|
|
20
|
+
view.setUint32(16, 16, true);
|
|
21
|
+
view.setUint16(20, 1, true);
|
|
22
|
+
view.setUint16(22, 1, true);
|
|
23
|
+
view.setUint32(24, sampleRate, true);
|
|
24
|
+
view.setUint32(28, sampleRate * 2, true);
|
|
25
|
+
view.setUint16(32, 2, true);
|
|
26
|
+
view.setUint16(34, 16, true);
|
|
27
|
+
output.set(new TextEncoder().encode("data"), 36);
|
|
28
|
+
view.setUint32(40, data.length, true);
|
|
29
|
+
output.set(data, 44);
|
|
30
|
+
return output.buffer;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test("chunk merging rejects incompatible WAV sample rates", () => {
|
|
34
|
+
assert.throws(
|
|
35
|
+
() => mergeAudioBuffers([wav([1, 2], 16_000), wav([3, 4], 24_000)], { format: "riff-16khz-16bit-mono-pcm" }),
|
|
36
|
+
(error: unknown) => error instanceof AudioFormatMismatchError,
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("safe chunk synthesis retries transient errors and preserves chunk order", async () => {
|
|
41
|
+
let attempts = 0;
|
|
42
|
+
let oneAttempts = 0;
|
|
43
|
+
let active = 0;
|
|
44
|
+
let maximumActive = 0;
|
|
45
|
+
const completed: string[] = [];
|
|
46
|
+
const retryEvents: number[] = [];
|
|
47
|
+
const result = await synthesizeSsmlChunksSafe(
|
|
48
|
+
{
|
|
49
|
+
synthesizeSsml: async (ssml) => {
|
|
50
|
+
attempts += 1;
|
|
51
|
+
active += 1;
|
|
52
|
+
maximumActive = Math.max(maximumActive, active);
|
|
53
|
+
await new Promise((resolve) => setTimeout(resolve, ssml.includes("one") ? 5 : 1));
|
|
54
|
+
active -= 1;
|
|
55
|
+
if (ssml.includes("one")) {
|
|
56
|
+
oneAttempts += 1;
|
|
57
|
+
if (oneAttempts < 3) throw new AzureTtsError(503, "Unavailable", "", null);
|
|
58
|
+
}
|
|
59
|
+
completed.push(ssml.includes("one") ? "one" : "two");
|
|
60
|
+
return { audioData: Uint8Array.of(ssml.includes("one") ? 1 : 2).buffer, durationMs: 10 };
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
[validSsml("one"), validSsml("two")],
|
|
64
|
+
{
|
|
65
|
+
concurrency: 2,
|
|
66
|
+
retryOptions: { maxRetries: 2, initialDelayMs: 0, maxDelayMs: 0 },
|
|
67
|
+
onProgress: (event) => {
|
|
68
|
+
if (event.isRetrying && event.retryAttempt) retryEvents.push(event.retryAttempt);
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
assert.equal(result.ok, true);
|
|
73
|
+
assert.equal(maximumActive, 2);
|
|
74
|
+
assert.deepEqual(retryEvents, [1, 2]);
|
|
75
|
+
assert.equal(attempts, 4);
|
|
76
|
+
if (result.ok) assert.deepEqual([...new Uint8Array(result.value.audioData)], [1, 2]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("safe chunk synthesis does not retry permanent HTTP errors", async () => {
|
|
80
|
+
let calls = 0;
|
|
81
|
+
const result = await synthesizeSsmlChunksSafe(
|
|
82
|
+
{
|
|
83
|
+
synthesizeSsml: async () => {
|
|
84
|
+
calls += 1;
|
|
85
|
+
throw new AzureTtsError(400, "Bad Request", "", null);
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
[validSsml("bad")],
|
|
89
|
+
{ retryOptions: { maxRetries: 3, initialDelayMs: 0, maxDelayMs: 0 } },
|
|
90
|
+
);
|
|
91
|
+
assert.equal(result.ok, false);
|
|
92
|
+
assert.equal(calls, 1);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("custom mergers receive input specifications and cancellation", async () => {
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
let received: { format: string; inputSpecs: unknown[]; signal: AbortSignal } | undefined;
|
|
98
|
+
const result = await mergeSynthesisResults([{ audioData: Uint8Array.of(1).buffer, durationMs: 1 }], {
|
|
99
|
+
format: "webm-24khz-16bit-mono-opus",
|
|
100
|
+
signal: controller.signal,
|
|
101
|
+
customMerger: (buffers, context) => {
|
|
102
|
+
received = { format: context.format, inputSpecs: context.inputSpecs, signal: context.signal };
|
|
103
|
+
return buffers[0] ?? new ArrayBuffer(0);
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
assert.equal(result.mimeType, "audio/webm");
|
|
107
|
+
assert.equal(received?.format, "webm-24khz-16bit-mono-opus");
|
|
108
|
+
assert.equal(received?.inputSpecs.length, 1);
|
|
109
|
+
assert.equal(received?.signal, controller.signal);
|
|
110
|
+
});
|