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,13 +1,13 @@
1
1
  import { SynthesisVoice } from '../api/API.js'
2
- import { decodeToChannels } from '../audio/AudioBufferConversion.js'
3
- import { SampleFormat } from '../codecs/WaveCodec.js'
2
+ import { decodeToChannels, SampleFormat } from '../audio/AudioBufferConversion.js'
4
3
  import { bandwidthToQFactor } from '../dsp/BiquadFilter.js'
5
4
  import { Logger } from '../utilities/Logger.js'
6
- import { WasmMemoryManager } from '../utilities/WasmMemoryManager.js'
7
5
  import { RawAudio } from '../audio/AudioUtilities.js'
8
6
  import { readFileAsBinary } from '../utilities/FileSystem.js'
9
7
  import { concatUint8Arrays } from '../utilities/Utilities.js'
10
8
 
9
+ import { wrapEmscriptenModuleHeap } from 'wasm-heap-manager'
10
+
11
11
  let svoxPicoInstance: any
12
12
 
13
13
  export async function synthesize(text: string, textAnalysisFilePath: string, signalGenerationFilePath: string, postprocessOutput = true) {
@@ -18,7 +18,7 @@ export async function synthesize(text: string, textAnalysisFilePath: string, sig
18
18
 
19
19
  logger.start('Initialize pico engine')
20
20
 
21
- const wasmMemory = new WasmMemoryManager(m)
21
+ const wasmHeap = wrapEmscriptenModuleHeap(m)
22
22
 
23
23
  const pico_initialize = m._pico_initialize
24
24
  const picoext_setTraceLevel = m._picoext_setTraceLevel
@@ -38,11 +38,11 @@ export async function synthesize(text: string, textAnalysisFilePath: string, sig
38
38
  const pico_unloadResource = m._pico_unloadResource
39
39
 
40
40
  const picoMemSize = 2500000
41
- const picoMemAreaRef = wasmMemory.allocUint8Array(picoMemSize)
41
+ const picoMemAreaRef = wasmHeap.allocUint8Array(picoMemSize)
42
42
 
43
- const systemPtrRef = wasmMemory.allocPointer()
43
+ const systemPtrRef = wasmHeap.allocPointer32()
44
44
 
45
- let resultCode = pico_initialize(picoMemAreaRef.address, picoMemAreaRef.length, systemPtrRef.address)
45
+ let resultCode = pico_initialize(picoMemAreaRef.address, picoMemAreaRef.allocatedByteCount, systemPtrRef.address)
46
46
  const systemPtr = systemPtrRef.value
47
47
 
48
48
  throwErrorIfFailed(resultCode, 'Failed Pico initialization.')
@@ -55,8 +55,8 @@ export async function synthesize(text: string, textAnalysisFilePath: string, sig
55
55
  const fileData = await readFileAsBinary(localFilePath)
56
56
  m.FS.writeFile(virtualFilePath, fileData)
57
57
 
58
- const virtualFilePathRef = wasmMemory.allocNullTerminatedUtf8String(virtualFilePath)
59
- const resourcePtrRef = wasmMemory.allocPointer()
58
+ const virtualFilePathRef = wasmHeap.allocNullTerminatedUtf8String(virtualFilePath)
59
+ const resourcePtrRef = wasmHeap.allocPointer32()
60
60
 
61
61
  resultCode = pico_loadResource(systemPtr, virtualFilePathRef.address, resourcePtrRef.address)
62
62
  const resourcePtr = resourcePtrRef.value
@@ -70,12 +70,12 @@ export async function synthesize(text: string, textAnalysisFilePath: string, sig
70
70
  const { resourcePtr: signalGenerationResourcePtr, resourcePtrRef: signalGenerationResourcePtrRef } = await loadResource(signalGenerationFilePath)
71
71
 
72
72
  function getResourceName(resourcePtr: number) {
73
- const resourceNameRef = wasmMemory.allocUint8Array(32)
73
+ const resourceNameRef = wasmHeap.allocNullTerminatedUtf8String(32)
74
74
  resultCode = pico_getResourceName(systemPtr, resourcePtr, resourceNameRef.address)
75
75
 
76
76
  throwErrorIfFailed(resultCode, `Failed getting Pico resource name.`)
77
77
 
78
- const resourceName = resourceNameRef.readAsNullTerminatedUtf8String()
78
+ const resourceName = resourceNameRef.value
79
79
 
80
80
  return { resourceName, resourceNameRef }
81
81
  }
@@ -83,7 +83,7 @@ export async function synthesize(text: string, textAnalysisFilePath: string, sig
83
83
  const { resourceName: textAnalysisResourceName, resourceNameRef: textAnalysisResourceNameRef } = getResourceName(textAnalysisResourcePtr)
84
84
  const { resourceName: signalGenerationResourceName, resourceNameRef: signalGenerationResourceNameRef } = getResourceName(signalGenerationResourcePtr)
85
85
 
86
- const voiceNameRef = wasmMemory.allocNullTerminatedUtf8String('PicoVoice')
86
+ const voiceNameRef = wasmHeap.allocNullTerminatedUtf8String('PicoVoice')
87
87
 
88
88
  resultCode = pico_createVoiceDefinition(systemPtr, voiceNameRef.address)
89
89
 
@@ -96,7 +96,7 @@ export async function synthesize(text: string, textAnalysisFilePath: string, sig
96
96
  addResourceToVoiceDefinition(textAnalysisResourceNameRef.address)
97
97
  addResourceToVoiceDefinition(signalGenerationResourceNameRef.address)
98
98
 
99
- const enginePtrRef = wasmMemory.allocPointer()
99
+ const enginePtrRef = wasmHeap.allocPointer32()
100
100
  resultCode = pico_newEngine(systemPtr, voiceNameRef.address, enginePtrRef.address)
101
101
 
102
102
  throwErrorIfFailed(resultCode, `Failed creating new engine.`)
@@ -105,15 +105,17 @@ export async function synthesize(text: string, textAnalysisFilePath: string, sig
105
105
 
106
106
  logger.start('Synthesize with pico')
107
107
 
108
- const textRef = wasmMemory.allocNullTerminatedUtf8String(text)
108
+ const textRef = wasmHeap.allocNullTerminatedUtf8String(text)
109
109
 
110
- const bytesWrittenRef = wasmMemory.allocInt32()
110
+ const bytesWrittenRef = wasmHeap.allocInt32()
111
111
 
112
112
  const audioParts: Uint8Array[] = []
113
113
 
114
- for (let textByteOffset = 0; textByteOffset < textRef.length;) {
114
+ const textRefEncodedByteCountIncludingTerminator = textRef.encodedByteCount + 1
115
+
116
+ for (let textByteOffset = 0; textByteOffset < textRefEncodedByteCountIncludingTerminator;) {
115
117
  bytesWrittenRef.value = 0
116
- resultCode = pico_putTextUtf8(enginePtr, textRef.address + textByteOffset, textRef.length - textByteOffset, bytesWrittenRef.address)
118
+ resultCode = pico_putTextUtf8(enginePtr, textRef.address + textByteOffset, textRefEncodedByteCountIncludingTerminator - textByteOffset, bytesWrittenRef.address)
117
119
  const bytesWritten = bytesWrittenRef.value
118
120
 
119
121
  throwErrorIfFailed(resultCode, `Failed writing text to engine.`)
@@ -130,13 +132,13 @@ export async function synthesize(text: string, textAnalysisFilePath: string, sig
130
132
  const outBuffers: Uint8Array[] = []
131
133
 
132
134
  const outBufferLength = 16384
133
- const outBufferRef = wasmMemory.allocUint8Array(outBufferLength)
135
+ const outBufferRef = wasmHeap.allocUint8Array(outBufferLength)
134
136
 
135
- const outByteCountRef = wasmMemory.allocInt16()
136
- const outDataTypeRef = wasmMemory.allocInt16()
137
+ const outByteCountRef = wasmHeap.allocInt16()
138
+ const outDataTypeRef = wasmHeap.allocInt16()
137
139
 
138
140
  while (true) {
139
- resultCode = pico_getData(enginePtr, outBufferRef.address, outBufferRef.length, outByteCountRef.address, outDataTypeRef.address)
141
+ resultCode = pico_getData(enginePtr, outBufferRef.address, outBufferRef.allocatedByteCount, outByteCountRef.address, outDataTypeRef.address)
140
142
 
141
143
  throwErrorIfFailed(resultCode, `Failed getting audio data from engine.`, [200, 201])
142
144
 
@@ -148,7 +150,7 @@ export async function synthesize(text: string, textAnalysisFilePath: string, sig
148
150
  }
149
151
 
150
152
  if (outByteCount > 0) {
151
- outBuffers.push(outBufferRef.slice(0, outByteCount) as Uint8Array)
153
+ outBuffers.push(outBufferRef.view.slice(0, outByteCount))
152
154
  }
153
155
  }
154
156
 
@@ -180,7 +182,7 @@ export async function synthesize(text: string, textAnalysisFilePath: string, sig
180
182
 
181
183
  pico_terminate(systemPtrRef.address)
182
184
 
183
- wasmMemory.freeAll()
185
+ wasmHeap.freeAll()
184
186
  }
185
187
 
186
188
  function throwErrorIfFailed(resultCode: number, title: string, successCodes = [0]) {
@@ -188,9 +190,9 @@ export async function synthesize(text: string, textAnalysisFilePath: string, sig
188
190
  return
189
191
  }
190
192
 
191
- const picoErrorMessageRef = wasmMemory.allocUint8Array(200)
193
+ const picoErrorMessageRef = wasmHeap.allocNullTerminatedUtf8String(200)
192
194
  pico_getSystemStatusMessage(systemPtr, resultCode, picoErrorMessageRef)
193
- const picoErrorMessage = picoErrorMessageRef.readAsNullTerminatedUtf8String()
195
+ const picoErrorMessage = picoErrorMessageRef.value
194
196
 
195
197
  dispose()
196
198
  throw new Error(`${title} ${picoErrorMessage}`)
@@ -1,6 +1,6 @@
1
1
  import { request } from "gaxios"
2
2
  import { Logger } from "../utilities/Logger.js"
3
- import { splitToSentences } from "../nlp/Segmentation.js"
3
+ import { parseText } from "../nlp/Segmentation.js"
4
4
  import { TranslationPair } from "../api/TextTranslation.js"
5
5
  import { getChromeOnWindowsHeaders } from "../utilities/BrowserRequestHeaders.js"
6
6
 
@@ -9,7 +9,8 @@ export async function translateText(text: string, sourceLanguage: string, target
9
9
 
10
10
  logger.start(`Prepare request`)
11
11
 
12
- const textSentences = splitToSentences(text, sourceLanguage)
12
+ const segmentedText = await parseText(text, sourceLanguage)
13
+ const sentences = segmentedText.sentences
13
14
 
14
15
  const requestBodyObject = {
15
16
  jsonrpc: '2.0',
@@ -20,7 +21,7 @@ export async function translateText(text: string, sourceLanguage: string, target
20
21
  kind: 'default',
21
22
  sentences: [
22
23
  {
23
- text: textSentences[0],
24
+ text: sentences[0].text,
24
25
  id: 1,
25
26
  prefix: '',
26
27
  }
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk'
2
2
  import { TranslationPair } from '../api/TextTranslation.js'
3
- import { splitToSentences } from '../nlp/Segmentation.js'
3
+ import { parseText } from '../nlp/Segmentation.js'
4
4
  import { Logger } from '../utilities/Logger.js'
5
5
  import { loadPackage } from '../utilities/PackageManager.js'
6
6
 
@@ -30,19 +30,21 @@ export async function translateText(sourceText: string, sourceLanguage: string,
30
30
  tgt_lang: 'eng_Latn'
31
31
  }
32
32
 
33
- const sentences = splitToSentences(sourceText, sourceLanguage)
33
+ const segmentedText = await parseText(sourceText, sourceLanguage)
34
+
35
+ const sentences = segmentedText.sentences
34
36
 
35
37
  logger.end()
36
38
 
37
39
  const translationPairs: TranslationPair[] = []
38
40
 
39
41
  for (let i = 0; i < sentences.length; i++) {
40
- const sentence = sentences[i]
42
+ const sentenceText = sentences[i].text
41
43
 
42
- logger.logTitledMessage(`Translate sentence ${i + 1}/${sentences.length}`, `"${sentence.trim()}"`, chalk.magentaBright)
44
+ logger.logTitledMessage(`Translate sentence ${i + 1}/${sentences.length}`, `"${sentenceText.trim()}"`, chalk.magentaBright)
43
45
 
44
46
  logger.start(`Tokenize sentence`)
45
- const inputs = (tokenizer as any)._build_translation_inputs(sentence, {
47
+ const inputs = (tokenizer as any)._build_translation_inputs(sentenceText, {
46
48
  padding: true,
47
49
  truncation: true,
48
50
  }, config)
@@ -67,7 +69,7 @@ export async function translateText(sourceText: string, sourceLanguage: string,
67
69
  .trim()
68
70
 
69
71
  translationPairs.push({
70
- sourceText: sentence,
72
+ sourceText: sentenceText,
71
73
  translatedText
72
74
  })
73
75
 
@@ -1,5 +1,3 @@
1
- declare module 'moving-median'
2
-
3
1
  declare module 'html-to-text' {
4
2
  export function htmlToText(html: string, options: {
5
3
  wordwrap: boolean | number
@@ -7,20 +5,6 @@ declare module 'html-to-text' {
7
5
  })
8
6
  }
9
7
 
10
- declare module 'cldr-segmentation' {
11
- export function sentenceSplit(text: string, suppressions: any)
12
- export function wordSplit(text: string, suppressions: any)
13
- export const suppressions: Record<string, Suppressions>
14
-
15
- export class Suppressions {
16
- constructor(forwardTrie: any, backwardTrie: any, list: string)
17
-
18
- merge(other: Suppressions)
19
-
20
- static create(list: string[]): Suppressions
21
- }
22
- }
23
-
24
8
  declare module 'winax'
25
9
 
26
10
  declare module 'tinyld' {
@@ -52,7 +52,7 @@ export function formatListWithQuotedElements(strings: string[], quoteSymbol = `'
52
52
 
53
53
  export function getUTF32Chars(str: string) {
54
54
  const utf32chars: string[] = []
55
- const mapping: number[] = []
55
+ const utf16To32Mapping: number[] = []
56
56
 
57
57
  let utf32Index = 0
58
58
 
@@ -60,15 +60,15 @@ export function getUTF32Chars(str: string) {
60
60
  utf32chars.push(utf32char)
61
61
 
62
62
  for (let i = 0; i < utf32char.length; i++) {
63
- mapping.push(utf32Index)
63
+ utf16To32Mapping.push(utf32Index)
64
64
  }
65
65
 
66
66
  utf32Index += 1
67
67
  }
68
68
 
69
- mapping.push(utf32Index)
69
+ utf16To32Mapping.push(utf32Index)
70
70
 
71
- return { utf32chars, mapping }
71
+ return { utf32chars, utf16To32Mapping }
72
72
  }
73
73
 
74
74
  export function containsInvalidCodepoint(str: string) {
@@ -81,10 +81,9 @@ export function containsInvalidCodepoint(str: string) {
81
81
  return false
82
82
  }
83
83
 
84
-
85
84
  export function splitAndPreserveSeparators(text: string, separatorRegex: RegExp): string[] {
86
85
  if (!separatorRegex.flags.includes('g')) {
87
- throw new Error('RegExp must be global')
86
+ throw new Error('Separator regular expression must have a global flag')
88
87
  }
89
88
 
90
89
  // Use the match method to find all matches for the separators
@@ -1,7 +1,8 @@
1
+ import { addMissingPunctuationWordsToWordSequence, segmentWordSequence, WordSequence } from '@echogarden/text-segmentation'
1
2
  import { ParagraphBreakType, WhitespaceProcessing } from '../api/Common.js'
2
- import { applyWhitespaceProcessing, isWord, isWordOrSymbolWord, splitToParagraphs, splitToSentences, splitToWords } from '../nlp/Segmentation.js'
3
+ import { isWord, isWordOrSymbolWord, splitToParagraphs, splitToWords } from '../nlp/Segmentation.js'
3
4
  import { deepClone } from './ObjectUtilities.js'
4
- import { getUTF32Chars, splitAndPreserveSeparators } from './StringUtilities.js'
5
+ import { getUTF32Chars } from './StringUtilities.js'
5
6
  import { roundToDigits } from './Utilities.js'
6
7
 
7
8
  export function addTimeOffsetToTimeline(targetTimeline: Timeline, timeOffset: number) {
@@ -62,199 +63,155 @@ export function roundTimelineProperties(targetTimeline: Timeline, decimalDigits
62
63
  return roundedTimeline
63
64
  }
64
65
 
65
- export async function wordTimelineToSegmentSentenceTimeline(wordTimeline: Timeline, transcript: string, language: string, paragraphBreaks: ParagraphBreakType = 'double', whitespace: WhitespaceProcessing = 'collapse') {
66
- let segments: string[][] = []
66
+ export async function wordTimelineToSegmentSentenceTimeline(wordTimelineWithOffsets: Timeline, transcript: string, language: string, paragraphBreaks: ParagraphBreakType = 'double', whitespace: WhitespaceProcessing = 'collapse') {
67
+ const wordSequence = new WordSequence()
67
68
 
68
- {
69
- // Ensure word entries with words that include potential sentence ending characters,
70
- // like '.', '?' or '!', aren't causing the sentence segmentation
71
- // to identify them as sentence breaks.
72
- const maskedTranscript = replaceSentenceEndersWithinWordsWithMaskingCharacter(transcript, wordTimeline, '_')
73
-
74
- // Split to segments and sentences, based on the masked transcript,
75
- // don't apply any whitespace processing yet.
76
- let paragraphs: string[]
77
-
78
- if (paragraphBreaks === 'single') {
79
- paragraphs = splitAndPreserveSeparators(maskedTranscript, /(\r?\n)+/g)
80
- } else if (paragraphBreaks === 'double') {
81
- paragraphs = splitAndPreserveSeparators(maskedTranscript, /(\r?\n)(\r?\n)+/g)
82
- } else {
83
- throw new Error(`Invalid paragraph break type: '${paragraphBreaks}'`)
84
- }
85
-
86
- const maskedSegments = paragraphs.map(paragraph => splitToSentences(paragraph, language))
87
-
88
- // Restore the sentence text the original text, using the original transcript,
89
- // and apply whitespace processing to each sentence.
90
- let offset = 0
69
+ for (const wordEntry of wordTimelineWithOffsets) {
70
+ const wordStartOffset = wordEntry.startOffsetUtf16!
71
+ const wordEndOffset = wordEntry.endOffsetUtf16!
91
72
 
92
- for (const segment of maskedSegments) {
93
- const newSegment: string[] = []
73
+ const isPunctuation = !isWordOrSymbolWord(wordEntry.text)
94
74
 
95
- for (let sentenceIndex = 0; sentenceIndex < segment.length; sentenceIndex++) {
96
- const sentence = segment[sentenceIndex]
97
- const sentenceLength = sentence.length
98
-
99
- const restoredSentence = transcript.substring(offset, offset + sentenceLength)
100
- const restoredAndProcessedSentence = applyWhitespaceProcessing(restoredSentence, whitespace).trim()
75
+ wordSequence.addWord(wordEntry.text, wordStartOffset, isPunctuation)
76
+ }
101
77
 
102
- if (restoredAndProcessedSentence.length > 0) {
103
- newSegment.push(restoredAndProcessedSentence)
104
- }
78
+ const { wordSequenceWithPunctuation, originalWordsReverseMapping } = addMissingPunctuationWordsToWordSequence(wordSequence, transcript)
105
79
 
106
- offset += sentenceLength
107
- }
80
+ const segmentedWordSequence = await segmentWordSequence(wordSequenceWithPunctuation)
108
81
 
109
- segments.push(newSegment)
110
- }
82
+ const paragraphs = splitToParagraphs(transcript, paragraphBreaks, whitespace)
83
+ const sentenceIndexesForParagraph = paragraphs.map(_ => [] as number[])
111
84
 
112
- segments = segments.filter(segment => segment.length > 0)
113
- }
85
+ {
86
+ let sentenceIndex = 0
87
+ let charOffset = 0
114
88
 
115
- // Create a new text based on the processed sentences, new segment and sentence timeline,
116
- // and store mapping between character indexes and the corresponding sentence they belong to.
117
- let text = ''
118
- const charIndexToSentenceEntryMapping: TimelineEntry[] = []
89
+ for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
90
+ const paragraph = paragraphs[paragraphIndex]
119
91
 
120
- const segmentTimeline: Timeline = []
92
+ const paragraphStartOffset = charOffset
93
+ const paragraphEndOffset = paragraphStartOffset + paragraph.length
121
94
 
122
- for (const segment of segments) {
123
- const sentencesInSegment: Timeline = []
95
+ while (sentenceIndex < segmentedWordSequence.sentences.length) {
96
+ const sentenceEntry = segmentedWordSequence.sentences[sentenceIndex]
97
+ const sentenceStartOffset = sentenceEntry.charRange.start
98
+ const sentenceEndOffset = sentenceEntry.charRange.end
124
99
 
125
- const segmentEntry: TimelineEntry = {
126
- type: 'segment',
127
- text: '',
128
- startTime: -1,
129
- endTime: -1,
130
- timeline: sentencesInSegment
131
- }
132
-
133
- for (const sentence of segment) {
134
- const sentenceEntry: TimelineEntry = {
135
- type: 'sentence',
136
- text: sentence,
137
- startTime: -1,
138
- endTime: -1,
139
- timeline: []
100
+ if (sentenceStartOffset < paragraphEndOffset) {
101
+ sentenceIndexesForParagraph[paragraphIndex].push(sentenceIndex)
102
+ sentenceIndex++
103
+ } else {
104
+ break
105
+ }
140
106
  }
141
107
 
142
- for (const char of sentence + ' ') {
143
- text += char
144
- charIndexToSentenceEntryMapping.push(sentenceEntry)
108
+ if (sentenceIndex === segmentedWordSequence.sentences.length) {
109
+ break
145
110
  }
146
111
 
147
- sentencesInSegment.push(sentenceEntry)
112
+ charOffset += paragraph.length
148
113
  }
149
-
150
- segmentTimeline.push(segmentEntry)
151
114
  }
152
115
 
153
- // Add the word entries to their corresponding sentence timelines
154
- {
155
- let wordSearchStartOffset = 0
156
-
157
- for (let wordIndex = 0; wordIndex < wordTimeline.length; wordIndex++) {
158
- const wordEntry = wordTimeline[wordIndex]
159
- const wordText = wordEntry.text
160
-
161
- if (!isWordOrSymbolWord(wordText)) {
162
- continue
163
- }
164
-
165
- const indexOfWordInText = text.indexOf(wordText, wordSearchStartOffset)
166
-
167
- if (indexOfWordInText == -1) {
168
- throw new Error(`Couldn't find the word '${wordText}' in the text at start position ${wordSearchStartOffset}`)
169
- }
116
+ const segmentTimeline: Timeline = []
117
+ let wordIndex = 0
170
118
 
171
- const targetSentenceEntry = charIndexToSentenceEntryMapping[indexOfWordInText]
172
- targetSentenceEntry.timeline!.push(deepClone(wordEntry))
119
+ for (let paragraphIndex = 0; paragraphIndex < paragraphs.length; paragraphIndex++) {
120
+ const paragraph = paragraphs[paragraphIndex]
121
+ const sentencesIndexes = sentenceIndexesForParagraph[paragraphIndex]
122
+ const sentenceEntries = sentencesIndexes.map(index => segmentedWordSequence.sentences[index])
173
123
 
174
- wordSearchStartOffset = indexOfWordInText + wordText.length
175
- }
176
- }
124
+ const sentenceTimeline: Timeline = []
177
125
 
178
- // Produce a new segment/sentence timeline with rewritten entries,
179
- // that match the assigned words.
180
- const newSegmentTimeline: Timeline = []
126
+ for (const sentenceEntry of sentenceEntries) {
127
+ const wordTimeline: Timeline = []
181
128
 
182
- for (const segmentEntry of segmentTimeline) {
183
- const oldSentenceTimeline = segmentEntry.timeline!
129
+ for (const _ of sentenceEntry.words.entries) {
130
+ const originalWordIndex = originalWordsReverseMapping.get(wordIndex)
184
131
 
185
- const newSentenceTimeline: Timeline = []
132
+ if (originalWordIndex !== undefined) {
133
+ const wordTimelineEntry = wordTimelineWithOffsets[originalWordIndex]
186
134
 
187
- for (const sentenceEntry of oldSentenceTimeline) {
188
- const wordTimeline = sentenceEntry.timeline
135
+ wordTimeline.push(wordTimelineEntry)
136
+ }
189
137
 
190
- if (!wordTimeline || wordTimeline.length === 0) {
191
- continue
138
+ wordIndex += 1
192
139
  }
193
140
 
194
- sentenceEntry.startTime = wordTimeline[0].startTime
195
- sentenceEntry.endTime = wordTimeline[wordTimeline.length - 1].endTime
141
+ const sentenceTimelineEntry: TimelineEntry = {
142
+ type: 'sentence',
143
+ text: sentenceEntry.text,
144
+ startTime: wordTimeline[0].startTime,
145
+ endTime: wordTimeline[wordTimeline.length - 1].endTime,
196
146
 
197
- newSentenceTimeline.push(sentenceEntry)
198
- }
147
+ timeline: wordTimeline,
148
+ }
199
149
 
200
- if (newSentenceTimeline.length === 0) {
201
- continue
150
+ sentenceTimeline.push(sentenceTimelineEntry)
202
151
  }
203
152
 
204
- segmentEntry.text = newSentenceTimeline.map(sentenceEntry => sentenceEntry.text).join(' ')
153
+ const segmentTimelineEntry: TimelineEntry = {
154
+ type: 'segment',
155
+ text: paragraph,
156
+ startTime: sentenceTimeline[0].startTime,
157
+ endTime: sentenceTimeline[sentenceTimeline.length - 1].endTime,
205
158
 
206
- segmentEntry.startTime = newSentenceTimeline[0].startTime
207
- segmentEntry.endTime = newSentenceTimeline[newSentenceTimeline.length - 1].endTime
159
+ timeline: sentenceTimeline,
160
+ }
208
161
 
209
- newSegmentTimeline.push(segmentEntry)
162
+ segmentTimeline.push(segmentTimelineEntry)
210
163
  }
211
164
 
212
- return { segmentTimeline: newSegmentTimeline }
165
+ return { segmentTimeline }
213
166
  }
214
167
 
215
- export function addWordTextOffsetsToTimeline(timeline: Timeline, text: string, currentOffset = 0) {
216
- const { mapping } = getUTF32Chars(text)
168
+ export function addWordTextOffsetsToTimelineInPlace(timeline: Timeline, text: string) {
169
+ const { utf16To32Mapping } = getUTF32Chars(text)
217
170
 
218
- for (const entry of timeline) {
219
- if (entry.type == 'word') {
220
- let word = entry.text
171
+ let currentOffset = 0
221
172
 
222
- word = word.trim().replaceAll(/\s+/g, ' ')
173
+ function processTimeline(timeline: Timeline) {
174
+ for (const entry of timeline) {
175
+ if (entry.type === 'word') {
176
+ let word = entry.text
223
177
 
224
- const wordParts = word.split(' ')
178
+ word = word.trim().replaceAll(/\s+/g, ' ')
225
179
 
226
- let startOffset: number | undefined
227
- let endOffset: number | undefined
180
+ const wordParts = word.split(' ')
228
181
 
229
- for (let i = 0; i < wordParts.length; i++) {
230
- let wordPart = wordParts[i]
182
+ let startOffset: number | undefined
183
+ let endOffset: number | undefined
231
184
 
232
- let wordPartOffset = text.indexOf(wordPart, currentOffset)
185
+ for (let i = 0; i < wordParts.length; i++) {
186
+ const wordPart = wordParts[i]
233
187
 
234
- if (wordPartOffset == -1) {
235
- continue
236
- }
188
+ const wordPartOffset = text.indexOf(wordPart, currentOffset)
237
189
 
238
- currentOffset = wordPartOffset + wordParts[i].length
190
+ if (wordPartOffset === -1) {
191
+ continue
192
+ }
239
193
 
240
- if (i == 0) {
241
- startOffset = wordPartOffset
242
- }
194
+ currentOffset = wordPartOffset + wordParts[i].length
243
195
 
244
- endOffset = currentOffset
245
- }
196
+ if (i === 0) {
197
+ startOffset = wordPartOffset
198
+ }
246
199
 
247
- entry.startOffsetUtf16 = startOffset
248
- entry.endOffsetUtf16 = endOffset
200
+ endOffset = currentOffset
201
+ }
249
202
 
250
- entry.startOffsetUtf32 = startOffset != undefined ? mapping[startOffset] : undefined
251
- entry.endOffsetUtf32 = endOffset != undefined ? mapping[endOffset] : undefined
252
- } else if (entry.timeline) {
253
- currentOffset = addWordTextOffsetsToTimeline(entry.timeline, text, currentOffset)
203
+ entry.startOffsetUtf16 = startOffset
204
+ entry.endOffsetUtf16 = endOffset
205
+
206
+ entry.startOffsetUtf32 = startOffset !== undefined ? utf16To32Mapping[startOffset] : undefined
207
+ entry.endOffsetUtf32 = endOffset !== undefined ? utf16To32Mapping[endOffset] : undefined
208
+ } else if (entry.timeline) {
209
+ processTimeline(entry.timeline)
210
+ }
254
211
  }
255
212
  }
256
213
 
257
- return currentOffset
214
+ return processTimeline(timeline)
258
215
  }
259
216
 
260
217
  function replaceSentenceEndersWithinWordsWithMaskingCharacter(transcript: string, wordTimeline: Timeline, maskingCharacter: string) {
@@ -344,30 +301,3 @@ export type TimelineEntry = {
344
301
  }
345
302
 
346
303
  export type Timeline = TimelineEntry[]
347
-
348
- ////
349
-
350
- export async function testTimelineFix() {
351
- let transcript = 'Hello world how are you? Do you want to play chess?'
352
- const words = (await splitToWords(transcript, 'en')).filter(word => word.trim() !== '')
353
-
354
- let wordTimeline: Timeline = words.map(wordText => ({
355
- type: 'word',
356
-
357
- text: wordText,
358
-
359
- startTime: 0,
360
- endTime: 0,
361
- }))
362
-
363
- addWordTextOffsetsToTimeline(wordTimeline, transcript)
364
-
365
- wordTimeline[1].text = 'wor.d'
366
- wordTimeline[8].text = 'wa.t'
367
-
368
- transcript = transcript.replace('world', 'wor.d').replace('want', 'wa.t')
369
-
370
- const result = await wordTimelineToSegmentSentenceTimeline(wordTimeline, transcript, 'en')
371
-
372
- const x = 1
373
- }
@@ -1,4 +1,4 @@
1
- import { splitToParagraphs, wordCharacterPattern } from '../nlp/Segmentation.js'
1
+ import { splitToParagraphs, wordCharacterRegExp } from '../nlp/Segmentation.js'
2
2
  import { Logger } from './Logger.js'
3
3
 
4
4
  export async function parseWikipediaArticle(articleName: string, language: string) {
@@ -20,7 +20,7 @@ export async function parseWikipediaArticle(articleName: string, language: strin
20
20
  for (const section of sections) {
21
21
  const sectionTitle = section.title()
22
22
 
23
- if (wordCharacterPattern.test(sectionTitle)) {
23
+ if (wordCharacterRegExp.test(sectionTitle)) {
24
24
  sectionsText.push(sectionTitle)
25
25
  }
26
26
 
@@ -29,7 +29,7 @@ export async function parseWikipediaArticle(articleName: string, language: strin
29
29
  for (const paragraph of sectionParagraphs) {
30
30
  const paragraphText = paragraph
31
31
 
32
- if (wordCharacterPattern.test(paragraphText)) {
32
+ if (wordCharacterRegExp.test(paragraphText)) {
33
33
  sectionsText.push(paragraphText)
34
34
  }
35
35
  }