ssml-builder-js 2.15.0 → 2.17.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/README.md CHANGED
@@ -165,7 +165,7 @@ const parsed = parseSsml(ssml);
165
165
  | `fromPlainTextToSsml(text, options?)` | プレーンテキストを `<speak>`、段落 `<p>`、文 `<s>` を含む SSML に変換 |
166
166
  | `validateSsmlStructureIntegrity(original, translated)` | 翻訳前後のタグ階層・タグ名・属性の一致を検証 |
167
167
  | `validateAzureSsml(xml, options?)` | Azure Speech 向けの意味検証結果を Diagnostic 配列で返す(各診断に `source: "ssml-static-validator"` を付与) |
168
- | `getAzureVoiceCatalogMetadata()` / `getBuiltInVoiceCatalogMetadata()` | 組み込み音声カタログの生成日時、API バージョン、リージョン、収録数を返す |
168
+ | `getAzureVoiceCatalogMetadata()` / `getBuiltInVoiceCatalogMetadata()` | 組み込み音声カタログの生成日時、API バージョン、リージョン、収録数、有効期限、リージョン差分を返す |
169
169
 
170
170
  ### 3段階の検証モデル
171
171
 
@@ -670,7 +670,7 @@ The main `buildSsml` and `parseSsml` signatures are:
670
670
  | `fromPlainTextToSsml(text, options?)` | Converts plain text into an initial `<speak>` document containing paragraphs (`<p>`) and sentences (`<s>`) |
671
671
  | `validateSsmlStructureIntegrity(original, translated)` | Checks that tag hierarchy, element names, and attributes are unchanged after translation |
672
672
  | `validateAzureSsml(xml, options?)` | Returns Azure Speech semantic-validation diagnostics (each diagnostic has `source: "ssml-static-validator"`) |
673
- | `getAzureVoiceCatalogMetadata()` / `getBuiltInVoiceCatalogMetadata()` | Returns generation time, API version, regions, and voice count for the bundled voice catalog |
673
+ | `getAzureVoiceCatalogMetadata()` / `getBuiltInVoiceCatalogMetadata()` | Returns generation time, API version, regions, voice count, expiry, and known regional differences for the bundled voice catalog |
674
674
 
675
675
  ### Three-stage validation model
676
676
 
@@ -913,7 +913,11 @@ result.bookmarks; // { name, audioOffsetMs }[]
913
913
 
914
914
  長文を分割して合成する場合は `synthesizeSsmlChunks` または `AzureTtsClient.synthesizeChunks` を使います。音声バイナリを連結し、`boundaries`、`visemes`、`bookmarks` のオフセットを累積 `durationMs` 分だけ補正します。`synthesizeSsmlSafe(client, ssml, { validation })` は検証エラー時に Azure API を呼び出さず、`status: "validation-error"` / `"azure-api-error"` / `"success"` の結果を返します。
915
915
 
