@supertone/supertone 0.1.2 → 0.1.4

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 (52) hide show
  1. package/README.md +4 -4
  2. package/custom_test/realtime_tts_player.ts +120 -16
  3. package/custom_test/test_pronunciation_dictionary.ts +227 -0
  4. package/custom_test/test_real_api.ts +580 -0
  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 +1 -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 +21 -4
  21. package/dist/commonjs/lib/custom_utils/text_utils.js.map +1 -1
  22. package/dist/commonjs/sdk/texttospeech.d.ts +17 -6
  23. package/dist/commonjs/sdk/texttospeech.d.ts.map +1 -1
  24. package/dist/commonjs/sdk/texttospeech.js +48 -25
  25. package/dist/commonjs/sdk/texttospeech.js.map +1 -1
  26. package/dist/esm/lib/config.d.ts +2 -2
  27. package/dist/esm/lib/config.d.ts.map +1 -1
  28. package/dist/esm/lib/config.js +2 -2
  29. package/dist/esm/lib/config.js.map +1 -1
  30. package/dist/esm/lib/custom_utils/index.d.ts +1 -0
  31. package/dist/esm/lib/custom_utils/index.d.ts.map +1 -1
  32. package/dist/esm/lib/custom_utils/index.js +2 -0
  33. package/dist/esm/lib/custom_utils/index.js.map +1 -1
  34. package/dist/esm/lib/custom_utils/pronunciation_utils.d.ts +24 -0
  35. package/dist/esm/lib/custom_utils/pronunciation_utils.d.ts.map +1 -0
  36. package/dist/esm/lib/custom_utils/pronunciation_utils.js +140 -0
  37. package/dist/esm/lib/custom_utils/pronunciation_utils.js.map +1 -0
  38. package/dist/esm/lib/custom_utils/text_utils.d.ts +1 -1
  39. package/dist/esm/lib/custom_utils/text_utils.d.ts.map +1 -1
  40. package/dist/esm/lib/custom_utils/text_utils.js +21 -4
  41. package/dist/esm/lib/custom_utils/text_utils.js.map +1 -1
  42. package/dist/esm/sdk/texttospeech.d.ts +17 -6
  43. package/dist/esm/sdk/texttospeech.d.ts.map +1 -1
  44. package/dist/esm/sdk/texttospeech.js +49 -26
  45. package/dist/esm/sdk/texttospeech.js.map +1 -1
  46. package/jsr.json +1 -1
  47. package/package.json +1 -1
  48. package/src/lib/config.ts +41 -41
  49. package/src/lib/custom_utils/index.ts +7 -0
  50. package/src/lib/custom_utils/pronunciation_utils.ts +193 -0
  51. package/src/lib/custom_utils/text_utils.ts +25 -4
  52. package/src/sdk/texttospeech.ts +99 -68
