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,329 @@
|
|
|
1
|
+
import { extendDeep } from "../utilities/ObjectUtilities.js"
|
|
2
|
+
|
|
3
|
+
import { logToStderr } from "../utilities/Utilities.js"
|
|
4
|
+
import { AudioSourceParam, RawAudio, downmixToMonoAndNormalize, ensureRawAudio, getRawAudioDuration, normalizeAudioLevel, trimAudioEnd } from "../audio/AudioUtilities.js"
|
|
5
|
+
import { Logger } from "../utilities/Logger.js"
|
|
6
|
+
import { resampleAudioSpeex } from "../dsp/SpeexResampler.js"
|
|
7
|
+
|
|
8
|
+
import * as API from "./API.js"
|
|
9
|
+
import { Timeline, addTimeOffsetToTimeline, addWordTextOffsetsToTimeline, wordTimelineToSegmentSentenceTimeline } from "../utilities/Timeline.js"
|
|
10
|
+
import { formatLanguageCodeWithName, getDefaultDialectForLanguageCodeIfPossible, getShortLanguageCode, normalizeLanguageCode } from "../utilities/Locale.js"
|
|
11
|
+
import { WhisperOptions, whisperOptionsDefaults } from "../recognition/WhisperSTT.js"
|
|
12
|
+
import chalk from "chalk"
|
|
13
|
+
import { DtwGranularity } from "../alignment/SpeechAlignment.js"
|
|
14
|
+
import { SubtitlesConfig, defaultSubtitlesBaseConfig } from "../subtitles/Subtitles.js"
|
|
15
|
+
import { synthesize } from "./API.js"
|
|
16
|
+
import { EspeakOptions, defaultEspeakOptions } from "../synthesis/EspeakTTS.js"
|
|
17
|
+
|
|
18
|
+
const log = logToStderr
|
|
19
|
+
|
|
20
|
+
export async function align(input: AudioSourceParam, transcript: string, options: AlignmentOptions): Promise<AlignmentResult> {
|
|
21
|
+
const logger = new Logger()
|
|
22
|
+
const startTimestamp = logger.getTimestamp()
|
|
23
|
+
|
|
24
|
+
logger.start("Prepare for alignment")
|
|
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(defaultAlignmentOptions, options)
|
|
33
|
+
|
|
34
|
+
if (options.dtw!.windowDuration == null) {
|
|
35
|
+
const sourceAudioDuration = getRawAudioDuration(sourceRawAudio)
|
|
36
|
+
|
|
37
|
+
if (sourceAudioDuration < 5 * 60) { // If up to 5 minutes, set window to one minute
|
|
38
|
+
options.dtw!.windowDuration = 60
|
|
39
|
+
} else if (sourceAudioDuration < 60 * 60) { // If up to 1 hour, set window to 20% of total duration
|
|
40
|
+
options.dtw!.windowDuration = Math.ceil(sourceAudioDuration * 0.2)
|
|
41
|
+
} else { // If 1 hour or more, set window to 12 minutes
|
|
42
|
+
options.dtw!.windowDuration = 12 * 60
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let language: string
|
|
47
|
+
|
|
48
|
+
if (options.language) {
|
|
49
|
+
language = normalizeLanguageCode(options.language!)
|
|
50
|
+
} else {
|
|
51
|
+
logger.start("No language specified. Detecting language")
|
|
52
|
+
const { detectedLanguage } = await API.detectTextLanguage(transcript, options.languageDetection || {})
|
|
53
|
+
language = detectedLanguage
|
|
54
|
+
|
|
55
|
+
logger.end()
|
|
56
|
+
logger.logTitledMessage('Language detected', formatLanguageCodeWithName(detectedLanguage))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
language = getDefaultDialectForLanguageCodeIfPossible(language)
|
|
60
|
+
|
|
61
|
+
logger.start("Load alignment module")
|
|
62
|
+
|
|
63
|
+
const { alignUsingDtwWithRecognition, alignUsingDtw } = await import("../alignment/SpeechAlignment.js")
|
|
64
|
+
|
|
65
|
+
async function getAlignmentReference() {
|
|
66
|
+
logger.start("Create alignment reference with eSpeak")
|
|
67
|
+
|
|
68
|
+
const synthesisOptions: API.SynthesisOptions = {
|
|
69
|
+
engine: "espeak",
|
|
70
|
+
language,
|
|
71
|
+
plainText: options.plainText,
|
|
72
|
+
customLexiconPaths: options.customLexiconPaths,
|
|
73
|
+
|
|
74
|
+
espeak: {
|
|
75
|
+
useKlatt: false
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let { audio: referenceRawAudio, timeline: segmentTimeline, voice: espeakVoice } = await synthesize(transcript, synthesisOptions)
|
|
80
|
+
|
|
81
|
+
const sentenceTimeline = segmentTimeline.flatMap(entry => entry.timeline!)
|
|
82
|
+
const wordTimeline = sentenceTimeline.flatMap(entry => entry.timeline!)
|
|
83
|
+
|
|
84
|
+
referenceRawAudio = await resampleAudioSpeex(referenceRawAudio as RawAudio, 16000)
|
|
85
|
+
referenceRawAudio = downmixToMonoAndNormalize(referenceRawAudio)
|
|
86
|
+
|
|
87
|
+
return { referenceRawAudio, referenceTimeline: wordTimeline, espeakVoice }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getDtwWindowDurationsAndGranularities() {
|
|
91
|
+
let granularities: DtwGranularity[]
|
|
92
|
+
let windowDurations: number[]
|
|
93
|
+
|
|
94
|
+
if (typeof options.dtw!.granularity == 'string') {
|
|
95
|
+
granularities = [options.dtw!.granularity]
|
|
96
|
+
} else if (Array.isArray(options.dtw!.granularity)) {
|
|
97
|
+
granularities = options.dtw!.granularity
|
|
98
|
+
} else {
|
|
99
|
+
granularities = ['auto']
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (typeof options.dtw!.windowDuration == 'number') {
|
|
103
|
+
if (granularities.length == 1) {
|
|
104
|
+
windowDurations = [options.dtw!.windowDuration]
|
|
105
|
+
} else if (granularities.length == 2) {
|
|
106
|
+
windowDurations = [options.dtw!.windowDuration, 15]
|
|
107
|
+
} else {
|
|
108
|
+
throw new Error(`More than two passes requested, this requires window durations to be explicitly specified for each pass. For example 'dtw.windowDuration=[600,60,10]'.`)
|
|
109
|
+
}
|
|
110
|
+
} else if (Array.isArray(options.dtw!.windowDuration)) {
|
|
111
|
+
windowDurations = options.dtw!.windowDuration
|
|
112
|
+
} else {
|
|
113
|
+
throw new Error('No window duration given')
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (granularities.length != windowDurations.length) {
|
|
117
|
+
throw new Error(`Unequal element counts in options. 'dtw.granularity' has ${granularities.length} items, but 'dtw.windowDuration' has ${windowDurations.length} items. Can't infer what number of DTW passes were intended.`)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { windowDurations, granularities }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let mappedTimeline: Timeline
|
|
124
|
+
|
|
125
|
+
switch (options.engine) {
|
|
126
|
+
case "dtw": {
|
|
127
|
+
const { referenceRawAudio, referenceTimeline } = await getAlignmentReference()
|
|
128
|
+
logger.end()
|
|
129
|
+
|
|
130
|
+
const { windowDurations, granularities } = getDtwWindowDurationsAndGranularities()
|
|
131
|
+
|
|
132
|
+
mappedTimeline = await alignUsingDtw(sourceRawAudio, referenceRawAudio, referenceTimeline, granularities, windowDurations)
|
|
133
|
+
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case "dtw-ra": {
|
|
138
|
+
/*
|
|
139
|
+
const promptWords = (await splitToWords(prompt, language)).filter(word => isWord(word))
|
|
140
|
+
|
|
141
|
+
shuffleArrayInPlace(promptWords, this.randomGen)
|
|
142
|
+
//promptWords.reverse()
|
|
143
|
+
|
|
144
|
+
prompt = promptWords.join(" ")
|
|
145
|
+
*/
|
|
146
|
+
|
|
147
|
+
const recognitionOptionsDefaults: API.RecognitionOptions = {
|
|
148
|
+
engine: "whisper",
|
|
149
|
+
language,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const recognitionOptions: API.RecognitionOptions = extendDeep(recognitionOptionsDefaults, options.recognition || {})
|
|
153
|
+
|
|
154
|
+
logger.end()
|
|
155
|
+
|
|
156
|
+
const { wordTimeline: recognitionTimeline } = await API.recognize(sourceRawAudio, recognitionOptions)
|
|
157
|
+
|
|
158
|
+
const { referenceRawAudio, referenceTimeline, espeakVoice } = await getAlignmentReference()
|
|
159
|
+
|
|
160
|
+
logger.end()
|
|
161
|
+
|
|
162
|
+
const { windowDurations, granularities } = getDtwWindowDurationsAndGranularities()
|
|
163
|
+
|
|
164
|
+
const espeakOptions: EspeakOptions = { ...defaultEspeakOptions, voice: espeakVoice, useKlatt: false }
|
|
165
|
+
|
|
166
|
+
const phoneAlignmentMethod = options.dtw!.phoneAlignmentMethod!
|
|
167
|
+
|
|
168
|
+
mappedTimeline = await alignUsingDtwWithRecognition(sourceRawAudio, referenceRawAudio, referenceTimeline, recognitionTimeline, granularities, windowDurations, espeakOptions, phoneAlignmentMethod)
|
|
169
|
+
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case "whisper": {
|
|
174
|
+
const WhisperSTT = await import("../recognition/WhisperSTT.js")
|
|
175
|
+
|
|
176
|
+
const whisperOptions = options.whisper!
|
|
177
|
+
|
|
178
|
+
const shortLanguageCode = getShortLanguageCode(language)
|
|
179
|
+
|
|
180
|
+
const { modelName, modelDir, tokenizerDir } = await WhisperSTT.loadPackagesAndGetPaths(whisperOptions.model, language)
|
|
181
|
+
|
|
182
|
+
if (modelName.endsWith(".en") && shortLanguageCode != "en") {
|
|
183
|
+
throw new Error(`The model '${modelName}' is English only and cannot transcribe language '${shortLanguageCode}'`)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (getRawAudioDuration(sourceRawAudio) > 30) {
|
|
187
|
+
throw new Error("Whisper based alignment currently only supports audio inputs that are 30s or less")
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
logger.end()
|
|
191
|
+
|
|
192
|
+
mappedTimeline = await WhisperSTT.align(sourceRawAudio, transcript, modelName, modelDir, tokenizerDir, shortLanguageCode)
|
|
193
|
+
|
|
194
|
+
break
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
default: {
|
|
198
|
+
throw new Error(`Engine '${options.engine}' is not supported`)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
addWordTextOffsetsToTimeline(mappedTimeline, transcript)
|
|
203
|
+
|
|
204
|
+
const { segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(mappedTimeline, transcript, language, options.plainText?.paragraphBreaks, options.plainText?.whitespace)
|
|
205
|
+
|
|
206
|
+
logger.end()
|
|
207
|
+
logger.logDuration(`Total alignment time`, startTimestamp, chalk.magentaBright)
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
timeline: segmentTimeline,
|
|
211
|
+
wordTimeline: mappedTimeline,
|
|
212
|
+
inputRawAudio,
|
|
213
|
+
transcript,
|
|
214
|
+
language
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function alignSegments(sourceRawAudio: RawAudio, segmentTimeline: Timeline, alignmentOptions: AlignmentOptions) {
|
|
219
|
+
const timeline: Timeline = []
|
|
220
|
+
|
|
221
|
+
for (const segmentEntry of segmentTimeline) {
|
|
222
|
+
const segmentText = segmentEntry.text
|
|
223
|
+
|
|
224
|
+
const segmentStartTime = segmentEntry.startTime
|
|
225
|
+
const segmentEndTime = segmentEntry.endTime
|
|
226
|
+
|
|
227
|
+
const segmentStartSampleIndex = Math.floor(segmentStartTime * sourceRawAudio.sampleRate)
|
|
228
|
+
const segmentEndSampleIndex = Math.floor(segmentEndTime * sourceRawAudio.sampleRate)
|
|
229
|
+
|
|
230
|
+
const segmentAudioSamples = sourceRawAudio.audioChannels[0].slice(segmentStartSampleIndex, segmentEndSampleIndex)
|
|
231
|
+
const segmentRawAudio: RawAudio = {
|
|
232
|
+
audioChannels: [segmentAudioSamples],
|
|
233
|
+
sampleRate: sourceRawAudio.sampleRate
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const { wordTimeline: mappedTimeline } = await align(segmentRawAudio, segmentText, alignmentOptions)
|
|
237
|
+
|
|
238
|
+
const segmentTimelineWithOffset = addTimeOffsetToTimeline(mappedTimeline, segmentStartTime)
|
|
239
|
+
|
|
240
|
+
timeline.push(...segmentTimelineWithOffset)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return timeline
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export interface AlignmentResult {
|
|
247
|
+
timeline: Timeline
|
|
248
|
+
wordTimeline: Timeline
|
|
249
|
+
transcript: string
|
|
250
|
+
language: string
|
|
251
|
+
inputRawAudio: RawAudio
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export type AlignmentEngine = "dtw" | "dtw-ra" | "whisper"
|
|
255
|
+
export type PhoneAlignmentMethod = "interpolation" | "dtw"
|
|
256
|
+
|
|
257
|
+
export interface AlignmentOptions {
|
|
258
|
+
engine?: AlignmentEngine
|
|
259
|
+
|
|
260
|
+
language?: string
|
|
261
|
+
|
|
262
|
+
languageDetection?: API.TextLanguageDetectionOptions
|
|
263
|
+
|
|
264
|
+
customLexiconPaths?: string[]
|
|
265
|
+
|
|
266
|
+
plainText?: API.PlainTextOptions
|
|
267
|
+
|
|
268
|
+
subtitles?: SubtitlesConfig
|
|
269
|
+
|
|
270
|
+
dtw?: {
|
|
271
|
+
granularity?: DtwGranularity | DtwGranularity[]
|
|
272
|
+
windowDuration?: number | number[]
|
|
273
|
+
phoneAlignmentMethod?: PhoneAlignmentMethod
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
recognition?: API.RecognitionOptions
|
|
277
|
+
|
|
278
|
+
whisper?: WhisperOptions
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export const defaultAlignmentOptions: AlignmentOptions = {
|
|
282
|
+
engine: "dtw",
|
|
283
|
+
|
|
284
|
+
language: undefined,
|
|
285
|
+
|
|
286
|
+
languageDetection: {
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
customLexiconPaths: undefined,
|
|
290
|
+
|
|
291
|
+
plainText: {
|
|
292
|
+
paragraphBreaks: 'double',
|
|
293
|
+
whitespace: 'collapse'
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
subtitles: defaultSubtitlesBaseConfig,
|
|
297
|
+
|
|
298
|
+
dtw: {
|
|
299
|
+
granularity: 'auto',
|
|
300
|
+
windowDuration: undefined,
|
|
301
|
+
phoneAlignmentMethod: 'dtw'
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
recognition: {
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
whisper: whisperOptionsDefaults
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export const alignmentEngines: API.EngineMetadata[] = [
|
|
311
|
+
{
|
|
312
|
+
id: 'dtw',
|
|
313
|
+
name: 'Dynamic Time Warping',
|
|
314
|
+
description: 'Makes use of synthesis to find the best mapping between the original audio and its transcript.',
|
|
315
|
+
type: 'local'
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
id: 'dtw-ra',
|
|
319
|
+
name: 'Dynamic Time Warping with Recognition Assist',
|
|
320
|
+
description: 'Makes use of both synthesis and recognition to find the best mapping between the original audio and its transcript.',
|
|
321
|
+
type: 'local'
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: 'whisper',
|
|
325
|
+
name: 'OpenAI Whisper',
|
|
326
|
+
description: 'Extracts timestamps from the internal state of the Whisper recognition model (note: currently limited to a maximum of 30s audio length).',
|
|
327
|
+
type: 'local'
|
|
328
|
+
}
|
|
329
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const appName = "echogarden"
|
|
2
|
+
|
|
3
|
+
export interface EngineMetadata {
|
|
4
|
+
id: string
|
|
5
|
+
name: string
|
|
6
|
+
description: string
|
|
7
|
+
type: 'local' | 'server' | 'cloud'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PlainTextOptions {
|
|
11
|
+
paragraphBreaks?: ParagraphBreakType
|
|
12
|
+
whitespace?: WhitespaceProcessing
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ParagraphBreakType = 'single' | 'double'
|
|
16
|
+
export type WhitespaceProcessing = 'preserve' | 'removeLineBreaks' | 'collapse'
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { extendDeep } from "../utilities/ObjectUtilities.js"
|
|
2
|
+
|
|
3
|
+
import { AudioSourceParam, RawAudio, applyGainDecibels, ensureRawAudio, getAudioPeakDecibels, mixAudio, normalizeAudioLevel } from "../audio/AudioUtilities.js"
|
|
4
|
+
import { Logger } from "../utilities/Logger.js"
|
|
5
|
+
|
|
6
|
+
import { logToStderr } from "../utilities/Utilities.js"
|
|
7
|
+
import { resampleAudioSpeex } from "../dsp/SpeexResampler.js"
|
|
8
|
+
import { EngineMetadata } from "./Common.js"
|
|
9
|
+
import chalk from "chalk"
|
|
10
|
+
|
|
11
|
+
const log = logToStderr
|
|
12
|
+
|
|
13
|
+
export async function denoise(input: AudioSourceParam, options: DenoisingOptions) {
|
|
14
|
+
const logger = new Logger()
|
|
15
|
+
const startTime = logger.getTimestamp()
|
|
16
|
+
|
|
17
|
+
logger.start("Prepare for denoising")
|
|
18
|
+
|
|
19
|
+
options = extendDeep(defaultDenoisingOptions, options)
|
|
20
|
+
|
|
21
|
+
const inputRawAudio = await ensureRawAudio(input)
|
|
22
|
+
|
|
23
|
+
const processingSampleRate = 48000
|
|
24
|
+
|
|
25
|
+
logger.start(`Resample audio to ${processingSampleRate} Hz`)
|
|
26
|
+
const resampledRawAudio = await resampleAudioSpeex(inputRawAudio, processingSampleRate, 3)
|
|
27
|
+
|
|
28
|
+
logger.start(`Initialize ${options.method} module`)
|
|
29
|
+
|
|
30
|
+
let denoisedAudio: RawAudio
|
|
31
|
+
|
|
32
|
+
switch (options.method) {
|
|
33
|
+
case "rnnoise": {
|
|
34
|
+
const RNNoise = await import("../denoising/RNNoise.js")
|
|
35
|
+
logger.end()
|
|
36
|
+
|
|
37
|
+
const denoisedAudioChannels: Float32Array[] = []
|
|
38
|
+
|
|
39
|
+
for (const audioChannel of resampledRawAudio.audioChannels) {
|
|
40
|
+
const audioChannelRawAudio: RawAudio = { audioChannels: [audioChannel], sampleRate: processingSampleRate }
|
|
41
|
+
|
|
42
|
+
const { denoisedRawAudio, frameVadProbabilities } = await RNNoise.denoiseAudio(audioChannelRawAudio)
|
|
43
|
+
denoisedAudioChannels.push(denoisedRawAudio.audioChannels[0])
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
denoisedAudio = { audioChannels: denoisedAudioChannels, sampleRate: processingSampleRate }
|
|
47
|
+
|
|
48
|
+
break
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
default: {
|
|
52
|
+
throw new Error(`Method: '${options.method}' is not supported`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
logger.start("Postprocess audio")
|
|
57
|
+
|
|
58
|
+
const shouldNormalize = options.postProcessing!.normalizeAudio!
|
|
59
|
+
const targetPeakDb = options.postProcessing!.targetPeakDb!
|
|
60
|
+
const maxIncreaseDb = options.postProcessing!.maxIncreaseDb!
|
|
61
|
+
const dryMixGainDb = options.postProcessing!.dryMixGainDb!
|
|
62
|
+
|
|
63
|
+
const preMixPeakDb = getAudioPeakDecibels(denoisedAudio.audioChannels)
|
|
64
|
+
denoisedAudio = mixAudio(denoisedAudio, applyGainDecibels(resampledRawAudio, dryMixGainDb))
|
|
65
|
+
const postMixPeakDb = getAudioPeakDecibels(denoisedAudio.audioChannels)
|
|
66
|
+
|
|
67
|
+
if (shouldNormalize) {
|
|
68
|
+
denoisedAudio = normalizeAudioLevel(denoisedAudio, targetPeakDb, maxIncreaseDb)
|
|
69
|
+
} else {
|
|
70
|
+
denoisedAudio = applyGainDecibels(denoisedAudio, preMixPeakDb - postMixPeakDb)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
logger.end()
|
|
74
|
+
|
|
75
|
+
logger.log('')
|
|
76
|
+
logger.logDuration("Total denoising time", startTime, chalk.magentaBright)
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
denoisedAudio,
|
|
80
|
+
inputRawAudio
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface DenoisingResult {
|
|
85
|
+
denoisedAudio: RawAudio
|
|
86
|
+
inputRawAudio: RawAudio
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export type DenoisingMethod = "rnnoise"
|
|
90
|
+
|
|
91
|
+
export interface DenoisingOptions {
|
|
92
|
+
method?: DenoisingMethod,
|
|
93
|
+
postProcessing?: {
|
|
94
|
+
normalizeAudio: boolean
|
|
95
|
+
targetPeakDb: number
|
|
96
|
+
maxIncreaseDb: number
|
|
97
|
+
|
|
98
|
+
dryMixGainDb?: number
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export const defaultDenoisingOptions: DenoisingOptions = {
|
|
103
|
+
method: "rnnoise",
|
|
104
|
+
|
|
105
|
+
postProcessing: {
|
|
106
|
+
normalizeAudio: false,
|
|
107
|
+
targetPeakDb: -3,
|
|
108
|
+
maxIncreaseDb: 30,
|
|
109
|
+
dryMixGainDb: -20,
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const denoisingEngines: EngineMetadata[] = [
|
|
114
|
+
{
|
|
115
|
+
id: 'rnnoise',
|
|
116
|
+
name: 'RNNoise',
|
|
117
|
+
description: 'A noise suppression library based on a recurrent neural network.',
|
|
118
|
+
type: 'local'
|
|
119
|
+
}
|
|
120
|
+
]
|