ssml-builder-js 2.11.0 → 2.13.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
  }
@@ -2850,6 +3069,7 @@ function tokenizeElements(source) {
2850
3069
  attributes,
2851
3070
  childElementIndex,
2852
3071
  end,
3072
+ depth: openElements.length + 1,
2853
3073
  name: tokenName,
2854
3074
  parentName: parent?.name,
2855
3075
  parentVoiceName,
@@ -3030,13 +3250,18 @@ function validateAudioSource(token, source, diagnostics, options, elementName2)
3030
3250
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must be an absolute HTTP(S) URL.`);
3031
3251
  return;
3032
3252
  }
3253
+ if (parsed.username || parsed.password)
3254
+ addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must not contain URL credentials.`);
3033
3255
  if (parsed.protocol !== "https:" && !(options.allowHttpAudio && parsed.protocol === "http:"))
3034
3256
  addDiagnostic(diagnostics, source, token.start, `<${elementName2} src> must use HTTPS.`);
3035
3257
  const isAllowedOrigin = options.allowedAudioOrigins?.some((allowedOrigin) => {
3036
3258
  try {
3037
- return new URL(allowedOrigin).origin === parsed.origin;
3259
+ const configured = new URL(allowedOrigin);
3260
+ if (configured.protocol !== "https:" && configured.protocol !== "http:" || configured.username || configured.password || configured.pathname !== "/" || configured.search || configured.hash)
3261
+ return false;
3262
+ return configured.origin === parsed.origin;
3038
3263
  } catch {
3039
- return allowedOrigin === parsed.origin;
3264
+ return false;
3040
3265
  }
3041
3266
  }) ?? false;
3042
3267
  if (options.allowedAudioOrigins && !isAllowedOrigin)
@@ -3228,7 +3453,7 @@ function validateElement(token, source, diagnostics, voiceName, options, voiceCa
3228
3453
  addDiagnostic(diagnostics, source, token.start, "<mstts:backgroundaudio> must be self-closing.");
3229
3454
  }
3230
3455
  }
