@supertone/supertone 0.1.1 → 0.1.3

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.
Files changed (92) hide show
  1. package/README.md +119 -69
  2. package/custom_test/realtime_tts_player.ts +177 -12
  3. package/custom_test/test_pronunciation_dictionary.ts +227 -0
  4. package/custom_test/test_real_api.ts +1677 -162
  5. package/custom_test/test_text_utils_chunk_text_punctuation.ts +55 -0
  6. package/dist/commonjs/lib/config.d.ts +2 -2
  7. package/dist/commonjs/lib/config.d.ts.map +1 -1
  8. package/dist/commonjs/lib/config.js +2 -2
  9. package/dist/commonjs/lib/config.js.map +1 -1
  10. package/dist/commonjs/lib/custom_utils/index.d.ts +1 -0
  11. package/dist/commonjs/lib/custom_utils/index.d.ts.map +1 -1
  12. package/dist/commonjs/lib/custom_utils/index.js +5 -1
  13. package/dist/commonjs/lib/custom_utils/index.js.map +1 -1
  14. package/dist/commonjs/lib/custom_utils/pronunciation_utils.d.ts +24 -0
  15. package/dist/commonjs/lib/custom_utils/pronunciation_utils.d.ts.map +1 -0
  16. package/dist/commonjs/lib/custom_utils/pronunciation_utils.js +145 -0
  17. package/dist/commonjs/lib/custom_utils/pronunciation_utils.js.map +1 -0
  18. package/dist/commonjs/lib/custom_utils/text_utils.d.ts +8 -1
  19. package/dist/commonjs/lib/custom_utils/text_utils.d.ts.map +1 -1
  20. package/dist/commonjs/lib/custom_utils/text_utils.js +125 -7
  21. package/dist/commonjs/lib/custom_utils/text_utils.js.map +1 -1
  22. package/dist/commonjs/models/apiconverttexttospeechusingcharacterrequest.d.ts +92 -1
  23. package/dist/commonjs/models/apiconverttexttospeechusingcharacterrequest.d.ts.map +1 -1
  24. package/dist/commonjs/models/apiconverttexttospeechusingcharacterrequest.js +48 -3
  25. package/dist/commonjs/models/apiconverttexttospeechusingcharacterrequest.js.map +1 -1
  26. package/dist/commonjs/models/predictttsdurationusingcharacterrequest.d.ts +92 -1
  27. package/dist/commonjs/models/predictttsdurationusingcharacterrequest.d.ts.map +1 -1
  28. package/dist/commonjs/models/predictttsdurationusingcharacterrequest.js +46 -3
  29. package/dist/commonjs/models/predictttsdurationusingcharacterrequest.js.map +1 -1
  30. package/dist/commonjs/sdk/texttospeech.d.ts +17 -6
  31. package/dist/commonjs/sdk/texttospeech.d.ts.map +1 -1
  32. package/dist/commonjs/sdk/texttospeech.js +48 -25
  33. package/dist/commonjs/sdk/texttospeech.js.map +1 -1
  34. package/dist/esm/lib/config.d.ts +2 -2
  35. package/dist/esm/lib/config.d.ts.map +1 -1
  36. package/dist/esm/lib/config.js +2 -2
  37. package/dist/esm/lib/config.js.map +1 -1
  38. package/dist/esm/lib/custom_utils/index.d.ts +1 -0
  39. package/dist/esm/lib/custom_utils/index.d.ts.map +1 -1
  40. package/dist/esm/lib/custom_utils/index.js +2 -0
  41. package/dist/esm/lib/custom_utils/index.js.map +1 -1
  42. package/dist/esm/lib/custom_utils/pronunciation_utils.d.ts +24 -0
  43. package/dist/esm/lib/custom_utils/pronunciation_utils.d.ts.map +1 -0
  44. package/dist/esm/lib/custom_utils/pronunciation_utils.js +140 -0
  45. package/dist/esm/lib/custom_utils/pronunciation_utils.js.map +1 -0
  46. package/dist/esm/lib/custom_utils/text_utils.d.ts +8 -1
  47. package/dist/esm/lib/custom_utils/text_utils.d.ts.map +1 -1
  48. package/dist/esm/lib/custom_utils/text_utils.js +125 -7
  49. package/dist/esm/lib/custom_utils/text_utils.js.map +1 -1
  50. package/dist/esm/models/apiconverttexttospeechusingcharacterrequest.d.ts +92 -1
  51. package/dist/esm/models/apiconverttexttospeechusingcharacterrequest.d.ts.map +1 -1
  52. package/dist/esm/models/apiconverttexttospeechusingcharacterrequest.js +47 -2
  53. package/dist/esm/models/apiconverttexttospeechusingcharacterrequest.js.map +1 -1
  54. package/dist/esm/models/predictttsdurationusingcharacterrequest.d.ts +92 -1
  55. package/dist/esm/models/predictttsdurationusingcharacterrequest.d.ts.map +1 -1
  56. package/dist/esm/models/predictttsdurationusingcharacterrequest.js +45 -2
  57. package/dist/esm/models/predictttsdurationusingcharacterrequest.js.map +1 -1
  58. package/dist/esm/sdk/texttospeech.d.ts +17 -6
  59. package/dist/esm/sdk/texttospeech.d.ts.map +1 -1
  60. package/dist/esm/sdk/texttospeech.js +49 -26
  61. package/dist/esm/sdk/texttospeech.js.map +1 -1
  62. package/examples/custom_voices/create_cloned_voice.ts +4 -3
  63. package/examples/custom_voices/delete_custom_voice.ts +2 -7
  64. package/examples/custom_voices/edit_custom_voice.ts +2 -6
  65. package/examples/custom_voices/get_custom_voice.ts +2 -7
  66. package/examples/custom_voices/list_custom_voices.ts +2 -7
  67. package/examples/custom_voices/search_custom_voices.ts +2 -6
  68. package/examples/text_to_speech/create_speech.ts +3 -8
  69. package/examples/text_to_speech/create_speech_long_text.ts +3 -7
  70. package/examples/text_to_speech/create_speech_with_phonemes.ts +3 -7
  71. package/examples/text_to_speech/create_speech_with_voice_settings.ts +3 -8
  72. package/examples/text_to_speech/predict_duration.ts +3 -7
  73. package/examples/text_to_speech/stream_speech.ts +3 -7
  74. package/examples/text_to_speech/stream_speech_long_text.ts +3 -7
  75. package/examples/text_to_speech/stream_speech_with_phonemes.ts +3 -7
  76. package/examples/text_to_speech/stream_speech_with_voice_settings.ts +3 -7
  77. package/examples/usage/get_credit_balance.ts +2 -6
  78. package/examples/usage/get_usage.ts +2 -6
  79. package/examples/usage/get_voice_usage.ts +2 -7
  80. package/examples/voices/get_voice.ts +2 -6
  81. package/examples/voices/list_voices.ts +2 -6
  82. package/examples/voices/search_voices.ts +2 -7
  83. package/jsr.json +1 -1
  84. package/openapi.json +101 -9
  85. package/package.json +1 -1
  86. package/src/lib/config.ts +41 -41
  87. package/src/lib/custom_utils/index.ts +7 -0
  88. package/src/lib/custom_utils/pronunciation_utils.ts +193 -0
  89. package/src/lib/custom_utils/text_utils.ts +138 -7
  90. package/src/models/apiconverttexttospeechusingcharacterrequest.ts +62 -3
  91. package/src/models/predictttsdurationusingcharacterrequest.ts +64 -3
  92. package/src/sdk/texttospeech.ts +99 -68
