echogarden 2.7.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 (110) 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/SpeechTranslation.js +2 -2
  10. package/dist/api/SpeechTranslation.js.map +1 -1
  11. package/dist/api/Synthesis.js +7 -7
  12. package/dist/api/Synthesis.js.map +1 -1
  13. package/dist/api/TimelineTranslationAlignment.js +2 -2
  14. package/dist/api/TimelineTranslationAlignment.js.map +1 -1
  15. package/dist/api/TranslationAlignment.js +2 -2
  16. package/dist/api/TranslationAlignment.js.map +1 -1
  17. package/dist/audio/AudioPlayer.js +2 -2
  18. package/dist/audio/AudioPlayer.js.map +1 -1
  19. package/dist/audio/AudioUtilities.d.ts +1 -1
  20. package/dist/cli/CLI.js +4 -4
  21. package/dist/cli/CLI.js.map +1 -1
  22. package/dist/denoising/RNNoise.js +5 -5
  23. package/dist/denoising/RNNoise.js.map +1 -1
  24. package/dist/dsp/FFT.js +13 -19
  25. package/dist/dsp/FFT.js.map +1 -1
  26. package/dist/dsp/Rubberband.js +5 -5
  27. package/dist/dsp/Rubberband.js.map +1 -1
  28. package/dist/dsp/Sonic.js +6 -6
  29. package/dist/dsp/Sonic.js.map +1 -1
  30. package/dist/dsp/SpeexResampler.js +11 -11
  31. package/dist/dsp/SpeexResampler.js.map +1 -1
  32. package/dist/nlp/EspeakPhonemizer.js +1 -1
  33. package/dist/nlp/EspeakPhonemizer.js.map +1 -1
  34. package/dist/nlp/Segmentation.d.ts +13 -10
  35. package/dist/nlp/Segmentation.js +129 -106
  36. package/dist/nlp/Segmentation.js.map +1 -1
  37. package/dist/nlp/TextNormalizer.js +74 -42
  38. package/dist/nlp/TextNormalizer.js.map +1 -1
  39. package/dist/recognition/AmazonTranscribeSTT.js +2 -2
  40. package/dist/recognition/AmazonTranscribeSTT.js.map +1 -1
  41. package/dist/recognition/SileroSTT.js +2 -2
  42. package/dist/recognition/SileroSTT.js.map +1 -1
  43. package/dist/recognition/WhisperSTT.js +4 -16
  44. package/dist/recognition/WhisperSTT.js.map +1 -1
  45. package/dist/subtitles/Subtitles.js +52 -7
  46. package/dist/subtitles/Subtitles.js.map +1 -1
  47. package/dist/synthesis/ElevenLabsTTS.js +11 -10
  48. package/dist/synthesis/ElevenLabsTTS.js.map +1 -1
  49. package/dist/synthesis/EspeakTTS.js +8 -8
  50. package/dist/synthesis/EspeakTTS.js.map +1 -1
  51. package/dist/synthesis/KokoroTTS.js +21 -10
  52. package/dist/synthesis/KokoroTTS.js.map +1 -1
  53. package/dist/synthesis/SvoxPicoTTS.js +24 -23
  54. package/dist/synthesis/SvoxPicoTTS.js.map +1 -1
  55. package/dist/text-translation/DeepLTextTranslation.js +4 -3
  56. package/dist/text-translation/DeepLTextTranslation.js.map +1 -1
  57. package/dist/text-translation/NLLBTextTranslation.js +7 -6
  58. package/dist/text-translation/NLLBTextTranslation.js.map +1 -1
  59. package/dist/utilities/StringUtilities.d.ts +1 -1
  60. package/dist/utilities/StringUtilities.js +5 -5
  61. package/dist/utilities/StringUtilities.js.map +1 -1
  62. package/dist/utilities/Timeline.d.ts +2 -3
  63. package/dist/utilities/Timeline.js +96 -147
  64. package/dist/utilities/Timeline.js.map +1 -1
  65. package/dist/utilities/WikipediaReader.js +3 -3
  66. package/dist/utilities/WikipediaReader.js.map +1 -1
  67. package/dist/voice-activity-detection/WebRtcVAD.js +4 -4
  68. package/dist/voice-activity-detection/WebRtcVAD.js.map +1 -1
  69. package/docs/Tasklist.md +1 -6
  70. package/package.json +13 -11
  71. package/src/alignment/SemanticTextAlignment.ts +6 -7
  72. package/src/api/Alignment.ts +2 -2
  73. package/src/api/Recognition.ts +2 -2
  74. package/src/api/SpeechTranslation.ts +2 -2
  75. package/src/api/Synthesis.ts +7 -7
  76. package/src/api/TimelineTranslationAlignment.ts +2 -2
  77. package/src/api/TranslationAlignment.ts +2 -2
  78. package/src/audio/AudioPlayer.ts +2 -2
  79. package/src/cli/CLI.ts +4 -4
  80. package/src/denoising/RNNoise.ts +5 -5
  81. package/src/dsp/FFT.ts +22 -19
  82. package/src/dsp/Rubberband.ts +5 -5
  83. package/src/dsp/Sonic.ts +6 -6
  84. package/src/dsp/SpeexResampler.ts +11 -11
  85. package/src/nlp/EspeakPhonemizer.ts +1 -1
  86. package/src/nlp/Segmentation.ts +146 -111
  87. package/src/nlp/TextNormalizer.ts +94 -52
  88. package/src/recognition/AmazonTranscribeSTT.ts +2 -2
  89. package/src/recognition/SileroSTT.ts +2 -2
  90. package/src/recognition/WhisperSTT.ts +4 -18
  91. package/src/subtitles/Subtitles.ts +69 -8
  92. package/src/synthesis/ElevenLabsTTS.ts +12 -11
  93. package/src/synthesis/EspeakTTS.ts +9 -8
  94. package/src/synthesis/KokoroTTS.ts +24 -12
  95. package/src/synthesis/SvoxPicoTTS.ts +26 -23
  96. package/src/text-translation/DeepLTextTranslation.ts +4 -3
  97. package/src/text-translation/NLLBTextTranslation.ts +8 -6
  98. package/src/typings/Fillers.d.ts +0 -16
  99. package/src/utilities/StringUtilities.ts +5 -6
  100. package/src/utilities/Timeline.ts +100 -170
  101. package/src/utilities/WikipediaReader.ts +3 -3
  102. package/src/voice-activity-detection/WebRtcVAD.ts +5 -4
  103. package/dist/nlp/CompromiseNLP.d.ts +0 -15
  104. package/dist/nlp/CompromiseNLP.js +0 -79
  105. package/dist/nlp/CompromiseNLP.js.map +0 -1
  106. package/dist/utilities/WasmMemoryManager.d.ts +0 -153
  107. package/dist/utilities/WasmMemoryManager.js +0 -440
  108. package/dist/utilities/WasmMemoryManager.js.map +0 -1
  109. package/src/nlp/CompromiseNLP.ts +0 -119
  110. package/src/utilities/WasmMemoryManager.ts +0 -566
