ssml-builder-js 2.12.0 → 2.14.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/dist/react.js CHANGED
@@ -33,6 +33,7 @@ var react_exports = {};
33
33
  __export(react_exports, {
34
34
  EDITOR_COPY: () => EDITOR_COPY,
35
35
  INLINE_BADGE_COPY: () => INLINE_BADGE_COPY,
36
+ SSML_ELEMENT_COPY: () => SSML_ELEMENT_COPY,
36
37
  SSML_HOVER_COPY: () => SSML_HOVER_COPY,
37
38
  SSML_INSERTIONS: () => SSML_INSERTIONS,
38
39
  SsmlEditor: () => SsmlEditor,
@@ -820,6 +821,152 @@ var SILENCE_VALUE_DESCRIPTIONS = {
820
821
  };
821
822
 
822
823
  // packages/ssml-editor-react/src/locales.ts
824
+ var SSML_ELEMENT_COPY = {
825
+ ja: {
826
+ dialog: {
827
+ label: "\u30C0\u30A4\u30A2\u30ED\u30B0",
828
+ description: "\u8907\u6570\u306E\u8A71\u8005\u30BF\u30FC\u30F3\u3092\u307E\u3068\u3081\u307E\u3059\u3002",
829
+ validationErrors: { turn: "turn \u306F dialog \u306E\u76F4\u4E0B\u306B\u914D\u7F6E\u3057\u3066\u304F\u3060\u3055\u3044\u3002" }
830
+ },
831
+ "mstts:dialog": {
832
+ label: "\u30C0\u30A4\u30A2\u30ED\u30B0",
833
+ description: "\u8907\u6570\u306E\u8A71\u8005\u30BF\u30FC\u30F3\u3092\u307E\u3068\u3081\u307E\u3059\u3002",
834
+ validationErrors: { turn: "turn \u306F dialog \u306E\u76F4\u4E0B\u306B\u914D\u7F6E\u3057\u3066\u304F\u3060\u3055\u3044\u3002" }
835
+ },
836
+ turn: {
837
+ label: "\u8A71\u8005\u30BF\u30FC\u30F3",
838
+ description: "\u30C0\u30A4\u30A2\u30ED\u30B0\u5185\u306E\u8A71\u8005\u3068\u767A\u8A71\u5185\u5BB9\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
839
+ validationErrors: { voice: "voice \u307E\u305F\u306F speaker \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002" }
840
+ },
841
+ "mstts:turn": {
842
+ label: "\u8A71\u8005\u30BF\u30FC\u30F3",
843
+ description: "\u30C0\u30A4\u30A2\u30ED\u30B0\u5185\u306E\u8A71\u8005\u3068\u767A\u8A71\u5185\u5BB9\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
844
+ validationErrors: { voice: "voice \u307E\u305F\u306F speaker \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002" }
845
+ },
846
+ backgroundaudio: {
847
+ label: "\u80CC\u666F\u97F3\u58F0",
848
+ description: "\u5408\u6210\u97F3\u58F0\u306E\u80CC\u5F8C\u3067\u518D\u751F\u3059\u308B\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
849
+ validationErrors: { src: "\u80CC\u666F\u97F3\u58F0\u306B\u306F URL \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002" }
850
+ },
851
+ "mstts:backgroundaudio": {
852
+ label: "\u80CC\u666F\u97F3\u58F0",
853
+ description: "\u5408\u6210\u97F3\u58F0\u306E\u80CC\u5F8C\u3067\u518D\u751F\u3059\u308B\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
854
+ validationErrors: { src: "\u80CC\u666F\u97F3\u58F0\u306B\u306F URL \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002" }
855
+ },
856
+ ttsembedding: {
857
+ label: "\u97F3\u58F0\u57CB\u3081\u8FBC\u307F",
858
+ description: "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u307E\u305F\u306F\u8A71\u8005\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB\u306E\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u57CB\u3081\u8FBC\u307F\u307E\u3059\u3002",
859
+ validationErrors: { speakerProfileId: "\u8A71\u8005\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB ID \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002" }
860
+ },
861
+ "mstts:ttsembedding": {
862
+ label: "\u97F3\u58F0\u57CB\u3081\u8FBC\u307F",
863
+ description: "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u307E\u305F\u306F\u8A71\u8005\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB\u306E\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u57CB\u3081\u8FBC\u307F\u307E\u3059\u3002",
864
+ validationErrors: { speakerProfileId: "\u8A71\u8005\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB ID \u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002" }
865
+ },
866
+ embedding: {
867
+ label: "\u57CB\u3081\u8FBC\u307F",
868
+ description: "\u8A71\u8005\u57CB\u3081\u8FBC\u307F\u306E\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
869
+ validationErrors: {}
870
+ },
871
+ "mstts:embedding": {
872
+ label: "\u57CB\u3081\u8FBC\u307F",
873
+ description: "\u8A71\u8005\u57CB\u3081\u8FBC\u307F\u306E\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
874
+ validationErrors: {}
875
+ },
876
+ silence: {
877
+ label: "\u7121\u97F3",
878
+ description: "\u767A\u8A71\u524D\u5F8C\u307E\u305F\u306F\u53E5\u8AAD\u70B9\u306E\u5883\u754C\u306B\u7121\u97F3\u3092\u8FFD\u52A0\u3057\u307E\u3059\u3002",
879
+ validationErrors: { value: "\u7121\u97F3\u306E\u9577\u3055\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002" }
880
+ },
881
+ "mstts:silence": {
882
+ label: "\u7121\u97F3",
883
+ description: "\u767A\u8A71\u524D\u5F8C\u307E\u305F\u306F\u53E5\u8AAD\u70B9\u306E\u5883\u754C\u306B\u7121\u97F3\u3092\u8FFD\u52A0\u3057\u307E\u3059\u3002",
884
+ validationErrors: { value: "\u7121\u97F3\u306E\u9577\u3055\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002" }
885
+ },
886
+ voiceconversion: {
887
+ label: "\u97F3\u58F0\u5909\u63DB",
888
+ description: "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u306E\u5909\u63DB\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
889
+ validationErrors: {}
890
+ },
891
+ "mstts:voiceconversion": {
892
+ label: "\u97F3\u58F0\u5909\u63DB",
893
+ description: "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u306E\u5909\u63DB\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
894
+ validationErrors: {}
895
+ }
896
+ },
897
+ en: {
898
+ dialog: {
899
+ label: "Dialog",
900
+ description: "Groups multiple speaker turns.",
901
+ validationErrors: { turn: "Place turns directly inside a dialog." }
902
+ },
903
+ "mstts:dialog": {
904
+ label: "Dialog",
905
+ description: "Groups multiple speaker turns.",
906
+ validationErrors: { turn: "Place turns directly inside a dialog." }
907
+ },
908
+ turn: {
909
+ label: "Dialog turn",
910
+ description: "Specifies a speaker and utterance within a dialog.",
911
+ validationErrors: { voice: "Provide a voice or speaker." }
912
+ },
913
+ "mstts:turn": {
914
+ label: "Dialog turn",
915
+ description: "Specifies a speaker and utterance within a dialog.",
916
+ validationErrors: { voice: "Provide a voice or speaker." }
917
+ },
918
+ backgroundaudio: {
919
+ label: "Background audio",
920
+ description: "Specifies an audio file played behind synthesized speech.",
921
+ validationErrors: { src: "Provide a background audio URL." }
922
+ },
923
+ "mstts:backgroundaudio": {
924
+ label: "Background audio",
925
+ description: "Specifies an audio file played behind synthesized speech.",
926
+ validationErrors: { src: "Provide a background audio URL." }
927
+ },
928
+ ttsembedding: {
929
+ label: "TTS embedding",
930
+ description: "Embeds custom voice or speaker profile metadata.",
931
+ validationErrors: { speakerProfileId: "Provide a speaker profile ID." }
932
+ },
933
+ "mstts:ttsembedding": {
934
+ label: "TTS embedding",
935
+ description: "Embeds custom voice or speaker profile metadata.",
936
+ validationErrors: { speakerProfileId: "Provide a speaker profile ID." }
937
+ },
938
+ embedding: {
939
+ label: "Embedding",
940
+ description: "Specifies speaker embedding metadata.",
941
+ validationErrors: {}
942
+ },
943
+ "mstts:embedding": {
944
+ label: "Embedding",
945
+ description: "Specifies speaker embedding metadata.",
946
+ validationErrors: {}
947
+ },
948
+ silence: {
949
+ label: "Silence",
950
+ description: "Adds silence before, after, or around punctuation.",
951
+ validationErrors: { value: "Provide a silence duration." }
952
+ },
953
+ "mstts:silence": {
954
+ label: "Silence",
955
+ description: "Adds silence before, after, or around punctuation.",
956
+ validationErrors: { value: "Provide a silence duration." }
957
+ },
958
+ voiceconversion: {
959
+ label: "Voice conversion",
960
+ description: "Specifies custom voice conversion metadata.",
961
+ validationErrors: {}
962
+ },
963
+ "mstts:voiceconversion": {
964
+ label: "Voice conversion",
965
+ description: "Specifies custom voice conversion metadata.",
966
+ validationErrors: {}
967
+ }
968
+ }
969
+ };
823
970
  var EDITOR_COPY = {
824
971
  ja: {
825
972
  editorAriaLabel: "SSML\u30A8\u30C7\u30A3\u30BF\u30FC",
@@ -1037,6 +1184,42 @@ var SSML_HOVER_COPY = {
1037
1184
  parameters: {
1038
1185
  value: { title: "value", description: "\u76EE\u6A19\u6642\u9593\u3002\u4F8B: `10s`\u3001`5000ms`\u3001`00:00:10`\u3002" }
1039
1186
  }
1187
+ },
1188
+ "mstts:dialog": { title: "\u30C0\u30A4\u30A2\u30ED\u30B0", description: "\u8907\u6570\u306E\u8A71\u8005\u30BF\u30FC\u30F3\u3092\u307E\u3068\u3081\u307E\u3059\u3002", parameters: {} },
1189
+ "mstts:turn": {
1190
+ title: "\u8A71\u8005\u30BF\u30FC\u30F3",
1191
+ description: "\u30C0\u30A4\u30A2\u30ED\u30B0\u5185\u306E\u8A71\u8005\u3068\u767A\u8A71\u5185\u5BB9\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
1192
+ parameters: {
1193
+ voice: { title: "voice", description: "\u3053\u306E\u30BF\u30FC\u30F3\u3067\u4F7F\u7528\u3059\u308B Azure \u97F3\u58F0\u540D\u3002" },
1194
+ speaker: { title: "speaker", description: "\u8A71\u8005\u8B58\u5225\u5B50\u3002" }
1195
+ }
1196
+ },
1197
+ "mstts:backgroundaudio": {
1198
+ title: "\u80CC\u666F\u97F3\u58F0",
1199
+ description: "\u5408\u6210\u97F3\u58F0\u306E\u80CC\u5F8C\u3067\u518D\u751F\u3059\u308B\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
1200
+ parameters: { src: { title: "src", description: "\u97F3\u58F0\u30D5\u30A1\u30A4\u30EB\u306E HTTPS URL\u3002" } }
1201
+ },
1202
+ "mstts:ttsembedding": {
1203
+ title: "\u97F3\u58F0\u57CB\u3081\u8FBC\u307F",
1204
+ description: "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u307E\u305F\u306F\u8A71\u8005\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB\u306E\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u57CB\u3081\u8FBC\u307F\u307E\u3059\u3002",
1205
+ parameters: { speakerProfileId: { title: "speakerProfileId", description: "\u8A71\u8005\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB ID\u3002" } }
1206
+ },
1207
+ "mstts:embedding": {
1208
+ title: "\u57CB\u3081\u8FBC\u307F",
1209
+ description: "\u8A71\u8005\u57CB\u3081\u8FBC\u307F\u306E\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
1210
+ parameters: {
1211
+ id: { title: "id", description: "\u57CB\u3081\u8FBC\u307F ID\u3002" },
1212
+ speakerProfileId: { title: "speakerProfileId", description: "\u8A71\u8005\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB ID\u3002" }
1213
+ }
1214
+ },
1215
+ "mstts:voiceconversion": {
1216
+ title: "\u97F3\u58F0\u5909\u63DB",
1217
+ description: "\u30AB\u30B9\u30BF\u30E0\u97F3\u58F0\u306E\u5909\u63DB\u30E1\u30BF\u30C7\u30FC\u30BF\u3092\u6307\u5B9A\u3057\u307E\u3059\u3002",
1218
+ parameters: {
1219
+ url: { title: "url", description: "\u97F3\u58F0\u5909\u63DB\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB URL\u3002" },
1220
+ profile: { title: "profile", description: "\u5909\u63DB\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB\u3002" },
1221
+ speakerProfileId: { title: "speakerProfileId", description: "\u8A71\u8005\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB ID\u3002" }
1222
+ }
1040
1223
  }
1041
1224
  }
1042
1225
  },
@@ -1201,6 +1384,42 @@ var SSML_HOVER_COPY = {
1201
1384
  parameters: {
1202
1385
  value: { title: "value", description: "The target duration, such as `10s`, `5000ms`, or `00:00:10`." }
1203
1386
  }
1387
+ },
1388
+ "mstts:dialog": { title: "Dialog", description: "Groups multiple speaker turns.", parameters: {} },
1389
+ "mstts:turn": {
1390
+ title: "Dialog turn",
1391
+ description: "Specifies a speaker and utterance within a dialog.",
1392
+ parameters: {
1393
+ voice: { title: "voice", description: "The Azure voice used by this turn." },
1394
+ speaker: { title: "speaker", description: "The speaker identifier." }
1395
+ }
1396
+ },
1397
+ "mstts:backgroundaudio": {
1398
+ title: "Background audio",
1399
+ description: "Specifies an audio file played behind synthesized speech.",
1400
+ parameters: { src: { title: "src", description: "The HTTPS URL of the audio file." } }
1401
+ },
1402
+ "mstts:ttsembedding": {
1403
+ title: "TTS embedding",
1404
+ description: "Embeds custom voice or speaker profile metadata.",
1405
+ parameters: { speakerProfileId: { title: "speakerProfileId", description: "The speaker profile ID." } }
1406
+ },
1407
+ "mstts:embedding": {
1408
+ title: "Embedding",
1409
+ description: "Specifies speaker embedding metadata.",
1410
+ parameters: {
1411
+ id: { title: "id", description: "The embedding ID." },
1412
+ speakerProfileId: { title: "speakerProfileId", description: "The speaker profile ID." }
1413
+ }
1414
+ },
1415
+ "mstts:voiceconversion": {
1416
+ title: "Voice conversion",
1417
+ description: "Specifies custom voice conversion metadata.",
1418
+ parameters: {
1419
+ url: { title: "url", description: "The voice conversion profile URL." },
1420
+ profile: { title: "profile", description: "The conversion profile." },
1421
+ speakerProfileId: { title: "speakerProfileId", description: "The speaker profile ID." }
1422
+ }
1204
1423
  }
1205
1424
  }
1206
1425
  }
