@ssml-builder-js/azure-tts-client 2.14.0 → 2.15.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 +113 -17
- package/dist/index.d.ts +113 -17
- package/dist/index.js +310 -86
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +303 -84
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/client.ts +19 -5
- package/src/errors.ts +63 -0
- package/src/index.ts +14 -2
- package/src/outputFormats.ts +14 -3
- package/src/safe.ts +160 -38
- package/src/synthesis.ts +215 -52
- package/src/types.ts +17 -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/src/safe.ts
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
1
|
import { validateAzureSsml, type AzureValidationOptions, type SsmlDiagnostic } from "@ssml-builder-js/ssml-core";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type AzureTtsError,
|
|
4
|
+
type AzureTtsSynthesisError,
|
|
5
|
+
type SynthesisErrorKind,
|
|
6
|
+
toSynthesisError,
|
|
7
|
+
} from "./errors.ts";
|
|
3
8
|
import type { AzureTtsClient } from "./client.ts";
|
|
4
9
|
import { mergeSynthesisResults } from "./synthesis.ts";
|
|
5
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
SsmlSynthesisChunk,
|
|
12
|
+
SsmlSynthesisResult,
|
|
13
|
+
SynthesisProgressEvent,
|
|
14
|
+
SynthesizeChunksOptions,
|
|
15
|
+
} from "./types.ts";
|
|
16
|
+
import type { AzureTtsOutputFormat } from "./outputFormats.ts";
|
|
6
17
|
|
|
7
18
|
export interface SsmlValidationError {
|
|
8
|
-
readonly kind: "validation";
|
|
19
|
+
readonly kind: "validation-error";
|
|
9
20
|
readonly message: string;
|
|
10
21
|
readonly diagnostics: readonly SsmlDiagnostic[];
|
|
11
22
|
}
|
|
12
23
|
|
|
24
|
+
export type SsmlSynthesisError = SsmlValidationError | AzureTtsSynthesisError;
|
|
25
|
+
|
|
13
26
|
export class ChunkValidationError extends Error {
|
|
14
|
-
readonly kind = "
|
|
27
|
+
readonly kind = "validation-error" as const;
|
|
15
28
|
readonly chunkIndex: number;
|
|
16
29
|
readonly diagnostics: readonly SsmlDiagnostic[];
|
|
17
30
|
|
|
@@ -25,15 +38,26 @@ export class ChunkValidationError extends Error {
|
|
|
25
38
|
|
|
26
39
|
export type Result<T, E> =
|
|
27
40
|
| { readonly ok: true; readonly success: true; readonly status: "success"; readonly value: T }
|
|
28
|
-
| {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
| (E extends { readonly kind: infer Kind extends SynthesisErrorKind }
|
|
42
|
+
? {
|
|
43
|
+
readonly ok: false;
|
|
44
|
+
readonly success: false;
|
|
45
|
+
readonly status: Kind;
|
|
46
|
+
readonly error: E;
|
|
47
|
+
}
|
|
48
|
+
: {
|
|
49
|
+
readonly ok: false;
|
|
50
|
+
readonly success: false;
|
|
51
|
+
readonly status: SynthesisErrorKind;
|
|
52
|
+
readonly error: E;
|
|
53
|
+
});
|
|
34
54
|
|
|
35
55
|
export type SynthesisResult<T, E> = Result<T, E>;
|
|
36
56
|
|
|
57
|
+
function failure<E extends { readonly kind: SynthesisErrorKind }>(error: E): Result<never, E> {
|
|
58
|
+
return { ok: false, success: false, status: error.kind, error } as Result<never, E>;
|
|
59
|
+
}
|
|
60
|
+
|
|
37
61
|
export type Success<T> = Extract<Result<T, never>, { readonly ok: true }>;
|
|
38
62
|
export type ValidationErrorResult = Extract<
|
|
39
63
|
Result<never, SsmlValidationError>,
|
|
@@ -44,29 +68,33 @@ export type AzureApiErrorResult = Extract<Result<never, AzureTtsError>, { readon
|
|
|
44
68
|
export type SsmlSynthesisSafeResult =
|
|
45
69
|
| Result<SsmlSynthesisResult, never>
|
|
46
70
|
| Result<never, SsmlValidationError>
|
|
47
|
-
| Result<never,
|
|
71
|
+
| Result<never, SsmlSynthesisError>;
|
|
48
72
|
|
|
49
73
|
export interface SynthesizeSsmlSafeOptions extends AzureValidationOptions {
|
|
50
74
|
/** Optional nested form for callers that want to keep validation settings grouped. */
|
|
51
75
|
validation?: AzureValidationOptions;
|
|
76
|
+
signal?: AbortSignal;
|
|
52
77
|
}
|
|
53
78
|
|
|
54
79
|
export interface SynthesizeSsmlChunksSafeOptions extends AzureValidationOptions {
|
|
55
80
|
validation?: AzureValidationOptions;
|
|
56
81
|
outputFormat?: string;
|
|
82
|
+
signal?: AbortSignal;
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
sourceNodePath?: string[];
|
|
57
85
|
onProgress?: (event: SynthesisProgressEvent) => void;
|
|
58
86
|
}
|
|
59
87
|
|
|
60
88
|
export type SsmlSynthesisChunksSafeResult =
|
|
61
89
|
| Result<SsmlSynthesisResult, never>
|
|
62
90
|
| Result<never, ChunkValidationError>
|
|
63
|
-
| Result<never,
|
|
91
|
+
| Result<never, SsmlSynthesisError | ChunkValidationError>;
|
|
64
92
|
|
|
65
93
|
interface SynthesisClient {
|
|
66
|
-
synthesizeSsml(ssml: string): Promise<SsmlSynthesisResult>;
|
|
94
|
+
synthesizeSsml(ssml: string, options?: Partial<SynthesizeChunksOptions>): Promise<SsmlSynthesisResult>;
|
|
67
95
|
synthesizeChunks?(
|
|
68
96
|
chunks: readonly (SsmlSynthesisChunk | string)[],
|
|
69
|
-
options?:
|
|
97
|
+
options?: SynthesizeChunksOptions,
|
|
70
98
|
): Promise<SsmlSynthesisResult>;
|
|
71
99
|
}
|
|
72
100
|
|
|
@@ -76,27 +104,31 @@ export async function synthesizeSsmlSafe(
|
|
|
76
104
|
ssml: string,
|
|
77
105
|
options: SynthesizeSsmlSafeOptions = {},
|
|
78
106
|
): Promise<SsmlSynthesisSafeResult> {
|
|
79
|
-
const validationOptions = options.validation ?? options;
|
|
107
|
+
const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
|
|
80
108
|
const diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
|
|
109
|
+
if (options.signal?.aborted) {
|
|
110
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
111
|
+
return failure(error);
|
|
112
|
+
}
|
|
81
113
|
const errors = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
82
114
|
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
|
-
};
|
|
115
|
+
return failure({
|
|
116
|
+
kind: "validation-error",
|
|
117
|
+
message: "SSML validation failed; the Azure Speech API was not called.",
|
|
118
|
+
diagnostics: errors,
|
|
119
|
+
});
|
|
93
120
|
}
|
|
94
121
|
|
|
95
122
|
try {
|
|
96
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
success: true,
|
|
126
|
+
status: "success",
|
|
127
|
+
value: await client.synthesizeSsml(ssml, { signal: options.signal }),
|
|
128
|
+
};
|
|
97
129
|
} catch (error) {
|
|
98
|
-
const
|
|
99
|
-
return
|
|
130
|
+
const synthesisError = toSynthesisError(error);
|
|
131
|
+
return failure(synthesisError);
|
|
100
132
|
}
|
|
101
133
|
}
|
|
102
134
|
|
|
@@ -106,7 +138,11 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
106
138
|
chunks: readonly (SsmlSynthesisChunk | string)[],
|
|
107
139
|
options: SynthesizeSsmlChunksSafeOptions = {},
|
|
108
140
|
): Promise<SsmlSynthesisChunksSafeResult> {
|
|
109
|
-
const validationOptions = options.validation ?? options;
|
|
141
|
+
const validationOptions = withValidationSignal(options.validation ?? options, options.signal);
|
|
142
|
+
if (options.signal?.aborted) {
|
|
143
|
+
const error = toSynthesisError(new Error("Speech synthesis was cancelled."));
|
|
144
|
+
return failure(error);
|
|
145
|
+
}
|
|
110
146
|
const pending = (index: number, status: SynthesisProgressEvent["status"], error?: unknown): void => {
|
|
111
147
|
options.onProgress?.({
|
|
112
148
|
currentChunk: status === "success" ? index + 1 : index,
|
|
@@ -126,7 +162,11 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
126
162
|
const validations = await Promise.all(
|
|
127
163
|
chunks.map(async (chunk) => {
|
|
128
164
|
const ssml = typeof chunk === "string" ? chunk : chunk.ssml;
|
|
129
|
-
const
|
|
165
|
+
const sourceNodePath =
|
|
166
|
+
typeof chunk === "string" ? options.sourceNodePath : (chunk.sourceNodePath ?? options.sourceNodePath);
|
|
167
|
+
const diagnostics = await Promise.resolve(
|
|
168
|
+
validateAzureSsml(ssml, { ...validationOptions, ...(sourceNodePath ? { sourceNodePath } : {}) }),
|
|
169
|
+
);
|
|
130
170
|
return diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
131
171
|
}),
|
|
132
172
|
);
|
|
@@ -134,22 +174,38 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
134
174
|
if (firstInvalidIndex >= 0) {
|
|
135
175
|
const error = new ChunkValidationError(firstInvalidIndex, validations[firstInvalidIndex] ?? []);
|
|
136
176
|
pending(firstInvalidIndex, "failed", error);
|
|
137
|
-
return
|
|
177
|
+
return failure(error);
|
|
138
178
|
}
|
|
139
179
|
|
|
140
180
|
try {
|
|
141
181
|
if (client.synthesizeChunks) {
|
|
142
|
-
const
|
|
182
|
+
const normalizedChunks = chunks.map((chunk) => {
|
|
183
|
+
if (typeof chunk === "string" || chunk.sourceNodePath || !options.sourceNodePath) return chunk;
|
|
184
|
+
return { ...chunk, sourceNodePath: [...options.sourceNodePath] };
|
|
185
|
+
});
|
|
186
|
+
const value = await client.synthesizeChunks(normalizedChunks, {
|
|
187
|
+
onProgress: options.onProgress,
|
|
188
|
+
outputFormat: options.outputFormat,
|
|
189
|
+
signal: options.signal,
|
|
190
|
+
timeoutMs: options.timeoutMs,
|
|
191
|
+
sourceNodePath: options.sourceNodePath,
|
|
192
|
+
});
|
|
143
193
|
return { ok: true, success: true, status: "success", value };
|
|
144
194
|
}
|
|
145
195
|
const results: SsmlSynthesisResult[] = [];
|
|
146
196
|
for (const [index, chunk] of chunks.entries()) {
|
|
147
197
|
const input = typeof chunk === "string" ? { ssml: chunk } : chunk;
|
|
148
198
|
const sourceNodePath = input.sourceNodePath;
|
|
199
|
+
const originalTextRange = input.originalTextRange;
|
|
149
200
|
pending(index, "synthesizing");
|
|
150
201
|
const startedAt = Date.now();
|
|
151
202
|
try {
|
|
152
|
-
const result = await client.synthesizeSsml(input.ssml
|
|
203
|
+
const result = await client.synthesizeSsml(input.ssml, {
|
|
204
|
+
outputFormat: options.outputFormat,
|
|
205
|
+
signal: options.signal,
|
|
206
|
+
timeoutMs: options.timeoutMs,
|
|
207
|
+
sourceNodePath: input.sourceNodePath ?? options.sourceNodePath,
|
|
208
|
+
});
|
|
153
209
|
results.push({
|
|
154
210
|
...result,
|
|
155
211
|
...(input.originalTextRange ? { textRange: { ...input.originalTextRange } } : {}),
|
|
@@ -158,9 +214,64 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
158
214
|
boundaries: result.boundaries?.map((event) => ({
|
|
159
215
|
...event,
|
|
160
216
|
sourceNodePath: [...sourceNodePath],
|
|
217
|
+
...(event.originalTextRange
|
|
218
|
+
? { originalTextRange: { ...event.originalTextRange } }
|
|
219
|
+
: input.originalTextRange
|
|
220
|
+
? { originalTextRange: { ...input.originalTextRange } }
|
|
221
|
+
: {}),
|
|
222
|
+
})),
|
|
223
|
+
visemes: result.visemes?.map((event) => ({
|
|
224
|
+
...event,
|
|
225
|
+
sourceNodePath: [...sourceNodePath],
|
|
226
|
+
...(event.originalTextRange
|
|
227
|
+
? { originalTextRange: { ...event.originalTextRange } }
|
|
228
|
+
: input.originalTextRange
|
|
229
|
+
? { originalTextRange: { ...input.originalTextRange } }
|
|
230
|
+
: {}),
|
|
231
|
+
})),
|
|
232
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
233
|
+
...event,
|
|
234
|
+
sourceNodePath: [...sourceNodePath],
|
|
235
|
+
...(event.originalTextRange
|
|
236
|
+
? { originalTextRange: { ...event.originalTextRange } }
|
|
237
|
+
: input.originalTextRange
|
|
238
|
+
? { originalTextRange: { ...input.originalTextRange } }
|
|
239
|
+
: {}),
|
|
240
|
+
})),
|
|
241
|
+
}
|
|
242
|
+
: {}),
|
|
243
|
+
...(originalTextRange
|
|
244
|
+
? {
|
|
245
|
+
boundaries: result.boundaries?.map((event) => ({
|
|
246
|
+
...event,
|
|
247
|
+
originalTextRange: event.originalTextRange
|
|
248
|
+
? { ...event.originalTextRange }
|
|
249
|
+
: { ...originalTextRange },
|
|
250
|
+
})),
|
|
251
|
+
wordBoundary: result.wordBoundary?.map((event) => ({
|
|
252
|
+
...event,
|
|
253
|
+
originalTextRange: event.originalTextRange
|
|
254
|
+
? { ...event.originalTextRange }
|
|
255
|
+
: { ...originalTextRange },
|
|
256
|
+
})),
|
|
257
|
+
wordBoundaries: result.wordBoundaries?.map((event) => ({
|
|
258
|
+
...event,
|
|
259
|
+
originalTextRange: event.originalTextRange
|
|
260
|
+
? { ...event.originalTextRange }
|
|
261
|
+
: { ...originalTextRange },
|
|
262
|
+
})),
|
|
263
|
+
visemes: result.visemes?.map((event) => ({
|
|
264
|
+
...event,
|
|
265
|
+
originalTextRange: event.originalTextRange
|
|
266
|
+
? { ...event.originalTextRange }
|
|
267
|
+
: { ...originalTextRange },
|
|
268
|
+
})),
|
|
269
|
+
bookmarks: result.bookmarks?.map((event) => ({
|
|
270
|
+
...event,
|
|
271
|
+
originalTextRange: event.originalTextRange
|
|
272
|
+
? { ...event.originalTextRange }
|
|
273
|
+
: { ...originalTextRange },
|
|
161
274
|
})),
|
|
162
|
-
visemes: result.visemes?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] })),
|
|
163
|
-
bookmarks: result.bookmarks?.map((event) => ({ ...event, sourceNodePath: [...sourceNodePath] })),
|
|
164
275
|
}
|
|
165
276
|
: {}),
|
|
166
277
|
});
|
|
@@ -191,10 +302,21 @@ export async function synthesizeSsmlChunksSafe(
|
|
|
191
302
|
ok: true,
|
|
192
303
|
success: true,
|
|
193
304
|
status: "success",
|
|
194
|
-
value: mergeSynthesisResults(results,
|
|
305
|
+
value: mergeSynthesisResults(results, {
|
|
306
|
+
format: (options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3") as AzureTtsOutputFormat,
|
|
307
|
+
}),
|
|
195
308
|
};
|
|
196
309
|
} catch (error) {
|
|
197
|
-
const
|
|
198
|
-
return
|
|
310
|
+
const synthesisError = toSynthesisError(error);
|
|
311
|
+
return failure(synthesisError);
|
|
199
312
|
}
|
|
200
313
|
}
|
|
314
|
+
|
|
315
|
+
function withValidationSignal(options: AzureValidationOptions, signal?: AbortSignal): AzureValidationOptions {
|
|
316
|
+
if (!signal) return options;
|
|
317
|
+
return {
|
|
318
|
+
...options,
|
|
319
|
+
urlValidatorSignal: signal,
|
|
320
|
+
urlValidation: { ...(options.urlValidation ?? {}), signal },
|
|
321
|
+
};
|
|
322
|
+
}
|