3231
- function validateAzureSsml(ssml, options = {}) {
3456
+ function validateAzureSsmlStatic(ssml, options = {}) {
3232
3457
  const diagnostics = [];
3233
3458
  if (typeof ssml !== "string") {
3234
3459
  return [
@@ -3244,6 +3469,9 @@ function validateAzureSsml(ssml, options = {}) {
3244
3469
  const maxLength = options.maxLength ?? 1e4;
3245
3470
  if (ssml.length > maxLength)
3246
3471
  addDiagnostic(diagnostics, ssml, maxLength, `SSML exceeds the maximum length of ${maxLength} characters.`);
3472
+ if (options.maxXmlDepth !== void 0 && (!Number.isInteger(options.maxXmlDepth) || options.maxXmlDepth <= 0)) {
3473
+ addDiagnostic(diagnostics, ssml, 0, "maxXmlDepth must be a positive integer.");
3474
+ }
3247
3475
  try {
3248
3476
  parseSsml(ssml);
3249
3477
  } catch (error) {
@@ -3253,6 +3481,18 @@ function validateAzureSsml(ssml, options = {}) {
3253
3481
  return diagnostics;
3254
3482
  }
3255
3483
  const tokens = tokenizeElements(ssml);
3484
+ if (options.maxXmlDepth !== void 0) {
3485
+ for (const token of tokens) {
3486
+ if (token.depth > options.maxXmlDepth) {
3487
+ addDiagnostic(
3488
+ diagnostics,
3489
+ ssml,
3490
+ token.start,
3491
+ `XML nesting depth ${token.depth} exceeds the configured maximum of ${options.maxXmlDepth}.`
3492
+ );
3493
+ }
3494
+ }
3495
+ }
3256
3496
  const speak = tokens.find((token) => token.name.toLowerCase() === "speak");
3257
3497
  const voices = tokens.filter((token) => token.name.toLowerCase() === "voice");
3258
3498
  const backgroundAudioTokens = tokens.filter((token) => token.name.toLowerCase() === "mstts:backgroundaudio");
@@ -3337,6 +3577,51 @@ function validateAzureSsml(ssml, options = {}) {
3337
3577
  }
3338
3578
  return diagnostics;
3339
3579
  }
3580
+ function urlAttributes(token) {
3581
+ const tag = canonicalTagName(token.name);
3582
+ const attributes = tag === "audio" || tag === "mstts:backgroundaudio" ? ["src"] : tag === "lexicon" ? ["uri"] : tag === "mstts:voiceconversion" ? ["url"] : [];
3583
+ return attributes.flatMap((attribute) => {
3584
+ const value = attr(token, attribute);
3585
+ return value === void 0 ? [] : [{ attribute, value }];
3586
+ });
3587
+ }
3588
+ function validateAzureSsml(ssml, options = {}) {
3589
+ const diagnostics = validateAzureSsmlStatic(ssml, options);
3590
+ const validator = options.urlValidator ?? options.customUrlValidator;
3591
+ if (!validator || typeof ssml !== "string") return diagnostics;
3592
+ let tokens;
3593
+ try {
3594
+ tokens = tokenizeElements(ssml);
3595
+ } catch {
3596
+ return diagnostics;
3597
+ }
3598
+ const checks = tokens.flatMap(
3599
+ (token) => urlAttributes(token).map(async ({ attribute, value }) => {
3600
+ try {
3601
+ const result = await validator(value, { tag: token.name, attribute });
3602
+ const valid = typeof result === "boolean" ? result : result.valid;
3603
+ if (!valid) {
3604
+ const reason = typeof result === "boolean" ? void 0 : result.reason;
3605
+ addDiagnostic(
3606
+ diagnostics,
3607
+ ssml,
3608
+ token.start,
3609
+ `<${token.name} ${attribute}> was rejected by the custom URL validator${reason ? `: ${reason}` : "."}`
3610
+ );
3611
+ }
3612
+ } catch (error) {
3613
+ const reason = error instanceof Error ? error.message : String(error);
3614
+ addDiagnostic(
3615
+ diagnostics,
3616
+ ssml,
3617
+ token.start,
3618
+ `<${token.name} ${attribute}> could not be validated by the custom URL validator: ${reason}`
3619
+ );
3620
+ }
3621
+ })
3622
+ );
3623
+ return Promise.all(checks).then(() => diagnostics);
3624
+ }
3340
3625
  var AZURE_VOICE_CATALOG_METADATA = {
3341
3626
  apiVersion: "2025-10-01",
3342
3627
  generatedAt: "2026-08-28T00:00:00.000Z",
@@ -6383,6 +6668,210 @@ function updateOptionalElementProperty(document2, path, property, value) {
6383
6668
  return next;
6384
6669
  });
6385
6670
  }
6671
+ var VISUAL_ELEMENT_FIELDS = {
6672
+ voice: [
6673
+ { key: "name", label: "Voice name" },
6674
+ { key: "effect", label: "Effect" }
6675
+ ],
6676
+ prosody: [
6677
+ { key: "rate", label: "Rate" },
6678
+ { key: "pitch", label: "Pitch" },
6679
+ { key: "volume", label: "Volume" },
6680
+ { key: "contour", label: "Contour" },
6681
+ { key: "range", label: "Range" }
6682
+ ],
6683
+ "express-as": [
6684
+ { key: "style", label: "Style" },
6685
+ { key: "styleDegree", label: "Style degree" },
6686
+ { key: "role", label: "Role" }
6687
+ ],
6688
+ expressAs: [
6689
+ { key: "style", label: "Style" },
6690
+ { key: "styleDegree", label: "Style degree" },
6691
+ { key: "role", label: "Role" }
6692
+ ],
6693
+ "mstts:express-as": [
6694
+ { key: "style", label: "Style" },
6695
+ { key: "styleDegree", label: "Style degree" },
6696
+ { key: "role", label: "Role" }
6697
+ ],
6698
+ "say-as": [
6699
+ { key: "interpretAs", label: "Interpret as" },
6700
+ { key: "format", label: "Format" },
6701
+ { key: "detail", label: "Detail" }
6702
+ ],
6703
+ sayAs: [
6704
+ { key: "interpretAs", label: "Interpret as" },
6705
+ { key: "format", label: "Format" },
6706
+ { key: "detail", label: "Detail" }
6707
+ ],
6708
+ phoneme: [
6709
+ { key: "alphabet", label: "Alphabet" },
6710
+ { key: "ph", label: "Pronunciation" }
6711
+ ],
6712
+ emphasis: [{ key: "level", label: "Level" }],
6713
+ audio: [
6714
+ { key: "src", label: "Source URL" },
6715
+ { key: "desc", label: "Description" },
6716
+ { key: "clipBegin", label: "Clip begin" },
6717
+ { key: "clipEnd", label: "Clip end" },
6718
+ { key: "speed", label: "Speed" },
6719
+ { key: "repeatCount", label: "Repeat count" },
6720
+ { key: "repeatDuration", label: "Repeat duration" },
6721
+ { key: "soundLevel", label: "Sound level" }
6722
+ ],
6723
+ mark: [{ key: "name", label: "Mark name" }],
6724
+ bookmark: [{ key: "mark", label: "Bookmark name" }],
6725
+ sub: [{ key: "alias", label: "Alias" }],
6726
+ lang: [{ key: "lang", label: "Language" }],
6727
+ lexicon: [{ key: "uri", label: "Lexicon URI" }],
6728
+ "mstts:silence": [
6729
+ { key: "typeValue", label: "Silence type" },
6730
+ { key: "value", label: "Value" }
6731
+ ],
6732
+ silence: [
6733
+ { key: "typeValue", label: "Silence type" },
6734
+ { key: "value", label: "Value" }
6735
+ ],
6736
+ "mstts:audioduration": [{ key: "value", label: "Duration" }],
6737
+ "mstts:ttsembedding": [{ key: "speakerProfileId", label: "Speaker profile ID" }],
6738
+ "mstts:embedding": [
6739
+ { key: "id", label: "Embedding ID" },
6740
+ { key: "speakerProfileId", label: "Speaker profile ID" }
6741
+ ],
6742
+ "mstts:voiceconversion": [
6743
+ { key: "url", label: "Source URL" },
6744
+ { key: "profile", label: "Profile" },
6745
+ { key: "speakerProfileId", label: "Speaker profile ID" }
6746
+ ]
6747
+ };
6748
+ function getElementFields(element) {
6749
+ return VISUAL_ELEMENT_FIELDS[elementLabel(element)] ?? [];
6750
+ }
6751
+ function localizedElementLabel(element, locale) {
6752
+ return SSML_ELEMENT_COPY[locale][elementLabel(element)]?.label ?? elementLabel(element);
6753
+ }
6754
+ function filteredVoices(voiceCatalog, locale, region, style) {
6755
+ return (voiceCatalog ?? []).filter((voice) => {
6756
+ if (locale && voice.locale.toLowerCase() !== locale.toLowerCase()) return false;
6757
+ if (region && voice.region && voice.region.toLowerCase() !== region.toLowerCase()) return false;
6758
+ if (style && !voice.styles?.some((candidate) => candidate.toLowerCase() === style.toLowerCase())) return false;
6759
+ return true;
6760
+ });
6761
+ }
6762
+ function DefaultVoiceSelector({ value, voices, readOnly, locale, onChange }) {
6763
+ return /* @__PURE__ */ React.createElement(
6764
+ "select",
6765
+ {
6766
+ "aria-label": locale === "ja" ? "\u97F3\u58F0" : "Voice",
6767
+ value,
6768
+ disabled: readOnly,
6769
+ onChange: (event) => onChange(event.target.value)
6770
+ },
6771
+ /* @__PURE__ */ React.createElement("option", { value: "" }, locale === "ja" ? "\u97F3\u58F0\u3092\u9078\u629E" : "Select a voice"),
6772
+ voices.map((voice) => /* @__PURE__ */ React.createElement("option", { key: voice.name, value: voice.name }, voice.name, voice.preview || voice.status === "preview" ? " (preview)" : ""))
6773
+ );
6774
+ }
6775
+ function VisualElementInspector({
6776
+ document: document2,
6777
+ element,
6778
+ path,
6779
+ readOnly,
6780
+ commit,
6781
+ locale,
6782
+ customInspector,
6783
+ renderVoiceSelector,
6784
+ voiceCatalog,
6785
+ voiceLocale,
6786
+ voiceRegion,
6787
+ voiceStyle
6788
+ }) {
6789
+ if (customInspector) {
6790
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, customInspector({ document: document2, element, path, readOnly, onChange: commit, locale }));
6791
+ }
6792
+ const fields = getElementFields(element);
6793
+ const availableVoices = filteredVoices(voiceCatalog, voiceLocale, voiceRegion, voiceStyle);
6794
+ const renderSelector = renderVoiceSelector ?? DefaultVoiceSelector;
6795
+ 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) => {
6796
+ const value = element[field.key];
6797
+ const inputValue = value === void 0 ? "" : String(value);
6798
+ const inputId = `ssml-visual-${field.key}`;
6799
+ return /* @__PURE__ */ React.createElement("label", { key: field.key, htmlFor: inputId }, locale === "ja" ? {
6800
+ name: "\u540D\u524D",
6801
+ effect: "\u52B9\u679C",
6802
+ rate: "\u901F\u5EA6",
6803
+ pitch: "\u30D4\u30C3\u30C1",
6804
+ volume: "\u97F3\u91CF",
6805
+ contour: "\u30D4\u30C3\u30C1\u66F2\u7DDA",
6806
+ range: "\u7BC4\u56F2",
6807
+ style: "\u30B9\u30BF\u30A4\u30EB",
6808
+ styleDegree: "\u30B9\u30BF\u30A4\u30EB\u5F37\u5EA6",
6809
+ role: "\u5F79\u5272",
6810
+ interpretAs: "\u89E3\u91C8",
6811
+ alphabet: "\u30A2\u30EB\u30D5\u30A1\u30D9\u30C3\u30C8",
6812
+ ph: "\u767A\u97F3",
6813
+ level: "\u5F37\u8ABF\u30EC\u30D9\u30EB",
6814
+ src: "\u97F3\u58F0 URL",
6815
+ desc: "\u8AAC\u660E",
6816
+ clipBegin: "\u958B\u59CB\u4F4D\u7F6E",
6817
+ clipEnd: "\u7D42\u4E86\u4F4D\u7F6E",
6818
+ speed: "\u901F\u5EA6",
6819
+ repeatCount: "\u7E70\u308A\u8FD4\u3057\u56DE\u6570",
6820
+ repeatDuration: "\u7E70\u308A\u8FD4\u3057\u6642\u9593",
6821
+ soundLevel: "\u97F3\u91CF\u30EC\u30D9\u30EB",
6822
+ mark: "\u30D6\u30C3\u30AF\u30DE\u30FC\u30AF\u540D",
6823
+ alias: "\u5225\u540D",
6824
+ lang: "\u8A00\u8A9E",
6825
+ uri: "\u8F9E\u66F8 URI",
6826
+ typeValue: "\u7121\u97F3\u306E\u7A2E\u985E",
6827
+ value: "\u5024",
6828
+ id: "\u57CB\u3081\u8FBC\u307F ID",
6829
+ speakerProfileId: "\u8A71\u8005\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB ID",
6830
+ url: "\u97F3\u58F0\u5909\u63DB URL",
6831
+ profile: "\u30D7\u30ED\u30D5\u30A1\u30A4\u30EB"
6832
+ }[field.key] ?? field.label : field.label, field.key === "name" && elementLabel(element) === "voice" && voiceCatalog ? renderSelector({
6833
+ value: inputValue,
6834
+ voices: availableVoices,
6835
+ readOnly,
6836
+ locale,
6837
+ onChange: (value2) => commit(updateOptionalElementProperty(document2, path, field.key, value2))
6838
+ }) : field.multiline ? /* @__PURE__ */ React.createElement(
6839
+ "textarea",
6840
+ {
6841
+ id: inputId,
6842
+ value: inputValue,
6843
+ readOnly,
6844
+ onChange: (event) => commit(updateOptionalElementProperty(document2, path, field.key, event.target.value))
6845
+ }
6846
+ ) : /* @__PURE__ */ React.createElement(
6847
+ "input",
6848
+ {
6849
+ id: inputId,
6850
+ value: inputValue,
6851
+ readOnly,
6852
+ onChange: (event) => commit(updateOptionalElementProperty(document2, path, field.key, event.target.value))
6853
+ }
6854
+ ));
6855
+ }));
6856
+ }
6857
+ function getElementAncestors(nodes, path, ancestors = []) {
6858
+ const [index, ...rest] = path;
6859
+ const node = nodes[index];
6860
+ if (!node || !isElement(node)) return ancestors;
6861
+ if (rest.length === 0) return ancestors;
6862
+ return getElementAncestors(node.children ?? [], rest, [...ancestors, node]);
6863
+ }
6864
+ function buildVisualPreview(document2, leaf, selection) {
6865
+ const start = Math.max(0, Math.min(selection.start, leaf.value.length));
6866
+ const end = Math.max(start, Math.min(selection.end, leaf.value.length));
6867
+ const value = start === end ? leaf.value : leaf.value.slice(start, end);
6868
+ const ancestors = getElementAncestors(document2.children ?? [], leaf.path);
6869
+ const children = ancestors.reduceRight(
6870
+ (current, ancestor) => [{ ...ancestor, children: current }],
6871
+ [value]
6872
+ );
6873
+ return buildSsml({ ...document2, children });
6874
+ }
6386
6875
  function addDialogTurn(document2, path) {
6387
6876
  return updateElementAtPath(document2, path, (element) => ({
6388
6877
  ...element,
@@ -6437,13 +6926,21 @@ function VisualSsmlEditor({
6437
6926
  document: document2,
6438
6927
  readOnly = false,
6439
6928
  onChange,
6440
- onPreviewSelection
6929
+ onPreviewSelection,
6930
+ locale = "en",
6931
+ customInspectors,
6932
+ renderVoiceSelector,
6933
+ voiceCatalog,
6934
+ voiceLocale,
6935
+ voiceRegion,
6936
+ voiceStyle
6441
6937
  }) {
6442
6938
  const [selectedPath, setSelectedPath] = (0, import_react3.useState)(null);
6443
6939
  const [selection, setSelection] = (0, import_react3.useState)({ start: 0, end: 0 });
6444
6940
  const textLeaves = (0, import_react3.useMemo)(() => collectTextLeaves(document2.children ?? []), [document2]);
6445
6941
  const selectedLeaf = textLeaves.find((leaf) => leaf.path.join(".") === selectedPath?.join(".")) ?? textLeaves[0];
6446
6942
  const selectedElement = selectedPath ? getNodeAtPath(document2.children ?? [], selectedPath) : void 0;
6943
+ const selectedCustomInspector = selectedElement && isElement(selectedElement) ? customInspectors?.[elementLabel(selectedElement)] ?? customInspectors?.[selectedElement.type] : void 0;
6447
6944
  const diagnostics = (0, import_react3.useMemo)(() => validateAzureSsml(buildSsml(document2)), [document2]);
6448
6945
  const commit = (nextDocument) => onChange?.(nextDocument);
6449
6946
  const applyWrapper = (tag, attributes) => {
@@ -6461,29 +6958,43 @@ function VisualSsmlEditor({
6461
6958
  selectedPath,
6462
6959
  onSelect: setSelectedPath
6463
6960
  }
6464
- )))), /* @__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(
6961
+ )))), /* @__PURE__ */ React.createElement("div", { className: "ssml-editor-visual-form" }, selectedCustomInspector && selectedElement && isElement(selectedElement) ? selectedCustomInspector({
6962
+ document: document2,
6963
+ element: selectedElement,
6964
+ path: selectedPath ?? [],
6965
+ readOnly,
6966
+ onChange: commit,
6967
+ locale
6968
+ }) : 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(
6465
6969
  "button",
6466
6970
  {
6467
6971
  type: "button",
6468
6972
  disabled: readOnly,
6469
6973
  onClick: () => commit(addDialogTurn(document2, selectedPath ?? []))
6470
6974
  },
6471
- "Add turn"
6472
- )) : 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(
6975
+ locale === "ja" ? "\u30BF\u30FC\u30F3\u3092\u8FFD\u52A0" : "Add turn"
6976
+ )) : 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)({
6977
+ value: selectedElement.voice ?? "",
6978
+ voices: filteredVoices(voiceCatalog, voiceLocale, voiceRegion, voiceStyle),
6979
+ readOnly,
6980
+ locale,
6981
+ onChange: (value) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "voice", value))
6982
+ }) : /* @__PURE__ */ React.createElement(
6473
6983
  "input",
6474
6984
  {
6985
+ id: "ssml-visual-turn-voice",
6475
6986
  value: selectedElement.voice ?? "",
6476
6987
  readOnly,
6477
6988
  onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "voice", event.target.value))
