ssml-builder-js 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/README.md +6 -4
- package/dist/{chunk-UZSCXPC5.mjs → chunk-BDY2Q2JL.mjs} +2 -2
- package/dist/{chunk-AQ55MOPU.mjs → chunk-FXUM45ZY.mjs} +305 -173
- package/dist/chunk-FXUM45ZY.mjs.map +1 -0
- package/dist/{chunk-QFIBPCO4.mjs → chunk-WXFLUCLR.mjs} +164 -17
- package/dist/chunk-WXFLUCLR.mjs.map +1 -0
- package/dist/core.d.mts +48 -17
- package/dist/core.d.ts +48 -17
- package/dist/core.js +303 -170
- package/dist/core.js.map +1 -1
- package/dist/core.mjs +3 -1
- package/dist/elements.js +38 -16
- package/dist/elements.js.map +1 -1
- package/dist/elements.mjs +2 -2
- package/dist/{index.d-CUXRwSw0.d.mts → index.d-8BvkB9gz.d.mts} +22 -2
- package/dist/{index.d-CUXRwSw0.d.ts → index.d-8BvkB9gz.d.ts} +22 -2
- package/dist/index.d.mts +114 -18
- package/dist/index.d.ts +114 -18
- package/dist/index.js +1783 -1281
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +307 -86
- 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 +115 -28
- 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-BDY2Q2JL.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,13 @@ 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` が渡されます。
|
|
915
917
|
|
|
916
918
|
`validateAzureSsml` の `urlValidation` オプションは URL の重複排除、キャッシュ、`concurrency`、`signal`、`timeoutMs` を制御します。
|
|
917
919
|
|
|
918
920
|
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
921
|
|
|
920
|
-
In v2.
|
|
922
|
+
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`.
|
|
921
923
|
|
|
922
924
|
The `urlValidation` option of `validateAzureSsml` provides URL deduplication, in-memory caching, bounded `concurrency`, `signal`, and `timeoutMs` controls.
|
|
923
925
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildSsml,
|
|
3
3
|
parseSsml
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WXFLUCLR.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-BDY2Q2JL.mjs.map
|
|
@@ -931,6 +931,236 @@ function buildPartialSsml(textOrOptions, context) {
|
|
|
931
931
|
return serializePartialSsml(textOrOptions.text, textOrOptions);
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
+
// packages/ssml-core/src/textNodes.ts
|
|
935
|
+
function decodeXmlText(value) {
|
|
936
|
+
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
937
|
+
if (entity === "&") return "&";
|
|
938
|
+
if (entity === "'") return "'";
|
|
939
|
+
if (entity === ">") return ">";
|
|
940
|
+
if (entity === "<") return "<";
|
|
941
|
+
if (entity === """) return '"';
|
|
942
|
+
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
943
|
+
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
944
|
+
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
function encodeXmlText(value) {
|
|
948
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
949
|
+
}
|
|
950
|
+
function decodeXmlAttribute(value) {
|
|
951
|
+
return decodeXmlText(value);
|
|
952
|
+
}
|
|
953
|
+
function findTagEnd(source, start) {
|
|
954
|
+
let quote = "";
|
|
955
|
+
for (let index = start; index < source.length; index += 1) {
|
|
956
|
+
const character = source[index];
|
|
957
|
+
if (quote) {
|
|
958
|
+
if (character === quote) quote = "";
|
|
959
|
+
} else if (character === '"' || character === "'") {
|
|
960
|
+
quote = character;
|
|
961
|
+
} else if (character === ">") {
|
|
962
|
+
return index;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return source.length - 1;
|
|
966
|
+
}
|
|
967
|
+
function readTagName(tag) {
|
|
968
|
+
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
969
|
+
return match?.[1];
|
|
970
|
+
}
|
|
971
|
+
function readTagAttributes(tag, name) {
|
|
972
|
+
const attributes = {};
|
|
973
|
+
const nameStart = tag.indexOf(name);
|
|
974
|
+
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
975
|
+
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
976
|
+
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
977
|
+
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
978
|
+
}
|
|
979
|
+
return attributes;
|
|
980
|
+
}
|
|
981
|
+
function collectTextNodes(source) {
|
|
982
|
+
const nodes = [];
|
|
983
|
+
const elements = [];
|
|
984
|
+
let index = 0;
|
|
985
|
+
const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
|
|
986
|
+
if (!rawText) return;
|
|
987
|
+
const path = elements.map((element) => element.name);
|
|
988
|
+
const parent = elements[elements.length - 1];
|
|
989
|
+
nodes.push({
|
|
990
|
+
context: {
|
|
991
|
+
ancestorTags: path.slice(0, -1),
|
|
992
|
+
parentAttributes: { ...parent?.attributes ?? {} },
|
|
993
|
+
parentTag: parent?.name ?? "",
|
|
994
|
+
path
|
|
995
|
+
},
|
|
996
|
+
decodedText: decodeXmlText(rawText),
|
|
997
|
+
end,
|
|
998
|
+
sourceEnd,
|
|
999
|
+
sourceStart,
|
|
1000
|
+
start
|
|
1001
|
+
});
|
|
1002
|
+
};
|
|
1003
|
+
while (index < source.length) {
|
|
1004
|
+
if (source[index] !== "<") {
|
|
1005
|
+
const nextTag = source.indexOf("<", index);
|
|
1006
|
+
const end2 = nextTag === -1 ? source.length : nextTag;
|
|
1007
|
+
addText(index, end2, source.slice(index, end2));
|
|
1008
|
+
index = end2;
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
if (source.startsWith("<!--", index)) {
|
|
1012
|
+
const end2 = source.indexOf("-->", index + 4);
|
|
1013
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1014
|
+
continue;
|
|
1015
|
+
}
|
|
1016
|
+
if (source.startsWith("<![CDATA[", index)) {
|
|
1017
|
+
const contentStart = index + 9;
|
|
1018
|
+
const end2 = source.indexOf("]]>", contentStart);
|
|
1019
|
+
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1020
|
+
addText(
|
|
1021
|
+
contentStart,
|
|
1022
|
+
contentEnd,
|
|
1023
|
+
source.slice(contentStart, contentEnd),
|
|
1024
|
+
index,
|
|
1025
|
+
end2 === -1 ? source.length : end2 + 3
|
|
1026
|
+
);
|
|
1027
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
if (source.startsWith("<?", index)) {
|
|
1031
|
+
const end2 = source.indexOf("?>", index + 2);
|
|
1032
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
if (source.startsWith("</", index)) {
|
|
1036
|
+
const end2 = findTagEnd(source, index + 2);
|
|
1037
|
+
elements.pop();
|
|
1038
|
+
index = end2 + 1;
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
const end = findTagEnd(source, index + 1);
|
|
1042
|
+
const tag = source.slice(index, end + 1);
|
|
1043
|
+
const name = readTagName(tag);
|
|
1044
|
+
if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
|
|
1045
|
+
index = end + 1;
|
|
1046
|
+
}
|
|
1047
|
+
return nodes;
|
|
1048
|
+
}
|
|
1049
|
+
function collectSourceMap(source) {
|
|
1050
|
+
const segments = [];
|
|
1051
|
+
const markers = [];
|
|
1052
|
+
const elements = [];
|
|
1053
|
+
let textOffset = 0;
|
|
1054
|
+
let index = 0;
|
|
1055
|
+
const textParts = [];
|
|
1056
|
+
const addText = (value) => {
|
|
1057
|
+
if (!value) return;
|
|
1058
|
+
const parent = elements[elements.length - 1];
|
|
1059
|
+
if (parent) parent.nextChildIndex += 1;
|
|
1060
|
+
const sourceNodePath = parent?.path ?? ["speak"];
|
|
1061
|
+
const start = textOffset;
|
|
1062
|
+
textOffset += value.length;
|
|
1063
|
+
textParts.push(value);
|
|
1064
|
+
segments.push({ text: value, range: { start, end: textOffset }, sourceNodePath: [...sourceNodePath] });
|
|
1065
|
+
};
|
|
1066
|
+
while (index < source.length) {
|
|
1067
|
+
if (source[index] !== "<") {
|
|
1068
|
+
const end2 = source.indexOf("<", index);
|
|
1069
|
+
const textEnd = end2 === -1 ? source.length : end2;
|
|
1070
|
+
addText(decodeXmlText(source.slice(index, textEnd)));
|
|
1071
|
+
index = textEnd;
|
|
1072
|
+
continue;
|
|
1073
|
+
}
|
|
1074
|
+
if (source.startsWith("<!--", index)) {
|
|
1075
|
+
const end2 = source.indexOf("-->", index + 4);
|
|
1076
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
if (source.startsWith("<![CDATA[", index)) {
|
|
1080
|
+
const contentStart = index + 9;
|
|
1081
|
+
const end2 = source.indexOf("]]>", contentStart);
|
|
1082
|
+
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1083
|
+
addText(source.slice(contentStart, contentEnd));
|
|
1084
|
+
index = end2 === -1 ? source.length : end2 + 3;
|
|
1085
|
+
continue;
|
|
1086
|
+
}
|
|
1087
|
+
if (source.startsWith("<?", index)) {
|
|
1088
|
+
const end2 = source.indexOf("?>", index + 2);
|
|
1089
|
+
index = end2 === -1 ? source.length : end2 + 2;
|
|
1090
|
+
continue;
|
|
1091
|
+
}
|
|
1092
|
+
const end = findTagEnd(source, index + 1);
|
|
1093
|
+
const rawTag = source.slice(index, end + 1);
|
|
1094
|
+
if (rawTag.startsWith("</")) {
|
|
1095
|
+
elements.pop();
|
|
1096
|
+
index = end + 1;
|
|
1097
|
+
continue;
|
|
1098
|
+
}
|
|
1099
|
+
const name = readTagName(rawTag);
|
|
1100
|
+
if (!name) {
|
|
1101
|
+
index = end + 1;
|
|
1102
|
+
continue;
|
|
1103
|
+
}
|
|
1104
|
+
const parent = elements[elements.length - 1];
|
|
1105
|
+
const childIndex = parent?.nextChildIndex ?? 0;
|
|
1106
|
+
if (parent) parent.nextChildIndex += 1;
|
|
1107
|
+
const path = parent ? [...parent.path, `${name}[${childIndex}]`] : [name];
|
|
1108
|
+
const attributes = readTagAttributes(rawTag, name);
|
|
1109
|
+
const normalizedName = name.toLowerCase();
|
|
1110
|
+
if (normalizedName === "mark" || normalizedName === "bookmark") {
|
|
1111
|
+
const markerName = attributes[normalizedName === "mark" ? "name" : "mark"];
|
|
1112
|
+
if (markerName) {
|
|
1113
|
+
markers.push({
|
|
1114
|
+
kind: normalizedName,
|
|
1115
|
+
name: markerName,
|
|
1116
|
+
originalTextRange: { start: textOffset, end: textOffset },
|
|
1117
|
+
sourceNodePath: [...path]
|
|
1118
|
+
});
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
if (!/\/\s*>$/.test(rawTag)) elements.push({ name, path, nextChildIndex: 0 });
|
|
1122
|
+
index = end + 1;
|
|
1123
|
+
}
|
|
1124
|
+
return { text: textParts.join(""), segments, markers };
|
|
1125
|
+
}
|
|
1126
|
+
function getSsmlSourceMap(ssml) {
|
|
1127
|
+
parseSsml(ssml);
|
|
1128
|
+
return collectSourceMap(ssml);
|
|
1129
|
+
}
|
|
1130
|
+
function extractSsmlText(ssml) {
|
|
1131
|
+
parseSsml(ssml);
|
|
1132
|
+
return collectTextNodes(ssml).map((node) => node.decodedText);
|
|
1133
|
+
}
|
|
1134
|
+
async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
1135
|
+
parseSsml(ssml);
|
|
1136
|
+
const nodes = collectTextNodes(ssml);
|
|
1137
|
+
const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
|
|
1138
|
+
const replacements = await Promise.all(
|
|
1139
|
+
nodes.map(async (node) => {
|
|
1140
|
+
const context = {
|
|
1141
|
+
ancestorTags: [...node.context.ancestorTags],
|
|
1142
|
+
parentAttributes: { ...node.context.parentAttributes },
|
|
1143
|
+
parentTag: node.context.parentTag,
|
|
1144
|
+
path: [...node.context.path]
|
|
1145
|
+
};
|
|
1146
|
+
const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
|
|
1147
|
+
if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
|
|
1148
|
+
const transformed = await transform(node.decodedText, context);
|
|
1149
|
+
if (typeof transformed !== "string") {
|
|
1150
|
+
throw new TypeError("SSML text node transform must return a string");
|
|
1151
|
+
}
|
|
1152
|
+
return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
|
|
1153
|
+
})
|
|
1154
|
+
);
|
|
1155
|
+
let result = "";
|
|
1156
|
+
let cursor = 0;
|
|
1157
|
+
nodes.forEach((node, nodeIndex) => {
|
|
1158
|
+
result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
|
|
1159
|
+
cursor = node.sourceEnd;
|
|
1160
|
+
});
|
|
1161
|
+
return result + ssml.slice(cursor);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
934
1164
|
// packages/ssml-core/src/split.ts
|
|
935
1165
|
var DEFAULT_MAX_LENGTH = 1e4;
|
|
936
1166
|
function cloneElement(element, children) {
|
|
@@ -1062,7 +1292,7 @@ function findSourceNodePath(nodes, targetOffset) {
|
|
|
1062
1292
|
});
|
|
1063
1293
|
return foundPath ?? firstPath;
|
|
1064
1294
|
}
|
|
1065
|
-
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio) {
|
|
1295
|
+
function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, replicateBackgroundAudio, sourceMap, includeEndMarkers) {
|
|
1066
1296
|
const chunkNodes = backgroundAudio && (replicateBackgroundAudio || chunkIndex === 0) ? [backgroundAudio, ...nodes] : nodes;
|
|
1067
1297
|
const text = nodes.map(textFromNode).join("");
|
|
1068
1298
|
const marks = [];
|
|
@@ -1078,7 +1308,23 @@ function createChunk(document, nodes, chunkIndex, textStart, backgroundAudio, re
|
|
|
1078
1308
|
hasBackgroundAudio: chunkNodes.some(
|
|
1079
1309
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1080
1310
|
),
|
|
1081
|
-
sourceNodePath: findSourceNodePath(document.children ?? [], textStart)
|
|
1311
|
+
sourceNodePath: findSourceNodePath(document.children ?? [], textStart),
|
|
1312
|
+
sourceTextSegments: sourceMap.segments.filter(({ range }) => range.end > textStart && range.start < textStart + text.length).map((segment) => {
|
|
1313
|
+
const start = Math.max(segment.range.start, textStart);
|
|
1314
|
+
const end = Math.min(segment.range.end, textStart + text.length);
|
|
1315
|
+
return {
|
|
1316
|
+
text: segment.text.slice(start - segment.range.start, end - segment.range.start),
|
|
1317
|
+
range: { start, end },
|
|
1318
|
+
sourceNodePath: [...segment.sourceNodePath]
|
|
1319
|
+
};
|
|
1320
|
+
}),
|
|
1321
|
+
sourceMarkers: sourceMap.markers.filter(
|
|
1322
|
+
({ originalTextRange }) => originalTextRange.start >= textStart && (originalTextRange.start < textStart + text.length || includeEndMarkers && originalTextRange.start === textStart + text.length)
|
|
1323
|
+
).map((marker) => ({
|
|
1324
|
+
...marker,
|
|
1325
|
+
originalTextRange: { ...marker.originalTextRange },
|
|
1326
|
+
sourceNodePath: [...marker.sourceNodePath]
|
|
1327
|
+
}))
|
|
1082
1328
|
};
|
|
1083
1329
|
}
|
|
1084
1330
|
function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
@@ -1088,11 +1334,12 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1088
1334
|
throw new RangeError("maxLength must be a positive integer");
|
|
1089
1335
|
}
|
|
1090
1336
|
const document = parseSsml(ssml);
|
|
1337
|
+
const sourceMap = getSsmlSourceMap(ssml);
|
|
1091
1338
|
const backgroundAudio = (document.children ?? []).find(
|
|
1092
1339
|
(node) => typeof node !== "string" && node.type !== "text" && node.type === "mstts:backgroundaudio"
|
|
1093
1340
|
);
|
|
1094
1341
|
if (ssml.length <= resolvedMaxLength) {
|
|
1095
|
-
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true)];
|
|
1342
|
+
return [createChunk(document, document.children ?? [], 0, 0, backgroundAudio, true, sourceMap, true)];
|
|
1096
1343
|
}
|
|
1097
1344
|
const contentChildren = (document.children ?? []).filter((node) => node !== backgroundAudio);
|
|
1098
1345
|
const plainDocumentLength = documentWithChildren(document, []).length;
|
|
@@ -1116,7 +1363,16 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1116
1363
|
}
|
|
1117
1364
|
if (group.length > 0) chunks.push(group);
|
|
1118
1365
|
if (chunks.length === 0) {
|
|
1119
|
-
const result = createChunk(
|
|
1366
|
+
const result = createChunk(
|
|
1367
|
+
document,
|
|
1368
|
+
[],
|
|
1369
|
+
0,
|
|
1370
|
+
0,
|
|
1371
|
+
backgroundAudio,
|
|
1372
|
+
resolvedOptions.replicateBackgroundAudio ?? false,
|
|
1373
|
+
sourceMap,
|
|
1374
|
+
true
|
|
1375
|
+
);
|
|
1120
1376
|
if (result.ssml.length > resolvedMaxLength) {
|
|
1121
1377
|
throw new RangeError("maxLength is too small to contain the SSML document wrapper");
|
|
1122
1378
|
}
|
|
@@ -1130,7 +1386,9 @@ function splitSsmlDocument(ssml, maxLength = DEFAULT_MAX_LENGTH, options = {}) {
|
|
|
1130
1386
|
chunkIndex,
|
|
1131
1387
|
textStart,
|
|
1132
1388
|
backgroundAudio,
|
|
1133
|
-
resolvedOptions.replicateBackgroundAudio ?? false
|
|
1389
|
+
resolvedOptions.replicateBackgroundAudio ?? false,
|
|
1390
|
+
sourceMap,
|
|
1391
|
+
chunkIndex === chunks.length - 1
|
|
1134
1392
|
);
|
|
1135
1393
|
textStart = result.originalTextRange.end;
|
|
1136
1394
|
return result;
|
|
@@ -1153,155 +1411,6 @@ function validateSsml(xmlString) {
|
|
|
1153
1411
|
}
|
|
1154
1412
|
}
|
|
1155
1413
|
|
|
1156
|
-
// packages/ssml-core/src/textNodes.ts
|
|
1157
|
-
function decodeXmlText(value) {
|
|
1158
|
-
return value.replace(/&(?:amp|apos|gt|lt|quot);|&#(?:x[\da-f]+|\d+);/gi, (entity) => {
|
|
1159
|
-
if (entity === "&") return "&";
|
|
1160
|
-
if (entity === "'") return "'";
|
|
1161
|
-
if (entity === ">") return ">";
|
|
1162
|
-
if (entity === "<") return "<";
|
|
1163
|
-
if (entity === """) return '"';
|
|
1164
|
-
const hexadecimal = entity.toLowerCase().startsWith("&#x");
|
|
1165
|
-
const digits = entity.slice(hexadecimal ? 3 : 2, -1);
|
|
1166
|
-
return String.fromCodePoint(Number.parseInt(digits, hexadecimal ? 16 : 10));
|
|
1167
|
-
});
|
|
1168
|
-
}
|
|
1169
|
-
function encodeXmlText(value) {
|
|
1170
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1171
|
-
}
|
|
1172
|
-
function decodeXmlAttribute(value) {
|
|
1173
|
-
return decodeXmlText(value);
|
|
1174
|
-
}
|
|
1175
|
-
function findTagEnd(source, start) {
|
|
1176
|
-
let quote = "";
|
|
1177
|
-
for (let index = start; index < source.length; index += 1) {
|
|
1178
|
-
const character = source[index];
|
|
1179
|
-
if (quote) {
|
|
1180
|
-
if (character === quote) quote = "";
|
|
1181
|
-
} else if (character === '"' || character === "'") {
|
|
1182
|
-
quote = character;
|
|
1183
|
-
} else if (character === ">") {
|
|
1184
|
-
return index;
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
return source.length - 1;
|
|
1188
|
-
}
|
|
1189
|
-
function readTagName(tag) {
|
|
1190
|
-
const match = /^<\s*([A-Za-z_][A-Za-z0-9_.:-]*)/.exec(tag);
|
|
1191
|
-
return match?.[1];
|
|
1192
|
-
}
|
|
1193
|
-
function readTagAttributes(tag, name) {
|
|
1194
|
-
const attributes = {};
|
|
1195
|
-
const nameStart = tag.indexOf(name);
|
|
1196
|
-
const attributeSource = tag.slice(nameStart + name.length, tag.length - 1).replace(/\/\s*$/, "");
|
|
1197
|
-
const attributePattern = /([A-Za-z_][A-Za-z0-9_.:-]*)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
1198
|
-
for (const match of attributeSource.matchAll(attributePattern)) {
|
|
1199
|
-
attributes[match[1].toLowerCase()] = decodeXmlAttribute(match[3]);
|
|
1200
|
-
}
|
|
1201
|
-
return attributes;
|
|
1202
|
-
}
|
|
1203
|
-
function collectTextNodes(source) {
|
|
1204
|
-
const nodes = [];
|
|
1205
|
-
const elements = [];
|
|
1206
|
-
let index = 0;
|
|
1207
|
-
const addText = (start, end, rawText, sourceStart = start, sourceEnd = end) => {
|
|
1208
|
-
if (!rawText) return;
|
|
1209
|
-
const path = elements.map((element) => element.name);
|
|
1210
|
-
const parent = elements[elements.length - 1];
|
|
1211
|
-
nodes.push({
|
|
1212
|
-
context: {
|
|
1213
|
-
ancestorTags: path.slice(0, -1),
|
|
1214
|
-
parentAttributes: { ...parent?.attributes ?? {} },
|
|
1215
|
-
parentTag: parent?.name ?? "",
|
|
1216
|
-
path
|
|
1217
|
-
},
|
|
1218
|
-
decodedText: decodeXmlText(rawText),
|
|
1219
|
-
end,
|
|
1220
|
-
sourceEnd,
|
|
1221
|
-
sourceStart,
|
|
1222
|
-
start
|
|
1223
|
-
});
|
|
1224
|
-
};
|
|
1225
|
-
while (index < source.length) {
|
|
1226
|
-
if (source[index] !== "<") {
|
|
1227
|
-
const nextTag = source.indexOf("<", index);
|
|
1228
|
-
const end2 = nextTag === -1 ? source.length : nextTag;
|
|
1229
|
-
addText(index, end2, source.slice(index, end2));
|
|
1230
|
-
index = end2;
|
|
1231
|
-
continue;
|
|
1232
|
-
}
|
|
1233
|
-
if (source.startsWith("<!--", index)) {
|
|
1234
|
-
const end2 = source.indexOf("-->", index + 4);
|
|
1235
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1236
|
-
continue;
|
|
1237
|
-
}
|
|
1238
|
-
if (source.startsWith("<![CDATA[", index)) {
|
|
1239
|
-
const contentStart = index + 9;
|
|
1240
|
-
const end2 = source.indexOf("]]>", contentStart);
|
|
1241
|
-
const contentEnd = end2 === -1 ? source.length : end2;
|
|
1242
|
-
addText(
|
|
1243
|
-
contentStart,
|
|
1244
|
-
contentEnd,
|
|
1245
|
-
source.slice(contentStart, contentEnd),
|
|
1246
|
-
index,
|
|
1247
|
-
end2 === -1 ? source.length : end2 + 3
|
|
1248
|
-
);
|
|
1249
|
-
index = end2 === -1 ? source.length : end2 + 3;
|
|
1250
|
-
continue;
|
|
1251
|
-
}
|
|
1252
|
-
if (source.startsWith("<?", index)) {
|
|
1253
|
-
const end2 = source.indexOf("?>", index + 2);
|
|
1254
|
-
index = end2 === -1 ? source.length : end2 + 2;
|
|
1255
|
-
continue;
|
|
1256
|
-
}
|
|
1257
|
-
if (source.startsWith("</", index)) {
|
|
1258
|
-
const end2 = findTagEnd(source, index + 2);
|
|
1259
|
-
elements.pop();
|
|
1260
|
-
index = end2 + 1;
|
|
1261
|
-
continue;
|
|
1262
|
-
}
|
|
1263
|
-
const end = findTagEnd(source, index + 1);
|
|
1264
|
-
const tag = source.slice(index, end + 1);
|
|
1265
|
-
const name = readTagName(tag);
|
|
1266
|
-
if (name && !/\/\s*>$/.test(tag)) elements.push({ attributes: readTagAttributes(tag, name), name });
|
|
1267
|
-
index = end + 1;
|
|
1268
|
-
}
|
|
1269
|
-
return nodes;
|
|
1270
|
-
}
|
|
1271
|
-
function extractSsmlText(ssml) {
|
|
1272
|
-
parseSsml(ssml);
|
|
1273
|
-
return collectTextNodes(ssml).map((node) => node.decodedText);
|
|
1274
|
-
}
|
|
1275
|
-
async function mapSsmlTextNodes(ssml, transform, options = {}) {
|
|
1276
|
-
parseSsml(ssml);
|
|
1277
|
-
const nodes = collectTextNodes(ssml);
|
|
1278
|
-
const skipTags = new Set((options.skipTags ?? ["phoneme", "say-as", "sub"]).map((tag) => tag.toLowerCase()));
|
|
1279
|
-
const replacements = await Promise.all(
|
|
1280
|
-
nodes.map(async (node) => {
|
|
1281
|
-
const context = {
|
|
1282
|
-
ancestorTags: [...node.context.ancestorTags],
|
|
1283
|
-
parentAttributes: { ...node.context.parentAttributes },
|
|
1284
|
-
parentTag: node.context.parentTag,
|
|
1285
|
-
path: [...node.context.path]
|
|
1286
|
-
};
|
|
1287
|
-
const shouldTransform = !skipTags.has(context.parentTag.toLowerCase()) && (options.filter?.(context) ?? true);
|
|
1288
|
-
if (!shouldTransform) return ssml.slice(node.sourceStart, node.sourceEnd);
|
|
1289
|
-
const transformed = await transform(node.decodedText, context);
|
|
1290
|
-
if (typeof transformed !== "string") {
|
|
1291
|
-
throw new TypeError("SSML text node transform must return a string");
|
|
1292
|
-
}
|
|
1293
|
-
return transformed === node.decodedText ? ssml.slice(node.sourceStart, node.sourceEnd) : encodeXmlText(transformed);
|
|
1294
|
-
})
|
|
1295
|
-
);
|
|
1296
|
-
let result = "";
|
|
1297
|
-
let cursor = 0;
|
|
1298
|
-
nodes.forEach((node, nodeIndex) => {
|
|
1299
|
-
result += ssml.slice(cursor, node.sourceStart) + replacements[nodeIndex];
|
|
1300
|
-
cursor = node.sourceEnd;
|
|
1301
|
-
});
|
|
1302
|
-
return result + ssml.slice(cursor);
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
1414
|
// packages/ssml-core/src/migration.ts
|
|
1306
1415
|
var DEFAULT_TRANSLATION_SKIP_TAGS = ["phoneme", "say-as", "sayAs", "sub"];
|
|
1307
1416
|
function elementName2(element) {
|
|
@@ -1722,34 +1831,51 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
|
1722
1831
|
const inFlight = /* @__PURE__ */ new Map();
|
|
1723
1832
|
const waiters = [];
|
|
1724
1833
|
let active = 0;
|
|
1725
|
-
const
|
|
1834
|
+
const configuredSignal = options.signal ?? new AbortController().signal;
|
|
1835
|
+
const acquire = async (signal) => {
|
|
1836
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1726
1837
|
if (active < concurrency) {
|
|
1727
1838
|
active += 1;
|
|
1728
1839
|
return;
|
|
1729
1840
|
}
|
|
1730
|
-
await new Promise((resolve) =>
|
|
1841
|
+
await new Promise((resolve, reject) => {
|
|
1842
|
+
let waiter;
|
|
1843
|
+
const abortHandler = () => {
|
|
1844
|
+
const index = waiters.indexOf(waiter);
|
|
1845
|
+
if (index >= 0) waiters.splice(index, 1);
|
|
1846
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1847
|
+
reject(new Error("URL validation was aborted."));
|
|
1848
|
+
};
|
|
1849
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1850
|
+
waiter = () => {
|
|
1851
|
+
signal.removeEventListener("abort", abortHandler);
|
|
1852
|
+
resolve();
|
|
1853
|
+
};
|
|
1854
|
+
waiters.push(waiter);
|
|
1855
|
+
});
|
|
1731
1856
|
active += 1;
|
|
1732
1857
|
};
|
|
1733
1858
|
const release = () => {
|
|
1734
1859
|
active -= 1;
|
|
1735
1860
|
waiters.shift()?.();
|
|
1736
1861
|
};
|
|
1737
|
-
const check = async (url, context) => {
|
|
1738
|
-
if (
|
|
1739
|
-
const
|
|
1862
|
+
const check = async (url, context, signal = configuredSignal) => {
|
|
1863
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1864
|
+
const key = `${context.tag}:${context.attribute}:${url}`;
|
|
1865
|
+
const cached = cache.get(key);
|
|
1740
1866
|
if (cached !== void 0) return cached;
|
|
1741
|
-
const existing = inFlight.get(
|
|
1867
|
+
const existing = inFlight.get(key);
|
|
1742
1868
|
if (existing) return existing;
|
|
1743
1869
|
const promise = (async () => {
|
|
1744
|
-
await acquire();
|
|
1870
|
+
await acquire(signal);
|
|
1745
1871
|
try {
|
|
1746
|
-
if (
|
|
1747
|
-
const validation = Promise.resolve(validator(url, context));
|
|
1872
|
+
if (signal.aborted) throw new Error("URL validation was aborted.");
|
|
1873
|
+
const validation = Promise.resolve(validator(url, context, signal));
|
|
1748
1874
|
let timer;
|
|
1749
1875
|
let abortHandler;
|
|
1750
1876
|
const cancellation = new Promise((_resolve, reject) => {
|
|
1751
1877
|
abortHandler = () => reject(new Error("URL validation was aborted."));
|
|
1752
|
-
|
|
1878
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
1753
1879
|
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
1754
1880
|
timer = setTimeout(
|
|
1755
1881
|
() => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
|
|
@@ -1759,24 +1885,24 @@ function createAzureUrlValidatorRunner(validator, options = {}) {
|
|
|
1759
1885
|
});
|
|
1760
1886
|
try {
|
|
1761
1887
|
const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
|
|
1762
|
-
cache.set(
|
|
1888
|
+
cache.set(key, result);
|
|
1763
1889
|
return result;
|
|
1764
1890
|
} finally {
|
|
1765
1891
|
if (timer) clearTimeout(timer);
|
|
1766
|
-
if (abortHandler)
|
|
1892
|
+
if (abortHandler) signal.removeEventListener("abort", abortHandler);
|
|
1767
1893
|
}
|
|
1768
1894
|
} finally {
|
|
1769
1895
|
release();
|
|
1770
1896
|
}
|
|
1771
1897
|
})();
|
|
1772
|
-
inFlight.set(
|
|
1898
|
+
inFlight.set(key, promise);
|
|
1773
1899
|
try {
|
|
1774
1900
|
return await promise;
|
|
1775
1901
|
} finally {
|
|
1776
|
-
inFlight.delete(
|
|
1902
|
+
inFlight.delete(key);
|
|
1777
1903
|
}
|
|
1778
1904
|
};
|
|
1779
|
-
return (url, context) => check(url, context);
|
|
1905
|
+
return (url, context, signal) => check(url, context, signal ?? configuredSignal);
|
|
1780
1906
|
}
|
|
1781
1907
|
var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
|
|
1782
1908
|
var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
|
|
@@ -2432,6 +2558,7 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2432
2558
|
...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
|
|
2433
2559
|
...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
|
|
2434
2560
|
});
|
|
2561
|
+
const validationSignal = options.urlValidatorSignal ?? options.urlValidation?.signal ?? new AbortController().signal;
|
|
2435
2562
|
let tokens;
|
|
2436
2563
|
try {
|
|
2437
2564
|
tokens = tokenizeElements(ssml);
|
|
@@ -2441,7 +2568,11 @@ function validateAzureSsml(ssml, options = {}) {
|
|
|
2441
2568
|
const checks = tokens.flatMap(
|
|
2442
2569
|
(token) => urlAttributes(token).map(async ({ attribute, value }) => {
|
|
2443
2570
|
try {
|
|
2444
|
-
const result = await boundedValidator(
|
|
2571
|
+
const result = await boundedValidator(
|
|
2572
|
+
value,
|
|
2573
|
+
{ tag: token.name, attribute, ...options.sourceNodePath ? { sourceNodePath: options.sourceNodePath } : {} },
|
|
2574
|
+
validationSignal
|
|
2575
|
+
);
|
|
2445
2576
|
const valid = typeof result === "boolean" ? result : result.valid;
|
|
2446
2577
|
if (!valid) {
|
|
2447
2578
|
const reason = typeof result === "boolean" ? void 0 : result.reason;
|
|
@@ -2487,10 +2618,11 @@ export {
|
|
|
2487
2618
|
buildSsml,
|
|
2488
2619
|
parseSsml,
|
|
2489
2620
|
buildPartialSsml,
|
|
2490
|
-
|
|
2491
|
-
validateSsml,
|
|
2621
|
+
getSsmlSourceMap,
|
|
2492
2622
|
extractSsmlText,
|
|
2493
2623
|
mapSsmlTextNodes,
|
|
2624
|
+
splitSsmlDocument,
|
|
2625
|
+
validateSsml,
|
|
2494
2626
|
extractSsmlTranslatableText,
|
|
2495
2627
|
fromPlainTextToSsml,
|
|
2496
2628
|
validateSsmlStructureIntegrity,
|
|
@@ -2502,4 +2634,4 @@ export {
|
|
|
2502
2634
|
getAzureVoiceCatalogMetadata,
|
|
2503
2635
|
getBuiltInVoiceCatalogMetadata
|
|
2504
2636
|
};
|
|
2505
|
-
//# sourceMappingURL=chunk-
|
|
2637
|
+
//# sourceMappingURL=chunk-FXUM45ZY.mjs.map
|