@@ -2734,6 +2953,69 @@ var AZURE_VOICE_DEFINITIONS = [
2734
2953
  },
2735
2954
  { name: "zh-TW-HsiaoChenNeural", locale: "zh-TW" }
2736
2955
  ];
2956
+ function createAzureUrlValidatorRunner(validator, options = {}) {
2957
+ if (typeof validator !== "function") throw new TypeError("A URL validator function is required.");
2958
+ const concurrency = options.concurrency === void 0 ? Infinity : Number.isFinite(options.concurrency) ? Math.max(1, Math.floor(options.concurrency)) : Infinity;
2959
+ const cache = options.cache ?? /* @__PURE__ */ new Map();
2960
+ const inFlight = /* @__PURE__ */ new Map();
2961
+ const waiters = [];
2962
+ let active = 0;
2963
+ const acquire = async () => {
2964
+ if (active < concurrency) {
2965
+ active += 1;
2966
+ return;
2967
+ }
2968
+ await new Promise((resolve) => waiters.push(resolve));
2969
+ active += 1;
2970
+ };
2971
+ const release = () => {
2972
+ active -= 1;
2973
+ waiters.shift()?.();
2974
+ };
2975
+ const check = async (url, context) => {
2976
+ if (options.signal?.aborted) throw new Error("URL validation was aborted.");
2977
+ const cached = cache.get(url);
2978
+ if (cached !== void 0) return cached;
2979
+ const existing = inFlight.get(url);
2980
+ if (existing) return existing;
2981
+ const promise = (async () => {
2982
+ await acquire();
2983
+ try {
2984
+ if (options.signal?.aborted) throw new Error("URL validation was aborted.");
2985
+ const validation = Promise.resolve(validator(url, context));
2986
+ let timer;
2987
+ let abortHandler;
2988
+ const cancellation = new Promise((_resolve, reject) => {
2989
+ abortHandler = () => reject(new Error("URL validation was aborted."));
2990
+ options.signal?.addEventListener("abort", abortHandler, { once: true });
2991
+ if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
2992
+ timer = setTimeout(
2993
+ () => reject(new Error(`URL validation timed out after ${options.timeoutMs} ms.`)),
2994
+ options.timeoutMs
2995
+ );
2996
+ }
2997
+ });
2998
+ try {
2999
+ const result = await (timer || abortHandler ? Promise.race([validation, cancellation]) : validation);
3000
+ cache.set(url, result);
3001
+ return result;
3002
+ } finally {
3003
+ if (timer) clearTimeout(timer);
3004
+ if (abortHandler) options.signal?.removeEventListener("abort", abortHandler);
3005
+ }
3006
+ } finally {
3007
+ release();
3008
+ }
3009
+ })();
3010
+ inFlight.set(url, promise);
3011
+ try {
3012
+ return await promise;
3013
+ } finally {
3014
+ inFlight.delete(url);
3015
+ }
3016
+ };
3017
+ return (url, context) => check(url, context);
3018
+ }
2737
3019
  var ALLOWED_BREAK_STRENGTHS = /* @__PURE__ */ new Set(["none", "x-weak", "weak", "medium", "strong", "x-strong"]);
