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.
Files changed (122) hide show
  1. package/data/schemas/options.json +16 -0
  2. package/dist/api/Alignment.js +2 -2
  3. package/dist/api/Alignment.js.map +1 -1
  4. package/dist/api/Recognition.js +2 -2
  5. package/dist/api/Recognition.js.map +1 -1
  6. package/dist/api/Synthesis.js +5 -4
  7. package/dist/api/Synthesis.js.map +1 -1
  8. package/dist/api/Translation.js +2 -2
  9. package/dist/api/Translation.js.map +1 -1
  10. package/dist/audio/AudioUtilities.d.ts +1 -0
  11. package/dist/audio/AudioUtilities.js +25 -7
  12. package/dist/audio/AudioUtilities.js.map +1 -1
  13. package/dist/cli/CLI.js +2 -2
  14. package/dist/cli/CLI.js.map +1 -1
  15. package/dist/recognition/WhisperSTT.js +2 -2
  16. package/dist/recognition/WhisperSTT.js.map +1 -1
  17. package/dist/subtitles/Subtitles.d.ts +10 -7
  18. package/dist/subtitles/Subtitles.js +268 -207
  19. package/dist/subtitles/Subtitles.js.map +1 -1
  20. package/docs/Options.md +4 -2
  21. package/package.json +7 -6
  22. package/src/alignment/DTWMfccSequenceAlignment.ts +43 -0
  23. package/src/alignment/DTWSequenceAlignment.ts +121 -0
  24. package/src/alignment/DTWSequenceAlignmentWindowed.ts +210 -0
  25. package/src/alignment/LevenshteinSequenceAlignment.ts +126 -0
  26. package/src/alignment/SpeechAlignment.ts +488 -0
  27. package/src/api/API.ts +12 -0
  28. package/src/api/APIOptions.ts +15 -0
  29. package/src/api/Alignment.ts +329 -0
  30. package/src/api/Common.ts +16 -0
  31. package/src/api/Denoising.ts +120 -0
  32. package/src/api/LanguageDetection.ts +286 -0
  33. package/src/api/Recognition.ts +344 -0
  34. package/src/api/Synthesis.ts +1735 -0
  35. package/src/api/Translation.ts +143 -0
  36. package/src/api/Vad.ts +172 -0
  37. package/src/audio/AudioBufferConversion.ts +248 -0
  38. package/src/audio/AudioPlayer.ts +358 -0
  39. package/src/audio/AudioRecorder.ts +91 -0
  40. package/src/audio/AudioUtilities.ts +392 -0
  41. package/src/audio/SoxPath.ts +24 -0
  42. package/src/cli/CLI.ts +1360 -0
  43. package/src/cli/CLIConfigFile.ts +91 -0
  44. package/src/cli/CLILauncher.ts +26 -0
  45. package/src/cli/CLIOptionsSchema.ts +54 -0
  46. package/src/cli/CLIParser.ts +41 -0
  47. package/src/cli/CLIStarter.ts +40 -0
  48. package/src/codecs/FFMpegTranscoder.ts +214 -0
  49. package/src/codecs/TIMITCodec.ts +17 -0
  50. package/src/codecs/WaveCodec.ts +260 -0
  51. package/src/denoising/RNNoise.ts +95 -0
  52. package/src/dsp/BiquadFilter.ts +488 -0
  53. package/src/dsp/FFT.ts +187 -0
  54. package/src/dsp/MFCC.ts +227 -0
  55. package/src/dsp/MelSpectogram.ts +145 -0
  56. package/src/dsp/Rubberband.ts +249 -0
  57. package/src/dsp/Sonic.ts +59 -0
  58. package/src/dsp/SpeexResampler.ts +79 -0
  59. package/src/math/VectorMath.ts +812 -0
  60. package/src/nlp/ChineseSegmentation.ts +68 -0
  61. package/src/nlp/CompromiseNLP.ts +113 -0
  62. package/src/nlp/EspeakPhonemizer.ts +168 -0
  63. package/src/nlp/IPA.ts +139 -0
  64. package/src/nlp/JapaneseSegmentation.ts +53 -0
  65. package/src/nlp/Lexicon.ts +119 -0
  66. package/src/nlp/PhoneConversion.ts +508 -0
  67. package/src/nlp/Segmentation.ts +237 -0
  68. package/src/nlp/TextNormalizer.ts +160 -0
  69. package/src/recognition/AmazonTranscribeSTT.ts +112 -0
  70. package/src/recognition/AzureCognitiveServicesSTT.ts +76 -0
  71. package/src/recognition/GoogleCloudSTT.ts +92 -0
  72. package/src/recognition/SileroSTT.ts +173 -0
  73. package/src/recognition/VoskSTT.ts +112 -0
  74. package/src/recognition/WhisperSTT.ts +1518 -0
  75. package/src/server/Client.ts +297 -0
  76. package/src/server/Server.ts +178 -0
  77. package/src/server/ServerStarter.ts +12 -0
  78. package/src/server/Worker.ts +400 -0
  79. package/src/server/WorkerStarter.ts +38 -0
  80. package/src/speech-language-detection/SileroLanguageDetection.ts +105 -0
  81. package/src/subtitles/Subtitles.ts +478 -0
  82. package/src/synthesis/AwsPollyTTS.ts +78 -0
  83. package/src/synthesis/AzureCognitiveServicesTTS.ts +146 -0
  84. package/src/synthesis/CoquiServerTTS.ts +29 -0
  85. package/src/synthesis/ElevenLabsTTS.ts +104 -0
  86. package/src/synthesis/EspeakTTS.ts +552 -0
  87. package/src/synthesis/FliteTTS.ts +387 -0
  88. package/src/synthesis/GoogleCloudTTS.ts +112 -0
  89. package/src/synthesis/GoogleTranslateTTS.ts +210 -0
  90. package/src/synthesis/MicrosoftEdgeTTS.ts +298 -0
  91. package/src/synthesis/SamTTS.ts +30 -0
  92. package/src/synthesis/SapiTTS.ts +222 -0
  93. package/src/synthesis/StreamlabsPollyTTS.ts +114 -0
  94. package/src/synthesis/SvoxPicoTTS.ts +318 -0
  95. package/src/synthesis/VitsTTS.ts +734 -0
  96. package/src/tests/Test.ts +24 -0
  97. package/src/text-language-detection/FastTextLanguageDetection.ts +53 -0
  98. package/src/text-language-detection/TinyLDLanguageDetection.ts +16 -0
  99. package/src/typings/Fillers.d.ts +41 -0
  100. package/src/utilities/BinaryArrayConversion.ts +159 -0
  101. package/src/utilities/Compression.ts +91 -0
  102. package/src/utilities/FileDownloader.ts +201 -0
  103. package/src/utilities/FileSystem.ts +265 -0
  104. package/src/utilities/Hashing.ts +230 -0
  105. package/src/utilities/Locale.ts +119 -0
  106. package/src/utilities/Logger.ts +72 -0
  107. package/src/utilities/NdArrayUtilities.ts +31 -0
  108. package/src/utilities/ObjectUtilities.ts +169 -0
  109. package/src/utilities/OpenPromise.ts +13 -0
  110. package/src/utilities/PackageManager.ts +97 -0
  111. package/src/utilities/Queue.ts +17 -0
  112. package/src/utilities/RandomGenerator.ts +237 -0
  113. package/src/utilities/SignalChannel.ts +22 -0
  114. package/src/utilities/TarballMaker.ts +68 -0
  115. package/src/utilities/Timeline.ts +231 -0
  116. package/src/utilities/Timer.ts +93 -0
  117. package/src/utilities/Utilities.ts +574 -0
  118. package/src/utilities/WasmMemoryManager.ts +516 -0
  119. package/src/utilities/WebReader.ts +55 -0
  120. package/src/utilities/WikipediaReader.ts +41 -0
  121. package/src/voice-activity-detection/SileroVAD.ts +86 -0
  122. 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
+ }