@@ -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
  }
@@ -1,5 +1,6 @@
1
1
  import { getShortLanguageCode } from '../utilities/Locale.js'
2
2
  import { substituteCharactersUsingLookup } from '../utilities/StringUtilities.js'
3
+ import { anyOf, buildRegExp, charRange, inputEnd, inputStart, repeated, zeroOrMore } from 'regexp-composer'
3
4
 
4
5
  export function getNormalizedFragmentsForSpeech(
5
6
  words: string[],
@@ -13,36 +14,6 @@ export function getNormalizedFragmentsForSpeech(
13
14
  return { normalizedFragments: [...nonWhitespaceWords], referenceFragments: [...nonWhitespaceWords] }
14
15
  }
15
16
 
16
- const numberPattern = /^[0-9][0-9\,\.]*$/
17
-
18
- const fourDigitYearPattern = /^[0-9][0-9][0-9][0-9]$/
19
- const fourDigitDecadePattern = /^[0-9][0-9][0-9]0s$/
20
-
21
- const fourDigitYearRangePattern = /^[0-9][0-9][0-9][0-9][\-\–][0-9][0-9][0-9][0-9]$/
22
-
23
- const wordsPrecedingAYear = [
24
- 'in', 'the', 'a', 'to', 'of', 'since', 'from', 'between', 'by', 'until', 'around', 'before', 'after',
25
- 'his', 'her', 'year', 'years', 'during', 'copyright', '©', 'early', 'mid', 'late',
26
- 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december',
27
- 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'
28
- ]
29
-
30
- const wordsPrecedingADecade = [
31
- 'the', 'in', 'early', 'mid', 'late', 'a'
32
- ]
33
-
34
- const symbolsPrecedingACurrency = [
35
- '$', '€', '£', '¥'
36
- ]
37
-
38
- const symbolsPrecedingACurrencyAsWords = [
39
- 'dollars', 'euros', 'pounds', 'yen'
40
- ]
41
-
42
- const wordsSucceedingACurrency = [
43
- 'million', 'billion', 'trillion'
44
- ]
45
-
46
17
  const normalizedFragments: string[] = []
47
18
  const referenceFragments: string[] = []
48
19
 
@@ -56,10 +27,11 @@ export function getNormalizedFragmentsForSpeech(
56
27
  const originalWordIndex = nonWhitespaceWordOriginalIndex[wordIndex]
57
28
  const isFollowedByWhitespace = words[originalWordIndex + 1]?.trim().length === 0
58
29
 
59
- if ( // Normalize a four digit year pattern, e.g. 'in 1995'.
60
- wordsPrecedingAYear.includes(lowerCaseWord) &&
30
+ if (wordsPrecedingAYear.includes(lowerCaseWord) &&
61
31
  isFollowedByWhitespace &&
62
- fourDigitYearPattern.test(nextNonWhitespaceWord)) {
32
+ fourDigitYearPatternRegExp.test(nextNonWhitespaceWord)) {
33
+
34
+ // Normalize a four digit year pattern, e.g. 'in 1995'.
63
35
 
64
36
  const normalizedString = normalizeFourDigitYearString(nextNonWhitespaceWord)
65
37
 
@@ -70,10 +42,12 @@ export function getNormalizedFragmentsForSpeech(
70
42
  referenceFragments.push(nextNonWhitespaceWord)
71
43
 
72
44
  wordIndex += 1
73
- } else if ( // Normalize a four digit decade pattern, e.g. 'the 1980s'.
45
+ } else if (
74
46
  wordsPrecedingADecade.includes(lowerCaseWord) &&
75
47
  isFollowedByWhitespace &&
76
- fourDigitDecadePattern.test(nextNonWhitespaceWord)) {
48
+ fourDigitDecadePatternRegExp.test(nextNonWhitespaceWord)) {
49
+
50
+ // Normalize a four digit decade pattern, e.g. 'the 1980s'.
77
51
 
78
52
  const normalizedString = normalizeFourDigitDecadeString(nextNonWhitespaceWord)
79
53
 
@@ -84,9 +58,8 @@ export function getNormalizedFragmentsForSpeech(
84
58
  referenceFragments.push(nextNonWhitespaceWord)
85
59
 
86
60
  wordIndex += 1
87
- } else if ( // Normalize a year range pattern, e.g. '1835-1896', ensure there are no spaces between words
88
- fourDigitYearRangePattern.test(words.slice(originalWordIndex, originalWordIndex + 3).join(''))) {
89
-
61
+ } else if (fourDigitYearRangePatternRegExp.test(words.slice(originalWordIndex, originalWordIndex + 3).join(''))) {
62
+ // Normalize a year range pattern, e.g. '1835-1896', ensure there are no spaces between words
90
63
  normalizedFragments.push(normalizeFourDigitYearString(nonWhitespaceWords[wordIndex]))
91
64
  referenceFragments.push(nonWhitespaceWords[wordIndex])
92
65
 
@@ -97,32 +70,37 @@ export function getNormalizedFragmentsForSpeech(
97
70
  referenceFragments.push(nonWhitespaceWords[wordIndex + 2])
98
71
 
99
72
  wordIndex += 2
100
- } else if ( // Normalize a currency pattern, e.g. '$53.1 million', '€3.53'
101
- symbolsPrecedingACurrency.includes(lowerCaseWord) &&
102
- !isFollowedByWhitespace &&
103
- numberPattern.test(nextNonWhitespaceWord)) {
73
+ } else if (precedingCurrencyPatternRegExp.test(lowerCaseWord)) {
74
+ // Normalize a currency pattern with preceding currency symbol, e.g. '€3.53', '$53.1 million',
104
75
 
105
- let currencyWord = symbolsPrecedingACurrencyAsWords[symbolsPrecedingACurrency.indexOf(lowerCaseWord)]
76
+ const currencyWord = currencySymbolsAsWords[currencySymbols.indexOf(lowerCaseWord[0])]
106
77
 
107
- if (wordsSucceedingACurrency.includes(nextNonWhitespaceWords[1]?.toLowerCase())) {
108
- const normalizedString = `${nextNonWhitespaceWord} ${nextNonWhitespaceWords[1]} ${currencyWord}`
78
+ if (wordsFollowingACurrency.includes(nextNonWhitespaceWord?.toLowerCase())) {
79
+ const normalizedString = `${word.substring(1)} ${nextNonWhitespaceWord} ${currencyWord}`
109
80
 
110
81
  normalizedFragments.push(normalizedString)
111
82
 
112
- const referenceString = `${word}${nextNonWhitespaceWord} ${nextNonWhitespaceWords[1]}`
83
+ const referenceString = `${word} ${nextNonWhitespaceWord}`
113
84
  referenceFragments.push(referenceString)
114
85
 
115
- wordIndex += 2
86
+ wordIndex += 1
116
87
  } else {
117
- const normalizedString = `${nextNonWhitespaceWord} ${currencyWord}`
88
+ const normalizedString = `${word.substring(1)} ${currencyWord}`
118
89
 
119
90
  normalizedFragments.push(normalizedString)
120
91
 
121
- const referenceString = `${word}${nextNonWhitespaceWord}`
92
+ const referenceString = word
122
93
  referenceFragments.push(referenceString)
123
-
124
- wordIndex += 1
125
94
  }
95
+ } else if (followingCurrencyPatternRegExp.test(lowerCaseWord)) {
96
+ const currencyWord = currencySymbolsAsWords[currencySymbols.indexOf(lowerCaseWord[lowerCaseWord.length - 1])]
97
+
98
+ const normalizedString = `${word.substring(0, word.length - 1)} ${currencyWord}`
99
+
100
+ normalizedFragments.push(normalizedString)
101
+
102
+ const referenceString = word
103
+ referenceFragments.push(referenceString)
126
104
  } else {
127
105
  normalizedFragments.push(word)
128
106
  referenceFragments.push(word)
@@ -156,7 +134,7 @@ export function normalizeFourDigitDecadeString(decadeString: string) {
156
134
  let normalizedString: string
157
135
 
158
136
  const isBeforeSecondMillenium = firstTwoDigitsValue < 10
159
- const isMilleniumDecade = firstTwoDigitsValue % 10 == 0 && secondTwoDigitsValue == 0
137
+ const isMilleniumDecade = firstTwoDigitsValue % 10 == 0 && secondTwoDigitsValue == 0
160
138
 
161
139
  if (!isBeforeSecondMillenium && !isMilleniumDecade) {
162
140
  if (secondTwoDigitsValue != 0) {
@@ -229,3 +207,67 @@ export const punctuationSubstitutionLookup: Record<string, string> = {
229
207
  '!': `!`,
230
208
  '¡': `!`,
231
209
  }
210
+
211
+ const wordsPrecedingAYear = [
212
+ 'in', 'the', 'a', 'to', 'of', 'since', 'from', 'between', 'by', 'until', 'around', 'before', 'after',
213
+ 'his', 'her', 'year', 'years', 'during', 'copyright', '©', 'early', 'mid', 'late',
214
+ 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december',
215
+ 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec',
216
+ 'winter', 'spring', 'summer', 'fall', 'autumn'
217
+ ]
218
+
219
+ const wordsPrecedingADecade = [
220
+ 'the', 'in', 'early', 'mid', 'late', 'a'
221
+ ]
222
+
223
+ const currencySymbols = [
224
+ '$', '¥', '€', '£', '₩', '₭', '₽', '₫', '฿', '¢', '₮', '؋', '₦', '₱', '₴', '₪'
225
+ ]
226
+
227
+ const currencySymbolsAsWords = [
228
+ 'dollars', 'yen', 'euros', 'pounds', 'won', 'kip', 'rubles', 'dong', 'baht', 'cents', 'tögrög', 'afghanis', 'naira', 'pesos', 'hryvnia', 'shekels'
229
+ ]
230
+
231
+ const wordsFollowingACurrency = [
232
+ 'million', 'billion', 'trillion'
233
+ ]
234
+
235
+ const arabicNumeralPattern = charRange('0', '9')
236
+
237
+ const numberPattern = [
238
+ inputStart,
239
+ arabicNumeralPattern,
240
+ zeroOrMore(anyOf(arabicNumeralPattern, ',', '.')),
241
+ inputEnd
242
+ ]
243
+
244
+ const numberPatternRegExp = buildRegExp(numberPattern)
245
+
246
+ const precedingCurrencyPattern = [
247
+ inputStart,
248
+ anyOf(...currencySymbols),
249
+ arabicNumeralPattern,
250
+ zeroOrMore(anyOf(arabicNumeralPattern, ',', '.')),
251
+ inputEnd
252
+ ]
253
+
254
+ const precedingCurrencyPatternRegExp = buildRegExp(precedingCurrencyPattern)
255
+
256
+ const followingCurrencyPattern = [
257
+ inputStart,
258
+ arabicNumeralPattern,
259
+ zeroOrMore(anyOf(arabicNumeralPattern, ',', '.')),
260
+ anyOf(...currencySymbols),
261
+ inputEnd
262
+ ]
263
+
264
+ const followingCurrencyPatternRegExp = buildRegExp(followingCurrencyPattern)
265
+
266
+ const fourDigitYearPattern = [inputStart, repeated(4, arabicNumeralPattern), inputEnd]
267
+ const fourDigitYearPatternRegExp = buildRegExp(fourDigitYearPattern)
268
+
269
+ const fourDigitDecadePattern = [inputStart, repeated(3, arabicNumeralPattern), '0s', inputEnd]
270
+ const fourDigitDecadePatternRegExp = buildRegExp(fourDigitDecadePattern)
271
+
272
+ const fourDigitYearRangePattern = [inputStart, repeated(4, arabicNumeralPattern), anyOf('-', '–'), repeated(4, arabicNumeralPattern), inputEnd]
273
+ const fourDigitYearRangePatternRegExp = buildRegExp(fourDigitYearRangePattern)
@@ -1,5 +1,5 @@
1
1
  import { Item, LanguageCode, StartStreamTranscriptionCommandInput } from '@aws-sdk/client-transcribe-streaming'
2
- import { wordCharacterPattern } from '../nlp/Segmentation.js'
2
+ import { wordCharacterRegExp } from '../nlp/Segmentation.js'
3
3
  import * as FFMpegTranscoder from '../codecs/FFMpegTranscoder.js'
4
4
  import { Logger } from '../utilities/Logger.js'
5
5
  import { Timeline } from '../utilities/Timeline.js'
@@ -102,7 +102,7 @@ export async function recgonize(rawAudio: RawAudio, languageCode: string, region
102
102
  for (const event of events) {
103
103
  const text = event.Content!
104
104
 
105
- if (!wordCharacterPattern.test(text)) {
105
+ if (!wordCharacterRegExp.test(text)) {
106
106
  continue
107
107
  }
108
108
 
@@ -1,5 +1,5 @@
1
1
  import { indexOfMax } from '../math/VectorMath.js'
2
- import { wordCharacterPattern } from '../nlp/Segmentation.js'
2
+ import { wordCharacterRegExp } from '../nlp/Segmentation.js'
3
3
  import { Logger } from '../utilities/Logger.js'
4
4
  import { logToStderr } from '../utilities/Utilities.js'
5
5
  import { Timeline } from '../utilities/Timeline.js'
@@ -182,7 +182,7 @@ export class SileroSTT {
182
182
  for (let i = 0; i < words.length; i++) {
183
183
  const text = words[i]
184
184
 
185
- if (!wordCharacterPattern.test(text)) {
185
+ if (!wordCharacterRegExp.test(text)) {
186
186
  continue
187
187
  }
188
188
 
@@ -18,7 +18,7 @@ import chalk from 'chalk'
18
18
  import { XorShift32PRNG } from '../utilities/RandomGenerator.js'
19
19
  import { detectSpeechLanguageByParts } from '../api/SpeechLanguageDetection.js'
20
20
  import { type Tiktoken } from 'tiktoken/lite'
21
- import { isPunctuation, isWhitespace, isWord, splitToSentences, splitToWords } from '../nlp/Segmentation.js'
21
+ import { includesPunctuation, isWhitespace, splitToWords } from '../nlp/Segmentation.js'
22
22
  import { medianOf5Filter } from '../math/MedianFilter.js'
23
23
  import { getDeflateCompressionMetricsForString } from '../utilities/Compression.js'
24
24
  import { dmlProviderAvailable, getOnnxSessionOptions, makeOnnxLikeFloat32Tensor, OnnxExecutionProvider, OnnxLikeFloat32Tensor } from '../utilities/OnnxUtilities.js'
@@ -528,25 +528,11 @@ export class Whisper {
528
528
 
529
529
  const targetLanguage = task === 'transcribe' ? sourceLanguage : 'en'
530
530
 
531
- const shouldSplitToSentences = false
532
531
 
533
532
  let simplifiedTranscript = ''
534
533
 
535
- if (shouldSplitToSentences) {
536
- const sentences = splitToSentences(transcript, targetLanguage)
537
-
538
- for (const sentence of sentences) {
539
- let sentenceWords = await splitToWords(sentence, targetLanguage)
540
- sentenceWords = sentenceWords.filter(word => isWord(word))
541
-
542
- simplifiedTranscript += sentenceWords.join(' ')
543
- simplifiedTranscript += ' '
544
- }
545
- } else {
546
- let words = await splitToWords(transcript, targetLanguage)
547
-
548
- words = words.map(word => word.trim())
549
- words = words.filter(word => isWord(word))
534
+ {
535
+ const words = (await splitToWords(transcript, targetLanguage)).nonPunctuationWords
550
536
 
551
537
  simplifiedTranscript = words.join(' ')
552
538
  }
@@ -1253,7 +1239,7 @@ export class Whisper {
1253
1239
  return false
1254
1240
  }
1255
1241
 
1256
- return isWhitespace(char) || isPunctuation(char)
1242
+ return isWhitespace(char) || includesPunctuation(char)
1257
1243
  }
1258
1244
 
1259
1245
  function startsWithSeparatorCharacter(text: string) {