echogarden 0.11.12 → 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/api/Alignment.js +2 -2
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/Recognition.js +2 -2
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/Synthesis.js +5 -4
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/api/Translation.js +2 -2
- package/dist/api/Translation.js.map +1 -1
- package/dist/audio/AudioUtilities.d.ts +1 -0
- package/dist/audio/AudioUtilities.js +25 -7
- 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/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 +7 -6
- 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,286 @@
|
|
|
1
|
+
import { deepClone, extendDeep } from "../utilities/ObjectUtilities.js"
|
|
2
|
+
|
|
3
|
+
import { AudioSourceParam, RawAudio, ensureRawAudio, getRawAudioDuration, normalizeAudioLevel, sliceRawAudioByTime, trimAudioEnd } from "../audio/AudioUtilities.js"
|
|
4
|
+
import { Logger } from "../utilities/Logger.js"
|
|
5
|
+
|
|
6
|
+
import * as API from "./API.js"
|
|
7
|
+
import { logToStderr } from "../utilities/Utilities.js"
|
|
8
|
+
import path from "path"
|
|
9
|
+
import { type WhisperOptions } from "../recognition/WhisperSTT.js"
|
|
10
|
+
import { formatLanguageCodeWithName, languageCodeToName } from "../utilities/Locale.js"
|
|
11
|
+
import { loadPackage } from "../utilities/PackageManager.js"
|
|
12
|
+
import chalk from "chalk"
|
|
13
|
+
|
|
14
|
+
const log = logToStderr
|
|
15
|
+
|
|
16
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
17
|
+
// Speech language detection
|
|
18
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
19
|
+
export async function detectSpeechLanguage(input: AudioSourceParam, options: SpeechLanguageDetectionOptions): Promise<SpeechLanguageDetectionResult> {
|
|
20
|
+
const logger = new Logger()
|
|
21
|
+
|
|
22
|
+
const startTime = logger.getTimestamp()
|
|
23
|
+
|
|
24
|
+
logger.start("Prepare for speech language detection")
|
|
25
|
+
|
|
26
|
+
const inputRawAudio = await ensureRawAudio(input)
|
|
27
|
+
|
|
28
|
+
let sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
|
|
29
|
+
sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
|
|
30
|
+
sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
|
|
31
|
+
|
|
32
|
+
options = extendDeep(defaultSpeechLanguageDetectionOptions, options)
|
|
33
|
+
|
|
34
|
+
const defaultLanguage = options.defaultLanguage!
|
|
35
|
+
const fallbackThresholdProbability = options.fallbackThresholdProbability!
|
|
36
|
+
|
|
37
|
+
logger.start(`Initialize ${options.engine} module`)
|
|
38
|
+
|
|
39
|
+
let detectedLanguageProbabilities: LanguageDetectionResults
|
|
40
|
+
|
|
41
|
+
switch (options.engine) {
|
|
42
|
+
case "silero": {
|
|
43
|
+
const SileroLanguageDetection = await import("../speech-language-detection/SileroLanguageDetection.js")
|
|
44
|
+
|
|
45
|
+
logger.end()
|
|
46
|
+
|
|
47
|
+
const sileroOptions = options.silero!
|
|
48
|
+
|
|
49
|
+
const modelDir = await loadPackage("silero-lang-classifier-95")
|
|
50
|
+
|
|
51
|
+
const modelPath = path.join(modelDir, "lang_classifier_95.onnx")
|
|
52
|
+
const languageDictionaryPath = path.join(modelDir, "lang_dict_95.json")
|
|
53
|
+
const languageGroupDictionaryPath = path.join(modelDir, "lang_group_dict_95.json")
|
|
54
|
+
|
|
55
|
+
const languageResults = await SileroLanguageDetection.detectLanguage(
|
|
56
|
+
sourceRawAudio,
|
|
57
|
+
modelPath,
|
|
58
|
+
languageDictionaryPath,
|
|
59
|
+
languageGroupDictionaryPath)
|
|
60
|
+
|
|
61
|
+
detectedLanguageProbabilities = languageResults
|
|
62
|
+
|
|
63
|
+
break
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case "whisper": {
|
|
67
|
+
const WhisperSTT = await import("../recognition/WhisperSTT.js")
|
|
68
|
+
|
|
69
|
+
const whisperOptions = options.whisper!
|
|
70
|
+
|
|
71
|
+
const { modelName, modelDir, tokenizerDir } = await WhisperSTT.loadPackagesAndGetPaths(whisperOptions.model, undefined)
|
|
72
|
+
|
|
73
|
+
logger.end()
|
|
74
|
+
|
|
75
|
+
detectedLanguageProbabilities = await WhisperSTT.detectLanguage(sourceRawAudio, modelName, modelDir, tokenizerDir)
|
|
76
|
+
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
default: {
|
|
81
|
+
throw new Error(`Engine '${options.engine}' is not supported`)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let detectedLanguage: string
|
|
86
|
+
|
|
87
|
+
if (detectedLanguageProbabilities.length == 0 ||
|
|
88
|
+
detectedLanguageProbabilities[0].probability < fallbackThresholdProbability) {
|
|
89
|
+
|
|
90
|
+
detectedLanguage = defaultLanguage
|
|
91
|
+
} else {
|
|
92
|
+
detectedLanguage = detectedLanguageProbabilities[0].language
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
logger.end()
|
|
96
|
+
logger.logDuration("\nTotal detection time", startTime, chalk.magentaBright)
|
|
97
|
+
|
|
98
|
+
return { detectedLanguage, detectedLanguageName: languageCodeToName(detectedLanguage), detectedLanguageProbabilities, inputRawAudio }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface SpeechLanguageDetectionResult {
|
|
102
|
+
detectedLanguage: string
|
|
103
|
+
detectedLanguageName: string
|
|
104
|
+
detectedLanguageProbabilities: LanguageDetectionResults
|
|
105
|
+
inputRawAudio: RawAudio
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function detectSpeechLanguageByParts(sourceRawAudio: RawAudio, getResultsForAudioPart: (audioPart: RawAudio) => Promise<LanguageDetectionResults>, audioPartDuration = 30, hopDuration = 15) {
|
|
109
|
+
const logger = new Logger()
|
|
110
|
+
|
|
111
|
+
const audioDuration = getRawAudioDuration(sourceRawAudio)
|
|
112
|
+
|
|
113
|
+
const resultsForParts: LanguageDetectionResults[] = []
|
|
114
|
+
|
|
115
|
+
for (let audioTimeOffset = 0; audioTimeOffset < audioDuration; audioTimeOffset += hopDuration) {
|
|
116
|
+
const startOffset = audioTimeOffset
|
|
117
|
+
const endOffset = Math.min(audioTimeOffset + audioPartDuration, audioDuration)
|
|
118
|
+
const audioPartLength = endOffset - startOffset
|
|
119
|
+
|
|
120
|
+
logger.logTitledMessage(`\nDetecting speech language starting at audio offset`, `${startOffset.toFixed(1)}`, chalk.magentaBright)
|
|
121
|
+
const audioPart = sliceRawAudioByTime(sourceRawAudio, startOffset, endOffset)
|
|
122
|
+
|
|
123
|
+
const resultsForPart = await getResultsForAudioPart(audioPart)
|
|
124
|
+
|
|
125
|
+
resultsForParts.push(resultsForPart)
|
|
126
|
+
|
|
127
|
+
const sortedResultsForPart = deepClone(resultsForPart).sort((a, b) => b.probability - a.probability)
|
|
128
|
+
|
|
129
|
+
logger.logTitledMessage(`Top candidates`, `${formatLanguageCodeWithName(sortedResultsForPart[0].language)}: ${sortedResultsForPart[0].probability.toFixed(3)}, ${formatLanguageCodeWithName(sortedResultsForPart[1].language)}: ${sortedResultsForPart[1].probability.toFixed(3)}, ${formatLanguageCodeWithName(sortedResultsForPart[3].language)}: ${sortedResultsForPart[3].probability.toFixed(3)}`)
|
|
130
|
+
|
|
131
|
+
if (audioPartLength < audioPartDuration) {
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const averagedResults: LanguageDetectionResults = deepClone(resultsForParts[0])
|
|
137
|
+
averagedResults.forEach(entry => { entry.probability = 0.0 })
|
|
138
|
+
|
|
139
|
+
for (const partResults of resultsForParts) {
|
|
140
|
+
for (let i = 0; i < partResults.length; i++) {
|
|
141
|
+
averagedResults[i].probability += partResults[i].probability
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
for (const result of averagedResults) {
|
|
146
|
+
result.probability /= resultsForParts.length
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return averagedResults
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type SpeechLanguageDetectionEngine = "silero" | "whisper"
|
|
153
|
+
|
|
154
|
+
export interface SpeechLanguageDetectionOptions {
|
|
155
|
+
engine?: SpeechLanguageDetectionEngine
|
|
156
|
+
defaultLanguage?: string,
|
|
157
|
+
fallbackThresholdProbability?: number
|
|
158
|
+
|
|
159
|
+
silero?: {
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
whisper?: WhisperOptions
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const defaultSpeechLanguageDetectionOptions: SpeechLanguageDetectionOptions = {
|
|
166
|
+
engine: "silero",
|
|
167
|
+
|
|
168
|
+
silero: {
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
whisper: {
|
|
172
|
+
model: "tiny",
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
177
|
+
// Text language detection
|
|
178
|
+
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
179
|
+
export async function detectTextLanguage(input: string, options: TextLanguageDetectionOptions): Promise<TextLanguageDetectionResult> {
|
|
180
|
+
const logger = new Logger()
|
|
181
|
+
|
|
182
|
+
options = extendDeep(defaultTextLanguageDetectionOptions, options)
|
|
183
|
+
|
|
184
|
+
const defaultLanguage = options.defaultLanguage!
|
|
185
|
+
const fallbackThresholdProbability = options.fallbackThresholdProbability!
|
|
186
|
+
|
|
187
|
+
let detectedLanguageProbabilities: LanguageDetectionResults
|
|
188
|
+
|
|
189
|
+
logger.start(`Initialize ${options.engine} module`)
|
|
190
|
+
|
|
191
|
+
switch (options.engine) {
|
|
192
|
+
case "tinyld": {
|
|
193
|
+
const { detectLanguage } = await import("../text-language-detection/TinyLDLanguageDetection.js")
|
|
194
|
+
|
|
195
|
+
logger.start("Detecting text language using tinyld")
|
|
196
|
+
|
|
197
|
+
detectedLanguageProbabilities = await detectLanguage(input)
|
|
198
|
+
|
|
199
|
+
break
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case "fasttext": {
|
|
203
|
+
const { detectLanguage } = await import("../text-language-detection/FastTextLanguageDetection.js")
|
|
204
|
+
|
|
205
|
+
logger.start("Detecting text language using FastText")
|
|
206
|
+
|
|
207
|
+
detectedLanguageProbabilities = await detectLanguage(input)
|
|
208
|
+
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
default: {
|
|
213
|
+
throw new Error(`Engine '${options.engine}' is not supported`)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let detectedLanguage: string
|
|
218
|
+
|
|
219
|
+
if (detectedLanguageProbabilities.length == 0 ||
|
|
220
|
+
detectedLanguageProbabilities[0].probability < fallbackThresholdProbability) {
|
|
221
|
+
|
|
222
|
+
detectedLanguage = defaultLanguage
|
|
223
|
+
} else {
|
|
224
|
+
detectedLanguage = detectedLanguageProbabilities[0].language
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
logger.end()
|
|
228
|
+
|
|
229
|
+
return { detectedLanguage, detectedLanguageName: languageCodeToName(detectedLanguage), detectedLanguageProbabilities }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface TextLanguageDetectionResult {
|
|
233
|
+
detectedLanguage: string
|
|
234
|
+
detectedLanguageName: string
|
|
235
|
+
detectedLanguageProbabilities: LanguageDetectionResults
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export type LanguageDetectionResults = LanguageDetectionResultsEntry[]
|
|
239
|
+
export type LanguageDetectionResultsEntry = { language: string, languageName: string, probability: number }
|
|
240
|
+
|
|
241
|
+
export type LanguageDetectionGroupResults = LanguageDetectionGroupResultsEntry[]
|
|
242
|
+
export type LanguageDetectionGroupResultsEntry = { languageGroup: string, probability: number }
|
|
243
|
+
|
|
244
|
+
export type TextLanguageDetectionEngine = "tinyld" | "fasttext"
|
|
245
|
+
|
|
246
|
+
export type TextLanguageDetectionOptions = {
|
|
247
|
+
engine?: TextLanguageDetectionEngine,
|
|
248
|
+
defaultLanguage?: string,
|
|
249
|
+
fallbackThresholdProbability?: number
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export const defaultTextLanguageDetectionOptions: TextLanguageDetectionOptions = {
|
|
253
|
+
engine: "tinyld",
|
|
254
|
+
defaultLanguage: "en",
|
|
255
|
+
fallbackThresholdProbability: 0.05
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export const speechLanguageDetectionEngines: API.EngineMetadata[] = [
|
|
259
|
+
{
|
|
260
|
+
id: 'silero',
|
|
261
|
+
name: 'Silero',
|
|
262
|
+
description: 'A speech language classification model by Silero.',
|
|
263
|
+
type: 'local'
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: 'whisper',
|
|
267
|
+
name: 'OpenAI Whisper',
|
|
268
|
+
description: 'Uses the language token produced by the Whisper model to guess the language of the speech (first 30 seconds only).',
|
|
269
|
+
type: 'local'
|
|
270
|
+
},
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
export const textLanguageDetectionEngines: API.EngineMetadata[] = [
|
|
274
|
+
{
|
|
275
|
+
id: 'tinyld',
|
|
276
|
+
name: 'TinyLD',
|
|
277
|
+
description: 'A simple language detection library.',
|
|
278
|
+
type: 'local'
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
id: 'fasttext',
|
|
282
|
+
name: 'FastText',
|
|
283
|
+
description: 'a library for word representations and sentence classification by Facebook research.',
|
|
284
|
+
type: 'local'
|
|
285
|
+
},
|
|
286
|
+
]
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { extendDeep } from "../utilities/ObjectUtilities.js"
|
|
2
|
+
|
|
3
|
+
import { logToStderr } from "../utilities/Utilities.js"
|
|
4
|
+
import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevel, trimAudioEnd } from "../audio/AudioUtilities.js"
|
|
5
|
+
import { Logger } from "../utilities/Logger.js"
|
|
6
|
+
|
|
7
|
+
import * as API from "./API.js"
|
|
8
|
+
import { Timeline, addWordTextOffsetsToTimeline, wordTimelineToSegmentSentenceTimeline } from "../utilities/Timeline.js"
|
|
9
|
+
import { whisperOptionsDefaults, type WhisperOptions } from "../recognition/WhisperSTT.js"
|
|
10
|
+
import { formatLanguageCodeWithName, getShortLanguageCode, normalizeLanguageCode } from "../utilities/Locale.js"
|
|
11
|
+
import { loadPackage } from "../utilities/PackageManager.js"
|
|
12
|
+
import chalk from "chalk"
|
|
13
|
+
import { SubtitlesConfig, defaultSubtitlesBaseConfig } from "../subtitles/Subtitles.js"
|
|
14
|
+
|
|
15
|
+
const log = logToStderr
|
|
16
|
+
|
|
17
|
+
export async function recognize(input: AudioSourceParam, options: RecognitionOptions): Promise<RecognitionResult> {
|
|
18
|
+
const logger = new Logger()
|
|
19
|
+
const startTimestamp = logger.getTimestamp()
|
|
20
|
+
|
|
21
|
+
logger.start("Prepare for recognition")
|
|
22
|
+
|
|
23
|
+
const inputRawAudio = await ensureRawAudio(input)
|
|
24
|
+
|
|
25
|
+
let sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
|
|
26
|
+
sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
|
|
27
|
+
sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
|
|
28
|
+
|
|
29
|
+
options = extendDeep(defaultRecognitionOptions, options)
|
|
30
|
+
|
|
31
|
+
const engine = options.engine!
|
|
32
|
+
|
|
33
|
+
if (!options.language) { // && options.engine != "whisper") {
|
|
34
|
+
logger.start("No language specified. Detecting speech language")
|
|
35
|
+
const { detectedLanguage } = await API.detectSpeechLanguage(inputRawAudio, options.languageDetection || {})
|
|
36
|
+
|
|
37
|
+
logger.end()
|
|
38
|
+
logger.logTitledMessage('Language detected', formatLanguageCodeWithName(detectedLanguage))
|
|
39
|
+
|
|
40
|
+
options.language = detectedLanguage
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let language = normalizeLanguageCode(options.language)
|
|
44
|
+
|
|
45
|
+
let transcript: string
|
|
46
|
+
let timeline: Timeline | undefined
|
|
47
|
+
|
|
48
|
+
logger.start(`Load ${engine} module`)
|
|
49
|
+
|
|
50
|
+
switch (engine) {
|
|
51
|
+
case "whisper": {
|
|
52
|
+
const WhisperSTT = await import("../recognition/WhisperSTT.js")
|
|
53
|
+
|
|
54
|
+
const whisperOptions = options.whisper!
|
|
55
|
+
|
|
56
|
+
const shortLanguageCode = getShortLanguageCode(language)
|
|
57
|
+
|
|
58
|
+
const { modelName, modelDir, tokenizerDir } = await WhisperSTT.loadPackagesAndGetPaths(whisperOptions.model, shortLanguageCode)
|
|
59
|
+
|
|
60
|
+
if (shortLanguageCode != "en" && modelName.endsWith(".en")) {
|
|
61
|
+
throw new Error(`The model '${modelName}' is English only and cannot transcribe language '${shortLanguageCode}'`)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
logger.end();
|
|
65
|
+
|
|
66
|
+
({ transcript, timeline } = await WhisperSTT.recognize(sourceRawAudio, modelName, modelDir, tokenizerDir, "transcribe", language, whisperOptions))
|
|
67
|
+
|
|
68
|
+
break
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
case "vosk": {
|
|
72
|
+
const VoskSTT = await import("../recognition/VoskSTT.js")
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
await import('@echogarden/vosk')
|
|
76
|
+
} catch (e) {
|
|
77
|
+
log(e)
|
|
78
|
+
throw new Error(`The vosk npm package, which is required for Vosk support, was not found, or had an error loading. If missing, you can install it by running 'npm install @echogarden/vosk -g'.`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const voskOptions = options.vosk!
|
|
82
|
+
|
|
83
|
+
const modelPath = voskOptions.modelPath
|
|
84
|
+
|
|
85
|
+
if (!modelPath) {
|
|
86
|
+
throw new Error("Vosk models are not currently auto-downloaded. You'll need to download a model manually and set a model path in 'vosk.modelPath'.")
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
logger.end();
|
|
90
|
+
|
|
91
|
+
({ transcript, timeline } = await VoskSTT.recognize(sourceRawAudio, modelPath, true))
|
|
92
|
+
|
|
93
|
+
break
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case "silero": {
|
|
97
|
+
const SileroSTT = await import("../recognition/SileroSTT.js")
|
|
98
|
+
|
|
99
|
+
const sileroOptions = options.silero!
|
|
100
|
+
|
|
101
|
+
let modelPath = sileroOptions.modelPath
|
|
102
|
+
|
|
103
|
+
if (!modelPath) {
|
|
104
|
+
const shortLanguageCode = getShortLanguageCode(language)
|
|
105
|
+
const packageName = SileroSTT.languageCodeToPackageName[shortLanguageCode]
|
|
106
|
+
|
|
107
|
+
if (!packageName) {
|
|
108
|
+
throw new Error(`Language '${shortLanguageCode}' is not supported by Silero`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
modelPath = await loadPackage(packageName)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
logger.end();
|
|
115
|
+
|
|
116
|
+
({ transcript, timeline } = await SileroSTT.recognize(sourceRawAudio, modelPath))
|
|
117
|
+
|
|
118
|
+
break
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
case "google-cloud": {
|
|
122
|
+
const GoogleCloudSTT = await import("../recognition/GoogleCloudSTT.js")
|
|
123
|
+
|
|
124
|
+
const apiKey = options.googleCloud!.apiKey
|
|
125
|
+
|
|
126
|
+
if (!apiKey) {
|
|
127
|
+
throw new Error(`No API key given`)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
logger.end();
|
|
131
|
+
|
|
132
|
+
({ transcript, timeline } = await GoogleCloudSTT.recognize(sourceRawAudio, apiKey, language))
|
|
133
|
+
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case "microsoft-azure": {
|
|
138
|
+
const AzureCognitiveServicesSTT = await import("../recognition/AzureCognitiveServicesSTT.js")
|
|
139
|
+
|
|
140
|
+
const subscriptionKey = options.microsoftAzure!.subscriptionKey
|
|
141
|
+
|
|
142
|
+
if (!subscriptionKey) {
|
|
143
|
+
throw new Error(`No subscription key given`)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const serviceRegion = options.microsoftAzure!.serviceRegion
|
|
147
|
+
|
|
148
|
+
if (!serviceRegion) {
|
|
149
|
+
throw new Error(`No service region given`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
logger.end();
|
|
153
|
+
|
|
154
|
+
({ transcript, timeline } = await AzureCognitiveServicesSTT.recognize(sourceRawAudio, subscriptionKey, serviceRegion, language))
|
|
155
|
+
|
|
156
|
+
break
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
case "amazon-transcribe": {
|
|
160
|
+
const AmazonTranscribeSTT = await import("../recognition/AmazonTranscribeSTT.js")
|
|
161
|
+
|
|
162
|
+
const region = options.amazonTranscribe!.region
|
|
163
|
+
|
|
164
|
+
if (!region) {
|
|
165
|
+
throw new Error(`No region given`)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const accessKeyId = options.amazonTranscribe!.accessKeyId
|
|
169
|
+
|
|
170
|
+
if (!accessKeyId) {
|
|
171
|
+
throw new Error(`No access key id given`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const secretAccessKey = options.amazonTranscribe!.secretAccessKey
|
|
175
|
+
|
|
176
|
+
if (!secretAccessKey) {
|
|
177
|
+
throw new Error(`No secret access key given`)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
logger.end();
|
|
181
|
+
|
|
182
|
+
({ transcript, timeline } = await AmazonTranscribeSTT.recgonize(sourceRawAudio, language, region, accessKeyId, secretAccessKey))
|
|
183
|
+
|
|
184
|
+
break
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
default: {
|
|
188
|
+
throw new Error(`Engine '${options.engine}' is not supported`)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!timeline) {
|
|
193
|
+
logger.start(`Align audio to transcript`)
|
|
194
|
+
const alignmentOptions: API.AlignmentOptions = extendDeep(options.alignment, { language: language })
|
|
195
|
+
|
|
196
|
+
const { wordTimeline } = await API.align(sourceRawAudio, transcript, alignmentOptions)
|
|
197
|
+
|
|
198
|
+
timeline = wordTimeline
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
addWordTextOffsetsToTimeline(timeline, transcript)
|
|
202
|
+
|
|
203
|
+
const { segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(timeline, transcript, language, 'single', 'preserve')
|
|
204
|
+
|
|
205
|
+
logger.end()
|
|
206
|
+
logger.logDuration('Total recognition time', startTimestamp, chalk.magentaBright)
|
|
207
|
+
|
|
208
|
+
return { transcript, timeline: segmentTimeline, wordTimeline: timeline, inputRawAudio, language }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface RecognitionResult {
|
|
212
|
+
transcript: string
|
|
213
|
+
timeline: Timeline
|
|
214
|
+
wordTimeline: Timeline
|
|
215
|
+
language: string
|
|
216
|
+
inputRawAudio: RawAudio
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export type RecognitionEngine = "whisper" | "vosk" | "silero" | "google-cloud" | "microsoft-azure" | "amazon-transcribe"
|
|
220
|
+
|
|
221
|
+
export interface RecognitionOptions {
|
|
222
|
+
engine?: RecognitionEngine
|
|
223
|
+
|
|
224
|
+
language?: string
|
|
225
|
+
|
|
226
|
+
maxAlternatives?: number
|
|
227
|
+
|
|
228
|
+
alignment?: API.AlignmentOptions
|
|
229
|
+
|
|
230
|
+
languageDetection?: API.SpeechLanguageDetectionOptions
|
|
231
|
+
|
|
232
|
+
subtitles?: SubtitlesConfig
|
|
233
|
+
|
|
234
|
+
whisper?: WhisperOptions
|
|
235
|
+
|
|
236
|
+
vosk?: {
|
|
237
|
+
modelPath?: string
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
silero?: {
|
|
241
|
+
modelPath?: string
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
googleCloud?: {
|
|
245
|
+
apiKey?: string
|
|
246
|
+
alternativeLanguageCodes?: string[]
|
|
247
|
+
profanityFilter?: boolean
|
|
248
|
+
autoPunctuation?: boolean
|
|
249
|
+
useEnhancedModel?: boolean
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
microsoftAzure?: {
|
|
253
|
+
subscriptionKey?: string
|
|
254
|
+
serviceRegion?: string
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
amazonTranscribe?: {
|
|
258
|
+
region?: string
|
|
259
|
+
accessKeyId?: string
|
|
260
|
+
secretAccessKey?: string
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export const defaultRecognitionOptions: RecognitionOptions = {
|
|
265
|
+
engine: "whisper",
|
|
266
|
+
|
|
267
|
+
language: undefined,
|
|
268
|
+
|
|
269
|
+
maxAlternatives: 1,
|
|
270
|
+
|
|
271
|
+
alignment: undefined,
|
|
272
|
+
|
|
273
|
+
languageDetection: undefined,
|
|
274
|
+
|
|
275
|
+
subtitles: defaultSubtitlesBaseConfig,
|
|
276
|
+
|
|
277
|
+
whisper: whisperOptionsDefaults,
|
|
278
|
+
|
|
279
|
+
vosk: {
|
|
280
|
+
modelPath: undefined
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
silero: {
|
|
284
|
+
modelPath: undefined
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
googleCloud: {
|
|
288
|
+
apiKey: undefined,
|
|
289
|
+
alternativeLanguageCodes: [],
|
|
290
|
+
profanityFilter: false,
|
|
291
|
+
autoPunctuation: true,
|
|
292
|
+
useEnhancedModel: true,
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
microsoftAzure: {
|
|
296
|
+
subscriptionKey: undefined,
|
|
297
|
+
serviceRegion: undefined
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
amazonTranscribe: {
|
|
301
|
+
region: undefined,
|
|
302
|
+
accessKeyId: undefined,
|
|
303
|
+
secretAccessKey: undefined,
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export const recognitionEngines: API.EngineMetadata[] = [
|
|
308
|
+
{
|
|
309
|
+
id: 'whisper',
|
|
310
|
+
name: 'OpenAI Whisper',
|
|
311
|
+
description: 'A high accuracy transformer-based architecture by OpenAI.',
|
|
312
|
+
type: 'local'
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
id: 'vosk',
|
|
316
|
+
name: 'Vosk',
|
|
317
|
+
description: 'A speech recognition toolkit.',
|
|
318
|
+
type: 'local'
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: 'silero',
|
|
322
|
+
name: 'Silero',
|
|
323
|
+
description: 'Speech recognition models.',
|
|
324
|
+
type: 'local'
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: 'google-cloud',
|
|
328
|
+
name: 'Google Cloud',
|
|
329
|
+
description: 'Google Cloud speech-to-text service.',
|
|
330
|
+
type: 'cloud'
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
id: 'microsoft-azure',
|
|
334
|
+
name: 'Azure Cognitive Services',
|
|
335
|
+
description: 'Microsoft Azure speech-to-text service.',
|
|
336
|
+
type: 'cloud'
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
id: 'amazon-transcribe',
|
|
340
|
+
name: 'Amazon Transcribe',
|
|
341
|
+
description: 'Amazon cloud speech-to-text service.',
|
|
342
|
+
type: 'cloud'
|
|
343
|
+
},
|
|
344
|
+
]
|