@ssml-builder-js/azure-tts-client 2.14.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 +22 -0
- package/dist/index.d.mts +161 -18
- package/dist/index.d.ts +161 -18
- package/dist/index.js +754 -174
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +749 -173
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +23 -5
- package/src/errors.ts +85 -0
- package/src/index.ts +25 -2
- package/src/outputFormats.ts +14 -3
- package/src/safe.ts +330 -81
- package/src/synthesis.ts +555 -97
- package/src/types.ts +48 -1
- package/test/synthesis.test.ts +64 -0
- package/test/v213-pipeline.test.ts +23 -16
- package/test/v214-pipeline.test.ts +7 -3
- package/test/v215-pipeline.test.ts +104 -0
- package/test/v216-pipeline.test.ts +110 -0
package/src/index.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
export type {
|
|
6
6
|
AzureTtsClientOptions,
|
|
7
7
|
AzureTtsLogger,
|
|
8
|
+
AudioSpecification,
|
|
9
|
+
MappingStatus,
|
|
10
|
+
MergedSynthesisResult,
|
|
8
11
|
SsmlSynthesisBookmark,
|
|
9
12
|
SsmlSynthesisBoundary,
|
|
10
13
|
SsmlSynthesisResult,
|
|
@@ -13,9 +16,19 @@ export type {
|
|
|
13
16
|
SynthesisProgressEvent,
|
|
14
17
|
SynthesizeChunksOptions,
|
|
15
18
|
SynthesisChunkStatus,
|
|
19
|
+
RetryOptions,
|
|
16
20
|
TtsConfig,
|
|
17
21
|
} from "./types.ts";
|
|
18
|
-
export {
|
|
22
|
+
export {
|
|
23
|
+
AzureTtsError,
|
|
24
|
+
AzureTtsSdkError,
|
|
25
|
+
AudioFormatMismatchError,
|
|
26
|
+
MergeError,
|
|
27
|
+
SynthesisCancelledError,
|
|
28
|
+
SynthesisTimeoutError,
|
|
29
|
+
UnsupportedMergeFormatError,
|
|
30
|
+
} from "./errors.ts";
|
|
31
|
+
export type { AzureTtsSynthesisError, SynthesisErrorKind } from "./errors.ts";
|
|
19
32
|
export { AzureTtsClient } from "./client.ts";
|
|
20
33
|
export { synthesizeSpeech } from "./synthesis.ts";
|
|
21
34
|
export { synthesizeSsml } from "./synthesis.ts";
|
|
@@ -23,15 +36,25 @@ export {
|
|
|
23
36
|
canMergeAudioFormat,
|
|
24
37
|
mergeAudioBuffers,
|
|
25
38
|
mergeSynthesisResults,
|
|
39
|
+
inspectAudioSpecification,
|
|
26
40
|
resolveMergeAudioFormat,
|
|
27
41
|
synthesizeSsmlChunks,
|
|
28
42
|
} from "./synthesis.ts";
|
|
29
|
-
export type {
|
|
43
|
+
export type {
|
|
44
|
+
CustomMergerContext,
|
|
45
|
+
InputAudioSpecs,
|
|
46
|
+
MergeAudioFormat,
|
|
47
|
+
MergeAudioOptions,
|
|
48
|
+
MergeSynthesisOptions,
|
|
49
|
+
} from "./synthesis.ts";
|
|
50
|
+
export { DEFAULT_OUTPUT_FORMAT, resolveMimeType } from "./outputFormats.ts";
|
|
51
|
+
export type { AzureTtsOutputFormat } from "./outputFormats.ts";
|
|
30
52
|
export { ChunkValidationError, synthesizeSsmlChunksSafe, synthesizeSsmlSafe } from "./safe.ts";
|
|
31
53
|
export type {
|
|
32
54
|
AzureApiErrorResult,
|
|
33
55
|
Result,
|
|
34
56
|
SsmlSynthesisSafeResult,
|
|
57
|
+
SsmlSynthesisError,
|
|
35
58
|
SsmlValidationError as AzureSsmlValidationError,
|
|
36
59
|
Success,
|
|
37
60
|
SynthesisResult,
|
package/src/outputFormats.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as SpeechSDK from "microsoft-cognitiveservices-speech-sdk";
|
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_OUTPUT_FORMAT = "audio-16khz-128kbitrate-mono-mp3";
|
|
4
4
|
|
|
5
|
-
const OUTPUT_FORMATS
|
|
5
|
+
const OUTPUT_FORMATS = {
|
|
6
6
|
"raw-8khz-8bit-mono-mulaw": SpeechSDK.SpeechSynthesisOutputFormat.Raw8Khz8BitMonoMULaw,
|
|
7
7
|
"riff-16khz-16kbps-mono-siren": SpeechSDK.SpeechSynthesisOutputFormat.Riff16Khz16KbpsMonoSiren,
|
|
8
8
|
"audio-16khz-16kbps-mono-siren": SpeechSDK.SpeechSynthesisOutputFormat.Audio16Khz16KbpsMonoSiren,
|
|
@@ -42,10 +42,21 @@ const OUTPUT_FORMATS: Record<string, SpeechSDK.SpeechSynthesisOutputFormat> = {
|
|
|
42
42
|
"riff-44100hz-16bit-mono-pcm": SpeechSDK.SpeechSynthesisOutputFormat.Riff44100Hz16BitMonoPcm,
|
|
43
43
|
"amr-wb-16000hz": SpeechSDK.SpeechSynthesisOutputFormat.AmrWb16000Hz,
|
|
44
44
|
"g722-16khz-64kbps": SpeechSDK.SpeechSynthesisOutputFormat.G72216Khz64Kbps,
|
|
45
|
-
}
|
|
45
|
+
} satisfies Record<string, SpeechSDK.SpeechSynthesisOutputFormat>;
|
|
46
|
+
|
|
47
|
+
export type AzureTtsOutputFormat = keyof typeof OUTPUT_FORMATS;
|
|
48
|
+
|
|
49
|
+
export function resolveMimeType(outputFormat: string): string {
|
|
50
|
+
if (/(?:wav|wave|riff)/i.test(outputFormat)) return "audio/wav";
|
|
51
|
+
if (/(?:mp3|mpeg)/i.test(outputFormat)) return "audio/mpeg";
|
|
52
|
+
if (/ogg/i.test(outputFormat)) return "audio/ogg";
|
|
53
|
+
if (/webm/i.test(outputFormat)) return "audio/webm";
|
|
54
|
+
if (/raw/i.test(outputFormat)) return "audio/L16";
|
|
55
|
+
return "application/octet-stream";
|
|
56
|
+
}
|
|
46
57
|
|
|
47
58
|
export function resolveOutputFormat(outputFormat: string): SpeechSDK.SpeechSynthesisOutputFormat {
|
|
48
|
-
const resolvedFormat = OUTPUT_FORMATS[outputFormat];
|
|
59
|
+
const resolvedFormat = (OUTPUT_FORMATS as Record<string, SpeechSDK.SpeechSynthesisOutputFormat>)[outputFormat];
|
|
49
60
|
if (resolvedFormat === undefined) {
|
|
50
61
|
throw new Error(`Unsupported Azure Speech output format: ${outputFormat}`);
|
|
51
62
|
}
|
package/src/safe.ts
CHANGED
|
@@ -1,17 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
createAzureUrlValidatorRunner,
|
|
3
|
+
validateAzureSsml,
|
|
4
|
+
type AzureUrlValidator,
|
|
5
|
+
type AzureValidationOptions,
|
|
6
|
+
type SsmlDiagnostic,
|
|
7
|
+
} from "@ssml-builder-js/ssml-core";
|
|
8
|
+
import {
|
|
9
|
+
type AzureTtsError,
|
|
10
|
+
type AzureTtsSynthesisError,
|
|
11
|
+
type SynthesisErrorKind,
|
|
12
|
+
toSynthesisError,
|
|
13
|
+
} from "./errors.ts";
|
|
3
14
|
import type { AzureTtsClient } from "./client.ts";
|
|
4
15
|
import { mergeSynthesisResults } from "./synthesis.ts";
|
|
5
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
SsmlSynthesisChunk,
|
|
18
|
+
SsmlSynthesisResult,
|
|
19
|
+
SynthesisProgressEvent,
|
|
20
|
+
SynthesizeChunksOptions,
|
|
21
|
+
RetryOptions,
|
|
22
|
+
} from "./types.ts";
|
|
23
|
+
import type { AzureTtsOutputFormat } from "./outputFormats.ts";
|
|
6
24
|
|
|
7
25
|
export interface SsmlValidationError {
|
|
8
|
-
readonly kind: "validation";
|
|
26
|
+
readonly kind: "validation-error";
|
|
9
27
|
readonly message: string;
|
|
10
28
|
readonly diagnostics: readonly SsmlDiagnostic[];
|
|
11
29
|
}
|
|
12
30
|
|
|
31
|
+
export type SsmlSynthesisError = SsmlValidationError | AzureTtsSynthesisError;
|
|
32
|
+
|
|
13
33
|
export class ChunkValidationError extends Error {
|
|
14
|
-
readonly kind = "
|
|
34
|
+
readonly kind = "validation-error" as const;
|
|
15
35
|
readonly chunkIndex: number;
|
|
16
36
|
readonly diagnostics: readonly SsmlDiagnostic[];
|
|
17
37
|
|
|
@@ -25,15 +45,26 @@ export class ChunkValidationError extends Error {
|
|
|
25
45
|
|
|
26
46
|
export type Result<T, E> =
|
|
27
47
|
| { readonly ok: true; readonly success: true; readonly status: "success"; readonly value: T }
|
|
28
|
-
| {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
48
|
+
| (E extends { readonly kind: infer Kind extends SynthesisErrorKind }
|
|
49
|
+
? {
|
|
50
|
+
readonly ok: false;
|
|
51
|
+
readonly success: false;
|
|
52
|
+
readonly status: Kind;
|
|
53
|
+
readonly error: E;
|
|
54
|
+
}
|
|
55
|
+
: {
|
|
56
|
+
readonly ok: false;
|
|
57
|
+
readonly success: false;
|
|
58
|
+
readonly status: SynthesisErrorKind;
|
|
59
|
+
readonly error: E;
|
|
60
|
+
});
|
|
34
61
|
|
|
35
62
|
export type SynthesisResult<T, E> = Result<T, E>;
|
|
36
63
|
|
|
64
|
+
function failure<E extends { readonly kind: SynthesisErrorKind }>(error: E): Result<never, E> {
|
|
65
|
+
return { ok: false, success: false, status: error.kind, error } as Result<never, E>;
|
|
66
|
+
}
|
|
67
|
+
|
|
37
68
|
export type Success<T> = Extract<Result<T, never>, { readonly ok: true }>;
|
|
38
69
|
export type ValidationErrorResult = Extract<
|
|
39
70
|
Result<never, SsmlValidationError>,
|
|
@@ -44,59 +75,147 @@ export type AzureApiErrorResult = Extract<Result<never, AzureTtsError>, { readon
|
|
|
44
75
|
export type SsmlSynthesisSafeResult =
|
|
45
76
|
| Result<SsmlSynthesisResult, never>
|
|
46
77
|
| Result<never, SsmlValidationError>
|
|
47
|
-
| Result<never,
|
|
78
|
+
| Result<never, SsmlSynthesisError>;
|
|
48
79
|
|
|
49
80
|
export interface SynthesizeSsmlSafeOptions extends AzureValidationOptions {
|
|
50
81
|
/** Optional nested form for callers that want to keep validation settings grouped. */
|
|
51
82
|
validation?: AzureValidationOptions;
|
|
83
|
+
signal?: AbortSignal;
|
|
52
84
|
}
|
|
53
85
|
|
|
54
86
|
export interface SynthesizeSsmlChunksSafeOptions extends AzureValidationOptions {
|
|
55
87
|
validation?: AzureValidationOptions;
|
|
56
88
|
outputFormat?: string;
|
|
89
|
+
signal?: AbortSignal;
|
|
90
|
+
timeoutMs?: number;
|
|
91
|
+
sourceNodePath?: string[];
|
|
57
92
|
onProgress?: (event: SynthesisProgressEvent) => void;
|
|
93
|
+
concurrency?: number;
|
|
94
|
+
retryOptions?: RetryOptions;
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
export type SsmlSynthesisChunksSafeResult =
|
|
61
98
|
| Result<SsmlSynthesisResult, never>
|
|
62
99
|
| Result<never, ChunkValidationError>
|
|
63
|
-
| Result<never,
|
|
100
|
+
| Result<never, SsmlSynthesisError | ChunkValidationError>;
|
|
64
101
|
|
|
65
102
|
interface SynthesisClient {
|
|
66
|
-
synthesizeSsml(ssml: string): Promise<SsmlSynthesisResult>;
|
|
103
|
+
synthesizeSsml(ssml: string, options?: Partial<SynthesizeChunksOptions>): Promise<SsmlSynthesisResult>;
|
|
67
104
|
synthesizeChunks?(
|
|
68
105
|
chunks: readonly (SsmlSynthesisChunk | string)[],
|
|
69
|
-
options?:
|
|
106
|
+
options?: SynthesizeChunksOptions,
|
|
70
107
|
): Promise<SsmlSynthesisResult>;
|
|
71
108
|
}
|
|
72
109
|
|
|
110
|
+
function isRetryable(error: unknown): boolean {
|
|
111
|
+
if (error instanceof Error && /cancel|abort|tim(?:e|ed) ?out/i.test(error.message)) return false;
|
|
112
|
+
const status =
|
|
113
|
+
error && typeof error === "object" && "status" in error ? (error as { status?: unknown }).status : undefined;
|
|
114
|
+
if (typeof status === "number" && status !== 0) return status === 429 || (status >= 500 && status < 600);
|
|
115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
116
|
+
if (/\b4\d{2}\b/.test(message)) return false;
|
|
117
|
+
return /network|connection|connect|socket|fetch failed|econn|etimedout|temporar|transient|unavailable/i.test(message);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function delayForRetry(options: RetryOptions, attempt: number): number {
|
|
121
|
+
const maxDelay = Math.max(0, options.maxDelayMs);
|
|
122
|
+
const base = Math.min(maxDelay, Math.max(0, options.initialDelayMs) * 2 ** Math.max(0, attempt - 1));
|
|
123
|
+
return Math.floor(Math.random() * (base + 1));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveConcurrency(value: number | undefined, total: number): number {
|
|
127
|
+
if (value === undefined) return 1;
|
|
128
|
+
if (value === Infinity) return Math.max(1, total);
|
|
129
|
+
return Number.isFinite(value) ? Math.max(1, Math.floor(value)) : 1;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function retryableSynthesis(
|
|
133
|
+
synthesize: () => Promise<SsmlSynthesisResult>,
|
|
134
|
+
options: RetryOptions | undefined,
|
|
135
|
+
signal: AbortSignal | undefined,
|
|
136
|
+
onRetry: (attempt: number, delayMs: number) => void,
|
|
137
|
+
): Promise<SsmlSynthesisResult> {
|
|
138
|
+
const retry = options
|
|
139
|
+
? {
|
|
140
|
+
maxRetries: Math.max(0, Math.floor(options.maxRetries)),
|
|
141
|
+
initialDelayMs: options.initialDelayMs,
|
|
142
|
+
maxDelayMs: options.maxDelayMs,
|
|
143
|
+
}
|
|
144
|
+
: undefined;
|
|
145
|
+
let attempt = 0;
|
|
146
|
+
while (true) {
|
|
147
|
+
if (signal?.aborted) throw new Error("Speech synthesis was cancelled.");
|
|
148
|
+
try {
|
|
149
|
+
return await synthesize();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (!retry || attempt >= retry.maxRetries || !isRetryable(error)) throw error;
|
|
152
|
+
attempt += 1;
|
|
153
|
+
const delayMs = delayForRetry(retry, attempt);
|
|
154
|
+
onRetry(attempt, delayMs);
|
|
155
|
+
if (delayMs > 0)
|
|
156
|
+
await new Promise<void>((resolve, reject) => {
|
|
157
|
+
const timer = setTimeout(() => {
|
|
158
|
+
signal?.removeEventListener("abort", abort);
|
|
159
|
+
resolve();
|
|
160
|
+
}, delayMs);
|
|
161
|
+
const abort = () => {
|
|
162
|
+
clearTimeout(timer);
|
|
163
|
+
signal?.removeEventListener("abort", abort);
|
|
164
|
+
reject(new Error("Speech synthesis was cancelled."));
|
|
165
|
+
};
|
|
166
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function sharedValidationOptions(options: AzureValidationOptions, signal?: AbortSignal): AzureValidationOptions {
|
|
173
|
+
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
174
|
+
if (!validator) return signal ? withValidationSignal(options, signal) : options;
|
|
175
|
+
const runner = createAzureUrlValidatorRunner(validator as AzureUrlValidator, {
|
|
176
|
+
...(options.urlValidation ?? {}),
|
|
177
|
+
...(options.urlValidatorConcurrency !== undefined ? { concurrency: options.urlValidatorConcurrency } : {}),
|
|
178
|
+
...(options.urlValidatorTimeoutMs !== undefined ? { timeoutMs: options.urlValidatorTimeoutMs } : {}),
|
|
179
|
+
...(signal ? { signal } : {}),
|
|
180
|
+
...(options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}),
|
|
181
|
+
});
|
|
182
|
+
return {
|
|
183
|
+
...withValidationSignal(options, signal),
|
|
184
|
+
urlValidatorRunner: runner,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
73
188
|
/** Validates SSML before invoking Azure and converts validation/API failures to one result shape. */
|
|
74
189
|
export async function synthesizeSsmlSafe(
|
|
75
190
|
client: Pick<AzureTtsClient, "synthesizeSsml"> | SynthesisClient,
|
|
76
191
|
ssml: string,
|
|
77
192
|
options: SynthesizeSsmlSafeOptions = {},
|
|
78
193
|
): Promise<SsmlSynthesisSafeResult> {
|
|
79
|
-
const validationOptions = options.validation ?? options;
|
|
194
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
|
|
80
195
|
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
196
|
+
if (options.signal?.aborted) {
|
|
197
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
198
|
+
return failure(error);
|
|
199
|
+
}
|
|
81
200
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
82
201
|
if (errors.length > 0) {
|
|
83
|
-
return {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
kind: "validation",
|
|
89
|
-
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
90
|
-
diagnostics: errors,
|
|
91
|
-
},
|
|
92
|
-
};
|
|
202
|
+
return failure({
|
|
203
|
+
kind: "validation-error",
|
|
204
|
+
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
205
|
+
diagnostics: errors,
|
|
206
|
+
});
|
|
93
207
|
}
|
|
94
208
|
|
|
95
209
|
try {
|
|
96
|
-
return {
|
|
210
|
+
return {
|
|
211
|
+
ok: true,
|
|
212
|
+
success: true,
|
|
213
|
+
status: "success",
|
|
214
|
+
value: await client.synthesizeSsml(ssml, { signal: options.signal }),
|
|
215
|
+
};
|
|
97
216
|
} catch (error) {
|
|
98
|
-
const
|
|
99
|
-
return
|
|
217
|
+
const synthesisError = toSynthesisError(error);
|
|
218
|
+
return failure(synthesisError);
|
|
100
219
|
}
|
|
101
220
|
}
|
|
102
221
|
|
|
@@ -106,7 +225,11 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
106
225
|
chunks: readonly (SsmlSynthesisChunk | string)[],
|
|
107
226
|
options: SynthesizeSsmlChunksSafeOptions = {},
|
|
108
227
|
): Promise<SsmlSynthesisChunksSafeResult> {
|
|
109
|
-
const validationOptions = options.validation ?? options;
|
|
228
|
+
const validationOptions = sharedValidationOptions(options.validation ?? options, options.signal);
|
|
229
|
+
if (options.signal?.aborted) {
|
|
230
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
231
|
+
return failure(error);
|
|
232
|
+
}
|
|
110
233
|
const pending = (index: number, status: SynthesisProgressEvent["status"], error?: unknown): void => {
|
|
111
234
|
options.onProgress?.({
|
|
112
235
|
currentChunk: status === "success" ? index + 1 : index,
|
|
@@ -124,77 +247,203 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
124
247
|
pending(index, "pending");
|
|
125
248
|
});
|
|
126
249
|
const validations = await Promise.all(
|
|
127
|
-
chunks.map(async (chunk) => {
|
|
250
|
+
chunks.map(async (chunk, index) => {
|
|
128
251
|
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
129
|
-
const
|
|
252
|
+
const sourceNodePath =
|
|
253
|
+
typeof chunk === "string" ? options.sourceNodePath : (chunk.sourceNodePath ?? options.sourceNodePath);
|
|
254
|
+
const diagnostics = await Promise.resolve(
|
|
255
|
+
validateAzureSsml(ssml, {
|
|
256
|
+
...validationOptions,
|
|
257
|
+
...(sourceNodePath ? { sourceNodePath } : {}),
|
|
258
|
+
chunkIndex: index,
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
130
261
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
131
262
|
}),
|
|
132
263
|
);
|
|
133
264
|
const firstInvalidIndex = validations.findIndex((diagnostics) => diagnostics.length > 0);
|
|
265
|
+
if (options.signal?.aborted) {
|
|
266
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
267
|
+
return failure(error);
|
|
268
|
+
}
|
|
134
269
|
if (firstInvalidIndex >= 0) {
|
|
135
270
|
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
136
271
|
pending(firstInvalidIndex, "failed", error);
|
|
137
|
-
return
|
|
272
|
+
return failure(error);
|
|
138
273
|
}
|
|
139
274
|
|
|
140
275
|
try {
|
|
141
276
|
if (client.synthesizeChunks) {
|
|
142
|
-
const
|
|
277
|
+
const normalizedChunks = chunks.map((chunk) => {
|
|
278
|
+
if (typeof chunk === "string" || chunk.sourceNodePath || !options.sourceNodePath) return chunk;
|
|
279
|
+
return { ...chunk, sourceNodePath: [...options.sourceNodePath] };
|
|
280
|
+
});
|
|
281
|
+
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
282
|
+
onProgress: options.onProgress,
|
|
283
|
+
outputFormat: options.outputFormat,
|
|
284
|
+
signal: options.signal,
|
|
285
|
+
timeoutMs: options.timeoutMs,
|
|
286
|
+
sourceNodePath: options.sourceNodePath,
|
|
287
|
+
concurrency: options.concurrency,
|
|
288
|
+
retryOptions: options.retryOptions,
|
|
289
|
+
});
|
|
143
290
|
return { ok: true, success: true, status: "success", value };
|
|
144
291
|
}
|
|
145
|
-
const results: SsmlSynthesisResult
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
292
|
+
const results: Array<SsmlSynthesisResult | undefined> = new Array(chunks.length);
|
|
293
|
+
let completed = 0;
|
|
294
|
+
let nextIndex = 0;
|
|
295
|
+
const concurrency = resolveConcurrency(options.concurrency, chunks.length);
|
|
296
|
+
const worker = async (): Promise<void> => {
|
|
297
|
+
while (true) {
|
|
298
|
+
const index = nextIndex++;
|
|
299
|
+
if (index >= chunks.length) return;
|
|
300
|
+
const chunk = chunks[index];
|
|
301
|
+
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
302
|
+
const sourceNodePath = input.sourceNodePath;
|
|
303
|
+
const originalTextRange = input.originalTextRange;
|
|
304
|
+
pending(index, "synthesizing");
|
|
305
|
+
const startedAt = Date.now();
|
|
306
|
+
try {
|
|
307
|
+
const result = await retryableSynthesis(
|
|
308
|
+
() =>
|
|
309
|
+
client.synthesizeSsml(input.ssml, {
|
|
310
|
+
outputFormat: options.outputFormat,
|
|
311
|
+
signal: options.signal,
|
|
312
|
+
timeoutMs: options.timeoutMs,
|
|
313
|
+
sourceNodePath: input.sourceNodePath ?? options.sourceNodePath,
|
|
314
|
+
}),
|
|
315
|
+
options.retryOptions,
|
|
316
|
+
options.signal,
|
|
317
|
+
(retryAttempt, nextRetryDelayMs) =>
|
|
318
|
+
options.onProgress?.({
|
|
319
|
+
currentChunk: completed,
|
|
320
|
+
totalChunks: chunks.length,
|
|
321
|
+
percent: chunks.length === 0 ? 100 : Math.round((completed / chunks.length) * 100),
|
|
322
|
+
chunkIndex: index,
|
|
323
|
+
originalTextRange: input.originalTextRange,
|
|
324
|
+
status: "synthesizing",
|
|
325
|
+
durationMs: Date.now() - startedAt,
|
|
326
|
+
retryAttempt,
|
|
327
|
+
nextRetryDelayMs,
|
|
328
|
+
isRetrying: true,
|
|
329
|
+
}),
|
|
330
|
+
);
|
|
331
|
+
results[index] = {
|
|
332
|
+
...result,
|
|
333
|
+
...(input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {}),
|
|
334
|
+
...(sourceNodePath
|
|
335
|
+
? {
|
|
336
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
337
|
+
...event,
|
|
338
|
+
sourceNodePath: [...sourceNodePath],
|
|
339
|
+
...(event.originalTextRange
|
|
340
|
+
? { originalTextRange: { ...event.originalTextRange } }
|
|
341
|
+
: input.originalTextRange
|
|
342
|
+
? { originalTextRange: { ...input.originalTextRange } }
|
|
343
|
+
: {}),
|
|
344
|
+
})),
|
|
345
|
+
visemes: result.visemes?.map((event) => ({
|
|
346
|
+
...event,
|
|
347
|
+
sourceNodePath: [...sourceNodePath],
|
|
348
|
+
...(event.originalTextRange
|
|
349
|
+
? { originalTextRange: { ...event.originalTextRange } }
|
|
350
|
+
: input.originalTextRange
|
|
351
|
+
? { originalTextRange: { ...input.originalTextRange } }
|
|
352
|
+
: {}),
|
|
353
|
+
})),
|
|
354
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
355
|
+
...event,
|
|
356
|
+
sourceNodePath: [...sourceNodePath],
|
|
357
|
+
...(event.originalTextRange
|
|
358
|
+
? { originalTextRange: { ...event.originalTextRange } }
|
|
359
|
+
: input.originalTextRange
|
|
360
|
+
? { originalTextRange: { ...input.originalTextRange } }
|
|
361
|
+
: {}),
|
|
362
|
+
})),
|
|
363
|
+
}
|
|
364
|
+
: {}),
|
|
365
|
+
...(originalTextRange
|
|
366
|
+
? {
|
|
367
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
368
|
+
...event,
|
|
369
|
+
originalTextRange: event.originalTextRange
|
|
370
|
+
? { ...event.originalTextRange }
|
|
371
|
+
: { ...originalTextRange },
|
|
372
|
+
})),
|
|
373
|
+
wordBoundary: result.wordBoundary?.map((event) => ({
|
|
374
|
+
...event,
|
|
375
|
+
originalTextRange: event.originalTextRange
|
|
376
|
+
? { ...event.originalTextRange }
|
|
377
|
+
: { ...originalTextRange },
|
|
378
|
+
})),
|
|
379
|
+
wordBoundaries: result.wordBoundaries?.map((event) => ({
|
|
380
|
+
...event,
|
|
381
|
+
originalTextRange: event.originalTextRange
|
|
382
|
+
? { ...event.originalTextRange }
|
|
383
|
+
: { ...originalTextRange },
|
|
384
|
+
})),
|
|
385
|
+
visemes: result.visemes?.map((event) => ({
|
|
386
|
+
...event,
|
|
387
|
+
originalTextRange: event.originalTextRange
|
|
388
|
+
? { ...event.originalTextRange }
|
|
389
|
+
: { ...originalTextRange },
|
|
390
|
+
})),
|
|
391
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
392
|
+
...event,
|
|
393
|
+
originalTextRange: event.originalTextRange
|
|
394
|
+
? { ...event.originalTextRange }
|
|
395
|
+
: { ...originalTextRange },
|
|
396
|
+
})),
|
|
397
|
+
}
|
|
398
|
+
: {}),
|
|
399
|
+
};
|
|
400
|
+
completed += 1;
|
|
401
|
+
options.onProgress?.({
|
|
402
|
+
currentChunk: completed,
|
|
403
|
+
totalChunks: chunks.length,
|
|
404
|
+
percent: chunks.length === 0 ? 100 : Math.round((completed / chunks.length) * 100),
|
|
405
|
+
chunkIndex: index,
|
|
406
|
+
originalTextRange: input.originalTextRange,
|
|
407
|
+
status: "success",
|
|
408
|
+
durationMs: Date.now() - startedAt,
|
|
409
|
+
});
|
|
410
|
+
} catch (error) {
|
|
411
|
+
options.onProgress?.({
|
|
412
|
+
currentChunk: completed,
|
|
413
|
+
totalChunks: chunks.length,
|
|
414
|
+
percent: chunks.length === 0 ? 100 : Math.round((index / chunks.length) * 100),
|
|
415
|
+
chunkIndex: index,
|
|
416
|
+
originalTextRange: input.originalTextRange,
|
|
417
|
+
status: "failed",
|
|
418
|
+
durationMs: Date.now() - startedAt,
|
|
419
|
+
error,
|
|
420
|
+
});
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
188
423
|
}
|
|
189
|
-
}
|
|
424
|
+
};
|
|
425
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, chunks.length) }, () => worker()));
|
|
426
|
+
const orderedResults = results.filter((result): result is SsmlSynthesisResult => result !== undefined);
|
|
190
427
|
return {
|
|
191
428
|
ok: true,
|
|
192
429
|
success: true,
|
|
193
430
|
status: "success",
|
|
194
|
-
value: mergeSynthesisResults(
|
|
431
|
+
value: mergeSynthesisResults(orderedResults, {
|
|
432
|
+
format: (options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3") as AzureTtsOutputFormat,
|
|
433
|
+
signal: options.signal,
|
|
434
|
+
}),
|
|
195
435
|
};
|
|
196
436
|
} catch (error) {
|
|
197
|
-
const
|
|
198
|
-
return
|
|
437
|
+
const synthesisError = toSynthesisError(error);
|
|
438
|
+
return failure(synthesisError);
|
|
199
439
|
}
|
|
200
440
|
}
|
|
441
|
+
|
|
442
|
+
function withValidationSignal(options: AzureValidationOptions, signal?: AbortSignal): AzureValidationOptions {
|
|
443
|
+
if (!signal) return options;
|
|
444
|
+
return {
|
|
445
|
+
...options,
|
|
446
|
+
urlValidatorSignal: signal,
|
|
447
|
+
urlValidation: { ...(options.urlValidation ?? {}), signal },
|
|
448
|
+
};
|
|
449
|
+
}
|