916
- v2.15.0 では `synthesizeSsmlChunksSafe(client, chunks, options)` が全チャンクを事前検証し、`outputFormat`、`signal`、`timeoutMs`、`sourceNodePath` を各合成へ伝播します。`mergeAudioBuffers(buffers, { format })` と `mergeSynthesisResults(results, { format })` は形式指定を必須とし、結合結果には `mimeType` が含まれます。Ogg/WebM などは `customMerger` で外部 Muxer に委譲できます。エラーは `validation-error`、`azure-api-error`、`merge-error`、`unsupported-format-error`、`cancelled`、`timeout` の判別可能な `kind` を持ちます。同期イベントは個別の `sourceNodePath` と `originalTextRange` にマッピングされ、URL 検証コールバックには `AbortSignal` が渡されます。
916
+ v2.15.0 では `synthesizeSsmlChunksSafe(client, chunks, options)` が全チャンクを事前検証し、`outputFormat`、`signal`、`timeoutMs`、`sourceNodePath` を各合成へ伝播します。`mergeAudioBuffers(buffers, { format })` と `mergeSynthesisResults(results, { format })` は形式指定を必須とし、結合結果には `mimeType` が含まれます。Ogg/WebM などは `customMerger` で外部 Muxer に委譲できます。エラーは `validation-error`、`azure-api-error`、`merge-error`、`unsupported-format-error`、`cancelled`、`timeout` の判別可能な `kind` を持ちます。同期イベントは個別の `sourceNodePath` と `originalTextRange` にマッピングされ、URL 検証コールバックには `AbortSignal` が渡されます。
917
+
918
+ v2.16.0 では `concurrency` と `retryOptions`(429/5xx・ネットワーク障害のみ、Jitter 付き指数バックオフ)でチャンク合成を制御できます。`onProgress` には `retryAttempt`、`nextRetryDelayMs`、`isRetrying` が追加され、結合は常に `chunkIndex` 順です。`SsmlSynthesisResult.audioSpec` は WAV/MP3 ヘッダーから音声仕様を抽出し、チャンク間の仕様不一致は `AudioFormatMismatchError` になります。同期イベントには `mappingStatus`、Azure 診断には `nodePath`、`range`、`tagName`、`attributeName`、`voiceName`、`chunkIndex` が含まれます。`validateAzureSsmlChunks` は URL 検証プールを全チャンクで共有し、カスタム結合器には `inputSpecs` と `signal` を渡します。
919
+
920
+ v2.17.0 では `customMerger`、`outputMimeType`、`postMergeValidator` をチャンク合成の末尾まで構成でき、`BatchChunkValidationError` が全チャンクの診断と総エラー数を返します。`cancelOnFailure` と `resumeChunks` により成功済みバイナリを再利用でき、`timeouts`(URL 検証、チャンク、リトライ込みチャンク、ジョブ全体)を個別に設定できます。429 の `Retry-After` は指数バックオフより優先されます。`AudioSpecification` には `bitDepth`、`container`、`isVbr` が追加され、同期 `mappingStatus` は JSON 化後も保持されます。
917
921
 
918
922
  `validateAzureSsml` の `urlValidation` オプションは URL の重複排除、キャッシュ、`concurrency`、`signal`、`timeoutMs` を制御します。
919
923
 
