@ssml-builder-js/azure-tts-client 2.17.0 → 2.19.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 +20 -0
- package/dist/index.d.mts +123 -43
- package/dist/index.d.ts +123 -43
- package/dist/index.js +674 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +670 -90
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +64 -8
- package/src/deadline.ts +52 -0
- package/src/errors.ts +86 -4
- package/src/index.ts +15 -1
- package/src/outputFormats.ts +3 -0
- package/src/safe.ts +115 -25
- package/src/synthesis.ts +508 -50
- package/src/types.ts +32 -1
- package/test/v218-pipeline.test.ts +91 -0
- package/test/v219-pipeline.test.ts +131 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ssml-builder-js/azure-tts-client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.19.0",
|
|
4
4
|
"description": "Azure Text-to-Speech client using the Microsoft Speech SDK",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"typescript": "^6.0.3"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@ssml-builder-js/ssml-core": "^2.
|
|
38
|
+
"@ssml-builder-js/ssml-core": "^2.19.0",
|
|
39
39
|
"microsoft-cognitiveservices-speech-sdk": "1.51.0"
|
|
40
40
|
}
|
|
41
41
|
}
|
package/src/client.ts
CHANGED
|
@@ -19,16 +19,45 @@ export class AzureTtsClient {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
async synthesize(ssml: string): Promise<ArrayBuffer> {
|
|
22
|
-
const {
|
|
22
|
+
const {
|
|
23
|
+
region,
|
|
24
|
+
subscriptionKey,
|
|
25
|
+
outputFormat,
|
|
26
|
+
signal,
|
|
27
|
+
timeoutMs,
|
|
28
|
+
timeouts,
|
|
29
|
+
customHeaders,
|
|
30
|
+
fingerprintSchemaVersion,
|
|
31
|
+
} = this.#options;
|
|
23
32
|
const endpoint = this.#options.endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
24
33
|
this.#options.logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
25
34
|
|
|
26
|
-
const config = {
|
|
35
|
+
const config = {
|
|
36
|
+
endpoint,
|
|
37
|
+
region,
|
|
38
|
+
subscriptionKey,
|
|
39
|
+
outputFormat,
|
|
40
|
+
signal,
|
|
41
|
+
timeoutMs,
|
|
42
|
+
timeouts,
|
|
43
|
+
retryOptions: this.#options.retryOptions,
|
|
44
|
+
customHeaders,
|
|
45
|
+
fingerprintSchemaVersion,
|
|
46
|
+
};
|
|
27
47
|
return synthesizeSpeech(ssml, config);
|
|
28
48
|
}
|
|
29
49
|
|
|
30
50
|
async synthesizeSsml(ssml: string, options: Partial<TtsConfig> = {}): Promise<SsmlSynthesisResult> {
|
|
31
|
-
const {
|
|
51
|
+
const {
|
|
52
|
+
region,
|
|
53
|
+
subscriptionKey,
|
|
54
|
+
outputFormat,
|
|
55
|
+
signal,
|
|
56
|
+
timeoutMs,
|
|
57
|
+
timeouts,
|
|
58
|
+
customHeaders,
|
|
59
|
+
fingerprintSchemaVersion,
|
|
60
|
+
} = this.#options;
|
|
32
61
|
const endpoint = this.#options.endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
33
62
|
this.#options.logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
34
63
|
|
|
@@ -43,6 +72,14 @@ export class AzureTtsClient {
|
|
|
43
72
|
sourceNodePath: options.sourceNodePath,
|
|
44
73
|
sourceTextSegments: options.sourceTextSegments,
|
|
45
74
|
sourceMarkers: options.sourceMarkers,
|
|
75
|
+
retryOptions: options.retryOptions ?? this.#options.retryOptions,
|
|
76
|
+
cancelOnFailure: options.cancelOnFailure ?? this.#options.cancelOnFailure,
|
|
77
|
+
customMerger: options.customMerger ?? this.#options.customMerger,
|
|
78
|
+
outputMimeType: options.outputMimeType ?? this.#options.outputMimeType,
|
|
79
|
+
postMergeValidator: options.postMergeValidator ?? this.#options.postMergeValidator,
|
|
80
|
+
resumeValidation: options.resumeValidation ?? this.#options.resumeValidation,
|
|
81
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
82
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion,
|
|
46
83
|
});
|
|
47
84
|
}
|
|
48
85
|
|
|
@@ -50,7 +87,16 @@ export class AzureTtsClient {
|
|
|
50
87
|
chunks: readonly (SsmlSynthesisChunk | string)[],
|
|
51
88
|
options: SynthesizeChunksOptions = {},
|
|
52
89
|
): Promise<SsmlSynthesisResult> {
|
|
53
|
-
const {
|
|
90
|
+
const {
|
|
91
|
+
region,
|
|
92
|
+
subscriptionKey,
|
|
93
|
+
outputFormat,
|
|
94
|
+
signal,
|
|
95
|
+
timeoutMs,
|
|
96
|
+
timeouts,
|
|
97
|
+
customHeaders,
|
|
98
|
+
fingerprintSchemaVersion,
|
|
99
|
+
} = this.#options;
|
|
54
100
|
const endpoint = this.#options.endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
55
101
|
return synthesizeSsmlChunks(chunks, {
|
|
56
102
|
endpoint,
|
|
@@ -64,12 +110,15 @@ export class AzureTtsClient {
|
|
|
64
110
|
onProgress: options.onProgress ?? this.#options.onProgress,
|
|
65
111
|
concurrency: options.concurrency ?? this.#options.concurrency,
|
|
66
112
|
retryOptions: options.retryOptions ?? this.#options.retryOptions,
|
|
67
|
-
cancelOnFailure: options.cancelOnFailure,
|
|
113
|
+
cancelOnFailure: options.cancelOnFailure ?? this.#options.cancelOnFailure,
|
|
68
114
|
resumeChunks: options.resumeChunks,
|
|
69
115
|
resumeChunkIndices: options.resumeChunkIndices,
|
|
70
|
-
customMerger: options.customMerger,
|
|
71
|
-
outputMimeType: options.outputMimeType,
|
|
72
|
-
postMergeValidator: options.postMergeValidator,
|
|
116
|
+
customMerger: options.customMerger ?? this.#options.customMerger,
|
|
117
|
+
outputMimeType: options.outputMimeType ?? this.#options.outputMimeType,
|
|
118
|
+
postMergeValidator: options.postMergeValidator ?? this.#options.postMergeValidator,
|
|
119
|
+
resumeValidation: options.resumeValidation ?? this.#options.resumeValidation,
|
|
120
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
121
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion,
|
|
73
122
|
});
|
|
74
123
|
}
|
|
75
124
|
|
|
@@ -90,6 +139,13 @@ export class AzureTtsClient {
|
|
|
90
139
|
onProgress: options.onProgress ?? this.#options.onProgress,
|
|
91
140
|
concurrency: options.concurrency ?? this.#options.concurrency,
|
|
92
141
|
retryOptions: options.retryOptions ?? this.#options.retryOptions,
|
|
142
|
+
cancelOnFailure: options.cancelOnFailure ?? this.#options.cancelOnFailure,
|
|
143
|
+
customMerger: options.customMerger ?? this.#options.customMerger,
|
|
144
|
+
outputMimeType: options.outputMimeType ?? this.#options.outputMimeType,
|
|
145
|
+
postMergeValidator: options.postMergeValidator ?? this.#options.postMergeValidator,
|
|
146
|
+
resumeValidation: options.resumeValidation ?? this.#options.resumeValidation,
|
|
147
|
+
customHeaders: options.customHeaders ?? this.#options.customHeaders,
|
|
148
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? this.#options.fingerprintSchemaVersion,
|
|
93
149
|
});
|
|
94
150
|
}
|
|
95
151
|
|
package/src/deadline.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SynthesisTimeoutError } from "./errors.ts";
|
|
2
|
+
|
|
3
|
+
/** Coordinates one absolute deadline across validation, synthesis, retries, and merging. */
|
|
4
|
+
export class DeadlineController {
|
|
5
|
+
readonly deadlineAtMs: number | undefined;
|
|
6
|
+
readonly signal: AbortSignal;
|
|
7
|
+
readonly #controller = new AbortController();
|
|
8
|
+
readonly #parent?: AbortSignal;
|
|
9
|
+
readonly #onParentAbort: () => void;
|
|
10
|
+
#timer: ReturnType<typeof setTimeout> | undefined;
|
|
11
|
+
#timedOut = false;
|
|
12
|
+
|
|
13
|
+
constructor(totalJobMs: number | undefined, parent?: AbortSignal) {
|
|
14
|
+
this.#parent = parent;
|
|
15
|
+
this.#onParentAbort = () => this.#controller.abort();
|
|
16
|
+
this.deadlineAtMs = totalJobMs !== undefined && totalJobMs > 0 ? Date.now() + totalJobMs : undefined;
|
|
17
|
+
this.signal = this.deadlineAtMs === undefined && parent ? parent : this.#controller.signal;
|
|
18
|
+
if (parent?.aborted) this.#controller.abort();
|
|
19
|
+
parent?.addEventListener("abort", this.#onParentAbort, { once: true });
|
|
20
|
+
if (this.deadlineAtMs !== undefined) {
|
|
21
|
+
this.#timer = setTimeout(
|
|
22
|
+
() => {
|
|
23
|
+
this.#timedOut = true;
|
|
24
|
+
this.#controller.abort();
|
|
25
|
+
},
|
|
26
|
+
Math.max(0, this.deadlineAtMs - Date.now()),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get timedOut(): boolean {
|
|
32
|
+
return this.#timedOut || (this.deadlineAtMs !== undefined && this.remainingMs <= 0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get remainingMs(): number {
|
|
36
|
+
return this.deadlineAtMs === undefined ? Number.POSITIVE_INFINITY : Math.max(0, this.deadlineAtMs - Date.now());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throwIfExpired(): void {
|
|
40
|
+
if (this.timedOut) throw new SynthesisTimeoutError("Speech synthesis exceeded the total job deadline.");
|
|
41
|
+
if (this.signal.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
abort(): void {
|
|
45
|
+
this.#controller.abort();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
dispose(): void {
|
|
49
|
+
if (this.#timer) clearTimeout(this.#timer);
|
|
50
|
+
this.#parent?.removeEventListener("abort", this.#onParentAbort);
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -5,7 +5,29 @@ export type SynthesisErrorKind =
|
|
|
5
5
|
| "audio-format-mismatch"
|
|
6
6
|
| "unsupported-format-error"
|
|
7
7
|
| "cancelled"
|
|
8
|
-
| "timeout"
|
|
8
|
+
| "timeout"
|
|
9
|
+
| "incomplete-chunk-set";
|
|
10
|
+
|
|
11
|
+
export type SerializedChunkErrorCode =
|
|
12
|
+
| "VALIDATION_ERROR"
|
|
13
|
+
| "AZURE_API_ERROR"
|
|
14
|
+
| "TIMEOUT"
|
|
15
|
+
| "CANCELLED"
|
|
16
|
+
| "MERGE_ERROR"
|
|
17
|
+
| "FORMAT_MISMATCH";
|
|
18
|
+
|
|
19
|
+
export interface SerializedChunkError {
|
|
20
|
+
code: SerializedChunkErrorCode;
|
|
21
|
+
phase: "url-validation" | "synthesis" | "merge";
|
|
22
|
+
message: string;
|
|
23
|
+
isOriginalFailure: boolean;
|
|
24
|
+
isRetryable: boolean;
|
|
25
|
+
httpStatus?: number;
|
|
26
|
+
details?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Backward-compatible name used by persisted execution-state consumers. */
|
|
30
|
+
export type SerializedExecutionError = SerializedChunkError;
|
|
9
31
|
|
|
10
32
|
export class AzureTtsError extends Error {
|
|
11
33
|
readonly kind = "azure-api-error" as const;
|
|
@@ -95,6 +117,19 @@ export class SynthesisTimeoutError extends Error {
|
|
|
95
117
|
}
|
|
96
118
|
}
|
|
97
119
|
|
|
120
|
+
export class IncompleteChunkSetError extends Error {
|
|
121
|
+
readonly kind = "incomplete-chunk-set" as const;
|
|
122
|
+
readonly totalChunks: number;
|
|
123
|
+
readonly missingChunkIndices: readonly number[];
|
|
124
|
+
|
|
125
|
+
constructor(totalChunks: number, missingChunkIndices: readonly number[]) {
|
|
126
|
+
super(`Cannot merge an incomplete chunk set; missing chunk indices: ${missingChunkIndices.join(", ")}.`);
|
|
127
|
+
this.name = "IncompleteChunkSetError";
|
|
128
|
+
this.totalChunks = totalChunks;
|
|
129
|
+
this.missingChunkIndices = [...missingChunkIndices];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
98
133
|
export class MergeError extends Error {
|
|
99
134
|
readonly kind = "merge-error" as const;
|
|
100
135
|
readonly cause: unknown;
|
|
@@ -136,7 +171,52 @@ export type AzureTtsSynthesisError =
|
|
|
136
171
|
| AudioFormatMismatchError
|
|
137
172
|
| UnsupportedMergeFormatError
|
|
138
173
|
| SynthesisCancelledError
|
|
139
|
-
| SynthesisTimeoutError
|
|
174
|
+
| SynthesisTimeoutError
|
|
175
|
+
| IncompleteChunkSetError;
|
|
176
|
+
|
|
177
|
+
export function serializeChunkError(
|
|
178
|
+
error: unknown,
|
|
179
|
+
phase: SerializedChunkError["phase"],
|
|
180
|
+
isOriginalFailure: boolean,
|
|
181
|
+
): SerializedChunkError {
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
const status = error instanceof AzureTtsError ? error.status : undefined;
|
|
184
|
+
const kind = error && typeof error === "object" && "kind" in error ? String(error.kind) : "";
|
|
185
|
+
const code: SerializedChunkErrorCode =
|
|
186
|
+
kind === "validation-error"
|
|
187
|
+
? "VALIDATION_ERROR"
|
|
188
|
+
: kind === "timeout" || /tim(?:e|ed) ?out|deadline/i.test(message)
|
|
189
|
+
? "TIMEOUT"
|
|
190
|
+
: kind === "cancelled" || /cancel|abort/i.test(message)
|
|
191
|
+
? "CANCELLED"
|
|
192
|
+
: kind === "audio-format-mismatch" || kind === "unsupported-format-error"
|
|
193
|
+
? "FORMAT_MISMATCH"
|
|
194
|
+
: kind === "merge-error" || phase === "merge"
|
|
195
|
+
? "MERGE_ERROR"
|
|
196
|
+
: "AZURE_API_ERROR";
|
|
197
|
+
const details: Record<string, unknown> = {};
|
|
198
|
+
if (error instanceof AzureTtsError) {
|
|
199
|
+
details.statusText = error.statusText;
|
|
200
|
+
if (error.requestId) details.requestId = error.requestId;
|
|
201
|
+
}
|
|
202
|
+
if (error instanceof IncompleteChunkSetError) {
|
|
203
|
+
details.totalChunks = error.totalChunks;
|
|
204
|
+
details.missingChunkIndices = [...error.missingChunkIndices];
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
code,
|
|
208
|
+
phase,
|
|
209
|
+
message,
|
|
210
|
+
isOriginalFailure,
|
|
211
|
+
isRetryable:
|
|
212
|
+
code === "AZURE_API_ERROR" &&
|
|
213
|
+
(status === 429 ||
|
|
214
|
+
(status !== undefined && status >= 500) ||
|
|
215
|
+
/network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message)),
|
|
216
|
+
...(status !== undefined && status > 0 ? { httpStatus: status } : {}),
|
|
217
|
+
...(Object.keys(details).length > 0 ? { details } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
140
220
|
|
|
141
221
|
export function toSynthesisError(
|
|
142
222
|
error: unknown,
|
|
@@ -146,14 +226,16 @@ export function toSynthesisError(
|
|
|
146
226
|
| AudioFormatMismatchError
|
|
147
227
|
| UnsupportedMergeFormatError
|
|
148
228
|
| SynthesisCancelledError
|
|
149
|
-
| SynthesisTimeoutError
|
|
229
|
+
| SynthesisTimeoutError
|
|
230
|
+
| IncompleteChunkSetError {
|
|
150
231
|
if (
|
|
151
232
|
error instanceof AzureTtsError ||
|
|
152
233
|
error instanceof MergeError ||
|
|
153
234
|
error instanceof AudioFormatMismatchError ||
|
|
154
235
|
error instanceof UnsupportedMergeFormatError ||
|
|
155
236
|
error instanceof SynthesisCancelledError ||
|
|
156
|
-
error instanceof SynthesisTimeoutError
|
|
237
|
+
error instanceof SynthesisTimeoutError ||
|
|
238
|
+
error instanceof IncompleteChunkSetError
|
|
157
239
|
)
|
|
158
240
|
return error;
|
|
159
241
|
const message = error instanceof Error ? error.message : String(error);
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,9 @@ export type {
|
|
|
21
21
|
SynthesizedChunk,
|
|
22
22
|
PartialChunkSynthesisResult,
|
|
23
23
|
PartialSynthesisResult,
|
|
24
|
+
ChunkExecutionState,
|
|
25
|
+
ChunkExecutionStatus,
|
|
26
|
+
ResumeValidationMode,
|
|
24
27
|
CustomMergerContext,
|
|
25
28
|
CustomAudioMerger,
|
|
26
29
|
PostMergeValidator,
|
|
@@ -33,10 +36,19 @@ export {
|
|
|
33
36
|
MergeError,
|
|
34
37
|
SynthesisCancelledError,
|
|
35
38
|
SynthesisTimeoutError,
|
|
39
|
+
IncompleteChunkSetError,
|
|
40
|
+
serializeChunkError,
|
|
36
41
|
UnsupportedMergeFormatError,
|
|
37
42
|
getRetryAfterDelayMs,
|
|
38
43
|
} from "./errors.ts";
|
|
39
|
-
export type {
|
|
44
|
+
export type {
|
|
45
|
+
AzureTtsSynthesisError,
|
|
46
|
+
SerializedChunkError,
|
|
47
|
+
SerializedChunkErrorCode,
|
|
48
|
+
SerializedExecutionError,
|
|
49
|
+
SynthesisErrorKind,
|
|
50
|
+
} from "./errors.ts";
|
|
51
|
+
export { DeadlineController } from "./deadline.ts";
|
|
40
52
|
export { AzureTtsClient } from "./client.ts";
|
|
41
53
|
export { synthesizeSpeech } from "./synthesis.ts";
|
|
42
54
|
export { synthesizeSsml } from "./synthesis.ts";
|
|
@@ -47,9 +59,11 @@ export {
|
|
|
47
59
|
inspectAudioSpecification,
|
|
48
60
|
resolveMergeAudioFormat,
|
|
49
61
|
synthesizeSsmlChunks,
|
|
62
|
+
computeChunkFingerprint,
|
|
50
63
|
} from "./synthesis.ts";
|
|
51
64
|
export type {
|
|
52
65
|
InputAudioSpecs,
|
|
66
|
+
ChunkFingerprintOptions,
|
|
53
67
|
MergeAudioFormat,
|
|
54
68
|
MergeAudioOptions,
|
|
55
69
|
MergeSynthesisOptions,
|
package/src/outputFormats.ts
CHANGED
|
@@ -49,6 +49,9 @@ export type AzureTtsOutputFormat = keyof typeof OUTPUT_FORMATS;
|
|
|
49
49
|
export function resolveMimeType(outputFormat: string): string {
|
|
50
50
|
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
51
51
|
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
52
|
+
if (/mulaw|mu-law/i.test(outputFormat)) return "audio/basic";
|
|
53
|
+
if (/alaw|a-law/i.test(outputFormat)) return "audio/alaw";
|
|
54
|
+
if (/siren/i.test(outputFormat)) return "audio/siren";
|
|
52
55
|
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
53
56
|
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
54
57
|
if (/raw/i.test(outputFormat)) return "audio/L16";
|