ssml-builder-js 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/README.md +10 -4
- package/dist/{chunk-UZSCXPC5.mjs → chunk-F2EMU3HM.mjs} +2 -2
- package/dist/{chunk-QFIBPCO4.mjs → chunk-HI74FTKY.mjs} +197 -21
- package/dist/chunk-HI74FTKY.mjs.map +1 -0
- package/dist/{chunk-AQ55MOPU.mjs → chunk-NKLZGITR.mjs} +354 -177
- package/dist/chunk-NKLZGITR.mjs.map +1 -0
- package/dist/core.d.mts +67 -17
- package/dist/core.d.ts +67 -17
- package/dist/core.js +353 -174
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +5 -1
- package/dist/elements.js +70 -20
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +2 -2
- package/dist/{index.d-CUXRwSw0.d.mts → index.d-DR5Qz43p.d.mts} +37 -2
- package/dist/{index.d-CUXRwSw0.d.ts → index.d-DR5Qz43p.d.ts} +37 -2
- package/dist/index.d.mts +162 -19
- package/dist/index.d.ts +162 -19
- package/dist/index.js +2060 -1128
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +752 -174
- package/dist/index.mjs.map +1 -1
- package/dist/react.d.mts +11 -2
- package/dist/react.d.ts +11 -2
- package/dist/react.js +147 -32
- package/dist/react.js.map +1 -1
- package/dist/react.mjs +79 -14
- package/dist/react.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-AQ55MOPU.mjs.map +0 -1
- package/dist/chunk-QFIBPCO4.mjs.map +0 -1
- /package/dist/{chunk-UZSCXPC5.mjs.map → chunk-F2EMU3HM.mjs.map} +0 -0
package/README.md
CHANGED
|
@@ -240,7 +240,7 @@ const diagnostics = validateAzureSsml(ssml, {
|
|
|
240
240
|
});
|
|
241
241
|
```
|
|
242
242
|
|
|
243
|
-
`audio`、`mstts:backgroundaudio`、`lexicon`、`mstts:voiceconversion` の URL は `urlValidator`(または `customUrlValidator
|
|
243
|
+
`audio`、`mstts:backgroundaudio`、`lexicon`、`mstts:voiceconversion` の URL は `urlValidator`(または `customUrlValidator`)へ渡せます。コールバックは `(url, context, signal)` を受け取り、Promise を返す検証関数で DNS 解決後のプライベート IP 遮断などを実装できます。検証キャッシュはタグ・属性・URL ごとに分離されます。
|
|
244
244
|
|
|
245
245
|
`maxXmlDepth` は `<speak>` を深さ 1 として XML の過剰なネストを検出します。長文は `splitSsmlDocument` で `<p>`/`<s>` と親の `voice`、`prosody` コンテキストを保ったまま分割できます。
|
|
246
246
|
|
|
@@ -342,6 +342,7 @@ export function App() {
|
|
|
342
342
|
- `customInsertions` / `additionalInsertions`: カスタム SSML 挿入定義。`customInsertions` は同じ ID の標準定義を置き換え、`additionalInsertions` は標準定義へ追加します
|
|
343
343
|
- `customInspectors`: Visual Editor のタグ名ごとに Inspector を差し替えるレンダラー
|
|
344
344
|
- `renderVoiceSelector`: 音声セレクターのカスタムレンダラー。`voiceCatalog` と `voiceLocale`、`voiceRegion`、`voiceStyle` で候補を絞り込めます。プレビュー音声にはバッジ情報も渡されます
|
|
345
|
+
- `voiceModel`(または `model`): 選択中の Azure 音声モデル。音声カタログの対応モデル、SSML タグ、スタイル、ロケールとの不整合を Visual Editor にリアルタイム表示します
|
|
345
346
|
- `className` / `style`: エディター全体のクラス名とインラインスタイル
|
|
346
347
|
- `toolbarClassName` / `toolbarStyle`: ツールバーのクラス名とインラインスタイル
|
|
347
348
|
- `displayClassName` / `displayStyle`: 本文表示エリアのクラス名とインラインスタイル
|
|
@@ -708,7 +709,7 @@ const diagnostics = validateAzureSsml(ssml, {
|
|
|
708
709
|
});
|
|
709
710
|
```
|
|
710
711
|
|
|
711
|
-
URLs in `audio`, `mstts:backgroundaudio`, `lexicon`, and `mstts:voiceconversion` can be passed to `urlValidator` (or `customUrlValidator`). The callback may be asynchronous, making it suitable for host-side DNS/private-IP and SSRF policy checks.
|
|
712
|
+
URLs in `audio`, `mstts:backgroundaudio`, `lexicon`, and `mstts:voiceconversion` can be passed to `urlValidator` (or `customUrlValidator`). The callback receives `(url, context, signal)` and may be asynchronous, making it suitable for host-side DNS/private-IP and SSRF policy checks. Validation cache entries are isolated by tag, attribute, and URL.
|
|
712
713
|
|
|
713
714
|
The catalog is represented by `AzureVoiceDefinition` with `name`, `locale`, optional `secondaryLocales`, `styles`, `supportedTags`, `unsupportedTags`, `models`, `regions`, and `status`. Pass `voiceDefinitions` (or `voiceCatalog`) to supplement or override the built-in catalog with an external definition. A tag that violates `supportedTags` or `unsupportedTags` produces `azure-unsupported-tag-for-voice` with error severity. `customVoiceStyleMap` remains supported for backward compatibility and overrides styles for the named voice. Diagnostics distinguish an unregistered voice (`azure-unknown-voice`, controlled by `unknownVoicePolicy`), an unsupported style on a registered voice (`azure-unsupported-style`), and a locale mismatch (`azure-locale-mismatch`). The `<mstts:audioduration value="10s"/>` element accepts positive `ms` or `s` values and `hh:mm:ss[.fff]` clock values.
|
|
714
715
|
|
|
@@ -834,6 +835,7 @@ export function App() {
|
|
|
834
835
|
- `customInsertions` / `additionalInsertions`: Custom SSML insertion definitions. `customInsertions` replaces a built-in definition with the same ID, while `additionalInsertions` adds definitions to the built-ins
|
|
835
836
|
- `customInspectors`: Custom renderers keyed by element type or serialized tag name for the Visual Editor
|
|
836
837
|
- `renderVoiceSelector`: Custom voice selector renderer. Supply `voiceCatalog` and optional `voiceLocale`, `voiceRegion`, or `voiceStyle` filters; preview status is included in each voice entry
|
|
838
|
+
- `voiceModel` (or `model`): Selected Azure voice model. The Visual Editor reports live mismatches between the catalog's supported models, SSML tags, styles, and locale
|
|
837
839
|
- `className` / `style`: A class name and inline styles for the editor container
|
|
838
840
|
- `toolbarClassName` / `toolbarStyle`: A class name and inline styles for the toolbar
|
|
839
841
|
- `displayClassName` / `displayStyle`: A class name and inline styles for the text display area
|
|
@@ -911,13 +913,17 @@ result.bookmarks; // { name, audioOffsetMs }[]
|
|
|
911
913
|
|
|
912
914
|
長文を分割して合成する場合は `synthesizeSsmlChunks` または `AzureTtsClient.synthesizeChunks` を使います。音声バイナリを連結し、`boundaries`、`visemes`、`bookmarks` のオフセットを累積 `durationMs` 分だけ補正します。`synthesizeSsmlSafe(client, ssml, { validation })` は検証エラー時に Azure API を呼び出さず、`status: "validation-error"` / `"azure-api-error"` / `"success"` の結果を返します。
|
|
913
915
|
|
|
914
|
-
v2.
|
|
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` を渡します。
|
|
915
919
|
|
|
916
920
|
`validateAzureSsml` の `urlValidation` オプションは URL の重複排除、キャッシュ、`concurrency`、`signal`、`timeoutMs` を制御します。
|
|
917
921
|
|
|
918
922
|
For long documents, use `synthesizeSsmlChunks` or `AzureTtsClient.synthesizeChunks`; `onProgress` reports completed chunks while audio and synchronization offsets are merged. `synthesizeSsmlSafe(client, ssml, { validation })` validates before synthesis and returns a discriminated result without calling Azure when static validation fails.
|
|
919
923
|
|
|
920
|
-
In v2.
|
|
924
|
+
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`.
|
|
925
|
+
|
|
926
|
+
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`.
|
|
921
927
|
|
|
922
928
|
The `urlValidation` option of `validateAzureSsml` provides URL deduplication, in-memory caching, bounded `concurrency`, `signal`, and `timeoutMs` controls.
|
|
923
929
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildSsml,
|
|
3
3
|
parseSsml
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-HI74FTKY.mjs";
|
|
5
5
|
import {
|
|
6
6
|
__export
|
|
7
7
|
} from "./chunk-6S5ODO6A.mjs";
|
|
@@ -3112,4 +3112,4 @@ export {
|
|
|
3112
3112
|
findSsmlHoverTarget,
|
|
3113
3113
|
formatSsmlHover
|
|
3114
3114
|
};
|
|
3115
|
-
//# sourceMappingURL=chunk-
|
|
3115
|
+
//# sourceMappingURL=chunk-F2EMU3HM.mjs.map
|
|
@@ -925,6 +925,130 @@ function buildPartialSsml(textOrOptions, context) {
|
|
|
925
925
|
}
|
|
926
926
|
return serializePartialSsml(textOrOptions.text, textOrOptions);
|
|
927
927
|
}
|
|
928
|
+
function decodeXmlText(value) {
|
|
929
|
+
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
930
|
+
if (entity === "&") return "&";
|
|
931
|
+
if (entity === "'") return "'";
|
|
932
|
+
if (entity === ">") return ">";
|
|
933
|
+
if (entity === "<") return "<";
|
|
934
|
+
if (entity === """) return '"';
|
|
935
|
+
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
936
|
+
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
937
|
+
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
function decodeXmlAttribute(value) {
|
|
941
|
+
return decodeXmlText(value);
|
|
942
|
+
}
|
|
943
|
+
function findTagEnd(source, start) {
|
|
944
|
+
let quote = "";
|
|
945
|
+
for (let index = start; index < source.length; index += 1) {
|
|
946
|
+
const character = source[index];
|
|
947
|
+
if (quote) {
|
|
948
|
+
if (character === quote) quote = "";
|
|
949
|
+
} else if (character === '"' || character === "'") {
|
|
950
|
+
quote = character;
|
|
951
|
+
} else if (character === ">") {
|
|
952
|
+
return index;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
return source.length - 1;
|
|
956
|
+
}
|
|
957
|
+
function readTagName(tag) {
|
|
958
|
+
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
959
|
+
return match?.[1];
|
|
960
|
+
}
|
|
961
|
+
function readTagAttributes(tag, name) {
|
|
962
|
+
const attributes = {};
|
|
963
|
+
const nameStart = tag.indexOf(name);
|
|
964
|
+
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
965
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
966
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
967
|
+
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
968
|
+
}
|
|
969
|
+
return attributes;
|
|
970
|
+
}
|
|
971
|
+
function collectSourceMap(source) {
|
|
972
|
+
const segments = [];
|
|
973
|
+
const markers = [];
|
|
974
|
+
const elements = [];
|
|
975
|
+
let textOffset = 0;
|
|
976
|
+
let index = 0;
|
|
977
|
+
const textParts = [];
|
|
978
|
+
const addText = (value) => {
|
|
979
|
+
if (!value) return;
|
|
980
|
+
const parent = elements[elements.length - 1];
|
|
981
|
+
if (parent) parent.nextChildIndex += 1;
|
|
982
|
+
const sourceNodePath = parent?.path ?? ["speak"];
|
|
983
|
+
const start = textOffset;
|
|
984
|
+
textOffset += value.length;
|
|
985
|
+
textParts.push(value);
|
|
986
|
+
segments.push({ text: value, range: { start, end: textOffset }, sourceNodePath: [...sourceNodePath] });
|
|
987
|
+
};
|
|
988
|
+
while (index < source.length) {
|
|
989
|
+
if (source[index] !== "<") {
|
|
990
|
+
const end2 = source.indexOf("<", index);
|
|
991
|
+
const textEnd = end2 === -1 ? source.length : end2;
|
|
992
|
+
addText(decodeXmlText(source.slice(index, textEnd)));
|
|
993
|
+
index = textEnd;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (source.startsWith("<!--", index)) {
|
|
997
|
+
const end2 = source.indexOf("-->", index + 4);
|
|
998
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
if (source.startsWith("<![CDATA[", index)) {
|
|
1002
|
+
const contentStart = index + 9;
|
|
1003
|
+
const end2 = source.indexOf("]]>", contentStart);
|
|
1004
|
+
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1005
|
+
addText(source.slice(contentStart, contentEnd));
|
|
1006
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
if (source.startsWith("<?", index)) {
|
|
1010
|
+
const end2 = source.indexOf("?>", index + 2);
|
|
1011
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
const end = findTagEnd(source, index + 1);
|
|
1015
|
+
const rawTag = source.slice(index, end + 1);
|
|
1016
|
+
if (rawTag.startsWith("</")) {
|
|
1017
|
+
elements.pop();
|
|
1018
|
+
index = end + 1;
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
const name = readTagName(rawTag);
|
|
1022
|
+
if (!name) {
|
|
1023
|
+
index = end + 1;
|
|
1024
|
+
continue;
|
|
1025
|
+
}
|
|
1026
|
+
const parent = elements[elements.length - 1];
|
|
1027
|
+
const childIndex = parent?.nextChildIndex ?? 0;
|
|
1028
|
+
if (parent) parent.nextChildIndex += 1;
|
|
1029
|
+
const path = parent ? [...parent.path, `${name}[${childIndex}]`] : [name];
|
|
1030
|
+
const attributes = readTagAttributes(rawTag, name);
|
|
1031
|
+
const normalizedName = name.toLowerCase();
|
|
1032
|
+
if (normalizedName === "mark" || normalizedName === "bookmark") {
|
|
1033
|
+
const markerName = attributes[normalizedName === "mark" ? "name" : "mark"];
|
|
1034
|
+
if (markerName) {
|
|
1035
|
+
markers.push({
|
|
1036
|
+
kind: normalizedName,
|
|
1037
|
+
name: markerName,
|
|
1038
|
+
originalTextRange: { start: textOffset, end: textOffset },
|
|
1039
|
+
sourceNodePath: [...path]
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (!/\/\s*>$/.test(rawTag)) elements.push({ name, path, nextChildIndex: 0 });
|
|
1044
|
+
index = end + 1;
|
|
1045
|
+
}
|
|
1046
|
+
return { text: textParts.join(""), segments, markers };
|
|
1047
|
+
}
|
|
1048
|
+
function getSsmlSourceMap(ssml) {
|
|
1049
|
+
parseSsml(ssml);
|
|
1050
|
+
return collectSourceMap(ssml);
|
|
1051
|
+
}
|
|
928
1052
|
var PARSER_POSITION_SUFFIX = / at position (\d+)$/;
|
|
929
1053
|
function validateSsml(xmlString) {
|
|
930
1054
|
try {
|
|
@@ -1066,34 +1190,51 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
|
1066
1190
|
const inFlight = /* @__PURE__ */ new Map();
|
|
1067
1191
|
const waiters = [];
|
|
1068
1192
|
let active = 0;
|
|
1069
|
-
const
|
|
1193
|
+
const configuredSignal = options.signal ?? new AbortController().signal;
|
|
1194
|
+
const acquire = async (signal) => {
|
|
1195
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1070
1196
|
if (active < concurrency) {
|
|
1071
1197
|
active += 1;
|
|
1072
1198
|
return;
|
|
1073
1199
|
}
|
|
1074
|
-
await new Promise((resolve) =>
|
|
1200
|
+
await new Promise((resolve, reject) => {
|
|
1201
|
+
let waiter;
|
|
1202
|
+
const abortHandler = () => {
|
|
1203
|
+
const index = waiters.indexOf(waiter);
|
|
1204
|
+
if (index >= 0) waiters.splice(index, 1);
|
|
1205
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1206
|
+
reject(new Error("URL validation was aborted."));
|
|
1207
|
+
};
|
|
1208
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1209
|
+
waiter = () => {
|
|
1210
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1211
|
+
resolve();
|
|
1212
|
+
};
|
|
1213
|
+
waiters.push(waiter);
|
|
1214
|
+
});
|
|
1075
1215
|
active += 1;
|
|
1076
1216
|
};
|
|
1077
1217
|
const release = () => {
|
|
1078
1218
|
active -= 1;
|
|
1079
1219
|
waiters.shift()?.();
|
|
1080
1220
|
};
|
|
1081
|
-
const check = async (url, context) => {
|
|
1082
|
-
if (
|
|
1083
|
-
const
|
|
1221
|
+
const check = async (url, context, signal = configuredSignal) => {
|
|
1222
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1223
|
+
const key = `${context.tag}:${context.attribute}:${url}`;
|
|
1224
|
+
const cached = cache.get(key);
|
|
1084
1225
|
if (cached !== void 0) return cached;
|
|
1085
|
-
const existing = inFlight.get(
|
|
1226
|
+
const existing = inFlight.get(key);
|
|
1086
1227
|
if (existing) return existing;
|
|
1087
1228
|
const promise = (async () => {
|
|
1088
|
-
await acquire();
|
|
1229
|
+
await acquire(signal);
|
|
1089
1230
|
try {
|
|
1090
|
-
if (
|
|
1091
|
-
const validation = Promise.resolve(validator(url, context));
|
|
1231
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1232
|
+
const validation = Promise.resolve(validator(url, context, signal));
|
|
1092
1233
|
let timer;
|
|
1093
1234
|
let abortHandler;
|
|
1094
1235
|
const cancellation = new Promise((_resolve, reject) => {
|
|
1095
1236
|
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1096
|
-
|
|
1237
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1097
1238
|
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1098
1239
|
timer = setTimeout(
|
|
1099
1240
|
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
@@ -1103,24 +1244,24 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
|
1103
1244
|
});
|
|
1104
1245
|
try {
|
|
1105
1246
|
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1106
|
-
cache.set(
|
|
1247
|
+
cache.set(key, result);
|
|
1107
1248
|
return result;
|
|
1108
1249
|
} finally {
|
|
1109
1250
|
if (timer) clearTimeout(timer);
|
|
1110
|
-
if (abortHandler)
|
|
1251
|
+
if (abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
1111
1252
|
}
|
|
1112
1253
|
} finally {
|
|
1113
1254
|
release();
|
|
1114
1255
|
}
|
|
1115
1256
|
})();
|
|
1116
|
-
inFlight.set(
|
|
1257
|
+
inFlight.set(key, promise);
|
|
1117
1258
|
try {
|
|
1118
1259
|
return await promise;
|
|
1119
1260
|
} finally {
|
|
1120
|
-
inFlight.delete(
|
|
1261
|
+
inFlight.delete(key);
|
|
1121
1262
|
}
|
|
1122
1263
|
};
|
|
1123
|
-
return (url, context) => check(url, context);
|
|
1264
|
+
return (url, context, signal) => check(url, context, signal ?? configuredSignal);
|
|
1124
1265
|
}
|
|
1125
1266
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1126
1267
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
@@ -1233,6 +1374,7 @@ function tokenizeElements(source) {
|
|
|
1233
1374
|
if (parent) parent.childElementCount += 1;
|
|
1234
1375
|
const parentVoiceName = [...openElements].reverse().find((element) => element.voiceName)?.voiceName;
|
|
1235
1376
|
const tokenName = nameMatch[1];
|
|
1377
|
+
const path = parent ? [...parent.path, `${tokenName}[${childElementIndex ?? 0}]`] : [tokenName];
|
|
1236
1378
|
const tokenVoiceName = tokenName.toLowerCase() === "voice" ? attributes.get("name") : tokenName.toLowerCase() === "mstts:turn" ? attributes.get("voice") ?? parentVoiceName : parentVoiceName;
|
|
1237
1379
|
tokens.push({
|
|
1238
1380
|
attributes,
|
|
@@ -1243,12 +1385,14 @@ function tokenizeElements(source) {
|
|
|
1243
1385
|
parentName: parent?.name,
|
|
1244
1386
|
parentVoiceName,
|
|
1245
1387
|
selfClosing,
|
|
1246
|
-
start
|
|
1388
|
+
start,
|
|
1389
|
+
path
|
|
1247
1390
|
});
|
|
1248
1391
|
if (!selfClosing) {
|
|
1249
1392
|
openElements.push({
|
|
1250
1393
|
childElementCount: 0,
|
|
1251
1394
|
name: tokenName,
|
|
1395
|
+
path,
|
|
1252
1396
|
voiceName: tokenVoiceName
|
|
1253
1397
|
});
|
|
1254
1398
|
}
|
|
@@ -1261,13 +1405,14 @@ function location(source, offset) {
|
|
|
1261
1405
|
const line = before.split("\n").length;
|
|
1262
1406
|
return { line, column: before.length - (before.lastIndexOf("\n") + 1) + 1 };
|
|
1263
1407
|
}
|
|
1264
|
-
function addDiagnostic(diagnostics, source, offset, message, severity = "error", code) {
|
|
1408
|
+
function addDiagnostic(diagnostics, source, offset, message, severity = "error", code, metadata = {}) {
|
|
1265
1409
|
diagnostics.push({
|
|
1266
1410
|
...location(source, offset),
|
|
1267
1411
|
message,
|
|
1268
1412
|
severity,
|
|
1269
1413
|
source: "ssml-static-validator",
|
|
1270
|
-
...code ? { code } : {}
|
|
1414
|
+
...code ? { code } : {},
|
|
1415
|
+
...metadata
|
|
1271
1416
|
});
|
|
1272
1417
|
}
|
|
1273
1418
|
function isSupportedProsodyRate(value) {
|
|
@@ -1730,9 +1875,11 @@ function validateAzureSsmlStatic(ssml, options = {}) {
|
|
|
1730
1875
|
for (const token of tokens) {
|
|
1731
1876
|
const tokenName = token.name.toLowerCase();
|
|
1732
1877
|
const tokenVoiceName = tokenName === "voice" ? attr(token, "name")?.trim() : tokenName === "mstts:turn" ? attr(token, "voice")?.trim() || token.parentVoiceName : options.validateNestedVoices === false ? voiceName : token.parentVoiceName;
|
|
1878
|
+
const tokenDiagnosticStart = diagnostics.length;
|
|
1733
1879
|
validateElement(token, ssml, diagnostics, tokenVoiceName, options, voiceCatalog);
|
|
1734
1880
|
const definition = tokenVoiceName ? voiceCatalog.get(tokenVoiceName.toLowerCase()) : void 0;
|
|
1735
1881
|
validateVoiceFeatureMatrix(token, ssml, diagnostics, tokenVoiceName, definition);
|
|
1882
|
+
annotateTokenDiagnostics(diagnostics, tokenDiagnosticStart, token, options, tokenVoiceName);
|
|
1736
1883
|
if (tokenName === "voice" && options.model && definition?.models && !definition.models.some((model) => model.toLowerCase() === options.model?.toLowerCase())) {
|
|
1737
1884
|
addDiagnostic(
|
|
1738
1885
|
diagnostics,
|
|
@@ -1754,18 +1901,37 @@ function urlAttributes(token) {
|
|
|
1754
1901
|
return value === void 0 ? [] : [{ attribute, value }];
|
|
1755
1902
|
});
|
|
1756
1903
|
}
|
|
1904
|
+
function annotateTokenDiagnostics(diagnostics, startIndex, token, options, voiceName) {
|
|
1905
|
+
const attributes = [...token.attributes.keys()];
|
|
1906
|
+
for (const diagnostic of diagnostics.slice(startIndex)) {
|
|
1907
|
+
const attributeName = attributes.find(
|
|
1908
|
+
(attribute) => new RegExp(`(?:<[^> ]+\\s+|")${attribute}(?:"|>|\\s)`, "i").test(diagnostic.message)
|
|
1909
|
+
);
|
|
1910
|
+
const nodePath = options.sourceNodePath ? [...options.sourceNodePath] : [...token.path];
|
|
1911
|
+
Object.assign(diagnostic, {
|
|
1912
|
+
range: { start: token.start, end: token.end + 1 },
|
|
1913
|
+
tagName: token.name,
|
|
1914
|
+
...attributeName ? { attributeName } : {},
|
|
1915
|
+
...voiceName ? { voiceName } : {},
|
|
1916
|
+
...options.chunkIndex !== void 0 ? { chunkIndex: options.chunkIndex } : {},
|
|
1917
|
+
nodePath,
|
|
1918
|
+
targetNodePath: [...token.path]
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1757
1922
|
function validateAzureSsml(ssml, options = {}) {
|
|
1758
1923
|
const diagnostics = validateAzureSsmlStatic(ssml, options);
|
|
1759
1924
|
const validator = options.urlValidator ?? options.customUrlValidator;
|
|
1760
1925
|
if (!validator || typeof ssml !== "string") return diagnostics;
|
|
1761
1926
|
const runnerOptions = options.urlValidation ?? {};
|
|
1762
|
-
const boundedValidator = createAzureUrlValidatorRunner(validator, {
|
|
1927
|
+
const boundedValidator = options.urlValidatorRunner ?? createAzureUrlValidatorRunner(validator, {
|
|
1763
1928
|
...runnerOptions,
|
|
1764
1929
|
...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
|
|
1765
1930
|
...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
|
|
1766
1931
|
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
1767
1932
|
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
1768
1933
|
});
|
|
1934
|
+
const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
|
|
1769
1935
|
let tokens;
|
|
1770
1936
|
try {
|
|
1771
1937
|
tokens = tokenizeElements(ssml);
|
|
@@ -1775,25 +1941,33 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
1775
1941
|
const checks = tokens.flatMap(
|
|
1776
1942
|
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
1777
1943
|
try {
|
|
1778
|
-
const result = await boundedValidator(
|
|
1944
|
+
const result = await boundedValidator(
|
|
1945
|
+
value,
|
|
1946
|
+
{ tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
|
|
1947
|
+
validationSignal
|
|
1948
|
+
);
|
|
1779
1949
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
1780
1950
|
if (!valid) {
|
|
1781
1951
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
1952
|
+
const diagnosticStart = diagnostics.length;
|
|
1782
1953
|
addDiagnostic(
|
|
1783
1954
|
diagnostics,
|
|
1784
1955
|
ssml,
|
|
1785
1956
|
token.start,
|
|
1786
1957
|
`<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
|
|
1787
1958
|
);
|
|
1959
|
+
annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
|
|
1788
1960
|
}
|
|
1789
1961
|
} catch (error) {
|
|
1790
1962
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1963
|
+
const diagnosticStart = diagnostics.length;
|
|
1791
1964
|
addDiagnostic(
|
|
1792
1965
|
diagnostics,
|
|
1793
1966
|
ssml,
|
|
1794
1967
|
token.start,
|
|
1795
1968
|
`<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
|
|
1796
1969
|
);
|
|
1970
|
+
annotateTokenDiagnostics(diagnostics, diagnosticStart, token, options, token.parentVoiceName);
|
|
1797
1971
|
}
|
|
1798
1972
|
})
|
|
1799
1973
|
);
|
|
@@ -1810,7 +1984,9 @@ export {
|
|
|
1810
1984
|
buildSsml,
|
|
1811
1985
|
parseSsml,
|
|
1812
1986
|
buildPartialSsml,
|
|
1987
|
+
getSsmlSourceMap,
|
|
1813
1988
|
validateSsml,
|
|
1989
|
+
createAzureUrlValidatorRunner,
|
|
1814
1990
|
validateAzureSsml
|
|
1815
1991
|
};
|
|
1816
|
-
//# sourceMappingURL=chunk-
|
|
1992
|
+
//# sourceMappingURL=chunk-HI74FTKY.mjs.map
|