@ssml-builder-js/azure-tts-client 2.18.0 → 2.19.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/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/dist/index.js +453 -63
- package/package.json +8 -8
- package/src/client.ts +43 -3
- package/src/deadline.ts +52 -0
- package/src/errors.ts +86 -4
- package/src/index.ts +11 -1
- package/src/safe.ts +52 -31
- package/src/synthesis.ts +307 -22
- package/src/types.ts +8 -3
- package/test/synthesis.test.ts +20 -0
- package/test/v219-pipeline.test.ts +265 -0
- package/dist/index.d.mts +0 -487
- package/dist/index.d.ts +0 -487
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -1813
- package/dist/index.mjs.map +0 -1
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.1",
|
|
4
4
|
"description": "Azure Text-to-Speech client using the Microsoft Speech SDK",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -26,16 +26,16 @@
|
|
|
26
26
|
"access": "public",
|
|
27
27
|
"registry": "https://registry.npmjs.org/"
|
|
28
28
|
},
|
|
29
|
-
"scripts": {
|
|
30
|
-
"build": "tsup",
|
|
31
|
-
"typecheck": "tsc --noEmit",
|
|
32
|
-
"test": "node --experimental-strip-types --test test/*.test.ts"
|
|
33
|
-
},
|
|
34
29
|
"devDependencies": {
|
|
35
30
|
"typescript": "^6.0.3"
|
|
36
31
|
},
|
|
37
32
|
"dependencies": {
|
|
38
|
-
"@ssml-builder-js/ssml-core": "^2.
|
|
33
|
+
"@ssml-builder-js/ssml-core": "^2.19.1",
|
|
39
34
|
"microsoft-cognitiveservices-speech-sdk": "1.51.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
39
|
+
"test": "node --experimental-strip-types --test test/*.test.ts"
|
|
40
40
|
}
|
|
41
|
-
}
|
|
41
|
+
}
|
package/src/client.ts
CHANGED
|
@@ -19,7 +19,16 @@ 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
|
|
|
@@ -32,12 +41,23 @@ export class AzureTtsClient {
|
|
|
32
41
|
timeoutMs,
|
|
33
42
|
timeouts,
|
|
34
43
|
retryOptions: this.#options.retryOptions,
|
|
44
|
+
customHeaders,
|
|
45
|
+
fingerprintSchemaVersion,
|
|
35
46
|
};
|
|
36
47
|
return synthesizeSpeech(ssml, config);
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
async synthesizeSsml(ssml: string, options: Partial<TtsConfig> = {}): Promise<SsmlSynthesisResult> {
|
|
40
|
-
const {
|
|
51
|
+
const {
|
|
52
|
+
region,
|
|
53
|
+
subscriptionKey,
|
|
54
|
+
outputFormat,
|
|
55
|
+
signal,
|
|
56
|
+
timeoutMs,
|
|
57
|
+
timeouts,
|
|
58
|
+
customHeaders,
|
|
59
|
+
fingerprintSchemaVersion,
|
|
60
|
+
} = this.#options;
|
|
41
61
|
const endpoint = this.#options.endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
42
62
|
this.#options.logger?.debug?.("Using Azure TTS endpoint:", endpoint);
|
|
43
63
|
|
|
@@ -58,6 +78,8 @@ export class AzureTtsClient {
|
|
|
58
78
|
outputMimeType: options.outputMimeType ?? this.#options.outputMimeType,
|
|
59
79
|
postMergeValidator: options.postMergeValidator ?? this.#options.postMergeValidator,
|
|
60
80
|
resumeValidation: options.resumeValidation ?? this.#options.resumeValidation,
|
|
81
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
82
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion,
|
|
61
83
|
});
|
|
62
84
|
}
|
|
63
85
|
|
|
@@ -65,7 +87,16 @@ export class AzureTtsClient {
|
|
|
65
87
|
chunks: readonly (SsmlSynthesisChunk | string)[],
|
|
66
88
|
options: SynthesizeChunksOptions = {},
|
|
67
89
|
): Promise<SsmlSynthesisResult> {
|
|
68
|
-
const {
|
|
90
|
+
const {
|
|
91
|
+
region,
|
|
92
|
+
subscriptionKey,
|
|
93
|
+
outputFormat,
|
|
94
|
+
signal,
|
|
95
|
+
timeoutMs,
|
|
96
|
+
timeouts,
|
|
97
|
+
customHeaders,
|
|
98
|
+
fingerprintSchemaVersion,
|
|
99
|
+
} = this.#options;
|
|
69
100
|
const endpoint = this.#options.endpoint?.trim() || ENDPOINT_TEMPLATE.replace("{region}", region);
|
|
70
101
|
return synthesizeSsmlChunks(chunks, {
|
|
71
102
|
endpoint,
|
|
@@ -86,6 +117,8 @@ export class AzureTtsClient {
|
|
|
86
117
|
outputMimeType: options.outputMimeType ?? this.#options.outputMimeType,
|
|
87
118
|
postMergeValidator: options.postMergeValidator ?? this.#options.postMergeValidator,
|
|
88
119
|
resumeValidation: options.resumeValidation ?? this.#options.resumeValidation,
|
|
120
|
+
customHeaders: options.customHeaders ?? customHeaders,
|
|
121
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion ?? fingerprintSchemaVersion,
|
|
89
122
|
});
|
|
90
123
|
}
|
|
91
124
|
|
|
@@ -106,6 +139,13 @@ export class AzureTtsClient {
|
|
|
106
139
|
onProgress: options.onProgress ?? this.#options.onProgress,
|
|
107
140
|
concurrency: options.concurrency ?? this.#options.concurrency,
|
|
108
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,
|
|
109
149
|
});
|
|
110
150
|
}
|
|
111
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
|
@@ -36,10 +36,19 @@ export {
|
|
|
36
36
|
MergeError,
|
|
37
37
|
SynthesisCancelledError,
|
|
38
38
|
SynthesisTimeoutError,
|
|
39
|
+
IncompleteChunkSetError,
|
|
40
|
+
serializeChunkError,
|
|
39
41
|
UnsupportedMergeFormatError,
|
|
40
42
|
getRetryAfterDelayMs,
|
|
41
43
|
} from "./errors.ts";
|
|
42
|
-
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";
|
|
43
52
|
export { AzureTtsClient } from "./client.ts";
|
|
44
53
|
export { synthesizeSpeech } from "./synthesis.ts";
|
|
45
54
|
export { synthesizeSsml } from "./synthesis.ts";
|
|
@@ -54,6 +63,7 @@ export {
|
|
|
54
63
|
} from "./synthesis.ts";
|
|
55
64
|
export type {
|
|
56
65
|
InputAudioSpecs,
|
|
66
|
+
ChunkFingerprintOptions,
|
|
57
67
|
MergeAudioFormat,
|
|
58
68
|
MergeAudioOptions,
|
|
59
69
|
MergeSynthesisOptions,
|
package/src/safe.ts
CHANGED
|
@@ -29,6 +29,8 @@ import type {
|
|
|
29
29
|
ResumeValidationMode,
|
|
30
30
|
} from "./types.ts";
|
|
31
31
|
import type { AzureTtsOutputFormat } from "./outputFormats.ts";
|
|
32
|
+
import { IncompleteChunkSetError, serializeChunkError } from "./errors.ts";
|
|
33
|
+
import { DeadlineController } from "./deadline.ts";
|
|
32
34
|
|
|
33
35
|
export interface SsmlValidationError {
|
|
34
36
|
readonly kind: "validation-error";
|
|
@@ -129,6 +131,8 @@ export interface SynthesizeSsmlSafeOptions extends AzureValidationOptions {
|
|
|
129
131
|
export interface SynthesizeSsmlChunksSafeOptions extends AzureValidationOptions {
|
|
130
132
|
validation?: AzureValidationOptions;
|
|
131
133
|
outputFormat?: string;
|
|
134
|
+
customHeaders?: Readonly<Record<string, string>>;
|
|
135
|
+
fingerprintSchemaVersion?: string;
|
|
132
136
|
signal?: AbortSignal;
|
|
133
137
|
timeoutMs?: number;
|
|
134
138
|
timeouts?: SynthesisTimeouts;
|
|
@@ -303,14 +307,19 @@ export async function synthesizeSsmlSafe(
|
|
|
303
307
|
ssml: string,
|
|
304
308
|
options: SynthesizeSsmlSafeOptions = {},
|
|
305
309
|
): Promise<SsmlSynthesisSafeResult> {
|
|
306
|
-
const
|
|
310
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
311
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, deadline.signal);
|
|
307
312
|
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
308
|
-
if (
|
|
309
|
-
const error = toSynthesisError(
|
|
313
|
+
if (deadline.signal.aborted) {
|
|
314
|
+
const error = toSynthesisError(
|
|
315
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled."),
|
|
316
|
+
);
|
|
317
|
+
deadline.dispose();
|
|
310
318
|
return failure(error);
|
|
311
319
|
}
|
|
312
320
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
313
321
|
if (errors.length > 0) {
|
|
322
|
+
deadline.dispose();
|
|
314
323
|
return failure({
|
|
315
324
|
kind: "validation-error",
|
|
316
325
|
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
@@ -318,27 +327,23 @@ export async function synthesizeSsmlSafe(
|
|
|
318
327
|
});
|
|
319
328
|
}
|
|
320
329
|
|
|
321
|
-
const jobScope =
|
|
322
|
-
options.timeouts?.totalJobMs !== undefined
|
|
323
|
-
? createSafeAbortScope(options.signal, options.timeouts.totalJobMs)
|
|
324
|
-
: undefined;
|
|
325
330
|
try {
|
|
326
331
|
return {
|
|
327
332
|
ok: true,
|
|
328
333
|
success: true,
|
|
329
334
|
status: "success",
|
|
330
335
|
value: await client.synthesizeSsml(ssml, {
|
|
331
|
-
signal:
|
|
336
|
+
signal: deadline.signal,
|
|
332
337
|
timeoutMs: options.timeouts?.perChunkMs,
|
|
333
|
-
timeouts: options.timeouts,
|
|
338
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: undefined } : undefined,
|
|
334
339
|
}),
|
|
335
340
|
};
|
|
336
341
|
} catch (error) {
|
|
337
|
-
if (
|
|
342
|
+
if (deadline.timedOut) return failure(toSynthesisError(new Error("Speech synthesis timed out.")));
|
|
338
343
|
const synthesisError = toSynthesisError(error);
|
|
339
344
|
return failure(synthesisError);
|
|
340
345
|
} finally {
|
|
341
|
-
|
|
346
|
+
deadline.dispose();
|
|
342
347
|
}
|
|
343
348
|
}
|
|
344
349
|
|
|
@@ -348,12 +353,16 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
348
353
|
chunks: readonly (SsmlSynthesisChunk | string)[],
|
|
349
354
|
options: SynthesizeSsmlChunksSafeOptions = {},
|
|
350
355
|
): Promise<SsmlSynthesisChunksSafeResult> {
|
|
356
|
+
const deadline = new DeadlineController(options.timeouts?.totalJobMs, options.signal);
|
|
351
357
|
const validationOptions = sharedValidationOptions(
|
|
352
358
|
{ ...(options.validation ?? options), timeouts: options.timeouts },
|
|
353
|
-
|
|
359
|
+
deadline.signal,
|
|
354
360
|
);
|
|
355
|
-
if (
|
|
356
|
-
const error = toSynthesisError(
|
|
361
|
+
if (deadline.signal.aborted) {
|
|
362
|
+
const error = toSynthesisError(
|
|
363
|
+
new Error(deadline.timedOut ? "Speech synthesis timed out." : "Speech synthesis was cancelled."),
|
|
364
|
+
);
|
|
365
|
+
deadline.dispose();
|
|
357
366
|
return failure(error);
|
|
358
367
|
}
|
|
359
368
|
const pending = (index: number, status: SynthesisProgressEvent["status"], error?: unknown): void => {
|
|
@@ -387,8 +396,9 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
387
396
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
388
397
|
}),
|
|
389
398
|
);
|
|
390
|
-
if (
|
|
399
|
+
if (deadline.signal.aborted) {
|
|
391
400
|
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
401
|
+
deadline.dispose();
|
|
392
402
|
return failure(error);
|
|
393
403
|
}
|
|
394
404
|
const chunkDiagnostics = validations
|
|
@@ -397,6 +407,7 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
397
407
|
if (chunkDiagnostics.length > 0) {
|
|
398
408
|
const error = new BatchChunkValidationError(chunkDiagnostics);
|
|
399
409
|
for (const entry of chunkDiagnostics) pending(entry.chunkIndex, "failed", error);
|
|
410
|
+
deadline.dispose();
|
|
400
411
|
return failure(error);
|
|
401
412
|
}
|
|
402
413
|
|
|
@@ -410,9 +421,9 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
410
421
|
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
411
422
|
onProgress: options.onProgress,
|
|
412
423
|
outputFormat: options.outputFormat,
|
|
413
|
-
signal:
|
|
424
|
+
signal: deadline.signal,
|
|
414
425
|
timeoutMs: options.timeoutMs,
|
|
415
|
-
timeouts: options.timeouts,
|
|
426
|
+
timeouts: options.timeouts ? { ...options.timeouts, totalJobMs: undefined } : undefined,
|
|
416
427
|
sourceNodePath: options.sourceNodePath,
|
|
417
428
|
concurrency: options.concurrency,
|
|
418
429
|
retryOptions: options.retryOptions,
|
|
@@ -423,11 +434,18 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
423
434
|
outputMimeType: options.outputMimeType,
|
|
424
435
|
postMergeValidator: options.postMergeValidator,
|
|
425
436
|
resumeValidation: options.resumeValidation,
|
|
437
|
+
customHeaders: options.customHeaders,
|
|
438
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion,
|
|
426
439
|
});
|
|
427
440
|
return { ok: true, success: true, status: "success", value };
|
|
428
441
|
}
|
|
429
442
|
const inputs = chunks.map((chunk) => (typeof chunk === "string" ? { ssml: chunk } : chunk));
|
|
430
|
-
const fingerprints = inputs.map((chunk) =>
|
|
443
|
+
const fingerprints = inputs.map((chunk) =>
|
|
444
|
+
computeChunkFingerprint(chunk.ssml, options.outputFormat, {
|
|
445
|
+
customHeaders: options.customHeaders,
|
|
446
|
+
fingerprintSchemaVersion: options.fingerprintSchemaVersion,
|
|
447
|
+
}),
|
|
448
|
+
);
|
|
431
449
|
const results: Array<SsmlSynthesisResult | undefined> = new Array(chunks.length);
|
|
432
450
|
const chunkStates: ChunkExecutionState[] = inputs.map((_chunk, chunkIndex) => ({
|
|
433
451
|
chunkIndex,
|
|
@@ -449,14 +467,10 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
449
467
|
const shouldSynthesize = (index: number): boolean =>
|
|
450
468
|
(!cachedChunks.has(index) || invalidCachedIndices.has(index)) &&
|
|
451
469
|
(requestedIndices === undefined || requestedIndices.has(index) || invalidCachedIndices.has(index));
|
|
452
|
-
const
|
|
453
|
-
const jobDeadlineAt =
|
|
454
|
-
options.timeouts?.totalJobMs !== undefined && options.timeouts.totalJobMs > 0
|
|
455
|
-
? jobStartedAt + options.timeouts.totalJobMs
|
|
456
|
-
: undefined;
|
|
470
|
+
const jobDeadlineAt = deadline.deadlineAtMs;
|
|
457
471
|
const jobScope =
|
|
458
472
|
chunks.length > 1 || options.timeouts?.totalJobMs !== undefined
|
|
459
|
-
? createSafeAbortScope(
|
|
473
|
+
? createSafeAbortScope(deadline.signal, undefined)
|
|
460
474
|
: undefined;
|
|
461
475
|
fallbackJobScope = jobScope;
|
|
462
476
|
const failedIndices = new Set<number>();
|
|
@@ -483,9 +497,9 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
483
497
|
const chunkTimeout = options.timeouts?.chunkWithRetriesMs ?? options.timeouts?.perChunkMs;
|
|
484
498
|
const chunkScope =
|
|
485
499
|
chunkTimeout !== undefined || jobScope
|
|
486
|
-
? createSafeAbortScope(jobScope?.signal ??
|
|
500
|
+
? createSafeAbortScope(jobScope?.signal ?? deadline.signal, chunkTimeout ?? options.timeoutMs)
|
|
487
501
|
: undefined;
|
|
488
|
-
const chunkSignal = chunkScope?.signal ??
|
|
502
|
+
const chunkSignal = chunkScope?.signal ?? deadline.signal;
|
|
489
503
|
let result: SsmlSynthesisResult;
|
|
490
504
|
try {
|
|
491
505
|
result = await retryableSynthesis(
|
|
@@ -514,7 +528,7 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
514
528
|
jobDeadlineAt,
|
|
515
529
|
);
|
|
516
530
|
} catch (error) {
|
|
517
|
-
if (chunkScope?.timedOut())
|
|
531
|
+
if (chunkScope?.timedOut() || deadline.timedOut)
|
|
518
532
|
throw new Error(`Speech synthesis timed out after ${chunkTimeout ?? options.timeoutMs} ms.`);
|
|
519
533
|
throw error;
|
|
520
534
|
} finally {
|
|
@@ -601,15 +615,17 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
601
615
|
durationMs: Date.now() - startedAt,
|
|
602
616
|
});
|
|
603
617
|
} catch (error) {
|
|
604
|
-
const wasCancelled =
|
|
605
|
-
|
|
618
|
+
const wasCancelled =
|
|
619
|
+
firstError !== undefined ||
|
|
620
|
+
(!deadline.timedOut && Boolean(jobScope?.signal.aborted && !jobScope?.timedOut()));
|
|
621
|
+
firstError ??= deadline.timedOut ? new Error("Speech synthesis timed out.") : error;
|
|
606
622
|
if (!wasCancelled) failedIndices.add(index);
|
|
607
623
|
chunkStates[index] = {
|
|
608
624
|
chunkIndex: index,
|
|
609
625
|
status: wasCancelled ? "cancelled" : "failed",
|
|
610
626
|
isOriginalFailure: !wasCancelled,
|
|
611
627
|
canResume: true,
|
|
612
|
-
error: error
|
|
628
|
+
error: serializeChunkError(error, "synthesis", !wasCancelled),
|
|
613
629
|
};
|
|
614
630
|
options.onProgress?.({
|
|
615
631
|
currentChunk: completed,
|
|
@@ -656,6 +672,10 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
656
672
|
};
|
|
657
673
|
throw error;
|
|
658
674
|
}
|
|
675
|
+
const missingChunkIndices = Array.from({ length: chunks.length }, (_value, index) =>
|
|
676
|
+
results[index] === undefined ? index : undefined,
|
|
677
|
+
).filter((index): index is number => index !== undefined);
|
|
678
|
+
if (missingChunkIndices.length > 0) throw new IncompleteChunkSetError(chunks.length, missingChunkIndices);
|
|
659
679
|
const orderedResults = results.filter((result): result is SsmlSynthesisResult => result !== undefined);
|
|
660
680
|
return {
|
|
661
681
|
ok: true,
|
|
@@ -663,7 +683,7 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
663
683
|
status: "success",
|
|
664
684
|
value: await mergeSynthesisResults(orderedResults, {
|
|
665
685
|
format: (options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3") as AzureTtsOutputFormat,
|
|
666
|
-
signal: jobScope?.signal ??
|
|
686
|
+
signal: jobScope?.signal ?? deadline.signal,
|
|
667
687
|
customMerger: options.customMerger,
|
|
668
688
|
outputMimeType: options.outputMimeType,
|
|
669
689
|
postMergeValidator: options.postMergeValidator,
|
|
@@ -674,6 +694,7 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
674
694
|
return failure(synthesisError, partialResultFrom(error));
|
|
675
695
|
} finally {
|
|
676
696
|
fallbackJobScope?.dispose();
|
|
697
|
+
deadline.dispose();
|
|
677
698
|
}
|
|
678
699
|
}
|
|
679
700
|
|