6478
6989
  }
6479
- )), /* @__PURE__ */ React.createElement("label", null, "Speaker", /* @__PURE__ */ React.createElement(
6990
+ )), /* @__PURE__ */ React.createElement("label", null, locale === "ja" ? "\u8A71\u8005" : "Speaker", /* @__PURE__ */ React.createElement(
6480
6991
  "input",
6481
6992
  {
6482
6993
  value: selectedElement.speaker ?? "",
6483
6994
  readOnly,
6484
6995
  onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "speaker", event.target.value))
6485
6996
  }
6486
- )), /* @__PURE__ */ React.createElement("label", null, "Turn text", /* @__PURE__ */ React.createElement(
6997
+ )), /* @__PURE__ */ React.createElement("label", null, locale === "ja" ? "\u767A\u8A71\u30C6\u30AD\u30B9\u30C8" : "Turn text", /* @__PURE__ */ React.createElement(
6487
6998
  "textarea",
6488
6999
  {
6489
7000
  value: selectedElement.children?.filter((child) => typeof child === "string").join("") ?? "",
@@ -6496,35 +7007,51 @@ function VisualSsmlEditor({
6496
7007
  )
6497
7008
  )
6498
7009
  }
6499
- ))) : 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(
7010
+ ))) : 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(
6500
7011
  "input",
6501
7012
  {
6502
7013
  value: selectedElement.src ?? "",
6503
7014
  readOnly,
6504
7015
  onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "src", event.target.value))