2738
3020
  var ALLOWED_SAY_AS = /* @__PURE__ */ new Set([
2739
3021
  "characters",
@@ -3018,23 +3300,23 @@ function validateVoiceFeatureMatrix(token, source, diagnostics, voiceName, defin
3018
3300
  );
3019
3301
  }
3020
3302
  }
3021
- function validateAudioSource(token, source, diagnostics, options, elementName2) {
3303
+ function validateAudioSource(token, source, diagnostics, options, elementName3) {
3022
3304
  const src = attr(token, "src");
3023
3305
  if (!src) {
3024
- addDiagnostic(diagnostics, source, token.start, `<${elementName2}> requires a "src" attribute.`);
3306
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3}> requires a "src" attribute.`);
3025
3307
  return;
3026
3308
  }
3027
3309
  let parsed;
3028
3310
  try {
3029
3311
  parsed = new URL(src);
3030
3312
  } catch {
3031
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
3313
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must be an absolute HTTP(S) URL.`);
3032
3314
  return;
3033
3315
  }
3034
3316
  if (parsed.username || parsed.password)
3035
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
3317
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must not contain URL credentials.`);
3036
3318
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
3037
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
3319
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> must use HTTPS.`);
3038
3320
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
3039
3321
  try {
3040
3322
  const configured = new URL(allowedOrigin);
@@ -3046,13 +3328,13 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
3046
3328
  }