@@ -5,15 +5,11 @@
5
5
  * This example demonstrates how to list all available voices.
6
6
  */
7
7
 
8
- import { Supertone } from "../../src/index.js";
8
+ import { Supertone } from "@supertone/supertone";
9
9
  import * as dotenv from "dotenv";
10
- import * as path from "path";
11
- import { fileURLToPath } from "url";
12
10
 
13
11
  // Load environment variables
14
- const __filename = fileURLToPath(import.meta.url);
15
- const __dirname = path.dirname(__filename);
16
- dotenv.config({ path: path.join(__dirname, "../.env") });
12
+ dotenv.config();
17
13
 
18
14
  const API_KEY = process.env.SUPERTONE_API_KEY;
19
15
 
@@ -5,15 +5,11 @@
5
5
  * This example demonstrates how to search for voices by language and gender.
6
6
  */
7
7
 
8
- import { Supertone } from "../../src/index.js";
8
+ import { Supertone } from "@supertone/supertone";
9
9
  import * as dotenv from "dotenv";
10
- import * as path from "path";
11
- import { fileURLToPath } from "url";
12
10
 
13
11
  // Load environment variables
14
- const __filename = fileURLToPath(import.meta.url);
15
- const __dirname = path.dirname(__filename);
16
- dotenv.config({ path: path.join(__dirname, "../.env") });
12
+ dotenv.config();
17
13
 
