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,488 @@
|
|
|
1
|
+
import { clip } from "../utilities/Utilities.js"
|
|
2
|
+
|
|
3
|
+
import * as API from "../api/API.js"
|
|
4
|
+
|
|
5
|
+
import { computeMFCCs, extendDefaultMfccOptions, MfccOptions } from "../dsp/MFCC.js"
|
|
6
|
+
import { alignMFCC_DTW, getCostMatrixMemorySizeMB } from "./DTWMfccSequenceAlignment.js"
|
|
7
|
+
import { Logger } from "../utilities/Logger.js"
|
|
8
|
+
import { Timeline, TimelineEntry } from "../utilities/Timeline.js"
|
|
9
|
+
import { getEndingSilentSampleCount, getRawAudioDuration, getStartingSilentSampleCount, RawAudio } from "../audio/AudioUtilities.js"
|
|
10
|
+
import { type EspeakOptions } from "../synthesis/EspeakTTS.js"
|
|
11
|
+
import chalk from "chalk"
|
|
12
|
+
|
|
13
|
+
export async function alignUsingDtw(sourceRawAudio: RawAudio, referenceRawAudio: RawAudio, referenceTimeline: Timeline, granularities: DtwGranularity[], windowDurations: number[]) {
|
|
14
|
+
const logger = new Logger()
|
|
15
|
+
|
|
16
|
+
if (windowDurations.length == 0) {
|
|
17
|
+
throw new Error(`Window durations array has length 0.`)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (windowDurations.length != granularities.length) {
|
|
21
|
+
throw new Error(`Window durations and granularities are not the same length.`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const rawAudioDuration = getRawAudioDuration(sourceRawAudio)
|
|
25
|
+
|
|
26
|
+
let framesPerSecond: number
|
|
27
|
+
let compactedPath: CompactedPath
|
|
28
|
+
let relativeCenters: number[] | undefined
|
|
29
|
+
|
|
30
|
+
for (let passIndex = 0; passIndex < windowDurations.length; passIndex++) {
|
|
31
|
+
const windowDuration = windowDurations[passIndex]
|
|
32
|
+
const granularity = resolveAutoGranularityIfNeeded(granularities[passIndex], rawAudioDuration)
|
|
33
|
+
|
|
34
|
+
logger.logTitledMessage(`\nStarting alignment pass ${passIndex + 1}/${windowDurations.length}`, `max window duration: ${windowDuration}s, granularity: ${granularity}`, chalk.magentaBright)
|
|
35
|
+
|
|
36
|
+
const mfccOptions = extendDefaultMfccOptions({ ...getMfccOptionsForGranularity(granularity, rawAudioDuration), zeroFirstCoefficient: true }) as MfccOptions
|
|
37
|
+
|
|
38
|
+
framesPerSecond = 1 / mfccOptions.hopDuration!
|
|
39
|
+
|
|
40
|
+
// Compute reference MFCCs
|
|
41
|
+
logger.start("Compute reference MFCC features")
|
|
42
|
+
const referenceMfccs = await computeMFCCs(referenceRawAudio, mfccOptions)
|
|
43
|
+
|
|
44
|
+
// Compute source MFCCs
|
|
45
|
+
logger.start("Compute source MFCC features")
|
|
46
|
+
const sourceMfccs = await computeMFCCs(sourceRawAudio, mfccOptions)
|
|
47
|
+
logger.end()
|
|
48
|
+
|
|
49
|
+
// Compute path
|
|
50
|
+
logger.logTitledMessage(`DTW cost matrix memory size`, `${getCostMatrixMemorySizeMB(referenceMfccs.length, sourceMfccs.length, windowDuration * framesPerSecond).toFixed(1)}MB`)
|
|
51
|
+
|
|
52
|
+
if (passIndex == 0) {
|
|
53
|
+
const minRecommendedWindowDuration = 0.2 * rawAudioDuration
|
|
54
|
+
|
|
55
|
+
if (windowDuration < minRecommendedWindowDuration ) {
|
|
56
|
+
logger.logTitledMessage('Warning', `Maximum DTW window duration is set to ${windowDuration.toFixed(1)}s, which is smaller than 20% of the source audio duration of ${rawAudioDuration.toFixed(1)}s. This may lead to suboptimal results in some cases. Consider increasing window duration if needed.`, chalk.yellowBright)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
logger.start("Align MFCC features using DTW")
|
|
61
|
+
const dtwWindowLength = Math.floor(windowDuration * framesPerSecond)
|
|
62
|
+
|
|
63
|
+
let centerIndexes: number[] | undefined
|
|
64
|
+
|
|
65
|
+
if (relativeCenters) {
|
|
66
|
+
centerIndexes = []
|
|
67
|
+
|
|
68
|
+
for (let i = 0; i < referenceMfccs.length; i++) {
|
|
69
|
+
const relativeReferencePosition = i / referenceMfccs.length
|
|
70
|
+
|
|
71
|
+
const relativeCenterIndex = Math.floor(relativeReferencePosition * relativeCenters!.length)
|
|
72
|
+
const relativeCenter = relativeCenters[relativeCenterIndex]
|
|
73
|
+
const centerIndex = Math.floor(relativeCenter * sourceMfccs.length)
|
|
74
|
+
|
|
75
|
+
centerIndexes.push(centerIndex)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rawPath = await alignMFCC_DTW(referenceMfccs, sourceMfccs, dtwWindowLength, undefined, centerIndexes)
|
|
80
|
+
|
|
81
|
+
compactedPath = compactPath(rawPath)
|
|
82
|
+
|
|
83
|
+
relativeCenters = compactedPath.map(entry => (entry.first + entry.last) / 2 / sourceMfccs.length)
|
|
84
|
+
|
|
85
|
+
logger.end()
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
logger.start("\nConvert path to timeline")
|
|
89
|
+
|
|
90
|
+
function getMappedTimelineEntry(timelineEntry: TimelineEntry, recurse = true): TimelineEntry {
|
|
91
|
+
const referenceStartFrameIndex = Math.floor(timelineEntry.startTime * framesPerSecond)
|
|
92
|
+
const referenceEndFrameIndex = Math.floor(timelineEntry.endTime * framesPerSecond)
|
|
93
|
+
|
|
94
|
+
if (referenceStartFrameIndex < 0 || referenceEndFrameIndex < 0) {
|
|
95
|
+
throw new Error("Unexpected: encountered a negative timestamp in timeline")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const mappedStartFrameIndex = getMappedFrameIndexForPath(referenceStartFrameIndex, compactedPath, "first")
|
|
99
|
+
const mappedEndFrameIndex = getMappedFrameIndexForPath(referenceEndFrameIndex, compactedPath, "first")
|
|
100
|
+
|
|
101
|
+
let innerTimeline: Timeline | undefined
|
|
102
|
+
|
|
103
|
+
if (recurse && timelineEntry.timeline != null) {
|
|
104
|
+
innerTimeline = timelineEntry.timeline.map((entry) => getMappedTimelineEntry(entry))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Trim silent samples from start and end of mapped entry range
|
|
108
|
+
const sourceSamplesPerFrame = Math.floor(sourceRawAudio.sampleRate / framesPerSecond)
|
|
109
|
+
|
|
110
|
+
let startSampleIndex = mappedStartFrameIndex * sourceSamplesPerFrame
|
|
111
|
+
let endSampleIndex = mappedEndFrameIndex * sourceSamplesPerFrame
|
|
112
|
+
|
|
113
|
+
const frameSamples = sourceRawAudio.audioChannels[0].subarray(startSampleIndex, endSampleIndex)
|
|
114
|
+
|
|
115
|
+
const silenceThresholdDecibels = -40
|
|
116
|
+
|
|
117
|
+
startSampleIndex += getStartingSilentSampleCount(frameSamples, silenceThresholdDecibels)
|
|
118
|
+
endSampleIndex -= getEndingSilentSampleCount(frameSamples, silenceThresholdDecibels)
|
|
119
|
+
|
|
120
|
+
endSampleIndex = Math.max(endSampleIndex, startSampleIndex)
|
|
121
|
+
|
|
122
|
+
// Build mapped timeline entry
|
|
123
|
+
const startTime = startSampleIndex / sourceRawAudio.sampleRate
|
|
124
|
+
const endTime = endSampleIndex / sourceRawAudio.sampleRate
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
type: timelineEntry.type,
|
|
128
|
+
text: timelineEntry.text,
|
|
129
|
+
|
|
130
|
+
startTime,
|
|
131
|
+
endTime,
|
|
132
|
+
|
|
133
|
+
timeline: innerTimeline
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const mappedTimeline = referenceTimeline.map((timelineEntry) => getMappedTimelineEntry(timelineEntry))
|
|
138
|
+
|
|
139
|
+
logger.end()
|
|
140
|
+
|
|
141
|
+
return mappedTimeline
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function alignUsingDtwWithRecognition(sourceRawAudio: RawAudio, referenceRawAudio: RawAudio, referenceTimeline: Timeline, recognitionTimeline: Timeline, granularities: DtwGranularity[], windowDurations: number[], espeakOptions: EspeakOptions, phoneAlignmentMethod: API.PhoneAlignmentMethod = "interpolation") {
|
|
145
|
+
const logger = new Logger()
|
|
146
|
+
|
|
147
|
+
if (recognitionTimeline.length == 0) {
|
|
148
|
+
const sourceDuration = getRawAudioDuration(sourceRawAudio)
|
|
149
|
+
const referenceDuration = getRawAudioDuration(referenceRawAudio)
|
|
150
|
+
const ratio = sourceDuration / referenceDuration
|
|
151
|
+
|
|
152
|
+
const interpolatedTimeline: Timeline = []
|
|
153
|
+
|
|
154
|
+
for (const entry of referenceTimeline) {
|
|
155
|
+
interpolatedTimeline.push({
|
|
156
|
+
type: entry.type,
|
|
157
|
+
text: entry.text,
|
|
158
|
+
startTime: entry.startTime * ratio,
|
|
159
|
+
endTime: entry.endTime * ratio
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return interpolatedTimeline
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Synthesize the recognized transcript and get its timeline
|
|
167
|
+
logger.start("Synthesize recognized transcript with eSpeak")
|
|
168
|
+
const recognizedWords = recognitionTimeline.map(entry => entry.text)
|
|
169
|
+
|
|
170
|
+
const { rawAudio: synthesizedRecognizedTranscriptRawAudio, timeline: synthesizedRecognitionTimeline } = await createAlignmentReferenceUsingEspeakForFragments(recognizedWords, espeakOptions)
|
|
171
|
+
|
|
172
|
+
let recognitionTimelineWithPhones: Timeline
|
|
173
|
+
|
|
174
|
+
if (phoneAlignmentMethod == "interpolation") {
|
|
175
|
+
// Add phone timelines by interpolating from reference words
|
|
176
|
+
logger.start("Interpolate phone timing")
|
|
177
|
+
|
|
178
|
+
recognitionTimelineWithPhones = await interpolatePhoneTimelines(recognitionTimeline, synthesizedRecognitionTimeline)
|
|
179
|
+
} else if (phoneAlignmentMethod == "dtw") {
|
|
180
|
+
logger.start("Align phone timing")
|
|
181
|
+
|
|
182
|
+
// Add phone timelines by aligning each individual recognized word with the corresponding word
|
|
183
|
+
// in the reference timeline
|
|
184
|
+
recognitionTimelineWithPhones = await alignPhoneTimelines(sourceRawAudio, recognitionTimeline, synthesizedRecognizedTranscriptRawAudio, synthesizedRecognitionTimeline)
|
|
185
|
+
} else if (phoneAlignmentMethod == "dtw-knn") {
|
|
186
|
+
logger.start("Align phone timing")
|
|
187
|
+
throw new Error("Not implemented")
|
|
188
|
+
} else {
|
|
189
|
+
throw new Error(`Unknown phone alignment method: ${phoneAlignmentMethod}`)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
logger.start("Map from the synthesized recognized timeline to the recognized timeline")
|
|
193
|
+
// Create a mapping from the synthesized recognized timeline to the recognized timeline
|
|
194
|
+
type SynthesizedToRecognizedTimeMapping = SynthesizedToRecognizedTimeMappingEntry[]
|
|
195
|
+
type SynthesizedToRecognizedTimeMappingEntry = { synthesized: number, recognized: number }
|
|
196
|
+
|
|
197
|
+
const synthesizedToRecognizedTimeMapping: SynthesizedToRecognizedTimeMapping = []
|
|
198
|
+
|
|
199
|
+
for (let i = 0; i < synthesizedRecognitionTimeline.length; i++) {
|
|
200
|
+
const synthesizedTimelineEntry = synthesizedRecognitionTimeline[i]
|
|
201
|
+
const recognitionTimelineEntry = recognitionTimelineWithPhones[i]
|
|
202
|
+
|
|
203
|
+
synthesizedToRecognizedTimeMapping.push({ synthesized: synthesizedTimelineEntry.startTime, recognized: recognitionTimelineEntry.startTime })
|
|
204
|
+
|
|
205
|
+
if (synthesizedTimelineEntry.timeline) {
|
|
206
|
+
for (let j = 0; j < synthesizedTimelineEntry.timeline.length; j++) {
|
|
207
|
+
const synthesizedPhoneTimelineEntry = synthesizedTimelineEntry.timeline[j]
|
|
208
|
+
const recognitionPhoneTimelineEntry = recognitionTimelineEntry.timeline![j]
|
|
209
|
+
|
|
210
|
+
synthesizedToRecognizedTimeMapping.push({ synthesized: synthesizedPhoneTimelineEntry.startTime, recognized: recognitionPhoneTimelineEntry.startTime })
|
|
211
|
+
synthesizedToRecognizedTimeMapping.push({ synthesized: synthesizedPhoneTimelineEntry.endTime, recognized: recognitionPhoneTimelineEntry.endTime })
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
synthesizedToRecognizedTimeMapping.push({ synthesized: synthesizedTimelineEntry.endTime, recognized: recognitionTimelineEntry.endTime })
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
logger.start("Align the synthesized recognized transcript with the synthesized ground-truth transcript")
|
|
219
|
+
// Align the synthesized recognized transcript to the synthesized reference transcript
|
|
220
|
+
const alignedSynthesizedRecognitionTimeline = await alignUsingDtw(synthesizedRecognizedTranscriptRawAudio, referenceRawAudio, referenceTimeline, granularities, windowDurations)
|
|
221
|
+
|
|
222
|
+
let currentSynthesizedToRecognizedMappingIndex = 0
|
|
223
|
+
|
|
224
|
+
function mapSynthesizedToRecognizedTimeAndAdvance(synthesizedTime: number) {
|
|
225
|
+
for (; ; currentSynthesizedToRecognizedMappingIndex += 1) {
|
|
226
|
+
const left = synthesizedToRecognizedTimeMapping[currentSynthesizedToRecognizedMappingIndex].synthesized
|
|
227
|
+
|
|
228
|
+
let right: number
|
|
229
|
+
|
|
230
|
+
if (currentSynthesizedToRecognizedMappingIndex < synthesizedToRecognizedTimeMapping.length - 1) {
|
|
231
|
+
right = synthesizedToRecognizedTimeMapping[currentSynthesizedToRecognizedMappingIndex + 1].synthesized
|
|
232
|
+
} else {
|
|
233
|
+
right = Infinity
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (left > right) {
|
|
237
|
+
throw new Error("left is larger than right!")
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (Math.abs(synthesizedTime - left) < Math.abs(synthesizedTime - right)) {
|
|
241
|
+
return synthesizedToRecognizedTimeMapping[currentSynthesizedToRecognizedMappingIndex].recognized
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function mapTimeline(timeline: Timeline) {
|
|
247
|
+
const mappedTimeline: Timeline = []
|
|
248
|
+
|
|
249
|
+
for (const entry of timeline) {
|
|
250
|
+
const mappedEntry = { ...entry }
|
|
251
|
+
|
|
252
|
+
mappedEntry.startTime = mapSynthesizedToRecognizedTimeAndAdvance(entry.startTime)
|
|
253
|
+
|
|
254
|
+
if (entry.timeline) {
|
|
255
|
+
mappedEntry.timeline = mapTimeline(entry.timeline)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
mappedEntry.endTime = mapSynthesizedToRecognizedTimeAndAdvance(entry.endTime)
|
|
259
|
+
|
|
260
|
+
mappedTimeline.push(mappedEntry)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return mappedTimeline
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result = mapTimeline(alignedSynthesizedRecognitionTimeline)
|
|
267
|
+
|
|
268
|
+
logger.end()
|
|
269
|
+
|
|
270
|
+
return result
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function interpolatePhoneTimelines(sourceTimeline: Timeline, referenceTimeline: Timeline) {
|
|
274
|
+
const interpolatedTimeline: Timeline = []
|
|
275
|
+
|
|
276
|
+
for (let i = 0; i < sourceTimeline.length; i++) {
|
|
277
|
+
const referenceEntry = referenceTimeline[i]
|
|
278
|
+
|
|
279
|
+
const interpolatedEntry = { ...sourceTimeline[i] }
|
|
280
|
+
interpolatedTimeline.push(interpolatedEntry)
|
|
281
|
+
|
|
282
|
+
if (interpolatedEntry.type != "word") {
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const interpolatedEntryDuration = interpolatedEntry.endTime - interpolatedEntry.startTime
|
|
287
|
+
const synthesisEntryDuration = referenceEntry.endTime - referenceEntry.startTime
|
|
288
|
+
|
|
289
|
+
interpolatedEntry.timeline = []
|
|
290
|
+
|
|
291
|
+
for (const phoneEntry of referenceEntry.timeline!) {
|
|
292
|
+
const phoneStartTimePercentageRelativeToWord =
|
|
293
|
+
(phoneEntry.startTime - referenceEntry.startTime) / synthesisEntryDuration
|
|
294
|
+
|
|
295
|
+
const phoneEndTimePercentageRelativeToWord =
|
|
296
|
+
(phoneEntry.endTime - referenceEntry.startTime) / synthesisEntryDuration
|
|
297
|
+
|
|
298
|
+
const interpolatedPhoneStartTime = interpolatedEntry.startTime + (phoneStartTimePercentageRelativeToWord * interpolatedEntryDuration)
|
|
299
|
+
const interpolatedPhoneEndTime = interpolatedEntry.startTime + (phoneEndTimePercentageRelativeToWord * interpolatedEntryDuration)
|
|
300
|
+
|
|
301
|
+
interpolatedEntry.timeline.push({
|
|
302
|
+
...phoneEntry,
|
|
303
|
+
|
|
304
|
+
startTime: interpolatedPhoneStartTime,
|
|
305
|
+
endTime: interpolatedPhoneEndTime
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return interpolatedTimeline
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export async function alignPhoneTimelines(sourceRawAudio: RawAudio, sourceWordTimeline: Timeline, referenceRawAudio: RawAudio, referenceTimeline: Timeline) {
|
|
314
|
+
const mfccOptions: MfccOptions = extendDefaultMfccOptions({ zeroFirstCoefficient: true })
|
|
315
|
+
|
|
316
|
+
const framesPerSecond = 1 / mfccOptions.hopDuration!
|
|
317
|
+
|
|
318
|
+
const referenceMfccs = await computeMFCCs(referenceRawAudio, mfccOptions)
|
|
319
|
+
const sourceMfccs = await computeMFCCs(sourceRawAudio, mfccOptions)
|
|
320
|
+
|
|
321
|
+
const alignedWordTimeline: Timeline = []
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < referenceTimeline.length; i++) {
|
|
324
|
+
const referenceWordEntry = referenceTimeline[i]
|
|
325
|
+
|
|
326
|
+
const alignedWordEntry = { ...sourceWordTimeline[i] }
|
|
327
|
+
alignedWordTimeline.push(alignedWordEntry)
|
|
328
|
+
|
|
329
|
+
if (alignedWordEntry.type != "word") {
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const referenceWordStartFrameIndex = Math.floor(referenceWordEntry.startTime * framesPerSecond)
|
|
334
|
+
let referenceWordEndFrameIndex = Math.floor(referenceWordEntry.endTime * framesPerSecond)
|
|
335
|
+
|
|
336
|
+
// Ensure there is at least one frame in range
|
|
337
|
+
if (referenceWordEndFrameIndex <= referenceWordStartFrameIndex) {
|
|
338
|
+
referenceWordEndFrameIndex = referenceWordEndFrameIndex + 1
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const referenceWordMfccs = referenceMfccs.slice(referenceWordStartFrameIndex, referenceWordEndFrameIndex)
|
|
342
|
+
|
|
343
|
+
const alignedWordStartFrameIndex = Math.floor(alignedWordEntry.startTime * framesPerSecond)
|
|
344
|
+
let alignedWordEndFrameIndex = Math.floor(alignedWordEntry.endTime * framesPerSecond)
|
|
345
|
+
|
|
346
|
+
// Ensure there is at least one frame in range
|
|
347
|
+
if (alignedWordEndFrameIndex <= alignedWordStartFrameIndex) {
|
|
348
|
+
alignedWordEndFrameIndex = alignedWordStartFrameIndex + 1
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const sourceWordMfccs = sourceMfccs.slice(alignedWordStartFrameIndex, alignedWordEndFrameIndex)
|
|
352
|
+
|
|
353
|
+
// Compute DTW path
|
|
354
|
+
const rawPath = await alignMFCC_DTW(referenceWordMfccs, sourceWordMfccs, 60)
|
|
355
|
+
const compactedPath = compactPath(rawPath)
|
|
356
|
+
|
|
357
|
+
// Add phone timeline using the mapped time information
|
|
358
|
+
alignedWordEntry.timeline = []
|
|
359
|
+
|
|
360
|
+
for (const referencePhoneEntry of referenceWordEntry.timeline!) {
|
|
361
|
+
const referencePhoneStartFrameOffset = Math.floor((referencePhoneEntry.startTime - referenceWordEntry.startTime) * framesPerSecond)
|
|
362
|
+
const alignedPhoneStartFrameOffset = getMappedFrameIndexForPath(referencePhoneStartFrameOffset, compactedPath)
|
|
363
|
+
const alignedPhoneStartTime = alignedWordEntry.startTime + (alignedPhoneStartFrameOffset / framesPerSecond)
|
|
364
|
+
|
|
365
|
+
const referencePhoneEndFrameOffset = Math.floor((referencePhoneEntry.endTime - referenceWordEntry.startTime) * framesPerSecond)
|
|
366
|
+
const alignedPhoneEndFrameOffset = getMappedFrameIndexForPath(referencePhoneEndFrameOffset, compactedPath)
|
|
367
|
+
const alignedPhoneEndTime = alignedWordEntry.startTime + (alignedPhoneEndFrameOffset / framesPerSecond)
|
|
368
|
+
|
|
369
|
+
alignedWordEntry.timeline.push({
|
|
370
|
+
...referencePhoneEntry,
|
|
371
|
+
startTime: alignedPhoneStartTime,
|
|
372
|
+
endTime: alignedPhoneEndTime
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return alignedWordTimeline
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function createAlignmentReferenceUsingEspeakForFragments(fragments: string[], espeakOptions: EspeakOptions, insertSeparators = true) {
|
|
381
|
+
const progressLogger = new Logger()
|
|
382
|
+
|
|
383
|
+
progressLogger.start("Load espeak module")
|
|
384
|
+
const Espeak = await import("../synthesis/EspeakTTS.js")
|
|
385
|
+
|
|
386
|
+
progressLogger.start("Create alignment reference with eSpeak")
|
|
387
|
+
|
|
388
|
+
const result = await Espeak.synthesizeFragments(fragments, espeakOptions, insertSeparators)
|
|
389
|
+
|
|
390
|
+
result.timeline = result.timeline.flatMap(clause => clause.timeline!)
|
|
391
|
+
|
|
392
|
+
for (const wordEntry of result.timeline) {
|
|
393
|
+
wordEntry.timeline = wordEntry.timeline!.flatMap(tokenEntry => tokenEntry.timeline!)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
progressLogger.end()
|
|
397
|
+
|
|
398
|
+
return result
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function compactPath(path: AlignmentPath) {
|
|
402
|
+
const compactedPath: CompactedPath = []
|
|
403
|
+
|
|
404
|
+
for (let i = 0; i < path.length; i++) {
|
|
405
|
+
const pathEntry = path[i]
|
|
406
|
+
|
|
407
|
+
if (compactedPath.length <= pathEntry.source) {
|
|
408
|
+
compactedPath.push({ first: pathEntry.dest, last: pathEntry.dest })
|
|
409
|
+
} else {
|
|
410
|
+
compactedPath[compactedPath.length - 1].last = pathEntry.dest
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return compactedPath
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function getMappedFrameIndexForPath(referenceFrameIndex: number, compactedPath: CompactedPath, mappingKind: "first" | "last" = "first") {
|
|
418
|
+
if (compactedPath.length == 0) {
|
|
419
|
+
return 0
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
referenceFrameIndex = clip(referenceFrameIndex, 0, compactedPath.length - 1)
|
|
423
|
+
|
|
424
|
+
const compactedPathEntry = compactedPath[referenceFrameIndex]
|
|
425
|
+
|
|
426
|
+
let mappedFrameIndex: number
|
|
427
|
+
|
|
428
|
+
if (mappingKind == "first") {
|
|
429
|
+
mappedFrameIndex = compactedPathEntry.first
|
|
430
|
+
} else {
|
|
431
|
+
mappedFrameIndex = compactedPathEntry.last
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return mappedFrameIndex
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function resolveAutoGranularityIfNeeded(granularity: DtwGranularity, audioDuration: number) {
|
|
438
|
+
if (granularity != 'auto') {
|
|
439
|
+
return granularity
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (audioDuration < 60) {
|
|
443
|
+
return 'high'
|
|
444
|
+
} else if (audioDuration < 60 * 10) {
|
|
445
|
+
return 'medium'
|
|
446
|
+
} else {
|
|
447
|
+
return 'low'
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function getMfccOptionsForGranularity(granularity: DtwGranularity, audioDuration: number) {
|
|
452
|
+
let mfccOptions: MfccOptions
|
|
453
|
+
|
|
454
|
+
granularity = resolveAutoGranularityIfNeeded(granularity, audioDuration)
|
|
455
|
+
|
|
456
|
+
if (granularity == 'xx-low') {
|
|
457
|
+
mfccOptions = { windowDuration: 0.400, hopDuration: 0.160, fftOrder: 8192 }
|
|
458
|
+
} else if (granularity == 'x-low') {
|
|
459
|
+
mfccOptions = { windowDuration: 0.200, hopDuration: 0.080, fftOrder: 4096 }
|
|
460
|
+
} else if (granularity == 'low') {
|
|
461
|
+
mfccOptions = { windowDuration: 0.100, hopDuration: 0.040, fftOrder: 2048 }
|
|
462
|
+
} else if (granularity == 'medium') {
|
|
463
|
+
mfccOptions = { windowDuration: 0.050, hopDuration: 0.020, fftOrder: 1024 }
|
|
464
|
+
} else if (granularity == 'high') {
|
|
465
|
+
mfccOptions = { windowDuration: 0.025, hopDuration: 0.010, fftOrder: 512 }
|
|
466
|
+
} else if (granularity == 'x-high') {
|
|
467
|
+
mfccOptions = { windowDuration: 0.020, hopDuration: 0.005, fftOrder: 512 }
|
|
468
|
+
} else {
|
|
469
|
+
throw new Error(`Invalid granularity setting: '${granularity}'`)
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
return mfccOptions
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export type AlignmentPath = AlignmentPathEntry[]
|
|
476
|
+
|
|
477
|
+
export type AlignmentPathEntry = {
|
|
478
|
+
source: number,
|
|
479
|
+
dest: number
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export type CompactedPath = CompactedPathEntry[]
|
|
483
|
+
|
|
484
|
+
export type CompactedPathEntry = {
|
|
485
|
+
first: number, last: number
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export type DtwGranularity = 'auto' | 'xx-low' | 'x-low' | 'low' | 'medium' | 'high' | 'x-high'
|
package/src/api/API.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference path="../typings/Fillers.d.ts" />
|
|
2
|
+
|
|
3
|
+
export * from "./Common.js"
|
|
4
|
+
export * from "./Synthesis.js"
|
|
5
|
+
export * from "./Recognition.js"
|
|
6
|
+
export * from "./Alignment.js"
|
|
7
|
+
export * from "./Translation.js"
|
|
8
|
+
export * from "./LanguageDetection.js"
|
|
9
|
+
export * from "./Vad.js"
|
|
10
|
+
export * from "./Denoising.js"
|
|
11
|
+
export * from "../server/Server.js"
|
|
12
|
+
export * from "../server/Client.js"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as API from "./API.js"
|
|
2
|
+
import type { ServerOptions } from "../server/Server.js"
|
|
3
|
+
|
|
4
|
+
export type APIOptions = {
|
|
5
|
+
VoiceListRequestOptions: API.VoiceListRequestOptions,
|
|
6
|
+
SynthesisOptions: API.SynthesisOptions,
|
|
7
|
+
RecognitionOptions: API.RecognitionOptions,
|
|
8
|
+
AlignmentOptions: API.AlignmentOptions,
|
|
9
|
+
SpeechTranslationOptions: API.SpeechTranslationOptions
|
|
10
|
+
SpeechLanguageDetectionOptions: API.SpeechLanguageDetectionOptions,
|
|
11
|
+
TextLanguageDetectionOptions: API.TextLanguageDetectionOptions,
|
|
12
|
+
VADOptions: API.VADOptions,
|
|
13
|
+
DenoisingOptions: API.DenoisingOptions,
|
|
14
|
+
ServerOptions: ServerOptions
|
|
15
|
+
}
|