3047
3329
  }) ?? false;
3048
3330
  if (options.allowedAudioOrigins && !isAllowedOrigin)
3049
- addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> origin "${parsed.origin}" is not allowed.`);
3331
+ addDiagnostic(diagnostics, source, token.start, `<${elementName3} src> origin "${parsed.origin}" is not allowed.`);
3050
3332
  else if (!isAllowedOrigin && !options.allowExternalAudio)
3051
3333
  addDiagnostic(
3052
3334
  diagnostics,
3053
3335
  source,
3054
3336
  token.start,
3055
- `<${elementName2} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
3337
+ `<${elementName3} src> external origin "${parsed.origin}" is blocked by default; set allowExternalAudio to true or provide allowedAudioOrigins.`
3056
3338
  );
3057
3339
  }
3058
3340
  function validateElement(token, source, diagnostics, voiceName, options, voiceCatalog) {
@@ -3234,7 +3516,7 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
3234
3516
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
3235
3517
  }
3236
3518
  }
3237
- function validateAzureSsml(ssml, options = {}) {
3519
+ function validateAzureSsmlStatic(ssml, options = {}) {
3238
3520
  const diagnostics = [];
3239
3521
  if (typeof ssml !== "string") {
3240
3522
  return [
@@ -3358,6 +3640,59 @@ function validateAzureSsml(ssml, options = {}) {
3358
3640
  }
3359
3641
  return diagnostics;
3360
3642
  }
3643
+ function urlAttributes(token) {
3644
+ const tag = canonicalTagName(token.name);
3645
+ const attributes = tag === "audio" || tag === "mstts:backgroundaudio" ? ["src"] : tag === "lexicon" ? ["uri"] : tag === "mstts:voiceconversion" ? ["url"] : [];
3646
+ return attributes.flatMap((attribute) => {
3647
+ const value = attr(token, attribute);
3648
+ return value === void 0 ? [] : [{ attribute, value }];
3649
+ });
3650
+ }
3651
+ function validateAzureSsml(ssml, options = {}) {
3652
+ const diagnostics = validateAzureSsmlStatic(ssml, options);
3653
+ const validator = options.urlValidator ?? options.customUrlValidator;
3654
+ if (!validator || typeof ssml !== "string") return diagnostics;
3655
+ const runnerOptions = options.urlValidation ?? {};
3656
+ const boundedValidator = createAzureUrlValidatorRunner(validator, {
3657
+ ...runnerOptions,
3658
+ ...options.urlValidatorConcurrency !== void 0 ? { concurrency: options.urlValidatorConcurrency } : {},
3659
+ ...options.urlValidatorTimeoutMs !== void 0 ? { timeoutMs: options.urlValidatorTimeoutMs } : {},
3660
+ ...options.urlValidatorSignal ? { signal: options.urlValidatorSignal } : {},
3661
+ ...options.urlValidatorCache ? { cache: options.urlValidatorCache } : {}
3662
+ });
3663
+ let tokens;
3664
+ try {
3665
+ tokens = tokenizeElements(ssml);
3666
+ } catch {
3667
+ return diagnostics;
3668
+ }
3669
+ const checks = tokens.flatMap(
3670
+ (token) => urlAttributes(token).map(async ({ attribute, value }) => {
3671
+ try {
3672
+ const result = await boundedValidator(value, { tag: token.name, attribute });
3673
+ const valid = typeof result === "boolean" ? result : result.valid;
3674
+ if (!valid) {
3675
+ const reason = typeof result === "boolean" ? void 0 : result.reason;
3676
+ addDiagnostic(
3677
+ diagnostics,
3678
+ ssml,
3679
+ token.start,
3680
+ `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
3681
+ );
3682
+ }
3683
+ } catch (error) {
3684
+ const reason = error instanceof Error ? error.message : String(error);
3685
+ addDiagnostic(
3686
+ diagnostics,
3687
+ ssml,
3688
+ token.start,
3689
+ `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
3690
+ );
3691
+ }
3692
+ })
3693
+ );
3694
+ return Promise.all(checks).then(() => diagnostics);
3695
+ }
3361
3696
  var AZURE_VOICE_CATALOG_METADATA = {
3362
3697
  apiVersion: "2025-10-01",
3363
3698
  generatedAt: "2026-08-28T00:00:00.000Z",
@@ -6484,19 +6819,116 @@ var VISUAL_ELEMENT_FIELDS = {
6484
6819
  function getElementFields(element) {
6485
6820
  return VISUAL_ELEMENT_FIELDS[elementLabel(element)] ?? [];
6486
6821
  }
6822
+ function localizedElementLabel(element, locale) {
6823
+ return SSML_ELEMENT_COPY[locale][elementLabel(element)]?.label ?? elementLabel(element);
6824
+ }
6825
+ function filteredVoices(voiceCatalog, locale, region, style) {
6826
+ return (voiceCatalog ?? []).filter((voice) => {
6827
+ if (locale && voice.locale.toLowerCase() !== locale.toLowerCase()) return false;
6828
+ const regions = [voice.region, ...voice.regions ?? []].filter(
6829
+ (candidate) => Boolean(candidate)
6830
+ );
6831
+ if (region && regions.length > 0 && !regions.some((candidate) => candidate.toLowerCase() === region.toLowerCase()))
6832
+ return false;
6833
+ if (style && !voice.styles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase())) return false;
6834
+ return true;
6835
+ });
6836
+ }
6837
+ function VoiceCapabilityMatrix({
6838
+ voice,
6839
+ locale
6840
+ }) {
6841
+ const status = voice.status ?? (voice.preview ? "preview" : void 0);
6842
+ return /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-voice-capabilities", "data-voice-capabilities": "" }, /* @__PURE__ */ React.createElement("div", null, locale === "ja" ? "\u30B9\u30C6\u30FC\u30BF\u30B9" : "Status", ": ", status?.toUpperCase() ?? (locale === "ja" ? "GA" : "GA"), status === "preview" || status === "deprecated" ? /* @__PURE__ */ React.createElement("span", { role: "status", "aria-label": status, className: "ssml-editor-voice-warning-badge" }, status === "preview" ? "\u26A0 Preview" : "\u26A0 Deprecated") : null), (voice.regions?.length || voice.region) && /* @__PURE__ */ React.createElement("div", null, locale === "ja" ? "\u30EA\u30FC\u30B8\u30E7\u30F3" : "Regions", ":", " ", [...voice.regions ?? [], ...voice.region ? [voice.region] : []].filter((region, index, all) => all.indexOf(region) === index).join(", ")), voice.supportedTags && voice.supportedTags.length > 0 && /* @__PURE__ */ React.createElement("div", null, locale === "ja" ? "\u5BFE\u5FDC\u30BF\u30B0" : "Supported tags", ": ", voice.supportedTags.join(", ")), voice.unsupportedTags && voice.unsupportedTags.length > 0 && /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-voice-unsupported" }, locale === "ja" ? "\u975E\u5BFE\u5FDC\u30BF\u30B0" : "Unsupported tags", ": ", voice.unsupportedTags.join(", ")));
6843
+ }
6844
+ function DefaultVoiceSelector({
6845
+ value,
6846
+ voices,
6847
+ selectedVoice,
6848
+ readOnly,
6849
+ locale,
6850
+ onChange
6851
+ }) {
6852
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(
6853
+ "select",
6854
+ {
6855
+ "aria-label": locale === "ja" ? "\u97F3\u58F0" : "Voice",
6856
+ value,
6857
+ disabled: readOnly,
6858
+ onChange: (event) => onChange(event.target.value)
6859
+ },
6860
+ /* @__PURE__ */ React.createElement("option", { value: "" }, locale === "ja" ? "\u97F3\u58F0\u3092\u9078\u629E" : "Select a voice"),
6861
+ voices.map((voice) => /* @__PURE__ */ React.createElement("option", { key: voice.name, value: voice.name }, voice.name, voice.preview || voice.status === "preview" ? " (preview)" : ""))
6862
+ ), selectedVoice ? /* @__PURE__ */ React.createElement(VoiceCapabilityMatrix, { voice: selectedVoice, locale }) : null);
6863
+ }
6487
6864
  function VisualElementInspector({
6488
6865
  document: document2,
6489
6866
  element,
6490
6867
  path,
6491
6868
  readOnly,
6492
- commit
6869
+ commit,
6870
+ locale,
6871
+ customInspector,
6872
+ renderVoiceSelector,
6873
+ voiceCatalog,
6874
+ voiceLocale,
6875
+ voiceRegion,
6876
+ voiceStyle
6493
6877
  }) {
6878
+ if (customInspector) {
6879
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, customInspector({ document: document2, element, path, readOnly, onChange: commit, locale }));
6880
+ }
6494
6881
  const fields = getElementFields(element);
6882
+ const availableVoices = filteredVoices(voiceCatalog, voiceLocale, voiceRegion, voiceStyle);
6883
+ const selectedVoice = voiceCatalog?.find(
6884
+ (voice) => voice.name === (element.type === "voice" ? element.name : void 0)
6885
+ );
6886
+ const renderSelector = renderVoiceSelector ?? DefaultVoiceSelector;
6495
6887
  return /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, `<${elementLabel(element)}>`), fields.length === 0 ? /* @__PURE__ */ React.createElement("p", null, "This element is preserved in the visual tree. Edit its attributes in Code mode.") : fields.map((field) => {
6496
6888
  const value = element[field.key];
6497
6889
  const inputValue = value === void 0 ? "" : String(value);
6498
6890
  const inputId = `ssml-visual-${field.key}`;
6499
- return /* @__PURE__ */ React.createElement("label", { key: field.key, htmlFor: inputId }, field.label, field.multiline ? /* @__PURE__ */ React.createElement(
6891
+ return /* @__PURE__ */ React.createElement("label", { key: field.key, htmlFor: inputId }, locale === "ja" ? {
6892
+ name: "\u540D\u524D",
6893
+ effect: "\u52B9\u679C",
6894
+ rate: "\u901F\u5EA6",
6895
+ pitch: "\u30D4\u30C3\u30C1",
6896
+ volume: "\u97F3\u91CF",
6897
+ contour: "\u30D4\u30C3\u30C1\u66F2\u7DDA",
6898
+ range: "\u7BC4\u56F2",
6899
+ style: "\u30B9\u30BF\u30A4\u30EB",
6900
+ styleDegree: "\u30B9\u30BF\u30A4\u30EB\u5F37\u5EA6",
6901
+ role: "\u5F79\u5272",
6902
+ interpretAs: "\u89E3\u91C8",
6903
+ alphabet: "\u30A2\u30EB\u30D5\u30A1\u30D9\u30C3\u30C8",
6904
+ ph: "\u767A\u97F3",
6905
+ level: "\u5F37\u8ABF\u30EC\u30D9\u30EB",
6906
+ src: "\u97F3\u58F0 URL",
6907
+ desc: "\u8AAC\u660E",
6908
+ clipBegin: "\u958B\u59CB\u4F4D\u7F6E",
6909
+ clipEnd: "\u7D42\u4E86\u4F4D\u7F6E",
6910
+ speed: "\u901F\u5EA6",
6911
+ repeatCount: "\u7E70\u308A\u8FD4\u3057\u56DE\u6570",
6912
+ repeatDuration: "\u7E70\u308A\u8FD4\u3057\u6642\u9593",
6913
+ soundLevel: "\u97F3\u91CF\u30EC\u30D9\u30EB",
6914
+ mark: "\u30D6\u30C3\u30AF\u30DE\u30FC\u30AF\u540D",
6915
+ alias: "\u5225\u540D",
6916
+ lang: "\u8A00\u8A9E",
6917
+ uri: "\u8F9E\u66F8 URI",
6918
+ typeValue: "\u7121\u97F3\u306E\u7A2E\u985E",
6919
+ value: "\u5024",
6920
+ id: "\u57CB\u3081\u8FBC\u307F ID",
6921
+ speakerProfileId: "\u8A71\u8005\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB ID",
6922
+ url: "\u97F3\u58F0\u5909\u63DB URL",
6923
+ profile: "\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB"
6924
+ }[field.key] ?? field.label : field.label, field.key === "name" && elementLabel(element) === "voice" && voiceCatalog ? renderSelector({
6925
+ value: inputValue,
6926
+ voices: availableVoices,
6927
+ selectedVoice,
6928
+ readOnly,
6929
+ locale,
6930
+ onChange: (value2) => commit(updateOptionalElementProperty(document2, path, field.key, value2))
6931
+ }) : field.multiline ? /* @__PURE__ */ React.createElement(
6500
6932
  "textarea",
6501
6933
  {
6502
6934
  id: inputId,
@@ -6587,13 +7019,21 @@ function VisualSsmlEditor({
6587
7019
  document: document2,
6588
7020
  readOnly = false,
6589
7021
  onChange,
6590
- onPreviewSelection
7022
+ onPreviewSelection,
7023
+ locale = "en",
7024
+ customInspectors,
7025
+ renderVoiceSelector,
7026
+ voiceCatalog,
7027
+ voiceLocale,
7028
+ voiceRegion,
7029
+ voiceStyle
6591
7030
  }) {
6592
7031
  const [selectedPath, setSelectedPath] = (0, import_react3.useState)(null);
6593
7032
  const [selection, setSelection] = (0, import_react3.useState)({ start: 0, end: 0 });
6594
7033
  const textLeaves = (0, import_react3.useMemo)(() => collectTextLeaves(document2.children ?? []), [document2]);
6595
7034
  const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
6596
7035
  const selectedElement = selectedPath ? getNodeAtPath(document2.children ?? [], selectedPath) : void 0;
7036
+ const selectedCustomInspector = selectedElement && isElement(selectedElement) ? customInspectors?.[elementLabel(selectedElement)] ?? customInspectors?.[selectedElement.type] : void 0;
6597
7037
  const diagnostics = (0, import_react3.useMemo)(() => validateAzureSsml(buildSsml(document2)), [document2]);
6598
7038
  const commit = (nextDocument) => onChange?.(nextDocument);
6599
7039
  const applyWrapper = (tag, attributes) => {
@@ -6611,29 +7051,44 @@ function VisualSsmlEditor({
6611
7051
  selectedPath,
6612
7052
  onSelect: setSelectedPath
6613
7053
  }
6614
- )))), /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual-form" }, selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:dialog" ? /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, "Dialog"), /* @__PURE__ */ React.createElement("p", null, "Add and edit speaker turns in this dialog."), /* @__PURE__ */ React.createElement(
7054
+ )))), /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual-form" }, selectedCustomInspector && selectedElement && isElement(selectedElement) ? selectedCustomInspector({
7055
+ document: document2,
7056
+ element: selectedElement,
7057
+ path: selectedPath ?? [],
7058
+ readOnly,
7059
+ onChange: commit,
7060
+ locale
7061
+ }) : selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:dialog" ? /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, localizedElementLabel(selectedElement, locale)), /* @__PURE__ */ React.createElement("p", null, SSML_ELEMENT_COPY[locale][elementLabel(selectedElement)]?.description), /* @__PURE__ */ React.createElement(
6615
7062
  "button",
