echogarden 2.6.0 → 2.8.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.
Files changed (153) hide show
  1. package/data/lexicons/heteronyms.en.json +6 -2
  2. package/data/lexicons/words.en.json +379 -19
  3. package/dist/alignment/SemanticTextAlignment.js +6 -7
  4. package/dist/alignment/SemanticTextAlignment.js.map +1 -1
  5. package/dist/api/Alignment.js +2 -2
  6. package/dist/api/Alignment.js.map +1 -1
  7. package/dist/api/Recognition.js +2 -2
  8. package/dist/api/Recognition.js.map +1 -1
  9. package/dist/api/SourceSeparation.js +1 -1
  10. package/dist/api/SpeechTranslation.js +2 -2
  11. package/dist/api/SpeechTranslation.js.map +1 -1
  12. package/dist/api/Synthesis.js +7 -7
  13. package/dist/api/Synthesis.js.map +1 -1
  14. package/dist/api/TimelineTranslationAlignment.js +2 -2
  15. package/dist/api/TimelineTranslationAlignment.js.map +1 -1
  16. package/dist/api/TranslationAlignment.js +2 -2
  17. package/dist/api/TranslationAlignment.js.map +1 -1
  18. package/dist/audio/AudioBufferConversion.d.ts +5 -6
  19. package/dist/audio/AudioBufferConversion.js +16 -248
  20. package/dist/audio/AudioBufferConversion.js.map +1 -1
  21. package/dist/audio/AudioPlayer.js +2 -2
  22. package/dist/audio/AudioPlayer.js.map +1 -1
  23. package/dist/audio/AudioRecorder.js +1 -1
  24. package/dist/audio/AudioRecorder.js.map +1 -1
  25. package/dist/audio/AudioUtilities.d.ts +2 -8
  26. package/dist/audio/AudioUtilities.js +4 -3
  27. package/dist/audio/AudioUtilities.js.map +1 -1
  28. package/dist/cli/CLI.js +4 -4
  29. package/dist/cli/CLI.js.map +1 -1
  30. package/dist/codecs/FFMpegTranscoder.d.ts +1 -4
  31. package/dist/codecs/TIMITCodec.js +1 -2
  32. package/dist/codecs/TIMITCodec.js.map +1 -1
  33. package/dist/denoising/RNNoise.js +5 -5
  34. package/dist/denoising/RNNoise.js.map +1 -1
  35. package/dist/dsp/FFT.js +13 -19
  36. package/dist/dsp/FFT.js.map +1 -1
  37. package/dist/dsp/Rubberband.js +5 -5
  38. package/dist/dsp/Rubberband.js.map +1 -1
  39. package/dist/dsp/Sonic.js +6 -6
  40. package/dist/dsp/Sonic.js.map +1 -1
  41. package/dist/dsp/SpeexResampler.js +11 -11
  42. package/dist/dsp/SpeexResampler.js.map +1 -1
  43. package/dist/encodings/Ascii.js +1 -1
  44. package/dist/encodings/Ascii.js.map +1 -1
  45. package/dist/encodings/Utf16.js +1 -1
  46. package/dist/encodings/Utf16.js.map +1 -1
  47. package/dist/encodings/Utf8.js +1 -1
  48. package/dist/encodings/Utf8.js.map +1 -1
  49. package/dist/nlp/EspeakPhonemizer.js +1 -1
  50. package/dist/nlp/EspeakPhonemizer.js.map +1 -1
  51. package/dist/nlp/Segmentation.d.ts +13 -10
  52. package/dist/nlp/Segmentation.js +129 -106
  53. package/dist/nlp/Segmentation.js.map +1 -1
  54. package/dist/nlp/TextNormalizer.js +74 -42
  55. package/dist/nlp/TextNormalizer.js.map +1 -1
  56. package/dist/recognition/AmazonTranscribeSTT.js +2 -2
  57. package/dist/recognition/AmazonTranscribeSTT.js.map +1 -1
  58. package/dist/recognition/SileroSTT.js +2 -2
  59. package/dist/recognition/SileroSTT.js.map +1 -1
  60. package/dist/recognition/WhisperSTT.js +4 -16
  61. package/dist/recognition/WhisperSTT.js.map +1 -1
  62. package/dist/subtitles/Subtitles.js +52 -7
  63. package/dist/subtitles/Subtitles.js.map +1 -1
  64. package/dist/synthesis/AwsPollyTTS.d.ts +1 -4
  65. package/dist/synthesis/CoquiServerTTS.d.ts +1 -4
  66. package/dist/synthesis/DeepgramTTS.d.ts +1 -4
  67. package/dist/synthesis/ElevenLabsTTS.d.ts +1 -4
  68. package/dist/synthesis/ElevenLabsTTS.js +11 -10
  69. package/dist/synthesis/ElevenLabsTTS.js.map +1 -1
  70. package/dist/synthesis/EspeakTTS.js +8 -8
  71. package/dist/synthesis/EspeakTTS.js.map +1 -1
  72. package/dist/synthesis/FliteTTS.d.ts +1 -4
  73. package/dist/synthesis/GnuSpeechTTS.d.ts +1 -7
  74. package/dist/synthesis/GoogleCloudTTS.d.ts +1 -4
  75. package/dist/synthesis/KokoroTTS.js +21 -10
  76. package/dist/synthesis/KokoroTTS.js.map +1 -1
  77. package/dist/synthesis/SapiTTS.js +1 -2
  78. package/dist/synthesis/SapiTTS.js.map +1 -1
  79. package/dist/synthesis/SvoxPicoTTS.js +25 -25
  80. package/dist/synthesis/SvoxPicoTTS.js.map +1 -1
  81. package/dist/text-translation/DeepLTextTranslation.js +4 -3
  82. package/dist/text-translation/DeepLTextTranslation.js.map +1 -1
  83. package/dist/text-translation/NLLBTextTranslation.js +7 -6
  84. package/dist/text-translation/NLLBTextTranslation.js.map +1 -1
  85. package/dist/utilities/StringUtilities.d.ts +1 -1
  86. package/dist/utilities/StringUtilities.js +5 -5
  87. package/dist/utilities/StringUtilities.js.map +1 -1
  88. package/dist/utilities/Timeline.d.ts +2 -3
  89. package/dist/utilities/Timeline.js +96 -147
  90. package/dist/utilities/Timeline.js.map +1 -1
  91. package/dist/utilities/WikipediaReader.js +3 -3
  92. package/dist/utilities/WikipediaReader.js.map +1 -1
  93. package/dist/voice-activity-detection/WebRtcVAD.js +4 -4
  94. package/dist/voice-activity-detection/WebRtcVAD.js.map +1 -1
  95. package/docs/Tasklist.md +1 -6
  96. package/package.json +13 -11
  97. package/src/alignment/SemanticTextAlignment.ts +6 -7
  98. package/src/api/Alignment.ts +2 -2
  99. package/src/api/Recognition.ts +2 -2
  100. package/src/api/SourceSeparation.ts +1 -1
  101. package/src/api/SpeechTranslation.ts +2 -2
  102. package/src/api/Synthesis.ts +7 -7
  103. package/src/api/TimelineTranslationAlignment.ts +2 -2
  104. package/src/api/TranslationAlignment.ts +2 -2
  105. package/src/audio/AudioBufferConversion.ts +17 -263
  106. package/src/audio/AudioPlayer.ts +3 -3
  107. package/src/audio/AudioRecorder.ts +1 -1
  108. package/src/audio/AudioUtilities.ts +5 -3
  109. package/src/cli/CLI.ts +4 -4
  110. package/src/codecs/TIMITCodec.ts +1 -2
  111. package/src/denoising/RNNoise.ts +5 -5
  112. package/src/dsp/FFT.ts +22 -19
  113. package/src/dsp/Rubberband.ts +5 -5
  114. package/src/dsp/Sonic.ts +6 -6
  115. package/src/dsp/SpeexResampler.ts +11 -11
  116. package/src/encodings/Ascii.ts +1 -1
  117. package/src/encodings/Utf16.ts +1 -1
  118. package/src/encodings/Utf8.ts +1 -1
  119. package/src/nlp/EspeakPhonemizer.ts +1 -1
  120. package/src/nlp/Segmentation.ts +146 -111
  121. package/src/nlp/TextNormalizer.ts +94 -52
  122. package/src/recognition/AmazonTranscribeSTT.ts +2 -2
  123. package/src/recognition/SileroSTT.ts +2 -2
  124. package/src/recognition/WhisperSTT.ts +4 -18
  125. package/src/subtitles/Subtitles.ts +69 -8
  126. package/src/synthesis/ElevenLabsTTS.ts +12 -11
  127. package/src/synthesis/EspeakTTS.ts +9 -8
  128. package/src/synthesis/KokoroTTS.ts +24 -12
  129. package/src/synthesis/SapiTTS.ts +1 -2
  130. package/src/synthesis/SvoxPicoTTS.ts +27 -25
  131. package/src/text-translation/DeepLTextTranslation.ts +4 -3
  132. package/src/text-translation/NLLBTextTranslation.ts +8 -6
  133. package/src/typings/Fillers.d.ts +0 -16
  134. package/src/utilities/StringUtilities.ts +5 -6
  135. package/src/utilities/Timeline.ts +100 -170
  136. package/src/utilities/WikipediaReader.ts +3 -3
  137. package/src/voice-activity-detection/WebRtcVAD.ts +5 -4
  138. package/dist/codecs/WaveCodec.d.ts +0 -19
  139. package/dist/codecs/WaveCodec.js +0 -224
  140. package/dist/codecs/WaveCodec.js.map +0 -1
  141. package/dist/nlp/CompromiseNLP.d.ts +0 -15
  142. package/dist/nlp/CompromiseNLP.js +0 -79
  143. package/dist/nlp/CompromiseNLP.js.map +0 -1
  144. package/dist/utilities/BinaryArrayConversion.d.ts +0 -14
  145. package/dist/utilities/BinaryArrayConversion.js +0 -92
  146. package/dist/utilities/BinaryArrayConversion.js.map +0 -1
  147. package/dist/utilities/WasmMemoryManager.d.ts +0 -153
  148. package/dist/utilities/WasmMemoryManager.js +0 -440
  149. package/dist/utilities/WasmMemoryManager.js.map +0 -1
  150. package/src/codecs/WaveCodec.ts +0 -281
  151. package/src/nlp/CompromiseNLP.ts +0 -119
  152. package/src/utilities/BinaryArrayConversion.ts +0 -117
  153. package/src/utilities/WasmMemoryManager.ts +0 -566