package/README.md CHANGED
@@ -16,25 +16,25 @@ The SDK can be installed with either [npm](https://www.npmjs.com/), [pnpm](https
16
16
  ### NPM
17
17
 
18
18
  ```bash
19
- npm add <UNSET>
19
+ npm add @supertone/supertone
20
20
  ```
21
21
 
22
22
  ### PNPM
23
23
 
24
24
  ```bash
25
- pnpm add <UNSET>
25
+ pnpm add @supertone/supertone
26
26
  ```
27
27
 
28
28
  ### Bun
29
29
 
30
30
  ```bash
31
- bun add <UNSET>
31
+ bun add @supertone/supertone
32
32
  ```
33
33
 
34
34
  ### Yarn
35
35
 
36
36
  ```bash
37
- yarn add <UNSET> zod
37
+ yarn add @supertone/supertone zod
38
38
 
39
39
  # Note that Yarn does not install peer dependencies automatically. You will need
40
40
  # to install zod as shown above.
@@ -6,6 +6,7 @@
6
6
  import { spawn, ChildProcess } from "child_process";
7
7
  import { Supertone } from "../src/index.js";
8
8
  import * as models from "../src/models/index.js";
9
+ import type { PronunciationDictionaryEntry } from "../src/lib/custom_utils/index.js";
9
10
  import * as dotenv from "dotenv";
10
11
  import * as path from "path";
11
12
  import { fileURLToPath } from "url";
@@ -277,11 +278,15 @@ async function simpleStreamingTts(
277
278
  voiceId: string,
278
279
  text: string,
279
280
  language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage = models
280
- .APIConvertTextToSpeechUsingCharacterRequestLanguage.Ko
281
+ .APIConvertTextToSpeechUsingCharacterRequestLanguage.Ko,
282
+ pronunciationDictionary?: PronunciationDictionaryEntry[]
281
283
  ): Promise<boolean> {
282
284
  console.log(`📝 "${text.slice(0, 50)}${text.length > 50 ? "..." : ""}"`);
283
285
  console.log(`📏 Text length: ${text.length} characters`);
284
286
  console.log(`🌐 Language: ${language}`);
287
+ if (pronunciationDictionary && pronunciationDictionary.length > 0) {
288
+ console.log(`📖 Pronunciation dictionary: ${pronunciationDictionary.length} entries`);
289
+ }
285
290
 
286
291
  const player = new SimpleMpvPlayer();
287
292
 
@@ -295,17 +300,20 @@ async function simpleStreamingTts(
295
300
  player.markApiCallStart();
296
301
  console.log(" ⏱️ API call started...");
297
302
 
298
- const response = await client.textToSpeech.streamSpeech({
299
- voiceId: voiceId,
300
- apiConvertTextToSpeechUsingCharacterRequest: {
301
- text: text,
302
- language: language,
303
- outputFormat:
304
- models.APIConvertTextToSpeechUsingCharacterRequestOutputFormat.Wav,
305
- style: "neutral",
306
- model: "sona_speech_1",
303
+ const response = await client.textToSpeech.streamSpeech(
304
+ {
305
+ voiceId: voiceId,
306
+ apiConvertTextToSpeechUsingCharacterRequest: {
307
+ text: text,
308
+ language: language,
309
+ outputFormat:
310
+ models.APIConvertTextToSpeechUsingCharacterRequestOutputFormat.Wav,
311
+ style: "neutral",
312
+ model: "sona_speech_1",
313
+ },
307
314
  },
308
- });
315
+ pronunciationDictionary ? { pronunciationDictionary } : undefined
316
+ );
309
317
 
310
318
  // Mark API call end (response received)
311
319
  player.markApiCallEnd();
@@ -387,8 +395,7 @@ async function simpleStreamingTts(
387
395
  */
388
396
  async function simpleDemo(): Promise<void> {
389
397
  const voiceId = "91992bbd4758bdcf9c9b01";
390
- const scenarios: string[] = [];
391
- /*[
398
+ const scenarios: string[] = [
392
399
  "안녕하세요! 심플한 테스트입니다.",
393
400
 
394
401
  "실시간 텍스트 음성 변환 기술은 정말 놀랍습니다. 이 기술을 통해 긴 텍스트도 즉시 음성으로 들을 수 있게 되었습니다.",
@@ -403,10 +410,16 @@ async function simpleDemo(): Promise<void> {
403
410
 
404
411
  // Scenario 800+ characters (~850 chars)
405
412
  "옛날 한 작은 마을에 천재적인 재능을 가진 젊은 개발자가 살고 있었습니다. 그의 이름은 민준이였고, 어릴 때부터 컴퓨터와 프로그래밍에 남다른 관심을 보였습니다. 대학에서 컴퓨터 과학을 전공한 민준은 졸업 후 스타트업에 입사했습니다. 그곳에서 그는 인공지능과 음성 기술에 대한 깊은 지식을 쌓게 되었습니다. 어느 날, 민준은 시각 장애가 있는 친구 서연을 만났습니다. 서연은 인터넷의 수많은 정보를 텍스트로만 접할 수 있어 많은 불편함을 겪고 있었습니다. 당시의 음성 합성 기술은 로봇 같은 목소리를 내며, 긴 텍스트를 읽어주려면 모든 처리가 끝날 때까지 기다려야 했습니다. 이를 본 민준은 더 자연스럽고 빠른 음성 합성 기술을 만들기로 결심했습니다. 밤낮없이 연구에 매진한 민준은 혁신적인 아이디어를 떠올렸습니다. 긴 텍스트를 작은 단위로 나누어 실시간으로 처리하고, 첫 번째 부분이 완성되는 즉시 재생을 시작하는 스트리밍 방식이었습니다. 이 기술을 구현하기 위해 그는 최신 딥러닝 모델과 신경망 아키텍처를 연구했습니다. 수많은 시행착오를 거쳐 마침내 자연스러운 음성을 실시간으로 생성할 수 있는 시스템을 완성했습니다. 그의 기술은 문장의 문맥과 감정까지 이해하여 적절한 억양과 속도로 읽어주었습니다.",
406
- ];*/
413
+ ];
407
414
 
408
415
  // Additional test scenarios for word-based and character-based chunking
409
- const additionalScenarios = [
416
+ const additionalScenarios: Array<{
417
+ text: string;
418
+ label: string;
419
+ category: string;
420
+ language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage;
421
+ pronunciationDictionary?: PronunciationDictionaryEntry[];
422
+ }> = [
410
423
  {
411
424
  // Korean text WITHOUT punctuation to test word-based chunking
412
425
  // Text length: ~450 characters (exceeds 300 char limit)
@@ -425,6 +438,87 @@ async function simpleDemo(): Promise<void> {
425
438
  category: "Character-based Chunking Test",
426
439
  language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.Ja,
427
440
  },
441
+ {
442
+ // English text with ellipsis punctuation (… ‥) - tests fix/text_utils multilingual punctuation
443
+ // Text length: ~380 characters (exceeds 300 char limit)
444
+ text: "Sometimes we need to pause and think… The ellipsis character is used to indicate a trailing thought or a pause in speech… This test verifies that the text chunking system correctly handles Unicode ellipsis characters‥ There are multiple types of ellipsis in Unicode… The horizontal ellipsis and the two dot leader are both supported‥ When processing long texts the SDK should split at these punctuation marks… This ensures natural pauses in the generated speech output‥ Let us verify everything works correctly…",
445
+ label: "Ellipsis punctuation test (… ‥) - 380+ chars",
446
+ category: "Multilingual Punctuation Test",
447
+ language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.En,
448
+ },
449
+ {
450
+ // Korean text with ellipsis (…) - tests Korean with Unicode ellipsis
451
+ // Text length: ~350 characters (exceeds 300 char limit)
452
+ text: "한국어 텍스트에서 말줄임표는 생각의 흐름을 나타냅니다… 이 테스트는 유니코드 말줄임표 문자가 올바르게 처리되는지 확인합니다… 인공지능 기술이 발전하면서 음성 합성의 품질도 크게 향상되었습니다… 특히 딥러닝을 활용한 최신 시스템은 매우 자연스러운 음성을 생성할 수 있습니다… 긴 텍스트를 처리할 때 SDK는 이러한 구두점에서 적절히 분할해야 합니다… 이를 통해 자연스러운 음성 출력이 가능해집니다… 실시간 스트리밍 기술과 결합하면 더욱 빠른 응답을 제공할 수 있습니다… 음성 합성 기술은 접근성 도구부터 AI 어시스턴트까지 다양하게 활용됩니다… 모든 것이 제대로 작동하는지 확인해 봅시다…",
453
+ label: "Korean ellipsis punctuation test (…) - 350+ chars",
454
+ category: "Multilingual Punctuation Test",
455
+ language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.Ko,
456
+ },
457
+ {
458
+ // Japanese text WITH CJK punctuation (。!?) - tests CJK punctuation splitting
459
+ // Text length: ~320 characters (exceeds 300 char limit)
460
+ text: "日本語のテキストは通常スペースを含まないため特別な処理が必要です。このテストは日本語の句読点で正しく分割されることを確認します。自然言語処理技術の発展により音声合成の品質は大幅に向上しました。特にディープラーニングを活用した最新のテキスト音声変換システムは人間の発話に非常に近い自然な音声を生成できます。スペースがない言語では句読点での分割が重要です。このSDKはそのような状況を自動的に検出して適切に処理します。リアルタイムストリーミング技術と組み合わせることで待ち時間を大幅に短縮できます。これにより日本語でも問題なく長いテキストを音声に変換することができます。音声合成技術の未来はとても明るいです。",
461
+ label: "Japanese CJK punctuation test (。) - 320+ chars",
462
+ category: "Multilingual Punctuation Test",
463
+ language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.Ja,
464
+ },
465
+ // Pronunciation Dictionary Tests
466
+ {
467
+ // Basic pronunciation dictionary test with partial_match=true/false
468
+ text: "The CEO of OpenAI announced that GPT models are improving. Dr. Smith from MIT said AI research is accelerating.",
469
+ label: "Pronunciation dictionary (partial_match=true/false)",
470
+ category: "Pronunciation Dictionary Test",
471
+ language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.En,
472
+ pronunciationDictionary: [
473
+ // partial_match=false: exact word boundary match
474
+ { text: "CEO", pronunciation: "Chief Executive Officer", partial_match: false },
475
+ { text: "MIT", pronunciation: "Massachusetts Institute of Technology", partial_match: false },
476
+ { text: "AI", pronunciation: "Artificial Intelligence", partial_match: false },
477
+ // partial_match=true: substring match
478
+ { text: "GPT", pronunciation: "Generative Pre-trained Transformer", partial_match: true },
479
+ { text: "Dr.", pronunciation: "Doctor", partial_match: true },
480
+ ],
481
+ },
482
+ {
483
+ // Pronunciation dictionary causing text expansion to exceed 300 chars (triggers chunking)
484
+ // Original text: ~190 chars, After expansion: 400+ chars
485
+ text: "AI and ML are revolutionizing tech. The CEO discussed GPT advancements. Dr. Kim from MIT explained how NLP and CV work together. AWS and GCP provide cloud AI services.",
486
+ label: "Pronunciation dictionary + Long text chunking (~190 chars -> 400+ chars)",
487
+ category: "Pronunciation Dictionary + Chunking Test",
488
+ language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.En,
489
+ pronunciationDictionary: [
490
+ // partial_match=false: exact word boundary matches
491
+ { text: "AI", pronunciation: "Artificial Intelligence", partial_match: false },
492
+ { text: "ML", pronunciation: "Machine Learning", partial_match: false },
493
+ { text: "CEO", pronunciation: "Chief Executive Officer", partial_match: false },
494
+ { text: "MIT", pronunciation: "Massachusetts Institute of Technology", partial_match: false },
495
+ { text: "NLP", pronunciation: "Natural Language Processing", partial_match: false },
496
+ { text: "CV", pronunciation: "Computer Vision", partial_match: false },
497
+ { text: "AWS", pronunciation: "Amazon Web Services", partial_match: false },
498
+ { text: "GCP", pronunciation: "Google Cloud Platform", partial_match: false },
499
+ // partial_match=true: substring matches
500
+ { text: "GPT", pronunciation: "Generative Pre-trained Transformer", partial_match: true },
501
+ { text: "Dr.", pronunciation: "Doctor", partial_match: true },
502
+ { text: "tech", pronunciation: "technology", partial_match: true },
503
+ ],
504
+ },
505
+ {
506
+ // Korean pronunciation dictionary test
507
+ text: "SK와 LG의 CEO가 AI와 ML 기술에 대해 발표했습니다. Dr. 김 박사가 MIT에서 NLP 연구 성과를 공개했습니다.",
508
+ label: "Korean pronunciation dictionary test",
509
+ category: "Pronunciation Dictionary Test",
510
+ language: models.APIConvertTextToSpeechUsingCharacterRequestLanguage.Ko,
511
+ pronunciationDictionary: [
512
+ { text: "SK", pronunciation: "에스케이", partial_match: false },
513
+ { text: "LG", pronunciation: "엘지", partial_match: false },
514
+ { text: "CEO", pronunciation: "최고경영자", partial_match: false },
515
+ { text: "AI", pronunciation: "인공지능", partial_match: false },
516
+ { text: "ML", pronunciation: "머신러닝", partial_match: false },
517
+ { text: "MIT", pronunciation: "매사추세츠 공과대학교", partial_match: false },
518
+ { text: "NLP", pronunciation: "자연어처리", partial_match: false },
519
+ { text: "Dr.", pronunciation: "닥터", partial_match: true },
520
+ ],
521
+ },
428
522
  ];
429
523
 
430
524
  for (let i = 0; i < scenarios.length; i++) {
@@ -472,7 +566,8 @@ async function simpleDemo(): Promise<void> {
472
566
  const success = await simpleStreamingTts(
473
567
  voiceId,
474
568
  scenario.text,
475
- scenario.language
569
+ scenario.language,
570
+ scenario.pronunciationDictionary
476
571
  );
477
572
 
478
573
  if (!success) {
@@ -497,6 +592,15 @@ async function simpleDemo(): Promise<void> {
497
592
  console.log(
498
593
  " • Character-based chunking: Japanese/Chinese text without spaces"
499
594
  );
595
+ console.log("\n🌍 Multilingual punctuation tests:");
596
+ console.log(" • Ellipsis: English (… ‥)");
597
+ console.log(" • Korean ellipsis: Korean (…)");
598
+ console.log(" • CJK punctuation: Japanese (。)");
599
+ console.log("\n📖 Pronunciation dictionary tests:");
600
+ console.log(" • partial_match=false: Word boundary matching (CEO, MIT, AI)");
601
+ console.log(" • partial_match=true: Substring matching (GPT, Dr., tech)");
602
+ console.log(" • Long text chunking: Text expansion exceeding 300 chars");
603
+ console.log(" • Korean pronunciation: SK, LG, CEO, AI, ML, MIT, NLP");
500
604
  }
501
605
 
502
606
  /**
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Behavior tests for applyPronunciationDictionary().
4
+ *
5
+ * Run:
6
+ * npx ts-node custom_test/test_pronunciation_dictionary.ts
7
+ * # or after build:
8
+ * node dist/custom_test/test_pronunciation_dictionary.js
9
+ */
10
+
11
+ import {
12
+ applyPronunciationDictionary,
13
+ PronunciationDictionaryValidationError,
14
+ type PronunciationDictionaryEntry,
15
+ } from "../src/lib/custom_utils/pronunciation_utils.js";
16
+
17
+ function assertEqual(actual: string, expected: string, message: string): void {
18
+ if (actual !== expected) {
19
+ throw new Error(`${message}\nexpected=${expected}\nactual=${actual}`);
20
+ }
21
+ }
22
+
23
+ function assertThrows(fn: () => void, message: string): void {
24
+ let threw = false;
25
+ try {
26
+ fn();
27
+ } catch (e) {
28
+ threw = true;
29
+ if (!(e instanceof PronunciationDictionaryValidationError)) {
30
+ throw new Error(`${message}\nexpected PronunciationDictionaryValidationError`);
31
+ }
32
+ }
33
+ if (!threw) {
34
+ throw new Error(`${message}\nexpected to throw`);
35
+ }
36
+ }
37
+
38
+ function runTests(): void {
39
+ const d = (entries: PronunciationDictionaryEntry[]) => entries;
40
+
41
+ assertEqual(applyPronunciationDictionary("hello", undefined), "hello", "none returns original");
42
+ assertEqual(applyPronunciationDictionary("hello", []), "hello", "empty returns original");
43
+
44
+ assertEqual(
45
+ applyPronunciationDictionary("This is Supertone.", d([{ text: "Supertone", pronunciation: "super tone", partial_match: false }])),
46
+ "This is super tone.",
47
+ "basic exact match"
48
+ );
49
+
50
+ assertEqual(
51
+ applyPronunciationDictionary("K-TTS is different from TTSAPI.", d([{ text: "TTS", pronunciation: "text to speech", partial_match: true }])),
52
+ "K-text to speech is different from text to speechAPI.",
53
+ "partial match substrings"
54
+ );
55
+
56
+ assertEqual(
57
+ applyPronunciationDictionary("TTS and TTS and TTS.", d([{ text: "TTS", pronunciation: "text to speech", partial_match: true }])),
58
+ "text to speech and text to speech and text to speech.",
59
+ "multiple occurrences"
60
+ );
61
+
62
+ assertEqual(
63
+ applyPronunciationDictionary('He said, "Supertone", (Supertone)!', d([{ text: "Supertone", pronunciation: "super tone", partial_match: false }])),
64
+ 'He said, "super tone", (super tone)!',
65
+ "punctuation boundaries"
66
+ );
67
+
68
+ assertEqual(
69
+ applyPronunciationDictionary("API_test API test_API", d([{ text: "API", pronunciation: "A P I", partial_match: false }])),
70
+ "API_test A P I test_API",
71
+ "word boundary underscore"
72
+ );
73
+
74
+ assertEqual(
75
+ applyPronunciationDictionary("C++ is old, C++11 is newer.", d([{ text: "C++", pronunciation: "cplusplus", partial_match: false }])),
76
+ "cplusplus is old, C++11 is newer.",
77
+ "numbers break boundary"
78
+ );
79
+
80
+ assertEqual(
81
+ applyPronunciationDictionary("a(b)c a(b)c", d([{ text: "a(b)c", pronunciation: "X", partial_match: true }])),
82
+ "X X",
83
+ "regex meta chars"
84
+ );
85
+
86
+ assertEqual(
87
+ applyPronunciationDictionary("aaaa", d([{ text: "aa", pronunciation: "b", partial_match: true }])),
88
+ "bb",
89
+ "non-overlapping left-to-right"
90
+ );
91
+
92
+ assertEqual(
93
+ applyPronunciationDictionary("AAAA", d([
94
+ { text: "AA", pronunciation: "B", partial_match: true },
95
+ { text: "A", pronunciation: "C", partial_match: true },
96
+ ])),
97
+ "BB",
98
+ "order priority AA then A"
99
+ );
100
+
101
+ assertEqual(
102
+ applyPronunciationDictionary("AAAA", d([
103
+ { text: "A", pronunciation: "C", partial_match: true },
104
+ { text: "AA", pronunciation: "B", partial_match: true },
105
+ ])),
106
+ "CCCC",
107
+ "order priority A then AA"
108
+ );
109
+
110
+ assertEqual(
111
+ applyPronunciationDictionary("NY is not New Jersey.", d([
112
+ { text: "NY", pronunciation: "New York", partial_match: false },
113
+ { text: "New", pronunciation: "Old", partial_match: false },
114
+ ])),
115
+ "New York is not Old Jersey.",
116
+ "no resubstitution inside pronunciation"
117
+ );
118
+
119
+ assertEqual(
120
+ applyPronunciationDictionary("이번 APEC 은 한국에서 열립니다", d([
121
+ { text: "AP", pronunciation: "에이피", partial_match: true },
122
+ { text: "APEC", pronunciation: "에이팩", partial_match: true },
123
+ ])),
124
+ "이번 에이피EC 은 한국에서 열립니다",
125
+ "order AP then APEC"
126
+ );
127
+
128
+ assertEqual(
129
+ applyPronunciationDictionary("이번 APEC 은 한국에서 열립니다", d([
130
+ { text: "APEC", pronunciation: "에이팩", partial_match: true },
131
+ { text: "AP", pronunciation: "에이피", partial_match: true },
132
+ ])),
133
+ "이번 에이팩 은 한국에서 열립니다",
134
+ "order APEC then AP"
135
+ );
136
+
137
+ assertEqual(
138
+ applyPronunciationDictionary("Supertone tone", d([
139
+ { text: "Supertone", pronunciation: "super tone", partial_match: false },
140
+ { text: "tone", pronunciation: "TONE", partial_match: false },
141
+ ])),
142
+ "super tone TONE",
143
+ "no resubstitution across rules"
144
+ );
145
+
146
+ assertEqual(
147
+ applyPronunciationDictionary("TTS와 TTSAPI는 다릅니다.", d([{ text: "TTS", pronunciation: "text to speech", partial_match: true }])),
148
+ "text to speech와 text to speechAPI는 다릅니다.",
149
+ "Korean partial match"
150
+ );
151
+
152
+ assertEqual(
153
+ applyPronunciationDictionary("東京TTS東京", d([{ text: "TTS", pronunciation: "text to speech", partial_match: false }])),
154
+ "東京TTS東京",
155
+ "no boundary in Japanese exact"
156
+ );
157
+
158
+ assertEqual(
159
+ applyPronunciationDictionary("これは「TTS」です。", d([{ text: "TTS", pronunciation: "text to speech", partial_match: false }])),
160
+ "これは「text to speech」です。",
161
+ "boundary by punctuation Japanese"
162
+ );
163
+
164
+ assertEqual(
165
+ applyPronunciationDictionary("東京TTS東京", d([{ text: "TTS", pronunciation: "text to speech", partial_match: true }])),
166
+ "東京text to speech東京",
167
+ "partial match Japanese"
168
+ );
169
+
170
+ assertEqual(
171
+ applyPronunciationDictionary(`X \uE000PD0\uE001 Supertone`, d([{ text: "Supertone", pronunciation: "super tone", partial_match: false }])),
172
+ `X \uE000PD0\uE001 super tone`,
173
+ "token collision safe"
174
+ );
175
+
176
+ assertEqual(
177
+ applyPronunciationDictionary("TTS와 TTS.", d([{ text: "TTS", pronunciation: "text to speech", partial_match: false }])),
178
+ "TTS와 text to speech.",
179
+ "unicode boundary note"
180
+ );
181
+
182
+ assertThrows(
183
+ () => applyPronunciationDictionary("hi", {} as any),
184
+ "validation: dictionary must be array"
185
+ );
186
+
187
+ assertThrows(
188
+ () => applyPronunciationDictionary("hi", ["not-an-object"] as any),
189
+ "validation: entry must be object"
190
+ );
191
+
192
+ assertThrows(
193
+ () => applyPronunciationDictionary("hi", [{ text: 1, pronunciation: "b", partial_match: true }] as any),
194
+ "validation: text type"
195
+ );
196
+
197
+ assertThrows(
198
+ () => applyPronunciationDictionary("hi", [{ text: "a", pronunciation: 1, partial_match: true }] as any),
199
+ "validation: pronunciation type"
200
+ );
201
+
202
+ assertThrows(
203
+ () => applyPronunciationDictionary("hi", [{ text: "a", pronunciation: "b" }] as any),
204
+ "validation: missing partial_match"
205
+ );
206
+
207
+ assertThrows(
208
+ () => applyPronunciationDictionary("hi", [{ text: "a", pronunciation: "b", partial_match: "true" }] as any),
209
+ "validation: partial_match type"
210
+ );
211
+
212
+ assertThrows(
213
+ () => applyPronunciationDictionary("hi", [{ text: "", pronunciation: "b", partial_match: true }] as any),
214
+ "validation: empty text"
215
+ );
216
+
217
+ assertThrows(
218
+ () => applyPronunciationDictionary("hi", [{ text: "a", pronunciation: "", partial_match: true }] as any),
219
+ "validation: empty pronunciation"
220
+ );
221
+
222
+ console.log("OK: applyPronunciationDictionary tests passed");
223
+ }
224
+
225
+ runTests();
226
+
227
+