6616
7063
  {
6617
7064
  type: "button",
6618
7065
  disabled: readOnly,
6619
7066
  onClick: () => commit(addDialogTurn(document2, selectedPath ?? []))
6620
7067
  },
6621
- "Add turn"
6622
- )) : selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:turn" ? /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, "Dialog turn"), /* @__PURE__ */ React.createElement("label", null, "Voice", /* @__PURE__ */ React.createElement(
7068
+ locale === "ja" ? "\u30BF\u30FC\u30F3\u3092\u8FFD\u52A0" : "Add turn"
7069
+ )) : selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:turn" ? /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, localizedElementLabel(selectedElement, locale)), /* @__PURE__ */ React.createElement("label", { htmlFor: "ssml-visual-turn-voice" }, locale === "ja" ? "\u97F3\u58F0" : "Voice", voiceCatalog ? (renderVoiceSelector ?? DefaultVoiceSelector)({
7070
+ value: selectedElement.voice ?? "",
7071
+ voices: filteredVoices(voiceCatalog, voiceLocale, voiceRegion, voiceStyle),
7072
+ selectedVoice: voiceCatalog?.find((voice) => voice.name === selectedElement.voice),
7073
+ readOnly,
7074
+ locale,
7075
+ onChange: (value) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "voice", value))
7076
+ }) : /* @__PURE__ */ React.createElement(
6623
7077
  "input",
6624
7078
  {
7079
+ id: "ssml-visual-turn-voice",
6625
7080
  value: selectedElement.voice ?? "",
6626
7081
  readOnly,
6627
7082
  onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "voice", event.target.value))