@@ -921,6 +925,10 @@ For long documents, use `synthesizeSsmlChunks` or `AzureTtsClient.synthesizeChun
921
925
 
922
926
  In v2.15.0, `synthesizeSsmlChunksSafe(client, chunks, options)` validates every chunk before contacting Azure and propagates `outputFormat`, `signal`, `timeoutMs`, and `sourceNodePath` to each synthesis. `mergeAudioBuffers(buffers, { format })` and `mergeSynthesisResults(results, { format })` require an explicit format and merged results expose `mimeType`. Ogg and WebM can be delegated to an external Muxer through `customMerger`. Errors have discriminated `kind` values: `validation-error`, `azure-api-error`, `merge-error`, `unsupported-format-error`, `cancelled`, and `timeout`. Synchronization events receive individual `sourceNodePath` and `originalTextRange` mappings, and URL validators receive an `AbortSignal`.
923
927
 
928
+ In v2.16.0, use `concurrency` and `retryOptions` to control chunk synthesis; only 429/5xx and network failures are retried with jittered exponential backoff. Progress events include `retryAttempt`, `nextRetryDelayMs`, and `isRetrying`, while merging always follows `chunkIndex` order. `SsmlSynthesisResult.audioSpec` is extracted from WAV/MP3 headers, and incompatible chunk specs throw `AudioFormatMismatchError`. Synchronization events include `mappingStatus`, diagnostics include structured node/range/tag/attribute/voice/chunk fields, `validateAzureSsmlChunks` shares one URL validation pool, and custom mergers receive `inputSpecs` and an `AbortSignal`.
929
+
930
+ In v2.17.0, configure `customMerger`, `outputMimeType`, and `postMergeValidator` through the chunk synthesis pipeline. `BatchChunkValidationError` aggregates diagnostics and the total error count for every invalid chunk. `cancelOnFailure` and `resumeChunks` allow successful binary chunks to be reused, while `timeouts` independently bounds URL validation, individual chunks, retries, and the total job. HTTP 429 `Retry-After` takes priority over exponential backoff. `AudioSpecification` now includes `bitDepth`, `container`, and `isVbr`, and `mappingStatus` remains present after JSON serialization.
931
+
924
932
  The `urlValidation` option of `validateAzureSsml` provides URL deduplication, in-memory caching, bounded `concurrency`, `signal`, and `timeoutMs` controls.
925
933
 
926
934
  The public updater CLI is `npx ssml-builder sync-voices --region eastus --output ./azure-voices.json`. It reads the key from `AZURE_SPEECH_KEY` (or `--key`) and regions from `AZURE_SPEECH_REGION(S)` (or `--region(s)`).
@@ -2015,6 +2015,7 @@ function tokenizeElements(source) {
2015
2015
  if (parent) parent.childElementCount += 1;
2016
2016
  const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
2017
2017
  const tokenName = nameMatch[1];
2018
+ const path = parent ? [...parent.path, `${tokenName}[${childElementIndex ?? 0}]`] : [tokenName];
2018
2019
  const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
2019
2020
  tokens.push({
2020
2021
  attributes,
@@ -2025,12 +2026,14 @@ function tokenizeElements(source) {
2025
2026
  parentName: parent?.name,
2026
2027
  parentVoiceName,
2027
2028
  selfClosing,
2028
- start
2029
+ start,
2030
+ path
2029
2031
  });
2030
2032
  if (!selfClosing) {
2031
2033
  openElements.push({
2032
2034
  childElementCount: 0,
2033
2035
  name: tokenName,
2036
+ path,
2034
2037
  voiceName: tokenVoiceName
2035
2038
  });
2036
2039
  }
@@ -2043,13 +2046,14 @@ function location(source, offset) {
2043
2046
  const line = before.split("\n").length;
2044
2047
  return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
2045
2048
  }
2046
- function addDiagnostic(diagnostics, source, offset, message, severity = "error", code) {
2049
+ function addDiagnostic(diagnostics, source, offset, message, severity = "error", code, metadata = {}) {
2047
2050
  diagnostics.push({
2048
2051
  ...location(source, offset),
2049
2052
  message,
2050
2053
  severity,
2051
2054
  source: "ssml-static-validator",
2052
- ...code ? { code } : {}
2055
+ ...code ? { code } : {},
2056
+ ...metadata
2053
2057
  });
2054
2058
  }
2055
2059
  function isSupportedProsodyRate(value) {
@@ -2522,9 +2526,11 @@ function validateAzureSsmlStatic(ssml, options = {}) {
2522
2526
  for (const token of tokens) {
2523
2527
  const tokenName = token.name.toLowerCase();
2524
2528
  const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
2529
+ const tokenDiagnosticStart = diagnostics.length;
2525
2530
  validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
2526
2531
  const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
2527
2532
  validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
2533
+ annotateTokenDiagnostics(diagnostics, tokenDiagnosticStart, token, options, tokenVoiceName);
2528
2534
  if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
2529
2535
  addDiagnostic(
2530
2536
  diagnostics,
@@ -2546,12 +2552,30 @@ function urlAttributes(token) {
2546
2552
  return value === void 0 ? [] : [{ attribute, value }];
2547
2553
  });
2548
2554
  }
2555
+ function annotateTokenDiagnostics(diagnostics, startIndex, token, options, voiceName) {
2556
+ const attributes = [...token.attributes.keys()];
2557
+ for (const diagnostic of diagnostics.slice(startIndex)) {
2558
+ const attributeName = attributes.find(
2559
+ (attribute) => new RegExp(`(?:<[^> ]+\\s+|")${attribute}(?:"|>|\\s)`, "i").test(diagnostic.message)
2560
+ );
2561
+ const nodePath = options.sourceNodePath ? [...options.sourceNodePath] : [...token.path];
2562
+ Object.assign(diagnostic, {
2563
+ range: { start: token.start, end: token.end + 1 },
2564
+ tagName: token.name,
2565
+ ...attributeName ? { attributeName } : {},
2566
+ ...voiceName ? { voiceName } : {},
2567
+ ...options.chunkIndex !== void 0 ? { chunkIndex: options.chunkIndex } : {},
2568
+ nodePath,
2569
+ targetNodePath: [...token.path]
2570
+ });
2571
+ }
2572
+ }
2549
2573
  function validateAzureSsml(ssml, options = {}) {
2550
2574
  const diagnostics = validateAzureSsmlStatic(ssml, options);
2551
2575
  const validator = options.urlValidator ?? options.customUrlValidator;
2552
2576
  if (!validator || typeof ssml !== "string") return diagnostics;
2553
2577
  const runnerOptions = options.urlValidation ?? {};
2554
- const boundedValidator = createAzureUrlValidatorRunner(validator, {
2578
+ const boundedValidator = options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
2555
2579
  ...runnerOptions,
2556
2580
  ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
2557
2581
  ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
@@ -2576,33 +2600,55 @@ function validateAzureSsml(ssml, options = {}) {
2576
2600
  const valid = typeof result === "boolean" ? result : result.valid;
2577
2601
  if (!valid) {
2578
2602
  const reason = typeof result === "boolean" ? void 0 : result.reason;
2603
+ const diagnosticStart = diagnostics.length;
2579
2604
  addDiagnostic(
2580
2605
  diagnostics,
2581
2606
  ssml,
2582
2607
  token.start,
2583
2608
  `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