@@ -1,6 +1,6 @@
1
1
  import { RawAudio, cloneRawAudio } from '../audio/AudioUtilities.js'
2
2
  import { concatFloat32Arrays, isWasmSimdSupported } from '../utilities/Utilities.js'
3
- import { WasmMemoryManager } from '../utilities/WasmMemoryManager.js'
3
+ import { wrapEmscriptenModuleHeap } from 'wasm-heap-manager'
4
4
 
5
5
  let speexResamplerInstance: any
6
6
 
@@ -23,17 +23,17 @@ export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: numb
23
23
  }
24
24
 
25
25
  const m = await getSpeexResamplerInstance()
26
- const wasmMemory = new WasmMemoryManager(m)
26
+ const wasmHeap = wrapEmscriptenModuleHeap(m)
27
27
 
28
28
  function speexResultCodeToString(resultCode: number) {
29
29
  const errorStrPtr = m._speex_resampler_strerror(resultCode)
30
- const errorStrRef = wasmMemory.wrapUint8Array(errorStrPtr, 1024)
31
- const message = errorStrRef.readAsNullTerminatedUtf8String()
30
+ const messageRef = wasmHeap.wrapNullTerminatedUtf8String(errorStrPtr)
31
+ const message = messageRef.value
32
32
 
33
33
  return message
34
34
  }