18
14
  const API_KEY = process.env.SUPERTONE_API_KEY;
19
15
 
@@ -58,4 +54,3 @@ async function main() {
58
54
  }
59
55
 
60
56
  main();
61
-
package/jsr.json CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  {
4
4
  "name": "@supertone/supertone",
5
- "version": "0.1.1",
5
+ "version": "0.1.3",
6
6
  "exports": {
7
7
  ".": "./src/index.ts",
8
8
  "./models/errors": "./src/models/errors/index.ts",
package/openapi.json CHANGED
@@ -1870,8 +1870,26 @@
1870
1870
  "en",
1871
1871
  "ko",
1872
1872
  "ja",
1873
+ "bg",
1874
+ "cs",
1875
+ "da",
1876
+ "el",
1873
1877
  "es",
1874
- "pt"
1878
+ "et",
1879
+ "fi",
1880
+ "hu",
1881
+ "it",
1882
+ "nl",
1883
+ "pl",
1884
+ "pt",
1885
+ "ro",
1886
+ "ar",
1887
+ "de",
1888
+ "fr",
1889
+ "hi",
1890
+ "id",
1891
+ "ru",
1892
+ "vi"
1875
1893
  ]
1876
1894
  },
1877
1895
  "style": {
@@ -1881,6 +1899,12 @@
1881
1899
  "model": {
1882
1900
  "type": "string",
1883
1901
  "description": "The model type to use for the text-to-speech conversion",
1902
+ "enum": [
1903
+ "sona_speech_1",
1904
+ "sona_speech_2",
1905
+ "sona_speech_2t",
1906
+ "supertonic_api_1"
1907
+ ],
1884
1908
  "default": "sona_speech_1"
1885
1909
  },
1886
1910
  "output_format": {
@@ -1948,8 +1972,26 @@
1948
1972
  "en",
1949
1973
  "ko",
1950
1974
  "ja",
1975
+ "bg",
1976
+ "cs",
1977
+ "da",
1978
+ "el",
1951
1979
  "es",
1952
- "pt"
1980
+ "et",
1981
+ "fi",
1982
+ "hu",
1983
+ "it",
1984
+ "nl",
1985
+ "pl",
1986
+ "pt",
1987
+ "ro",
1988
+ "ar",
1989
+ "de",
1990
+ "fr",
1991
+ "hi",
1992
+ "id",
1993
+ "ru",
1994
+ "vi"
1953
1995
  ]
1954
1996
  },
