echogarden 0.11.11 → 0.11.13
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.
- package/data/schemas/options.json +16 -0
- package/dist/alignment/DTWMfccSequenceAlignment.js +2 -2
- package/dist/alignment/DTWMfccSequenceAlignment.js.map +1 -1
- package/dist/alignment/SpeechAlignment.d.ts +1 -1
- package/dist/alignment/SpeechAlignment.js +15 -3
- package/dist/alignment/SpeechAlignment.js.map +1 -1
- package/dist/api/Alignment.js +3 -3
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/LanguageDetection.js +1 -1
- package/dist/api/LanguageDetection.js.map +1 -1
- package/dist/api/Recognition.js +3 -3
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/Synthesis.js +7 -6
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/api/Translation.js +3 -3
- package/dist/api/Translation.js.map +1 -1
- package/dist/audio/AudioUtilities.d.ts +5 -2
- package/dist/audio/AudioUtilities.js +50 -22
- package/dist/audio/AudioUtilities.js.map +1 -1
- package/dist/cli/CLI.js +2 -2
- package/dist/cli/CLI.js.map +1 -1
- package/dist/dsp/SpeexResampler.js +1 -1
- package/dist/recognition/WhisperSTT.js +2 -2
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/subtitles/Subtitles.d.ts +10 -7
- package/dist/subtitles/Subtitles.js +268 -207
- package/dist/subtitles/Subtitles.js.map +1 -1
- package/docs/Options.md +4 -2
- package/package.json +12 -11
- package/src/alignment/DTWMfccSequenceAlignment.ts +43 -0
- package/src/alignment/DTWSequenceAlignment.ts +121 -0
- package/src/alignment/DTWSequenceAlignmentWindowed.ts +210 -0
- package/src/alignment/LevenshteinSequenceAlignment.ts +126 -0
- package/src/alignment/SpeechAlignment.ts +488 -0
- package/src/api/API.ts +12 -0
- package/src/api/APIOptions.ts +15 -0
- package/src/api/Alignment.ts +329 -0
- package/src/api/Common.ts +16 -0
- package/src/api/Denoising.ts +120 -0
- package/src/api/LanguageDetection.ts +286 -0
- package/src/api/Recognition.ts +344 -0
- package/src/api/Synthesis.ts +1735 -0
- package/src/api/Translation.ts +143 -0
- package/src/api/Vad.ts +172 -0
- package/src/audio/AudioBufferConversion.ts +248 -0
- package/src/audio/AudioPlayer.ts +358 -0
- package/src/audio/AudioRecorder.ts +91 -0
- package/src/audio/AudioUtilities.ts +392 -0
- package/src/audio/SoxPath.ts +24 -0
- package/src/cli/CLI.ts +1360 -0
- package/src/cli/CLIConfigFile.ts +91 -0
- package/src/cli/CLILauncher.ts +26 -0
- package/src/cli/CLIOptionsSchema.ts +54 -0
- package/src/cli/CLIParser.ts +41 -0
- package/src/cli/CLIStarter.ts +40 -0
- package/src/codecs/FFMpegTranscoder.ts +214 -0
- package/src/codecs/TIMITCodec.ts +17 -0
- package/src/codecs/WaveCodec.ts +260 -0
- package/src/denoising/RNNoise.ts +95 -0
- package/src/dsp/BiquadFilter.ts +488 -0
- package/src/dsp/FFT.ts +187 -0
- package/src/dsp/MFCC.ts +227 -0
- package/src/dsp/MelSpectogram.ts +145 -0
- package/src/dsp/Rubberband.ts +249 -0
- package/src/dsp/Sonic.ts +59 -0
- package/src/dsp/SpeexResampler.ts +79 -0
- package/src/math/VectorMath.ts +812 -0
- package/src/nlp/ChineseSegmentation.ts +68 -0
- package/src/nlp/CompromiseNLP.ts +113 -0
- package/src/nlp/EspeakPhonemizer.ts +168 -0
- package/src/nlp/IPA.ts +139 -0
- package/src/nlp/JapaneseSegmentation.ts +53 -0
- package/src/nlp/Lexicon.ts +119 -0
- package/src/nlp/PhoneConversion.ts +508 -0
- package/src/nlp/Segmentation.ts +237 -0
- package/src/nlp/TextNormalizer.ts +160 -0
- package/src/recognition/AmazonTranscribeSTT.ts +112 -0
- package/src/recognition/AzureCognitiveServicesSTT.ts +76 -0
- package/src/recognition/GoogleCloudSTT.ts +92 -0
- package/src/recognition/SileroSTT.ts +173 -0
- package/src/recognition/VoskSTT.ts +112 -0
- package/src/recognition/WhisperSTT.ts +1518 -0
- package/src/server/Client.ts +297 -0
- package/src/server/Server.ts +178 -0
- package/src/server/ServerStarter.ts +12 -0
- package/src/server/Worker.ts +400 -0
- package/src/server/WorkerStarter.ts +38 -0
- package/src/speech-language-detection/SileroLanguageDetection.ts +105 -0
- package/src/subtitles/Subtitles.ts +478 -0
- package/src/synthesis/AwsPollyTTS.ts +78 -0
- package/src/synthesis/AzureCognitiveServicesTTS.ts +146 -0
- package/src/synthesis/CoquiServerTTS.ts +29 -0
- package/src/synthesis/ElevenLabsTTS.ts +104 -0
- package/src/synthesis/EspeakTTS.ts +552 -0
- package/src/synthesis/FliteTTS.ts +387 -0
- package/src/synthesis/GoogleCloudTTS.ts +112 -0
- package/src/synthesis/GoogleTranslateTTS.ts +210 -0
- package/src/synthesis/MicrosoftEdgeTTS.ts +298 -0
- package/src/synthesis/SamTTS.ts +30 -0
- package/src/synthesis/SapiTTS.ts +222 -0
- package/src/synthesis/StreamlabsPollyTTS.ts +114 -0
- package/src/synthesis/SvoxPicoTTS.ts +318 -0
- package/src/synthesis/VitsTTS.ts +734 -0
- package/src/tests/Test.ts +24 -0
- package/src/text-language-detection/FastTextLanguageDetection.ts +53 -0
- package/src/text-language-detection/TinyLDLanguageDetection.ts +16 -0
- package/src/typings/Fillers.d.ts +41 -0
- package/src/utilities/BinaryArrayConversion.ts +159 -0
- package/src/utilities/Compression.ts +91 -0
- package/src/utilities/FileDownloader.ts +201 -0
- package/src/utilities/FileSystem.ts +265 -0
- package/src/utilities/Hashing.ts +230 -0
- package/src/utilities/Locale.ts +119 -0
- package/src/utilities/Logger.ts +72 -0
- package/src/utilities/NdArrayUtilities.ts +31 -0
- package/src/utilities/ObjectUtilities.ts +169 -0
- package/src/utilities/OpenPromise.ts +13 -0
- package/src/utilities/PackageManager.ts +97 -0
- package/src/utilities/Queue.ts +17 -0
- package/src/utilities/RandomGenerator.ts +237 -0
- package/src/utilities/SignalChannel.ts +22 -0
- package/src/utilities/TarballMaker.ts +68 -0
- package/src/utilities/Timeline.ts +231 -0
- package/src/utilities/Timer.ts +93 -0
- package/src/utilities/Utilities.ts +574 -0
- package/src/utilities/WasmMemoryManager.ts +516 -0
- package/src/utilities/WebReader.ts +55 -0
- package/src/utilities/WikipediaReader.ts +41 -0
- package/src/voice-activity-detection/SileroVAD.ts +86 -0
- package/src/voice-activity-detection/WebRtcVAD.ts +76 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import * as CldrSegmentation from 'cldr-segmentation'
|
|
2
|
+
import { splitChineseTextToWords_Jieba } from './ChineseSegmentation.js'
|
|
3
|
+
|
|
4
|
+
import { sumArray, includesAnyOf, indexOfAnyOf, logToStderr } from '../utilities/Utilities.js'
|
|
5
|
+
import { getShortLanguageCode } from '../utilities/Locale.js'
|
|
6
|
+
import { splitJapaneseTextToWords_Kuromoji } from './JapaneseSegmentation.js'
|
|
7
|
+
import { ParagraphBreakType, WhitespaceProcessing } from '../api/Common.js'
|
|
8
|
+
|
|
9
|
+
const log = logToStderr
|
|
10
|
+
|
|
11
|
+
export const wordCharacterPattern = /[\p{Letter}\p{Number}]/u
|
|
12
|
+
export const phraseSeparators = [",", ";", ":"]
|
|
13
|
+
export const sentenceSeparators = [".", "?", "!"]
|
|
14
|
+
export const symbolWords = ["$", "€", "¢", "£", "¥", "©", "®", "™", "%", "&", "#", "~", "@", "+", "±", "÷", "/", "*", "=", "¼", "½", "¾"]
|
|
15
|
+
|
|
16
|
+
export function isWordOrSymbolWord(str: string) {
|
|
17
|
+
return isWord(str) || symbolWords.includes(str)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isWord(str: string) {
|
|
21
|
+
str = str.trim()
|
|
22
|
+
return wordCharacterPattern.test(str) || symbolWords.includes(str)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class Sentence {
|
|
26
|
+
phrases: Phrase[] = []
|
|
27
|
+
|
|
28
|
+
readonly isSentenceFinalizer = true
|
|
29
|
+
|
|
30
|
+
get length() { return sumArray(this.phrases, (phrase) => phrase.length) }
|
|
31
|
+
|
|
32
|
+
get text() { return this.phrases.reduce<string>((result, phrase) => result + phrase.text, "") }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class Phrase {
|
|
36
|
+
words: Word[] = []
|
|
37
|
+
|
|
38
|
+
get length() { return sumArray(this.words, (word) => word.length) }
|
|
39
|
+
|
|
40
|
+
get text() { return this.words.reduce<string>((result, word) => result + word.text, "") }
|
|
41
|
+
|
|
42
|
+
get lastWord() {
|
|
43
|
+
if (this.words.length == 0) {
|
|
44
|
+
return undefined
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return this.words[this.words.length - 1]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get isSentenceFinalizer() { return this.lastWord != null ? this.lastWord.isSentenceFinalizer : false }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class Word {
|
|
54
|
+
readonly text: string
|
|
55
|
+
isSentenceFinalizer: boolean
|
|
56
|
+
|
|
57
|
+
constructor(text: string, isSentenceFinalizer: boolean) {
|
|
58
|
+
this.text = text
|
|
59
|
+
this.isSentenceFinalizer = isSentenceFinalizer
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get containsOnlyPunctuation() { return !wordCharacterPattern.test(this.text) && !this.isSymbolWord }
|
|
63
|
+
|
|
64
|
+
get isSymbolWord() { return symbolWords.includes(this.text) }
|
|
65
|
+
|
|
66
|
+
get isPhraseSeperator() { return this.containsOnlyPunctuation && includesAnyOf(this.text, phraseSeparators) }
|
|
67
|
+
|
|
68
|
+
get length() { return this.text.length }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type Segment = Sentence | Phrase | Word
|
|
72
|
+
|
|
73
|
+
export class Fragment {
|
|
74
|
+
segments: Segment[] = []
|
|
75
|
+
|
|
76
|
+
get length() { return sumArray(this.segments, (phrase) => phrase.length) }
|
|
77
|
+
|
|
78
|
+
get text() { return this.segments.reduce<string>((result, segment) => result + segment.text, "") }
|
|
79
|
+
|
|
80
|
+
get isEmpty() { return this.length == 0 }
|
|
81
|
+
|
|
82
|
+
get isNonempty() { return !this.isEmpty }
|
|
83
|
+
|
|
84
|
+
get lastSegment() {
|
|
85
|
+
if (this.isEmpty) {
|
|
86
|
+
return undefined
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return this.segments[this.segments.length - 1]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export async function splitToFragments(text: string, maxFragmentLength: number, langCode: string, preserveSentences = true, preservePhrases = true) {
|
|
94
|
+
const parsedText = await parse(text, langCode)
|
|
95
|
+
|
|
96
|
+
const fragments: Fragment[] = []
|
|
97
|
+
let currentFragment = new Fragment()
|
|
98
|
+
|
|
99
|
+
const remainingCharactersInCurrentFragment = () => maxFragmentLength - currentFragment.length
|
|
100
|
+
const createNewFragmentIfNeeded = () => {
|
|
101
|
+
if (currentFragment.isNonempty) {
|
|
102
|
+
fragments.push(currentFragment)
|
|
103
|
+
currentFragment = new Fragment()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const fitsCurrentFragment = (segment: Segment) => segment.length <= remainingCharactersInCurrentFragment()
|
|
108
|
+
|
|
109
|
+
for (const sentence of parsedText) {
|
|
110
|
+
if (fitsCurrentFragment(sentence)) {
|
|
111
|
+
currentFragment.segments.push(sentence)
|
|
112
|
+
continue
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (preserveSentences) {
|
|
116
|
+
createNewFragmentIfNeeded()
|
|
117
|
+
|
|
118
|
+
if (fitsCurrentFragment(sentence)) {
|
|
119
|
+
currentFragment.segments.push(sentence)
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const phrase of sentence.phrases) {
|
|
125
|
+
if (fitsCurrentFragment(phrase)) {
|
|
126
|
+
currentFragment.segments.push(phrase)
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
if (preservePhrases) {
|
|
132
|
+
createNewFragmentIfNeeded()
|
|
133
|
+
|
|
134
|
+
if (fitsCurrentFragment(phrase)) {
|
|
135
|
+
currentFragment.segments.push(phrase)
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (const word of phrase.words) {
|
|
141
|
+
if (fitsCurrentFragment(word)) {
|
|
142
|
+
currentFragment.segments.push(word)
|
|
143
|
+
continue
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
createNewFragmentIfNeeded()
|
|
147
|
+
|
|
148
|
+
if (fitsCurrentFragment(word)) {
|
|
149
|
+
currentFragment.segments.push(word)
|
|
150
|
+
continue
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
throw new Error(`Encountered a word of length ${word.length}, which excceeds the maximum fragment length of ${maxFragmentLength}`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
createNewFragmentIfNeeded()
|
|
159
|
+
|
|
160
|
+
return fragments
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function parse(text: string, langCode: string) {
|
|
164
|
+
const sentencesText = splitToSentences(text, langCode)
|
|
165
|
+
const sentences: Sentence[] = []
|
|
166
|
+
|
|
167
|
+
for (const sentenceText of sentencesText) {
|
|
168
|
+
const sentence = new Sentence()
|
|
169
|
+
|
|
170
|
+
let currentPhrase = new Phrase()
|
|
171
|
+
const wordTexts = await splitToWords(sentenceText, langCode)
|
|
172
|
+
|
|
173
|
+
for (let wordIndex = 0; wordIndex < wordTexts.length; wordIndex++) {
|
|
174
|
+
const word = new Word(wordTexts[wordIndex], wordIndex == wordTexts.length - 1)
|
|
175
|
+
|
|
176
|
+
if (word.isPhraseSeperator) {
|
|
177
|
+
const separatorIndex = indexOfAnyOf(word.text, phraseSeparators)
|
|
178
|
+
currentPhrase.words.push(new Word(word.text.substring(0, separatorIndex + 1), word.isSentenceFinalizer))
|
|
179
|
+
sentence.phrases.push(currentPhrase)
|
|
180
|
+
|
|
181
|
+
currentPhrase = new Phrase()
|
|
182
|
+
currentPhrase.words.push(new Word(word.text.substring(separatorIndex + 1), false))
|
|
183
|
+
} else {
|
|
184
|
+
currentPhrase.words.push(word)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (currentPhrase.words.length > 0) {
|
|
189
|
+
sentence.phrases.push(currentPhrase)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
sentences.push(sentence)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return sentences
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function splitToSentences(text: string, langCode: string): string[] {
|
|
199
|
+
const shortLangCode = getShortLanguageCode(langCode || "")
|
|
200
|
+
|
|
201
|
+
return CldrSegmentation.sentenceSplit(text, CldrSegmentation.suppressions[shortLangCode])
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function splitToWords(text: string, langCode: string): Promise<string[]> {
|
|
205
|
+
const shortLangCode = getShortLanguageCode(langCode || "")
|
|
206
|
+
|
|
207
|
+
if (shortLangCode == "zh" || shortLangCode == "cmn") {
|
|
208
|
+
return splitChineseTextToWords_Jieba(text, undefined, true)
|
|
209
|
+
} else if (shortLangCode == "ja") {
|
|
210
|
+
return splitJapaneseTextToWords_Kuromoji(text)
|
|
211
|
+
} else {
|
|
212
|
+
return CldrSegmentation.wordSplit(text, CldrSegmentation.suppressions[shortLangCode])
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function splitToParagraphs(text: string, paragraphBreaks: ParagraphBreakType, whitespace: WhitespaceProcessing) {
|
|
217
|
+
let paragraphs: string[] = []
|
|
218
|
+
|
|
219
|
+
if (paragraphBreaks == 'single') {
|
|
220
|
+
paragraphs = text.split(/(\r?\n)+/g)
|
|
221
|
+
} else if (paragraphBreaks == 'double') {
|
|
222
|
+
paragraphs = text.split(/(\r?\n)(\r?\n)+/g)
|
|
223
|
+
} else {
|
|
224
|
+
throw new Error(`Invalid paragraph break type: ${paragraphBreaks}`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (whitespace == "removeLineBreaks") {
|
|
228
|
+
paragraphs = paragraphs.map(p => p.replaceAll(/(\r?\n)+/g, " "))
|
|
229
|
+
} else if (whitespace == "collapse") {
|
|
230
|
+
paragraphs = paragraphs.map(p => p.replaceAll(/\s+/g, " "))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
paragraphs = paragraphs.map(p => p.trim())
|
|
234
|
+
paragraphs = paragraphs.filter(p => p.length > 0)
|
|
235
|
+
|
|
236
|
+
return paragraphs
|
|
237
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { getShortLanguageCode } from "../utilities/Locale.js"
|
|
2
|
+
|
|
3
|
+
export function getNormalizedFragmentsForSpeech(words: string[], language: string) {
|
|
4
|
+
language = getShortLanguageCode(language)
|
|
5
|
+
|
|
6
|
+
if (language != "en") {
|
|
7
|
+
return { normalizedFragments: [...words], referenceFragments: [...words] }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const numberPattern = /^[0-9][0-9\,\.]*$/
|
|
11
|
+
|
|
12
|
+
const fourDigitYearPattern = /^[0-9][0-9][0-9][0-9]$/
|
|
13
|
+
const fourDigitDecadePattern = /^[0-9][0-9][0-9]0s$/
|
|
14
|
+
|
|
15
|
+
const fourDigitYearRangePattern = /^[0-9][0-9][0-9][0-9][\-\–][0-9][0-9][0-9][0-9]$/
|
|
16
|
+
|
|
17
|
+
const wordsPrecedingAYear = [
|
|
18
|
+
"in", "the", "a", "to", "of", "since", "from", "between", "by", "until", "around", "before", "after",
|
|
19
|
+
"his", "her", "year", "years", "during", "copyright", "©", "early", "mid", "late",
|
|
20
|
+
"january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december",
|
|
21
|
+
"jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const wordsPrecedingADecade = [
|
|
25
|
+
"the", "in", "early", "mid", "late", "a"
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const symbolsPrecedingACurrency = [
|
|
29
|
+
"$", "€", "£", "¥"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const symbolsPrecedingACurrencyAsWords = [
|
|
33
|
+
"dollars", "euros", "pounds", "yen"
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
const wordsSucceedingACurrency = [
|
|
37
|
+
"million", "billion", "trillion"
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
const normalizedFragments: string[] = []
|
|
41
|
+
const referenceFragments: string[] = []
|
|
42
|
+
|
|
43
|
+
for (let wordIndex = 0; wordIndex < words.length; wordIndex++) {
|
|
44
|
+
const word = words[wordIndex]
|
|
45
|
+
const lowerCaseWord = word.toLowerCase()
|
|
46
|
+
|
|
47
|
+
const nextWords = words.slice(wordIndex + 1)
|
|
48
|
+
const nextWord = nextWords[0]
|
|
49
|
+
|
|
50
|
+
if ( // Normalize a four digit year pattern, e.g. "in 1995".
|
|
51
|
+
wordsPrecedingAYear.includes(lowerCaseWord) &&
|
|
52
|
+
fourDigitYearPattern.test(nextWord)) {
|
|
53
|
+
|
|
54
|
+
const normalizedString = normalizeFourDigitYearString(nextWord)
|
|
55
|
+
|
|
56
|
+
normalizedFragments.push(word)
|
|
57
|
+
referenceFragments.push(word)
|
|
58
|
+
|
|
59
|
+
normalizedFragments.push(normalizedString)
|
|
60
|
+
referenceFragments.push(nextWord)
|
|
61
|
+
|
|
62
|
+
wordIndex += 1
|
|
63
|
+
} else if ( // Normalize a four digit decade pattern, e.g. "the 1980s".
|
|
64
|
+
wordsPrecedingADecade.includes(lowerCaseWord) &&
|
|
65
|
+
fourDigitDecadePattern.test(nextWord)) {
|
|
66
|
+
|
|
67
|
+
const normalizedString = normalizeFourDigitDecadeString(nextWord)
|
|
68
|
+
|
|
69
|
+
normalizedFragments.push(word)
|
|
70
|
+
referenceFragments.push(word)
|
|
71
|
+
|
|
72
|
+
normalizedFragments.push(normalizedString)
|
|
73
|
+
referenceFragments.push(nextWord)
|
|
74
|
+
|
|
75
|
+
wordIndex += 1
|
|
76
|
+
} else if ( // Normalize a year range pattern, e.g. "1835-1896"
|
|
77
|
+
fourDigitYearRangePattern.test(words.slice(wordIndex, wordIndex + 3).join(""))) {
|
|
78
|
+
|
|
79
|
+
normalizedFragments.push(normalizeFourDigitYearString(words[wordIndex]))
|
|
80
|
+
referenceFragments.push(words[wordIndex])
|
|
81
|
+
|
|
82
|
+
normalizedFragments.push("to")
|
|
83
|
+
referenceFragments.push(words[wordIndex + 1])
|
|
84
|
+
|
|
85
|
+
normalizedFragments.push(normalizeFourDigitYearString(words[wordIndex + 2]))
|
|
86
|
+
referenceFragments.push(words[wordIndex + 2])
|
|
87
|
+
|
|
88
|
+
wordIndex += 2
|
|
89
|
+
} else if ( // Normalize a currency pattern, e.g. "$53.1 million", "€3.53"
|
|
90
|
+
symbolsPrecedingACurrency.includes(lowerCaseWord) &&
|
|
91
|
+
numberPattern.test(nextWord)) {
|
|
92
|
+
|
|
93
|
+
let currencyWord = symbolsPrecedingACurrencyAsWords[symbolsPrecedingACurrency.indexOf(lowerCaseWord)]
|
|
94
|
+
|
|
95
|
+
if (wordsSucceedingACurrency.includes(nextWords[1].toLowerCase())) {
|
|
96
|
+
const normalizedString = `${nextWord} ${nextWords[1]} ${currencyWord}`
|
|
97
|
+
|
|
98
|
+
normalizedFragments.push(normalizedString)
|
|
99
|
+
|
|
100
|
+
const referenceString = `${word}${nextWord} ${nextWords[1]}`
|
|
101
|
+
referenceFragments.push(referenceString)
|
|
102
|
+
|
|
103
|
+
wordIndex += 2
|
|
104
|
+
} else {
|
|
105
|
+
const normalizedString = `${nextWord} ${currencyWord}`
|
|
106
|
+
|
|
107
|
+
normalizedFragments.push(normalizedString)
|
|
108
|
+
|
|
109
|
+
const referenceString = `${word}${nextWord}`
|
|
110
|
+
referenceFragments.push(referenceString)
|
|
111
|
+
|
|
112
|
+
wordIndex += 1
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
normalizedFragments.push(word)
|
|
116
|
+
referenceFragments.push(word)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { normalizedFragments, referenceFragments }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeFourDigitYearString(yearString: string) {
|
|
124
|
+
const firstTwoDigitsValue = parseFloat(yearString.substring(0, 2))
|
|
125
|
+
const secondTwoDigitsValue = parseFloat(yearString.substring(2, 4))
|
|
126
|
+
|
|
127
|
+
let normalizedString: string
|
|
128
|
+
|
|
129
|
+
if (firstTwoDigitsValue >= 10 && secondTwoDigitsValue >= 10) {
|
|
130
|
+
normalizedString = `${firstTwoDigitsValue} ${secondTwoDigitsValue}`
|
|
131
|
+
} else if (firstTwoDigitsValue >= 10 && firstTwoDigitsValue % 10 != 0 && secondTwoDigitsValue < 10) {
|
|
132
|
+
normalizedString = `${firstTwoDigitsValue} oh ${secondTwoDigitsValue}`
|
|
133
|
+
} else {
|
|
134
|
+
normalizedString = yearString
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return normalizedString
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function normalizeFourDigitDecadeString(decadeString: string) {
|
|
141
|
+
const firstTwoDigitsValue = parseInt(decadeString.substring(0, 2))
|
|
142
|
+
const secondTwoDigitsValue = parseInt(decadeString.substring(2, 4))
|
|
143
|
+
|
|
144
|
+
let normalizedString: string
|
|
145
|
+
|
|
146
|
+
const isBeforeSecondMillenium = firstTwoDigitsValue < 10
|
|
147
|
+
const isMilleniumDecade = firstTwoDigitsValue % 10 == 0 && secondTwoDigitsValue == 0
|
|
148
|
+
|
|
149
|
+
if (!isBeforeSecondMillenium && !isMilleniumDecade) {
|
|
150
|
+
if (secondTwoDigitsValue != 0) {
|
|
151
|
+
normalizedString = `${firstTwoDigitsValue} ${secondTwoDigitsValue}s`
|
|
152
|
+
} else {
|
|
153
|
+
normalizedString = `${firstTwoDigitsValue} hundreds`
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
normalizedString = decadeString
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return normalizedString
|
|
160
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Item, StartStreamTranscriptionCommandInput } from "@aws-sdk/client-transcribe-streaming"
|
|
2
|
+
import { wordCharacterPattern } from "../nlp/Segmentation.js"
|
|
3
|
+
import * as FFMpegTranscoder from "../codecs/FFMpegTranscoder.js"
|
|
4
|
+
import { Logger } from "../utilities/Logger.js"
|
|
5
|
+
import { Timeline } from "../utilities/Timeline.js"
|
|
6
|
+
import { RawAudio } from "../audio/AudioUtilities.js"
|
|
7
|
+
|
|
8
|
+
export async function recgonize(rawAudio: RawAudio, languageCode: string, region: string, accessKeyId: string, secretAccessKey: string) {
|
|
9
|
+
const flac16Khz16bitMonoAudio = await FFMpegTranscoder.encodeFromChannels(rawAudio, { format: "flac", sampleRate: 16000, sampleFormat: "s16", channelCount: 1 })
|
|
10
|
+
|
|
11
|
+
const logger = new Logger()
|
|
12
|
+
logger.start("Initialize Amazon Transcribe streaming client module")
|
|
13
|
+
|
|
14
|
+
const streamingTranscribeSdk = await import("@aws-sdk/client-transcribe-streaming")
|
|
15
|
+
|
|
16
|
+
const streamingTranscribeClient = new streamingTranscribeSdk.TranscribeStreamingClient({
|
|
17
|
+
region,
|
|
18
|
+
credentials: {
|
|
19
|
+
accessKeyId,
|
|
20
|
+
secretAccessKey
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const audioStream = async function* () {
|
|
25
|
+
const chunkSize = 2 ** 12
|
|
26
|
+
//const audioSamples = encodeToAudioBuffer(rawAudio.audioChannels, 16, SampleFormat.PCM)
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < flac16Khz16bitMonoAudio.length; i += chunkSize) {
|
|
29
|
+
const chunk = flac16Khz16bitMonoAudio.subarray(i, i + chunkSize)
|
|
30
|
+
|
|
31
|
+
yield { AudioEvent: { AudioChunk: chunk } }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const params: StartStreamTranscriptionCommandInput = {
|
|
36
|
+
LanguageCode: languageCode,
|
|
37
|
+
MediaSampleRateHertz: rawAudio.sampleRate,
|
|
38
|
+
MediaEncoding: "flac",
|
|
39
|
+
AudioStream: audioStream(),
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
logger.start("Request recognition from Amazon Transcribe..")
|
|
43
|
+
|
|
44
|
+
const command = new streamingTranscribeSdk.StartStreamTranscriptionCommand(params)
|
|
45
|
+
|
|
46
|
+
const response = await streamingTranscribeClient.send(command)
|
|
47
|
+
|
|
48
|
+
let transcript = ""
|
|
49
|
+
let events: Item[] = []
|
|
50
|
+
|
|
51
|
+
for await (const event of response.TranscriptResultStream!) {
|
|
52
|
+
if (!event.TranscriptEvent) {
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const transcriptEvent = event.TranscriptEvent
|
|
57
|
+
|
|
58
|
+
const results = transcriptEvent.Transcript!.Results!
|
|
59
|
+
|
|
60
|
+
if (results.length == 0) {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const firstResult = results[0]
|
|
65
|
+
const alternatives = firstResult.Alternatives
|
|
66
|
+
|
|
67
|
+
if (!alternatives || alternatives.length == 0) {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const firstAlternative = alternatives[0]
|
|
72
|
+
//logger.log(firstAlternative.Transcript!)
|
|
73
|
+
|
|
74
|
+
if (firstResult.IsPartial === false) {
|
|
75
|
+
events = [...events, ...firstAlternative.Items!]
|
|
76
|
+
transcript += " " + firstAlternative.Transcript!
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
logger.start("Process result")
|
|
81
|
+
|
|
82
|
+
transcript = transcript.replace(/ +/g, " ").trim()
|
|
83
|
+
|
|
84
|
+
const timeline: Timeline = []
|
|
85
|
+
|
|
86
|
+
for (const event of events) {
|
|
87
|
+
const text = event.Content!
|
|
88
|
+
|
|
89
|
+
if (!wordCharacterPattern.test(text)) {
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const startTime = event.StartTime!
|
|
94
|
+
const endTime = event.EndTime!
|
|
95
|
+
const confidence = event.Confidence
|
|
96
|
+
|
|
97
|
+
timeline[timeline.length - 1].endTime = startTime
|
|
98
|
+
|
|
99
|
+
timeline.push(
|
|
100
|
+
{
|
|
101
|
+
type: "word",
|
|
102
|
+
text,
|
|
103
|
+
startTime,
|
|
104
|
+
endTime,
|
|
105
|
+
confidence
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
logger.end()
|
|
110
|
+
|
|
111
|
+
return { transcript, timeline }
|
|
112
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import SpeechSDK from 'microsoft-cognitiveservices-speech-sdk'
|
|
2
|
+
|
|
3
|
+
import { RawAudio, encodeWaveBuffer } from '../audio/AudioUtilities.js'
|
|
4
|
+
import { Logger } from '../utilities/Logger.js'
|
|
5
|
+
import { Timeline } from '../utilities/Timeline.js'
|
|
6
|
+
|
|
7
|
+
export async function recognize(rawAudio: RawAudio, subscriptionKey: string, serviceRegion: string, languageCode: string, profanity: SpeechSDK.ProfanityOption = SpeechSDK.ProfanityOption.Raw) {
|
|
8
|
+
const logger = new Logger()
|
|
9
|
+
logger.start("Request recognition from Azure Cognitive Services")
|
|
10
|
+
|
|
11
|
+
const result = await requestRecognition(rawAudio, subscriptionKey, serviceRegion, languageCode)
|
|
12
|
+
|
|
13
|
+
logger.start("Process result")
|
|
14
|
+
|
|
15
|
+
const transcript = result.text
|
|
16
|
+
|
|
17
|
+
const resultObject = JSON.parse(result.json)
|
|
18
|
+
const bestResult = resultObject.NBest[0]
|
|
19
|
+
|
|
20
|
+
const timeline: Timeline = []
|
|
21
|
+
|
|
22
|
+
for (const wordEntry of bestResult.Words) {
|
|
23
|
+
const text = wordEntry.Word
|
|
24
|
+
const startTime = wordEntry.Offset / 10000000
|
|
25
|
+
const endTime = (wordEntry.Offset + wordEntry.Duration) / 10000000
|
|
26
|
+
|
|
27
|
+
timeline.push({
|
|
28
|
+
type: "word",
|
|
29
|
+
text,
|
|
30
|
+
startTime,
|
|
31
|
+
endTime
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
logger.end()
|
|
36
|
+
|
|
37
|
+
return { transcript, timeline }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function requestRecognition(rawAudio: RawAudio, subscriptionKey: string, serviceRegion: string, languageCode: string, profanity: SpeechSDK.ProfanityOption = SpeechSDK.ProfanityOption.Raw) {
|
|
41
|
+
//const encodedAudio = await FFMpegTranscoder.encodeFromChannels(rawAudio, { format: "wav", sampleRate: 16000, sampleFormat: "s16", channelCount: 1 });
|
|
42
|
+
const encodedAudio = encodeWaveBuffer(rawAudio)
|
|
43
|
+
|
|
44
|
+
return new Promise<SpeechSDK.SpeechRecognitionResult>((resolve, reject) => {
|
|
45
|
+
const audioFormat = SpeechSDK.AudioStreamFormat.getWaveFormat(16000, 16, 1, SpeechSDK.AudioFormatTag.PCM)
|
|
46
|
+
|
|
47
|
+
const inputStream = SpeechSDK.AudioInputStream.createPushStream(audioFormat)
|
|
48
|
+
|
|
49
|
+
inputStream.write(encodedAudio)
|
|
50
|
+
inputStream.close()
|
|
51
|
+
|
|
52
|
+
const audioConfig = SpeechSDK.AudioConfig.fromStreamInput(inputStream)
|
|
53
|
+
|
|
54
|
+
const speechConfig = SpeechSDK.SpeechConfig.fromSubscription(subscriptionKey, serviceRegion)
|
|
55
|
+
|
|
56
|
+
speechConfig.speechRecognitionLanguage = languageCode
|
|
57
|
+
|
|
58
|
+
speechConfig.setProfanity(profanity)
|
|
59
|
+
speechConfig.requestWordLevelTimestamps()
|
|
60
|
+
|
|
61
|
+
speechConfig.outputFormat = SpeechSDK.OutputFormat.Detailed
|
|
62
|
+
|
|
63
|
+
const recognizer = new SpeechSDK.SpeechRecognizer(speechConfig, audioConfig)
|
|
64
|
+
|
|
65
|
+
recognizer.recognizeOnceAsync(
|
|
66
|
+
(result) => {
|
|
67
|
+
recognizer.close()
|
|
68
|
+
resolve(result)
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
(error) => {
|
|
72
|
+
recognizer.close()
|
|
73
|
+
reject(error)
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { request } from "gaxios"
|
|
2
|
+
|
|
3
|
+
import * as FFMpegTranscoder from "../codecs/FFMpegTranscoder.js"
|
|
4
|
+
import { Logger } from "../utilities/Logger.js"
|
|
5
|
+
import { Timeline } from "../utilities/Timeline.js"
|
|
6
|
+
import { RawAudio } from "../audio/AudioUtilities.js"
|
|
7
|
+
|
|
8
|
+
export type AudioEncoding = "LINEAR16" | "FLAC" | "MULAW" | "AMR" | "AMR" | "AMR_WB" | "OGG_OPUS" | "SPEEX_WITH_HEADER_BYTE" | "MP3" | "WEBM_OPUS"
|
|
9
|
+
|
|
10
|
+
export async function recognize(rawAudio: RawAudio, apiKey: string, languageCode = "en-US") {
|
|
11
|
+
const flac16Khz16bitMonoAudio = await FFMpegTranscoder.encodeFromChannels(rawAudio, { format: "flac", sampleRate: 16000, sampleFormat: "s16", channelCount: 1 })
|
|
12
|
+
|
|
13
|
+
const logger = new Logger()
|
|
14
|
+
logger.start("Request recognition from Google Cloud")
|
|
15
|
+
|
|
16
|
+
const requestBody = {
|
|
17
|
+
config: {
|
|
18
|
+
encoding: "FLAC",
|
|
19
|
+
sampleRateHertz: 16000,
|
|
20
|
+
audioChannelCount: 1,
|
|
21
|
+
languageCode,
|
|
22
|
+
alternativeLanguageCodes: [],
|
|
23
|
+
maxAlternatives: 1,
|
|
24
|
+
profanityFilter: false,
|
|
25
|
+
enableWordTimeOffsets: true,
|
|
26
|
+
enableWordConfidence: true,
|
|
27
|
+
enableAutomaticPunctuation: true,
|
|
28
|
+
model: "latest_long",
|
|
29
|
+
useEnhanced: true
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
audio: {
|
|
33
|
+
content: flac16Khz16bitMonoAudio.toString("base64")
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const response = await request<any>({
|
|
38
|
+
method: "POST",
|
|
39
|
+
|
|
40
|
+
url: `https://speech.googleapis.com/v1p1beta1/speech:recognize`,
|
|
41
|
+
|
|
42
|
+
params: {
|
|
43
|
+
"key": apiKey
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
headers: {
|
|
47
|
+
"User-Agent": ""
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
data: requestBody,
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
responseType: "json"
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
logger.start("Parse response body")
|
|
57
|
+
|
|
58
|
+
const result = parseResponseBody(response.data)
|
|
59
|
+
|
|
60
|
+
logger.end()
|
|
61
|
+
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseResponseBody(responseBody: any) {
|
|
66
|
+
const results = responseBody.results
|
|
67
|
+
|
|
68
|
+
let transcript = ""
|
|
69
|
+
const timeline: Timeline = []
|
|
70
|
+
|
|
71
|
+
for (const result of results) {
|
|
72
|
+
if (!result.alternatives || !result.alternatives[0] || !result.alternatives[0].transcript) {
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const firstAlternative = result.alternatives[0]
|
|
77
|
+
|
|
78
|
+
transcript += firstAlternative.transcript
|
|
79
|
+
|
|
80
|
+
for (const wordEvent of firstAlternative.words) {
|
|
81
|
+
timeline.push({
|
|
82
|
+
type: "word",
|
|
83
|
+
text: wordEvent.word,
|
|
84
|
+
startTime: parseFloat(wordEvent.startTime.replace("s","")),
|
|
85
|
+
endTime: parseFloat(wordEvent.endTime.replace("s", "")),
|
|
86
|
+
confidence: wordEvent.confidence
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { transcript, timeline }
|
|
92
|
+
}
|