35
35
 
36
- const initErrRef = wasmMemory.allocInt32()
36
+ const initErrRef = wasmHeap.allocInt32()
37
37
  const resamplerStateAddress = m._speex_resampler_init(channelCount, inSampleRate, outSampleRate, quality, initErrRef.address)
38
38
  let resultCode = initErrRef.value
39
39
 
@@ -46,11 +46,11 @@ export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: numb
46
46
 
47
47
  const maxChunkSize = 2 ** 20
48
48
 
49
- const inputChunkSampleCountRef = wasmMemory.allocInt32()
50
- const outputChunkSampleCountRef = wasmMemory.allocInt32()
49
+ const inputChunkSampleCountRef = wasmHeap.allocInt32()
50
+ const outputChunkSampleCountRef = wasmHeap.allocInt32()
51
51
 
52
- const inputChunkSamplesRef = wasmMemory.allocFloat32Array(maxChunkSize * 2)
53
- const outputChunkSamplesRef = wasmMemory.allocFloat32Array(Math.floor(maxChunkSize * sampleRateRatio) * 2)
52
+ const inputChunkSamplesRef = wasmHeap.allocFloat32Array(maxChunkSize * 2)
53
+ const outputChunkSamplesRef = wasmHeap.allocFloat32Array(Math.floor(maxChunkSize * sampleRateRatio) * 2)
54
54
 