1955
1997
  "style": {
@@ -1959,6 +2001,12 @@
1959
2001
  "model": {
1960
2002
  "type": "string",
1961
2003
  "description": "The model type to use for the text-to-speech conversion",
2004
+ "enum": [
2005
+ "sona_speech_1",
2006
+ "sona_speech_2",
2007
+ "sona_speech_2t",
2008
+ "supertonic_api_1"
2009
+ ],
1962
2010
  "default": "sona_speech_1"
1963
2011
  },
1964
2012
  "output_format": {
@@ -1995,7 +2043,7 @@
1995
2043
  "model": {
1996
2044
  "type": "string",
1997
2045
  "description": "Model of the sample",
1998
- "example": "sona_speech_1"
2046
+ "example": "supertonic_api_1"
1999
2047
  },
2000
2048
  "url": {
2001
2049
  "type": "string",
@@ -2057,9 +2105,29 @@
2057
2105
  "language": {
2058
2106
  "description": "Languages supported by the voice",
2059
2107
  "example": [
2060
- "ko",
2108
+ "ar",
2109
+ "bg",
2110
+ "cs",
2111
+ "da",
2112
+ "de",
2113
+ "el",
2061
2114
  "en",
2062
- "ja"
2115
+ "es",
2116
+ "et",
2117
+ "fi",
2118
+ "fr",
2119
+ "hi",
2120
+ "hu",
2121
+ "id",
2122
+ "it",
2123
+ "ja",
2124
+ "ko",
2125
+ "nl",
2126
+ "pl",
2127
+ "pt",
2128
+ "ro",
2129
+ "ru",
2130
+ "vi"
2063
2131
  ],
2064
2132
  "type": "array",
2065
2133
  "items": {
@@ -2081,7 +2149,9 @@
2081
2149
  "models": {
2082
2150
  "description": "Models available for the voice",
2083
2151
  "example": [
2084
- "sona_speech_1"
2152
+ "sona_speech_1",
2153
+ "sona_speech_2",
2154
+ "supertonic_api_1"
2085
2155
  ],
2086
2156
  "type": "array",
2087
2157
  "items": {
@@ -2187,9 +2257,29 @@
2187
2257
  "language": {
2188
2258
  "description": "Languages supported by the voice",
2189
2259
  "example": [
2190
- "ko",
2260
+ "ar",
2261
+ "bg",
2262
+ "cs",
2263
+ "da",
2264
+ "de",
2265
+ "el",
2191
2266
  "en",
2192
- "ja"
2267
+ "es",
2268
+ "et",
2269
+ "fi",
2270
+ "fr",
2271
+ "hi",
2272
+ "hu",
2273
+ "id",
2274
+ "it",
2275
+ "ja",
2276
+ "ko",
2277
+ "nl",
2278
+ "pl",
2279
+ "pt",
2280
+ "ro",
2281
+ "ru",
2282
+ "vi"
2193
2283
  ],
2194
2284
  "type": "array",
2195
2285
  "items": {
@@ -2211,7 +2301,9 @@
2211
2301
  "models": {
2212
2302
  "description": "Models available for the voice",
2213
2303
  "example": [
2214
- "sona_speech_1"
2304
+ "sona_speech_1",
2305
+ "sona_speech_2",
2306
+ "supertonic_api_1"
2215
2307
  ],
2216
2308
  "type": "array",
2217
2309
  "items": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supertone/supertone",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "author": "Supertone, Pillip Youn",
5
5
  "bugs": {
6
6
  "url": "https://github.com/supertone-inc/supertone-ts/issues"
package/src/lib/config.ts CHANGED
@@ -11,58 +11,58 @@ import { Params, pathToFunc } from "./url.js";
11
11
  * Contains the list of servers available to the SDK
12
12
  */
13
13
  export const ServerList = [
14
- /**
15
- * Production
16
- */
17
- "https://supertoneapi.com",
14
+ /**
15
+ * Production
16
+ */
17
+ "https://supertoneapi.com",
18
18
  ] as const;
19
19
 
20
20
  export type SDKOptions = {
21
- apiKey?: string | (() => Promise<string>) | undefined;
21
+ apiKey?: string | (() => Promise<string>) | undefined;
22
22
 
23
- httpClient?: HTTPClient;
24
- /**
25
- * Allows overriding the default server used by the SDK
26
- */
27
- serverIdx?: number | undefined;
28
- /**
29
- * Allows overriding the default server URL used by the SDK
30
- */
31
- serverURL?: string | undefined;
32
- /**
33
- * Allows overriding the default user agent used by the SDK
34
- */
35
- userAgent?: string | undefined;
36
- /**
37
- * Allows overriding the default retry config used by the SDK
38
- */
39
- retryConfig?: RetryConfig;
40
- timeoutMs?: number;
41
- debugLogger?: Logger;
23
+ httpClient?: HTTPClient;
24
+ /**
25
+ * Allows overriding the default server used by the SDK
26
+ */
27
+ serverIdx?: number | undefined;
28
+ /**
29
+ * Allows overriding the default server URL used by the SDK
30
+ */
31
+ serverURL?: string | undefined;
32
+ /**
33
+ * Allows overriding the default user agent used by the SDK
34
+ */
35
+ userAgent?: string | undefined;
36
+ /**
37
+ * Allows overriding the default retry config used by the SDK
38
+ */
39
+ retryConfig?: RetryConfig;
40
+ timeoutMs?: number;
41
+ debugLogger?: Logger;
42
42
  };
43
43
 
44
44
  export function serverURLFromOptions(options: SDKOptions): URL | null {
45
- let serverURL = options.serverURL;
45
+ let serverURL = options.serverURL;
46
46
 
47
- const params: Params = {};
47
+ const params: Params = {};
48
48
 
49
- if (!serverURL) {
50
- const serverIdx = options.serverIdx ?? 0;
51
- if (serverIdx < 0 || serverIdx >= ServerList.length) {
52
- throw new Error(`Invalid server index ${serverIdx}`);
53
- }
54
- serverURL = ServerList[serverIdx] || "";
55
- }
49
+ if (!serverURL) {
50
+ const serverIdx = options.serverIdx ?? 0;
51
+ if (serverIdx < 0 || serverIdx >= ServerList.length) {
52
+ throw new Error(`Invalid server index ${serverIdx}`);
53
+ }
54
+ serverURL = ServerList[serverIdx] || "";
55
+ }
56
56
 
57
- const u = pathToFunc(serverURL)(params);
58
- return new URL(u);
57
+ const u = pathToFunc(serverURL)(params);
58
+ return new URL(u);
59
59
  }
60
60
 
61
61
  export const SDK_METADATA = {
62
- language: "typescript",
63
- openapiDocVersion: "0.8.69",
64
- sdkVersion: "0.1.1",
65
- genVersion: "2.686.7",
66
- userAgent:
67
- "speakeasy-sdk/typescript 0.1.1 2.686.7 0.8.69 @supertone/supertone",
62
+ language: "typescript",
63
+ openapiDocVersion: "0.8.69",
64
+ sdkVersion: "0.1.3",
65
+ genVersion: "2.686.7",
66
+ userAgent:
67
+ "speakeasy-sdk/typescript 0.1.2 2.686.7 0.8.69 @supertone/supertone",
68
68
  } as const;
@@ -11,6 +11,13 @@ export * from "./constants.js";
11
11
  // Export text utilities
12
12
  export { chunkText, extractAudioFromNdjson } from "./text_utils.js";
13
13
 
14
+ // Export pronunciation utilities
15
+ export {
16
+ applyPronunciationDictionary,
17
+ PronunciationDictionaryValidationError,
18
+ type PronunciationDictionaryEntry,
19
+ } from "./pronunciation_utils.js";
20
+
14
21
  // Export audio utilities
15
22
  export {
16
23
  mergeWavBinary,
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Pronunciation dictionary substitution utilities.
3
+ *
4
+ * Mirrors the Python implementation policy:
5
+ * - Apply rules in input order
6
+ * - partial_match=false: word-boundary exact matches only
7
+ * - partial_match=true: substring matches (no boundaries)
8
+ * - No re-substitution: replaced segments are shielded via opaque tokens
9
+ *
10
+ * Validation:
11
+ * - pronunciation_dictionary omitted/undefined/null -> return original text
12
+ * - pronunciation_dictionary must be an array of objects
13
+ * - each object must have: text (string, non-empty), pronunciation (string, non-empty), partial_match (boolean)
14
+ */
15
+
16
+ export class PronunciationDictionaryValidationError extends Error {
17
+ constructor(message: string) {
18
+ super(message);
19
+ this.name = "PronunciationDictionaryValidationError";
20
+ }
21
+ }
22
+
23
+ export type PronunciationDictionaryEntry = {
24
+ text: string;
25
+ pronunciation: string;
26
+ partial_match: boolean;
27
+ };
28
+
29
+ export function applyPronunciationDictionary(
30
+ text: string,
31
+ pronunciation_dictionary?: unknown
32
+ ): string {
33
+ // Match Python behavior: return early for null, undefined, or empty array
34
+ if (
35
+ pronunciation_dictionary == null ||
36
+ (Array.isArray(pronunciation_dictionary) &&
37
+ pronunciation_dictionary.length === 0)
38
+ ) {
39
+ return text;
40
+ }
41
+
42
+ if (typeof text !== "string") {
43
+ throw new PronunciationDictionaryValidationError(
44
+ `\`text\` must be string, got ${typeof text}`
45
+ );
46
+ }
47
+
48
+ if (!Array.isArray(pronunciation_dictionary)) {
49
+ throw new PronunciationDictionaryValidationError(
50
+ "`pronunciation_dictionary` must be an array of objects"
51
+ );
52
+ }
53
+
54
+ // Prevent re-substitution:
55
+ // replace matches with unique opaque tokens first,
56
+ // then expand tokens to pronunciations at the end.
57
+ const tokenToPronunciation = new Map<string, string>();
58
+ let working = text;
59
+
60
+ for (let idx = 0; idx < pronunciation_dictionary.length; idx++) {
61
+ const entry = validateEntry(pronunciation_dictionary[idx], idx);
62
+ const src = entry.text;
63
+ const dst = entry.pronunciation;
64
+ const partial = entry.partial_match;
65
+
66
+ const token = makeUniqueToken(idx, working, tokenToPronunciation);
67
+
68
+ if (partial) {
69
+ const re = new RegExp(escapeRegExp(src), "g");
70
+ const newWorking = working.replace(re, token);
71
+ if (newWorking === working) continue; // No match found
72
+ tokenToPronunciation.set(token, dst);
73
+ working = newWorking;
74
+ continue;
75
+ }
76
+
77
+ // Exact match with word-boundary semantics (Unicode-aware-ish).
78
+ // Python uses Unicode \w; in JS, \w is ASCII-only. To mirror behavior better across scripts,
79
+ // we define "word char" as: letter or number or underscore.
80
+ //
81
+ // We avoid lookbehind for broader runtime compatibility by capturing the left boundary.
82
+ //
83
+ // Pattern: (^|[^WORD_CHARS]) (SRC) (?=[^WORD_CHARS]|$)
84
+ // (IMPORTANT) WORD_CHARS must not include surrounding [] because we embed it into other [].
85
+ const WORD_CHARS = "\\p{L}\\p{N}_";
86
+ const srcEsc = escapeRegExp(src);
87
+ const pattern = `(^|[^${WORD_CHARS}])(${srcEsc})(?=[^${WORD_CHARS}]|$)`;
88
+ const re = new RegExp(pattern, "gu");
89
+
90
+ // Replace keeping the left boundary (group 1)
91
+ const newWorking = working.replace(re, `$1${token}`);
92
+ if (newWorking === working) continue; // No match found
93
+ tokenToPronunciation.set(token, dst);
94
+ working = newWorking;
95
+ }
96
+
97
+ // Expand tokens into pronunciations.
98
+ for (const [token, pron] of tokenToPronunciation.entries()) {
99
+ working = working.split(token).join(pron);
100
+ }
101
+
102
+ return working;
103
+ }
104
+
105
+ function validateEntry(raw: unknown, idx: number): PronunciationDictionaryEntry {
106
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) {
107
+ throw new PronunciationDictionaryValidationError(
108
+ `pronunciation_dictionary[${idx}] must be an object, got ${
109
+ raw === null ? "null" : Array.isArray(raw) ? "array" : typeof raw
110
+ }`
111
+ );
112
+ }
113
+
114
+ const obj = raw as Record<string, unknown>;
115
+ const missing: string[] = [];
116
+ if (!("text" in obj)) missing.push("text");
117
+ if (!("pronunciation" in obj)) missing.push("pronunciation");
118
+ if (!("partial_match" in obj)) missing.push("partial_match");
119
+ if (missing.length) {
120
+ throw new PronunciationDictionaryValidationError(
121
+ `pronunciation_dictionary[${idx}] missing required field(s): ${missing.join(", ")}`
122
+ );
123
+ }
124
+
125
+ const src = obj["text"];
126
+ const dst = obj["pronunciation"];
127
+ const partial = obj["partial_match"];
128
+
129
+ if (typeof src !== "string") {
130
+ throw new PronunciationDictionaryValidationError(
131
+ `pronunciation_dictionary[${idx}].text must be string, got ${typeof src}`
132
+ );
133
+ }
134
+ if (typeof dst !== "string") {
135
+ throw new PronunciationDictionaryValidationError(
136
+ `pronunciation_dictionary[${idx}].pronunciation must be string, got ${typeof dst}`
137
+ );
138
+ }
139
+ if (typeof partial !== "boolean") {
140
+ throw new PronunciationDictionaryValidationError(
141
+ `pronunciation_dictionary[${idx}].partial_match must be boolean, got ${typeof partial}`
142
+ );
143
+ }
144
+
145
+ if (src === "") {
146
+ throw new PronunciationDictionaryValidationError(
147
+ `pronunciation_dictionary[${idx}].text must not be empty`
148
+ );
149
+ }
150
+ if (dst === "") {
151
+ throw new PronunciationDictionaryValidationError(
152
+ `pronunciation_dictionary[${idx}].pronunciation must not be empty`
153
+ );
154
+ }
155
+
156
+ return { text: src, pronunciation: dst, partial_match: partial };
157
+ }
158
+
159
+ function escapeRegExp(s: string): string {
160
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
161
+ }
162
+
163
+ function makeUniqueToken(
164
+ idx: number,
165
+ working: string,
166
+ existing: Map<string, string>
167
+ ): string {
168
+ // Private Use Area markers to minimize collision with typical text.
169
+ const base = `\uE000PD${idx}\uE001`;
170
+ if (!working.includes(base) && !existing.has(base)) return base;
171
+
172
+ while (true) {
173
+ const suffix = safeRandomHex();
174
+ const token = `\uE000PD${idx}_${suffix}\uE001`;
175
+ if (!working.includes(token) && !existing.has(token)) return token;
176
+ }
177
+ }
178
+
179
+ function safeRandomHex(): string {
180
+ // Prefer crypto.randomUUID when available (browser / modern runtimes)
181
+ const c = (globalThis as any).crypto;
182
+ if (c && typeof c.randomUUID === "function") {
183
+ return String(c.randomUUID()).replace(/-/g, "");
184
+ }
185
+ // Fallback: not cryptographically strong, but fine for uniqueness tokenization.
186
+ return (
187
+ Math.random().toString(16).slice(2) +
188
+ Math.random().toString(16).slice(2) +
189
+ Date.now().toString(16)
190
+ );
191
+ }
192
+
193
+