2584
2609
  );
2610
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
2585
2611
  }
2586
2612
  } catch (error) {
2587
2613
  const reason = error instanceof Error ? error.message : String(error);
2614
+ const diagnosticStart = diagnostics.length;
2588
2615
  addDiagnostic(
2589
2616
  diagnostics,
2590
2617
  ssml,
2591
2618
  token.start,
2592
2619
  `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
2593
2620
  );
2621
+ annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
2594
2622
  }
2595
2623
  })
2596
2624
  );
2597
2625
  return Promise.all(checks).then(() => diagnostics);
2598
2626
  }
2627
+ async function validateAzureSsmlChunks(chunks, options = {}) {
2628
+ const validator = options.urlValidator ?? options.customUrlValidator;
2629
+ const sharedOptions = validator ? {
2630
+ ...options,
2631
+ urlValidatorRunner: options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
2632
+ ...options.urlValidation ?? {},
2633
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
2634
+ ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
2635
+ ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
2636
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
2637
+ })
2638
+ } : options;
2639
+ return Promise.all(
2640
+ chunks.map((chunk, chunkIndex) => Promise.resolve(validateAzureSsml(chunk, { ...sharedOptions, chunkIndex })))
2641
+ );
2642
+ }
2599
2643
 
2600
2644
  // packages/ssml-core/src/generated/azureVoiceCatalog.ts
2601
2645
  var AZURE_VOICE_CATALOG_METADATA = {
2602
2646
  apiVersion: "2025-10-01",
2603
2647
  generatedAt: "2026-08-28T00:00:00.000Z",
2604
2648
  regions: [],
2605
- voiceCount: AZURE_VOICE_DEFINITIONS.length
2649
+ voiceCount: AZURE_VOICE_DEFINITIONS.length,
2650
+ expiresAt: "2026-09-04T00:00:00.000Z",
2651
+ regionDiffs: {}
2606
2652
  };
2607
2653
 
2608
2654
  // packages/ssml-core/src/voiceCatalog.ts
@@ -2631,7 +2677,8 @@ export {
2631
2677
  normalizeAzureLanguage,
2632
2678
  areAzureLanguagesEquivalent,
2633
2679
  validateAzureSsml,
2680
+ validateAzureSsmlChunks,
2634
2681
  getAzureVoiceCatalogMetadata,
2635
2682
  getBuiltInVoiceCatalogMetadata
2636
2683
  };
2637
- //# sourceMappingURL=chunk-FXUM45ZY.mjs.map
2684
+ //# sourceMappingURL=chunk-2SYLELUT.mjs.map