6628
7083
  }
6629
- )), /* @__PURE__ */ React.createElement("label", null, "Speaker", /* @__PURE__ */ React.createElement(
7084
+ )), /* @__PURE__ */ React.createElement("label", null, locale === "ja" ? "\u8A71\u8005" : "Speaker", /* @__PURE__ */ React.createElement(
6630
7085
  "input",
6631
7086
  {
6632
7087
  value: selectedElement.speaker ?? "",
6633
7088
  readOnly,
6634
7089
  onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "speaker", event.target.value))
6635
7090
  }
6636
- )), /* @__PURE__ */ React.createElement("label", null, "Turn text", /* @__PURE__ */ React.createElement(
7091
+ )), /* @__PURE__ */ React.createElement("label", null, locale === "ja" ? "\u767A\u8A71\u30C6\u30AD\u30B9\u30C8" : "Turn text", /* @__PURE__ */ React.createElement(
6637
7092
  "textarea",
6638
7093
  {
6639
7094
  value: selectedElement.children?.filter((child) => typeof child === "string").join("") ?? "",
@@ -6646,28 +7101,28 @@ function VisualSsmlEditor({
6646
7101
  )
6647
7102
  )
6648
7103
  }
6649
- ))) : selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:backgroundaudio" ? /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, "Background audio"), /* @__PURE__ */ React.createElement("label", null, "Source URL", /* @__PURE__ */ React.createElement(
7104
+ ))) : selectedElement && isElement(selectedElement) && selectedElement.type === "mstts:backgroundaudio" ? /* @__PURE__ */ React.createElement("fieldset", null, /* @__PURE__ */ React.createElement("legend", null, localizedElementLabel(selectedElement, locale)), /* @__PURE__ */ React.createElement("label", null, locale === "ja" ? "\u97F3\u58F0 URL" : "Source URL", /* @__PURE__ */ React.createElement(
6650
7105
  "input",
6651
7106
  {
6652
7107
  value: selectedElement.src ?? "",
6653
7108
  readOnly,
6654
7109
  onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "src", event.target.value))
6655
7110
  }
6656
- )), /* @__PURE__ */ React.createElement("label", null, "Volume", /* @__PURE__ */ React.createElement(
7111
+ )), /* @__PURE__ */ React.createElement("label", null, locale === "ja" ? "\u97F3\u91CF" : "Volume", /* @__PURE__ */ React.createElement(
6657
7112
  "input",
6658
7113
  {
6659
7114
  value: selectedElement.volume === void 0 ? "" : String(selectedElement.volume),
6660
7115
  readOnly,
6661
7116
  onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "volume", event.target.value))
6662
7117
  }
