@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/src/safe.ts CHANGED
@@ -1,17 +1,30 @@
1
1
  import { validateAzureSsml, type AzureValidationOptions, type SsmlDiagnostic } from "@ssml-builder-js/ssml-core";
2
- import { AzureTtsError, createSpeechSdkError } from "./errors.ts";
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 { SsmlSynthesisChunk, SsmlSynthesisResult, SynthesisProgressEvent } from "./types.ts";
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 = "chunk-validation" as const;
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
- readonly ok: false;
30
- readonly success: false;
31
- readonly status: "validation-error" | "azure-api-error";
32
- readonly error: E;
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, AzureTtsError>;
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, AzureTtsError>;
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?: { onProgress?: (event: SynthesisProgressEvent) => void },
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
- ok: false,
85
- success: false,
86
- status: "validation-error",
87
- error: {
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 { ok: true, success: true, status: "success", value: await client.synthesizeSsml(ssml) };
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 azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
99
- return { ok: false, success: false, status: "azure-api-error", error: azureError };
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 diagnostics = await Promise.resolve(validateAzureSsml(ssml, validationOptions));
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 { ok: false, success: false, status: "validation-error", error };
177
+ return failure(error);
138
178
  }
139
179
 
140
180
  try {
141
181
  if (client.synthesizeChunks) {
142
- const value = await client.synthesizeChunks(chunks, { onProgress: options.onProgress });
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, options.outputFormat),
305
+ value: mergeSynthesisResults(results, {
306
+ format: (options.outputFormat ?? "audio-16khz-128kbitrate-mono-mp3") as AzureTtsOutputFormat,
307
+ }),
195
308
  };
196
309
  } catch (error) {
197
- const azureError = error instanceof AzureTtsError ? error : createSpeechSdkError(error);
198
- return { ok: false, success: false, status: "azure-api-error", error: azureError };
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
+ }