6505
7016
  }
6506
- )), /* @__PURE__ */ React.createElement("label", null, "Volume", /* @__PURE__ */ React.createElement(
7017
+ )), /* @__PURE__ */ React.createElement("label", null, locale === "ja" ? "\u97F3\u91CF" : "Volume", /* @__PURE__ */ React.createElement(
6507
7018
  "input",
6508
7019
  {
6509
7020
  value: selectedElement.volume === void 0 ? "" : String(selectedElement.volume),
6510
7021
  readOnly,
6511
7022
  onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "volume", event.target.value))
6512
7023
  }
6513
- )), /* @__PURE__ */ React.createElement("label", null, "Fade in", /* @__PURE__ */ React.createElement(
7024
+ )), /* @__PURE__ */ React.createElement("label", null, locale === "ja" ? "\u30D5\u30A7\u30FC\u30C9\u30A4\u30F3" : "Fade in", /* @__PURE__ */ React.createElement(
6514
7025
  "input",
6515
7026
  {
6516
7027
  value: selectedElement.fadeIn === void 0 ? "" : String(selectedElement.fadeIn),
6517
7028
  readOnly,
6518
7029
  onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "fadeIn", event.target.value))
6519
7030
  }
6520
- )), /* @__PURE__ */ React.createElement("label", null, "Fade out", /* @__PURE__ */ React.createElement(
7031
+ )), /* @__PURE__ */ React.createElement("label", null, locale === "ja" ? "\u30D5\u30A7\u30FC\u30C9\u30A2\u30A6\u30C8" : "Fade out", /* @__PURE__ */ React.createElement(
6521
7032
  "input",
6522
7033
  {
6523
7034
  value: selectedElement.fadeOut === void 0 ? "" : String(selectedElement.fadeOut),
6524
7035
  readOnly,
6525
7036
  onChange: (event) => commit(updateOptionalElementProperty(document2, selectedPath ?? [], "fadeOut", event.target.value))
6526
7037
  }
6527
- ))) : selectedLeaf ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, "Text", /* @__PURE__ */ React.createElement(
7038
+ ))) : selectedElement && isElement(selectedElement) ? /* @__PURE__ */ React.createElement(
7039
+ VisualElementInspector,
7040
+ {
7041
+ document: document2,
7042
+ element: selectedElement,
7043
+ path: selectedPath ?? [],
7044
+ readOnly,
7045
+ commit,
7046
+ locale,
7047
+ customInspector: customInspectors?.[elementLabel(selectedElement)] ?? customInspectors?.[selectedElement.type],
7048
+ renderVoiceSelector,
7049
+ voiceCatalog,
7050
+ voiceLocale,
7051
+ voiceRegion,
7052
+ voiceStyle
7053
+ }
7054
+ ) : selectedLeaf ? /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("label", null, "Text", /* @__PURE__ */ React.createElement(
6528
7055
  "textarea",
6529
7056
  {
6530
7057
  value: selectedLeaf.value,
@@ -6552,7 +7079,7 @@ function VisualSsmlEditor({
6552
7079
  "button",
6553
7080
  {
6554
7081
  type: "button",
6555
- onClick: () => onPreviewSelection(buildSsml({ ...document2, children: [selectedLeaf.value] }))
7082
+ onClick: () => onPreviewSelection(buildVisualPreview(document2, selectedLeaf, selection))
6556
7083
  },
6557
7084
  "Preview selection"
6558
7085
  ))) : /* @__PURE__ */ React.createElement("p", null, "Select an element or text node to edit it."))));