55
55
  const resampledAudioChunksForChannels: Float32Array[][] = []
56
56
 
@@ -65,7 +65,7 @@ export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: numb
65
65
  const inputPaddingSize = isLastChunk ? inputLatency : 0
66
66
  const maxSamplesToRead = Math.min(maxChunkSize, totalSampleCount - readOffset) + inputPaddingSize
67
67
 
68
- const maxSamplesToWrite = outputChunkSamplesRef.length
68
+ const maxSamplesToWrite = outputChunkSamplesRef.elementCount
69
69
 
70
70
  const inputChunkSamplesForChannel = rawAudio.audioChannels[channelIndex].slice(readOffset, readOffset + maxSamplesToRead)
71
71
 
@@ -91,7 +91,7 @@ export async function resampleAudioSpeex(rawAudio: RawAudio, outSampleRate: numb
91
91
  }
92
92
 
93
93
  m._speex_resampler_destroy(resamplerStateAddress)
94
- wasmMemory.freeAll()
94
+ wasmHeap.freeAll()
95
95
 
96
96
  const resampledAudio: RawAudio = {
97
97
  audioChannels: [],
@@ -49,7 +49,7 @@ export class ChunkedAsciiDecoder {
49
49
  private readonly textDecoder = new TextDecoder('windows-1252')
50
50
 
51
51
  writeChunk(chunk: Uint8Array) {
52
- const decodedChunk = this.textDecoder.decode(chunk)
52
+ const decodedChunk = this.textDecoder.decode(chunk, { stream: true })
53
53
 
54
54
  this.str += decodedChunk
55
55
  }
@@ -41,7 +41,7 @@ export class ChunkedUtf16Decoder {
41
41
  private readonly textDecoder = new TextDecoder('utf-16le')
42
42
 
43
43
  writeChunk(chunk: Uint16Array) {
44
- const decodedChunk = this.textDecoder.decode(chunk)
44
+ const decodedChunk = this.textDecoder.decode(chunk, { stream: true })
45
45
 
46
46
  this.str += decodedChunk
47
47
  }
@@ -33,7 +33,7 @@ export class ChunkedUtf8Decoder {
33
33
  private readonly textDecoder = new TextDecoder('utf-8')
34
34
 
35
35
  writeChunk(chunk: Uint8Array) {
36
- const decodedChunk = this.textDecoder.decode(chunk)
36
+ const decodedChunk = this.textDecoder.decode(chunk, { stream: true })
37
37
 
38
38
  this.str += decodedChunk
39
39
  }
@@ -57,7 +57,7 @@ export async function phonemizeText(text: string, voice: string, substitutionMap
57
57
  .replaceAll('«', ', ')
58
58
  .replaceAll('»', ', ')
59
59
 
60
- const segmentedText = await Segmentation.parse(text, voice)
60
+ const segmentedText = await Segmentation.parseTextAndConvertToFragmentObjects(text, voice)
61
61
  const preparedPhrases: string[] = []
62
62
  const phraseBreakers: string[] = []
63
63
 
@@ -1,22 +1,26 @@
1
- import * as CldrSegmentation from 'cldr-segmentation'
2
- import { splitChineseTextToWords_Jieba } from './ChineseSegmentation.js'
3
-
4
1
  import { sumArray, logToStderr } from '../utilities/Utilities.js'
5
2
  import { getShortLanguageCode } from '../utilities/Locale.js'
6
- import { splitJapaneseTextToWords_Kuromoji } from './JapaneseSegmentation.js'
7
3
  import { ParagraphBreakType, WhitespaceProcessing } from '../api/Common.js'
8
- import { includesAnyOf, indexOfAnyOf } from '../utilities/StringUtilities.js'
4
+ import { includesAnyOf, splitAndPreserveSeparators } from '../utilities/StringUtilities.js'
5
+
6
+ import * as TextSegmentation from '@echogarden/text-segmentation'
7
+ import { splitChineseTextToWords_Jieba } from './ChineseSegmentation.js'
8
+ import { splitJapaneseTextToWords_Kuromoji } from './JapaneseSegmentation.js'
9
9
 
10
10
  const log = logToStderr
11
11
 
12
- export const wordCharacterPattern = /[\p{Letter}\p{Number}]/u
13
- export const punctuationPattern = /[\p{Punctuation}]/u
12
+ export const wordCharacterRegExp = /[\p{Letter}\p{Number}]/u
13
+ export const emojiCharacterRegExp = /[\p{Emoji}]/u
14
+ export const punctuationRegExp = /[\p{Punctuation}]/u
14
15
 
15
- export const phraseSeparators = [',', ';', ':', '', '']
16
+ export const phraseSeparators = [',', '、', ',', '،', ';', ';', ':', '', '']
16
17
  export const symbolWords = ['$', '€', '¢', '£', '¥', '©', '®', '™', '%', '&', '#', '~', '@', '+', '±', '÷', '/', '*', '=', '¼', '½', '¾']
17
18
 
19
+ ///////////////////////////////////////////////////////////////////////////////////////////////
20
+ // Predicates
21
+ ///////////////////////////////////////////////////////////////////////////////////////////////
18
22
  export function isWordOrSymbolWord(str: string) {
19
- return isWord(str) || symbolWords.includes(str)
23
+ return isWord(str) || includesEmoji(str) || symbolWords.includes(str)
20
24
  }
21
25
 
22
26
  export function isSymbolWord(str: string) {
@@ -24,87 +28,106 @@ export function isSymbolWord(str: string) {
24
28
  }
25
29
 
26
30
  export function isWord(str: string) {
27
- return wordCharacterPattern.test(str.trim())
31
+ return wordCharacterRegExp.test(str.trim())
32
+ }
33
+
34
+ export function includesPunctuation(str: string) {
35
+ return punctuationRegExp.test(str.trim())
28
36
  }
29
37
 
30
- export function isPunctuation(str: string) {
31
- return punctuationPattern.test(str.trim())
38
+ export function includesEmoji(str: string) {
39
+ return emojiCharacterRegExp.test(str.trim())
32
40
  }
33
41
 
34
42
  export function isWhitespace(str: string) {
35
43
  return str.trim().length === 0
36
44
  }
37
45
 
38
- export class Sentence {
39
- phrases: Phrase[] = []
46
+ ///////////////////////////////////////////////////////////////////////////////////////////////
47
+ // Paragraph, line, sentence, phrase, and word segmentation
48
+ ///////////////////////////////////////////////////////////////////////////////////////////////
49
+ export function splitToParagraphs(text: string, paragraphBreakType: ParagraphBreakType, whitespaceProcessingMethod: WhitespaceProcessing) {
50
+ let paragraphs: string[] = []
40
51
 
41
- readonly isSentenceFinalizer = true
52
+ if (paragraphBreakType === 'single') {
53
+ paragraphs = splitAndPreserveSeparators(text, /(\r?\n)+/g)
54
+ } else if (paragraphBreakType === 'double') {
55
+ paragraphs = splitAndPreserveSeparators(text, /(\r?\n)(\r?\n)+/g)
56
+ } else {
57
+ throw new Error(`Invalid paragraph break type: '${paragraphBreakType}'`)
58
+ }
42
59
 
43
- get length() { return sumArray(this.phrases, (phrase) => phrase.length) }
60
+ paragraphs = paragraphs.map(p => applyWhitespaceProcessing(p, whitespaceProcessingMethod))
61
+ paragraphs = paragraphs.filter(p => p.length > 0)
44
62
 
45
- get text() { return this.phrases.reduce<string>((result, phrase) => result + phrase.text, '') }
63
+ return paragraphs
46
64
  }
47
65
 
48
- export class Phrase {
49
- words: Word[] = []
50
-
51
- get length() { return sumArray(this.words, (word) => word.length) }
52
-
53
- get text() { return this.words.reduce<string>((result, word) => result + word.text, '') }
54
-
55
- get lastWord() {
56
- if (this.words.length == 0) {
57
- return undefined
58
- }
59
-
60
- return this.words[this.words.length - 1]
61
- }
62
-
63
- get isSentenceFinalizer() { return this.lastWord != null ? this.lastWord.isSentenceFinalizer : false }
66
+ export function splitToLines(text: string) {
67
+ return splitAndPreserveSeparators(text, /\r?\n/g)
64
68
  }
65
69
 
66
- export class Word {
67
- readonly text: string
68
- isSentenceFinalizer: boolean
69
-
70
- constructor(text: string, isSentenceFinalizer: boolean) {
71
- this.text = text
72
- this.isSentenceFinalizer = isSentenceFinalizer
73
- }
74
-
75
- get containsOnlyPunctuation() { return !wordCharacterPattern.test(this.text) && !this.isSymbolWord }
70
+ export async function parseText(text: string, langCode: string) {
71
+ const shortLangCode = getShortLanguageCode(langCode || '')
76
72
 
77
- get isSymbolWord() { return symbolWords.includes(this.text) }
73
+ const wordSequence = await splitToWords(text, shortLangCode)
78
74
 
79
- get isPhraseSeperator() { return this.containsOnlyPunctuation && includesAnyOf(this.text, phraseSeparators) }
75
+ const parsedText = await TextSegmentation.segmentWordSequence(wordSequence)
80
76
 
81
- get length() { return this.text.length }
77
+ return parsedText
82
78
  }
83
79
 
84
- export type Segment = Sentence | Phrase | Word
80
+ export async function splitToWords(text: string, langCode: string): Promise<TextSegmentation.WordSequence> {
81
+ const shortLangCode = getShortLanguageCode(langCode || '')
85
82
 
86
- export class Fragment {
87
- segments: Segment[] = []
83
+ if (shortLangCode === 'zh' || shortLangCode === 'cmn' || shortLangCode === 'ja') {
84
+ let wordArray: string[] = []
88
85
 
89
- get length() { return sumArray(this.segments, (phrase) => phrase.length) }
86
+ if (shortLangCode === 'zh' || shortLangCode === 'cmn') {
87
+ wordArray = await splitChineseTextToWords_Jieba(text)
88
+ } else {
89
+ wordArray = await splitJapaneseTextToWords_Kuromoji(text)
90
+ }
90
91
 
91
- get text() { return this.segments.reduce<string>((result, segment) => result + segment.text, '') }
92
+ const wordSequence = new TextSegmentation.WordSequence()
92
93
 
93
- get isEmpty() { return this.length == 0 }
94
+ let offset = 0
94
95
 
95
- get isNonempty() { return !this.isEmpty }
96
+ for (const wordText of wordArray) {
97
+ const startOffset = offset
98
+ const endOffset = startOffset + wordText.length
99
+ const isNonPunctuation = isWordOrSymbolWord(wordText)
96
100
 
97
- get lastSegment() {
98
- if (this.isEmpty) {
99
- return undefined
101
+ wordSequence.addWord(wordText, startOffset, isNonPunctuation)
102
+
103
+ offset = endOffset
100
104
  }
101
105
 
102
- return this.segments[this.segments.length - 1]
106
+ return wordSequence
107
+ } else {
108
+ return TextSegmentation.splitToWords(text, { language: langCode })
109
+ }
110
+ }
111
+
112
+ export function applyWhitespaceProcessing(text: string, whitespaceProcessingMethod: WhitespaceProcessing) {
113
+ if (whitespaceProcessingMethod === 'removeLineBreaks') {
114
+ return text.trim().replaceAll(/(\r?\n)+/g, ' ')
115
+ } else if (whitespaceProcessingMethod === 'collapse') {
116
+ return text.trim().replaceAll(/\s+/g, ' ')
117
+ } else if (whitespaceProcessingMethod === 'preserve') {
118
+ return text
119
+ } else {
120
+ throw new Error(`Invalid whitespace processing method: '${whitespaceProcessingMethod}'`)
103
121
  }
104
122
  }
105
123
 
124
+ ///////////////////////////////////////////////////////////////////////////////////////////////
125
+ // Fragment segmentation
126
+ //
127
+ // Used to split text to fragments, to fit particular size constraints.
128
+ ///////////////////////////////////////////////////////////////////////////////////////////////
106
129
  export async function splitToFragments(text: string, maxFragmentLength: number, langCode: string, preserveSentences = true, preservePhrases = true) {
107
- const parsedText = await parse(text, langCode)
130
+ const parsedText = await parseTextAndConvertToFragmentObjects(text, langCode)
108
131
 
109
132
  const fragments: Fragment[] = []
110
133
  let currentFragment = new Fragment()
@@ -173,33 +196,28 @@ export async function splitToFragments(text: string, maxFragmentLength: number,
173
196
  return fragments
174
197
  }
175
198
 
176
- export async function parse(text: string, langCode: string) {
177
- const sentencesText = splitToSentences(text, langCode)
199
+ export async function parseTextAndConvertToFragmentObjects(text: string, langCode: string) {
200
+ const segmentedText = await parseText(text, langCode)
201
+
178
202
  const sentences: Sentence[] = []
179
203
 
180
- for (const sentenceText of sentencesText) {
204
+ for (const sentenceEntry of segmentedText.sentences) {
181
205
  const sentence = new Sentence()
182
206
 
183
- let currentPhrase = new Phrase()
184
- const wordTexts = await splitToWords(sentenceText, langCode)
207
+ for (const phraseEntry of sentenceEntry.phrases) {
208
+ const phrase = new Phrase()
185
209
 
186
- for (let wordIndex = 0; wordIndex < wordTexts.length; wordIndex++) {
187
- const word = new Word(wordTexts[wordIndex], wordIndex == wordTexts.length - 1)
210
+ for (const wordEntry of phraseEntry.words.entries) {
211
+ const isSentenceFinalizer = wordEntry === sentenceEntry.words.lastEntry
188
212
 
189
- if (word.isPhraseSeperator) {
190
- const separatorIndex = indexOfAnyOf(word.text, phraseSeparators)
191
- currentPhrase.words.push(new Word(word.text.substring(0, separatorIndex + 1), word.isSentenceFinalizer))
192
- sentence.phrases.push(currentPhrase)
213
+ const word = new Word(wordEntry.text, isSentenceFinalizer)
193
214
 
194
- currentPhrase = new Phrase()
195
- currentPhrase.words.push(new Word(word.text.substring(separatorIndex + 1), false))
196
- } else {
197
- currentPhrase.words.push(word)
215
+ phrase.words.push(word)
198
216
  }
199
- }
200
217
 
201
- if (currentPhrase.words.length > 0) {
202
- sentence.phrases.push(currentPhrase)
218
+ if (phrase.words.length > 0) {
219
+ sentence.phrases.push(phrase)
220
+ }
203
221
  }
204
222
 
205
223
  sentences.push(sentence)
@@ -208,53 +226,70 @@ export async function parse(text: string, langCode: string) {
208
226
  return sentences
209
227
  }
210
228
 
211
- export function splitToSentences(text: string, langCode: string): string[] {
212
- const shortLangCode = getShortLanguageCode(langCode || '')
229
+ export class Sentence {
230
+ phrases: Phrase[] = []
231
+
232
+ readonly isSentenceFinalizer = true
213
233
 
214
- return CldrSegmentation.sentenceSplit(text, CldrSegmentation.suppressions[shortLangCode])
234
+ get length() { return sumArray(this.phrases, (phrase) => phrase.length) }
235
+
236
+ get text() { return this.phrases.reduce<string>((result, phrase) => result + phrase.text, '') }
215
237
  }
216
238
 
217
- export async function splitToWords(text: string, langCode: string): Promise<string[]> {
218
- const shortLangCode = getShortLanguageCode(langCode || '')
239
+ export class Phrase {
240
+ words: Word[] = []
219
241
 
220
- if (shortLangCode == 'zh' || shortLangCode == 'cmn') {
221
- return splitChineseTextToWords_Jieba(text, undefined, true)
222
- } else if (shortLangCode == 'ja') {
223
- return splitJapaneseTextToWords_Kuromoji(text)
224
- } else {
225
- return CldrSegmentation.wordSplit(text, CldrSegmentation.suppressions[shortLangCode])
242
+ get length() { return sumArray(this.words, (word) => word.length) }
243
+
244
+ get text() { return this.words.reduce<string>((result, word) => result + word.text, '') }
245
+
246
+ get lastWord() {
247
+ if (this.words.length == 0) {
248
+ return undefined
249
+ }
250
+
251
+ return this.words[this.words.length - 1]
226
252
  }
253
+
254
+ get isSentenceFinalizer() { return this.lastWord != null ? this.lastWord.isSentenceFinalizer : false }
227
255
  }
228
256
 
229
- export function splitToParagraphs(text: string, paragraphBreaks: ParagraphBreakType, whitespaceProcessingMethod: WhitespaceProcessing) {
230
- let paragraphs: string[] = []
257
+ export class Word {
258
+ readonly text: string
259
+ isSentenceFinalizer: boolean
231
260
 
232
- if (paragraphBreaks === 'single') {
233
- paragraphs = text.split(/(\r?\n)+/g)
234
- } else if (paragraphBreaks === 'double') {
235
- paragraphs = text.split(/(\r?\n)(\r?\n)+/g)
236
- } else {
237
- throw new Error(`Invalid paragraph break type: '${paragraphBreaks}'`)
261
+ constructor(text: string, isSentenceFinalizer: boolean) {
262
+ this.text = text
263
+ this.isSentenceFinalizer = isSentenceFinalizer
238
264
  }
239
265
 
240
- paragraphs = paragraphs.map(p => applyWhitespaceProcessing(p.trim(), whitespaceProcessingMethod))
241
- paragraphs = paragraphs.filter(p => p.length > 0)
266
+ get containsOnlyPunctuation() { return !wordCharacterRegExp.test(this.text) && !this.isSymbolWord }
242
267
 
243
- return paragraphs
244
- }
268
+ get isSymbolWord() { return symbolWords.includes(this.text) }
245
269
 
246
- export function applyWhitespaceProcessing(text: string, whitespaceProcessingMethod: WhitespaceProcessing) {
247
- if (whitespaceProcessingMethod === 'removeLineBreaks') {
248
- return text.replaceAll(/(\r?\n)+/g, ' ')
249
- } else if (whitespaceProcessingMethod === 'collapse') {
250
- return text.replaceAll(/\s+/g, ' ')
251
- } else if (whitespaceProcessingMethod === 'preserve') {
252
- return text
253
- } else {
254
- throw new Error(`Invalid whitespace processing method: '${whitespaceProcessingMethod}'`)
255
- }
270
+ get isPhraseSeperator() { return this.containsOnlyPunctuation && includesAnyOf(this.text, phraseSeparators) }
271
+
272
+ get length() { return this.text.length }
256
273
  }
257
274
 
258
- export function splitToLines(text: string) {
259
- return text.split(/\r?\n/g)
275
+ export type Segment = Sentence | Phrase | Word
276
+
277
+ export class Fragment {
278
+ segments: Segment[] = []
279
+
280
+ get length() { return sumArray(this.segments, (phrase) => phrase.length) }
281
+
282
+ get text() { return this.segments.reduce<string>((result, segment) => result + segment.text, '') }
283
+
284
+ get isEmpty() { return this.length == 0 }
285
+
286
+ get isNonempty() { return !this.isEmpty }
287
+
288
+ get lastSegment() {
289
+ if (this.isEmpty) {
290
+ return undefined
291
+ }
292
+
293
+ return this.segments[this.segments.length - 1]
294
+ }
260
295
  }