6663
- )), /* @__PURE__ */ React.createElement("label", null, "Fade in", /* @__PURE__ */ React.createElement(
7118
+ )), /* @__PURE__ */ React.createElement("label", null, locale === "ja" ? "\u30D5\u30A7\u30FC\u30C9\u30A4\u30F3" : "Fade in", /* @__PURE__ */ React.createElement(
6664
7119
  "input",
6665
7120
  {
6666
7121
  value: selectedElement.fadeIn === void 0 ? "" : String(selectedElement.fadeIn),
6667
7122
  readOnly,
6668
7123
  onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "fadeIn", event.target.value))
6669
7124
  }
6670
- )), /* @__PURE__ */ React.createElement("label", null, "Fade out", /* @__PURE__ */ React.createElement(
7125
+ )), /* @__PURE__ */ React.createElement("label", null, locale === "ja" ? "\u30D5\u30A7\u30FC\u30C9\u30A2\u30A6\u30C8" : "Fade out", /* @__PURE__ */ React.createElement(
6671
7126
  "input",
6672
7127
  {
6673
7128
  value: selectedElement.fadeOut === void 0 ? "" : String(selectedElement.fadeOut),
@@ -6681,7 +7136,14 @@ function VisualSsmlEditor({
6681
7136
  element: selectedElement,
6682
7137
  path: selectedPath ?? [],
6683
7138
  readOnly,
6684
- commit
7139
+ commit,
7140
+ locale,
7141
+ customInspector: customInspectors?.[elementLabel(selectedElement)] ?? customInspectors?.[selectedElement.type],
7142
+ renderVoiceSelector,
7143
+ voiceCatalog,
7144
+ voiceLocale,
7145
+ voiceRegion,
7146
+ voiceStyle
6685
7147
  }
6686
7148
  ) : selectedLeaf ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, "Text", /* @__PURE__ */ React.createElement(
6687
7149
  "textarea",
@@ -7299,6 +7761,12 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
7299
7761
  insertionGroups,
7300
7762
  customInsertions,
7301
7763
  additionalInsertions,
7764
+ customInspectors,
7765
+ renderVoiceSelector,
7766
+ voiceCatalog,
7767
+ voiceLocale,
7768
+ voiceRegion,
7769
+ voiceStyle,
7302
7770
  emotionStyles,
7303
7771
  className,
7304
7772
  style,
@@ -7690,7 +8158,14 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
7690
8158
  document: draftDocument,
7691
8159
  readOnly: isReadOnly,
7692
8160
  onChange: commit,
7693
- onPreviewSelection
8161
+ onPreviewSelection,
8162
+ locale: localeProp ?? languageProp,
8163
+ customInspectors,
8164
+ renderVoiceSelector,
8165
+ voiceCatalog,
8166
+ voiceLocale,
8167
+ voiceRegion,
8168
+ voiceStyle
7694
8169
  }
7695
8170
  )) : /* @__PURE__ */ React.createElement("div", { style: { ...editorStyles.editor, minHeight: editorMinHeight } }, /* @__PURE__ */ React.createElement(
7696
8171
  import_react5.default,
@@ -7751,6 +8226,7 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
7751
8226
  0 && (module.exports = {
7752
8227
  EDITOR_COPY,
7753
8228
  INLINE_BADGE_COPY,
8229
+ SSML_ELEMENT_COPY,
7754
8230
  SSML_HOVER_COPY,
7755
8231
  SSML_INSERTIONS,
7756
8232
  SsmlEditor,