@@ -7140,6 +7667,12 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
7140
7667
  insertionGroups,
7141
7668
  customInsertions,
7142
7669
  additionalInsertions,
7670
+ customInspectors,
7671
+ renderVoiceSelector,
7672
+ voiceCatalog,
7673
+ voiceLocale,
7674
+ voiceRegion,
7675
+ voiceStyle,
7143
7676
  emotionStyles,
7144
7677
  className,
7145
7678
  style,
@@ -7531,7 +8064,14 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
7531
8064
  document: draftDocument,
7532
8065
  readOnly: isReadOnly,
7533
8066
  onChange: commit,
7534
- onPreviewSelection
8067
+ onPreviewSelection,
8068
+ locale: localeProp ?? languageProp,
8069
+ customInspectors,
8070
+ renderVoiceSelector,
8071
+ voiceCatalog,
8072
+ voiceLocale,
8073
+ voiceRegion,
8074
+ voiceStyle
7535
8075
  }
7536
8076
  )) : /* @__PURE__ */ React.createElement("div", { style: { ...editorStyles.editor, minHeight: editorMinHeight } }, /* @__PURE__ */ React.createElement(
7537
8077
  import_react5.default,
@@ -7592,6 +8132,7 @@ var SsmlEditor = (0, import_react4.forwardRef)(function SsmlEditor2({
7592
8132
  0 && (module.exports = {
7593
8133
  EDITOR_COPY,
7594
8134
  INLINE_BADGE_COPY,
8135
+ SSML_ELEMENT_COPY,
7595
8136
  SSML_HOVER_COPY,
7596
8137
  SSML_INSERTIONS,
7597
8138
  SsmlEditor,