echogarden 1.7.0 → 1.8.1

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 (54) hide show
  1. package/data/schemas/options.json +22 -10
  2. package/dist/api/Alignment.js +2 -2
  3. package/dist/api/Alignment.js.map +1 -1
  4. package/dist/api/Denoising.js +5 -4
  5. package/dist/api/Denoising.js.map +1 -1
  6. package/dist/api/Recognition.js +2 -2
  7. package/dist/api/Recognition.js.map +1 -1
  8. package/dist/api/SourceSeparation.d.ts +2 -6
  9. package/dist/api/SourceSeparation.js +8 -9
  10. package/dist/api/SourceSeparation.js.map +1 -1
  11. package/dist/api/SpeechLanguageDetection.js +2 -2
  12. package/dist/api/SpeechLanguageDetection.js.map +1 -1
  13. package/dist/api/SpeechTranslation.js +2 -2
  14. package/dist/api/SpeechTranslation.js.map +1 -1
  15. package/dist/api/Synthesis.js +5 -5
  16. package/dist/api/Synthesis.js.map +1 -1
  17. package/dist/api/TranslationAlignment.js +2 -2
  18. package/dist/api/TranslationAlignment.js.map +1 -1
  19. package/dist/audio/AudioUtilities.d.ts +5 -3
  20. package/dist/audio/AudioUtilities.js +39 -25
  21. package/dist/audio/AudioUtilities.js.map +1 -1
  22. package/dist/dsp/FFT.d.ts +2 -1
  23. package/dist/dsp/FFT.js +37 -18
  24. package/dist/dsp/FFT.js.map +1 -1
  25. package/dist/nlp/IPA.d.ts +3 -3
  26. package/dist/recognition/WhisperSTT.d.ts +5 -0
  27. package/dist/recognition/WhisperSTT.js +47 -7
  28. package/dist/recognition/WhisperSTT.js.map +1 -1
  29. package/dist/source-separation/MDXNetSourceSeparation.d.ts +8 -3
  30. package/dist/source-separation/MDXNetSourceSeparation.js +35 -13
  31. package/dist/source-separation/MDXNetSourceSeparation.js.map +1 -1
  32. package/dist/utilities/PackageManager.js +3 -6
  33. package/dist/utilities/PackageManager.js.map +1 -1
  34. package/dist/utilities/WasmMemoryManager.d.ts +9 -1
  35. package/dist/utilities/WasmMemoryManager.js +24 -3
  36. package/dist/utilities/WasmMemoryManager.js.map +1 -1
  37. package/docs/Options.md +9 -9
  38. package/package.json +6 -5
  39. package/src/alignment/SpeechAlignment.ts +1 -1
  40. package/src/api/Alignment.ts +2 -2
  41. package/src/api/Denoising.ts +5 -4
  42. package/src/api/Recognition.ts +3 -3
  43. package/src/api/SourceSeparation.ts +12 -16
  44. package/src/api/SpeechLanguageDetection.ts +2 -2
  45. package/src/api/SpeechTranslation.ts +2 -2
  46. package/src/api/Synthesis.ts +5 -5
  47. package/src/api/TranslationAlignment.ts +2 -2
  48. package/src/audio/AudioUtilities.ts +53 -33
  49. package/src/dsp/FFT.ts +45 -19
  50. package/src/recognition/WhisperSTT.ts +66 -8
  51. package/src/source-separation/MDXNetSourceSeparation.ts +44 -12
  52. package/src/typings/Fillers.d.ts +2 -0
  53. package/src/utilities/PackageManager.ts +3 -7
  54. package/src/utilities/WasmMemoryManager.ts +38 -4
@@ -1,6 +1,6 @@
1
1
  import { deepClone, extendDeep } from '../utilities/ObjectUtilities.js'
2
2
 
3
- import { AudioSourceParam, RawAudio, ensureRawAudio, getRawAudioDuration, normalizeAudioLevel, sliceRawAudioByTime, trimAudioEnd } from '../audio/AudioUtilities.js'
3
+ import { AudioSourceParam, RawAudio, ensureRawAudio, getRawAudioDuration, normalizeAudioLevelInPlace, sliceRawAudioByTime, trimAudioEnd } from '../audio/AudioUtilities.js'
4
4
  import { Logger } from '../utilities/Logger.js'
5
5
 
6
6
  import * as API from './API.js'
@@ -28,7 +28,7 @@ export async function detectSpeechLanguage(input: AudioSourceParam, options: Spe
28
28
 
29
29
  logger.start(`Resample audio to 16kHz mono`)
30
30
  let sourceRawAudio = await ensureRawAudio(inputRawAudio, 16000, 1)
31
- sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
31
+ normalizeAudioLevelInPlace(sourceRawAudio)
32
32
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
33
33
 
34
34
  if (options.crop) {
@@ -1,7 +1,7 @@
1
1
  import { extendDeep } from '../utilities/ObjectUtilities.js'
2
2
 
3
3
  import { logToStderr } from '../utilities/Utilities.js'
4
- import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevel, trimAudioEnd } from '../audio/AudioUtilities.js'
4
+ import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevelInPlace, trimAudioEnd } from '../audio/AudioUtilities.js'
5
5
  import { Logger } from '../utilities/Logger.js'
6
6
 
7
7
  import { Timeline, addWordTextOffsetsToTimeline, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
@@ -61,7 +61,7 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
61
61
 
62
62
  logger.start('Normalize and trim audio')
63
63
 
64
- sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
64
+ normalizeAudioLevelInPlace(sourceRawAudio)
65
65
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
66
66
 
67
67
  if (options.sourceLanguage) {
@@ -5,7 +5,7 @@ import { deepClone, extendDeep } from '../utilities/ObjectUtilities.js'
5
5
  import * as FFMpegTranscoder from '../codecs/FFMpegTranscoder.js'
6
6
 
7
7
  import { clip, sha256AsHex, stringifyAndFormatJson, logToStderr, yieldToEventLoop, runOperationWithRetries } from '../utilities/Utilities.js'
8
- import { RawAudio, attenuateIfClipping, concatAudioSegments, downmixToMono, encodeRawAudioToWave, getSamplePeakDecibels, getEmptyRawAudio, getRawAudioDuration, normalizeAudioLevel, trimAudioEnd, trimAudioStart } from '../audio/AudioUtilities.js'
8
+ import { RawAudio, concatAudioSegments, downmixToMono, encodeRawAudioToWave, getSamplePeakDecibels, getEmptyRawAudio, getRawAudioDuration, trimAudioEnd, trimAudioStart, attenuateIfClippingInPlace, normalizeAudioLevelInPlace } from '../audio/AudioUtilities.js'
9
9
  import { Logger } from '../utilities/Logger.js'
10
10
 
11
11
  import { isWordOrSymbolWord, splitToParagraphs, splitToSentences } from '../nlp/Segmentation.js'
@@ -250,9 +250,9 @@ async function synthesizeSegments(segments: string[], options: SynthesisOptions,
250
250
  resultRawAudio = { audioChannels: joinedAudioBuffers, sampleRate: segmentsRawAudio[0].sampleRate }
251
251
 
252
252
  if (options.postProcessing!.normalizeAudio) {
253
- resultRawAudio = normalizeAudioLevel(resultRawAudio, options.postProcessing!.targetPeak, options.postProcessing!.maxGainIncrease)
253
+ normalizeAudioLevelInPlace(resultRawAudio, options.postProcessing!.targetPeak, options.postProcessing!.maxGainIncrease)
254
254
  } else {
255
- resultRawAudio = attenuateIfClipping(resultRawAudio)
255
+ attenuateIfClippingInPlace(resultRawAudio)
256
256
  }
257
257
  } else {
258
258
  resultRawAudio = getEmptyRawAudio(1, 24000)
@@ -866,9 +866,9 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
866
866
  synthesizedAudio = downmixToMono(synthesizedAudio)
867
867
 
868
868
  if (options.postProcessing!.normalizeAudio) {
869
- synthesizedAudio = normalizeAudioLevel(synthesizedAudio, options.postProcessing!.targetPeak!, options.postProcessing!.maxGainIncrease!)
869
+ normalizeAudioLevelInPlace(synthesizedAudio, options.postProcessing!.targetPeak!, options.postProcessing!.maxGainIncrease!)
870
870
  } else {
871
- synthesizedAudio = attenuateIfClipping(synthesizedAudio)
871
+ attenuateIfClippingInPlace(synthesizedAudio)
872
872
  }
873
873
 
874
874
  const preTrimSampleCount = synthesizedAudio.audioChannels[0].length
@@ -1,7 +1,7 @@
1
1
  import { extendDeep } from '../utilities/ObjectUtilities.js'
2
2
 
3
3
  import { logToStderr } from '../utilities/Utilities.js'
4
- import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevel, trimAudioEnd } from '../audio/AudioUtilities.js'
4
+ import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevelInPlace, trimAudioEnd } from '../audio/AudioUtilities.js'
5
5
  import { Logger } from '../utilities/Logger.js'
6
6
 
7
7
  import * as API from './API.js'
@@ -53,7 +53,7 @@ export async function alignTranslation(input: AudioSourceParam, translatedTransc
53
53
 
54
54
  logger.start('Normalize and trim audio')
55
55
 
56
- sourceRawAudio = normalizeAudioLevel(sourceRawAudio)
56
+ normalizeAudioLevelInPlace(sourceRawAudio)
57
57
  sourceRawAudio.audioChannels[0] = trimAudioEnd(sourceRawAudio.audioChannels[0])
58
58
 
59
59
  logger.end()
@@ -47,11 +47,13 @@ export function trimAudioEnd(audioSamples: Float32Array, targetEndSilentSampleCo
47
47
  }
48
48
 
49
49
  export function getStartingSilentSampleCount(audioSamples: Float32Array, amplitudeThresholdDecibels = defaultSilenceThresholdDecibels) {
50
+ const sampleCount = audioSamples.length
51
+
50
52
  const minSampleAmplitude = decibelsToGainFactor(amplitudeThresholdDecibels)
51
53
 
52
54
  let silentSampleCount = 0
53
55
 
54
- for (let i = 0; i < audioSamples.length - 1; i++) {
56
+ for (let i = 0; i < sampleCount - 1; i++) {
55
57
  if (Math.abs(audioSamples[i]) > minSampleAmplitude) {
56
58
  break
57
59
  }
@@ -63,11 +65,13 @@ export function getStartingSilentSampleCount(audioSamples: Float32Array, amplitu
63
65
  }
64
66
 
65
67
  export function getEndingSilentSampleCount(audioSamples: Float32Array, amplitudeThresholdDecibels = defaultSilenceThresholdDecibels) {
68
+ const sampleCount = audioSamples.length
69
+
66
70
  const minSampleAmplitude = decibelsToGainFactor(amplitudeThresholdDecibels)
67
71
 
68
72
  let silentSampleCount = 0
69
73
 
70
- for (let i = audioSamples.length - 1; i >= 0; i--) {
74
+ for (let i = sampleCount - 1; i >= 0; i--) {
71
75
  if (Math.abs(audioSamples[i]) > minSampleAmplitude) {
72
76
  break
73
77
  }
@@ -82,15 +86,27 @@ export function getEndingSilentSampleCount(audioSamples: Float32Array, amplitude
82
86
  // Gain, normalization, mixing, and channel downmixing
83
87
  ////////////////////////////////////////////////////////////////////////////////////////////////
84
88
  export function downmixToMonoAndNormalize(rawAudio: RawAudio, targetPeakDecibels = -3) {
85
- return normalizeAudioLevel(downmixToMono(rawAudio), targetPeakDecibels)
89
+ const downmixedAudio = downmixToMono(rawAudio)
90
+
91
+ normalizeAudioLevelInPlace(downmixedAudio, targetPeakDecibels)
92
+
93
+ return downmixedAudio
94
+ }
95
+
96
+ export function attenuateIfClippingInPlace(rawAudio: RawAudio, clippingThreshold = -0.1) {
97
+ normalizeAudioLevelInPlace(rawAudio, clippingThreshold, 0)
86
98
  }
87
99
 
88
- export function attenuateIfClipping(rawAudio: RawAudio) {
89
- return normalizeAudioLevel(rawAudio, -0.1, 0)
100
+ export function normalizeAudioLevel(rawAudio: RawAudio, targetPeakDecibels = -3, maxGainIncreaseDecibels = 30) {
101
+ const clonedRawAudio = cloneRawAudio(rawAudio)
102
+
103
+ normalizeAudioLevelInPlace(clonedRawAudio, targetPeakDecibels, maxGainIncreaseDecibels)
104
+
105
+ return clonedRawAudio
90
106
  }
91
107
 
92
- export function normalizeAudioLevel(rawAudio: RawAudio, targetPeakDecibels = -3, maxGainIncreaseDecibels = 30): RawAudio {
93
- //rawAudio = correctDCBias(rawAudio)
108
+ export function normalizeAudioLevelInPlace(rawAudio: RawAudio, targetPeakDecibels = -3, maxGainIncreaseDecibels = 30){
109
+ //correctDCBiasInPlace(rawAudio)
94
110
 
95
111
  const targetPeakAmplitude = decibelsToGainFactor(targetPeakDecibels)
96
112
  const maxGainFactor = decibelsToGainFactor(maxGainIncreaseDecibels)
@@ -99,12 +115,10 @@ export function normalizeAudioLevel(rawAudio: RawAudio, targetPeakDecibels = -3,
99
115
 
100
116
  const gainFactor = Math.min(targetPeakAmplitude / peakAmplitude, maxGainFactor)
101
117
 
102
- return applyGainFactor(rawAudio, gainFactor)
118
+ applyGainFactorInPlace(rawAudio, gainFactor)
103
119
  }
104
120
 
105
- export function correctDCBias(rawAudio: RawAudio): RawAudio {
106
- const outputAudioChannels: Float32Array[] = []
107
-
121
+ export function correctDCBiasInPlace(rawAudio: RawAudio) {
108
122
  for (const channelSamples of rawAudio.audioChannels) {
109
123
  const sampleCount = channelSamples.length
110
124
 
@@ -116,38 +130,36 @@ export function correctDCBias(rawAudio: RawAudio): RawAudio {
116
130
 
117
131
  const sampleAverage = sampleSum / sampleCount
118
132
 
119
- const outputChannelSamples = new Float32Array(sampleCount)
120
-
121
133
  for (let i = 0; i < sampleCount; i++) {
122
- outputChannelSamples[i] = channelSamples[i] - sampleAverage
134
+ channelSamples[i] -= sampleAverage
123
135
  }
124
-
125
- outputAudioChannels.push(outputChannelSamples)
126
136
  }
137
+ }
127
138
 
128
- return { audioChannels: outputAudioChannels, sampleRate: rawAudio.sampleRate } as RawAudio
139
+ export function applyGainDecibels(rawAudio: RawAudio, gainDecibels: number) {
140
+ const clonedRawAudio = cloneRawAudio(rawAudio)
141
+
142
+ applyGainDecibelsInPlace(clonedRawAudio, gainDecibels)
143
+
144
+ return clonedRawAudio
129
145
  }
130
146
 
131
- export function applyGainDecibels(rawAudio: RawAudio, gainDecibels: number): RawAudio {
132
- return applyGainFactor(rawAudio, decibelsToGainFactor(gainDecibels))
147
+ export function applyGainDecibelsInPlace(rawAudio: RawAudio, gainDecibels: number) {
148
+ applyGainFactorInPlace(rawAudio, decibelsToGainFactor(gainDecibels))
133
149
  }
134
150
 
135
- export function applyGainFactor(rawAudio: RawAudio, gainFactor: number): RawAudio {
136
- const outputAudioChannels: Float32Array[] = []
151
+ export function applyGainFactorInPlace(rawAudio: RawAudio, gainFactor: number) {
152
+ if (gainFactor === 1.0) {
153
+ return
154
+ }
137
155
 
138
156
  for (const channelSamples of rawAudio.audioChannels) {
139
157
  const sampleCount = channelSamples.length
140
158
 
141
- const outputChannelSamples = new Float32Array(sampleCount)
142
-
143
159
  for (let i = 0; i < sampleCount; i++) {
144
- outputChannelSamples[i] = channelSamples[i] * gainFactor
160
+ channelSamples[i] *= gainFactor
145
161
  }
146
-
147
- outputAudioChannels.push(outputChannelSamples)
148
162
  }
149
-
150
- return { audioChannels: outputAudioChannels, sampleRate: rawAudio.sampleRate } as RawAudio
151
163
  }
152
164
 
153
165
  export function downmixToMono(rawAudio: RawAudio): RawAudio {
@@ -183,8 +195,14 @@ export function getSamplePeakAmplitude(audioChannels: Float32Array[]) {
183
195
  let maxAmplitude = 0.00001
184
196
 
185
197
  for (const channelSamples of audioChannels) {
186
- for (const sample of channelSamples) {
187
- maxAmplitude = Math.max(maxAmplitude, Math.abs(sample))
198
+ const sampleCount = channelSamples.length
199
+
200
+ for (let i = 0; i < sampleCount; i++) {
201
+ const sampleAbsValue = Math.abs(channelSamples[i])
202
+
203
+ if (sampleAbsValue > maxAmplitude) {
204
+ maxAmplitude = sampleAbsValue
205
+ }
188
206
  }
189
207
  }
190
208
 
@@ -257,7 +275,7 @@ export function sliceAudioChannels(audioChannels: Float32Array[], startSampleInd
257
275
  }
258
276
 
259
277
  export function concatAudioSegments(audioSegments: Float32Array[][]) {
260
- if (audioSegments.length == 0) {
278
+ if (audioSegments.length === 0) {
261
279
  return []
262
280
  }
263
281
 
@@ -317,7 +335,9 @@ export function fadeAudioInOut(rawAudio: RawAudio, fadeTime: number): RawAudio {
317
335
  const outAudioChannels = rawAudio.audioChannels.map(channel => channel.slice())
318
336
 
319
337
  for (const channel of outAudioChannels) {
320
- if (channel.length < fadeSampleCount * 2) {
338
+ const sampleCount = channel.length
339
+
340
+ if (sampleCount < fadeSampleCount * 2) {
321
341
  continue
322
342
  }
323
343
 
@@ -331,7 +351,7 @@ export function fadeAudioInOut(rawAudio: RawAudio, fadeTime: number): RawAudio {
331
351
 
332
352
  factor = 1.0
333
353
 
334
- for (let i = channel.length - fadeSampleCount; i < channel.length; i++) {
354
+ for (let i = sampleCount - fadeSampleCount; i < sampleCount; i++) {
335
355
  channel[i] *= factor
336
356
 
337
357
  factor *= gainReductionPerFrameFactor
package/src/dsp/FFT.ts CHANGED
@@ -2,8 +2,6 @@ import { ComplexNumber } from '../math/VectorMath.js'
2
2
  import { concatFloat32Arrays } from '../utilities/Utilities.js'
3
3
  import { WasmMemoryManager } from '../utilities/WasmMemoryManager.js'
4
4
 
5
- let kissFFTInstance: any
6
-
7
5
  // Compute short-term Fourier transform (real-valued)
8
6
  export async function stftr(samples: Float32Array, fftOrder: number, windowSize: number, hopSize: number, windowType: WindowType) {
9
7
  const frames: Float32Array[] = []
@@ -36,15 +34,18 @@ export async function* stftrGenerator(samples: Float32Array, fftOrder: number, w
36
34
 
37
35
  const windowWeights = getWindowWeights(windowType, windowSize)
38
36
 
39
- const m = await getKissFFTInstance()
40
- const wasmMemory = new WasmMemoryManager(m)
37
+ const m = await getPFFFTInstance(isPffftSimdSupportedForFFTOrder(fftOrder))
38
+ const wasmMemory = new WasmMemoryManager(m, {
39
+ wasmAlloc: m._pffft_aligned_malloc,
40
+ wasmFree: m._pffft_aligned_free
41
+ })
41
42
 
42
- const statePtr = m._kiss_fftr_alloc(fftOrder, 0, 0, 0)
43
- wasmMemory.wrapPointer(statePtr)
43
+ const statePtr = m._pffft_new_setup(fftOrder, 0)
44
44
 
45
45
  const sampleCount = samples.length
46
46
  const frameBufferRef = wasmMemory.allocFloat32Array(fftOrder)
47
47
  const binsBufferRef = wasmMemory.allocFloat32Array(fftOrder * 2)
48
+ const workBufferRef = wasmMemory.allocFloat32Array(fftOrder * 2)
48
49
 
49
50
  for (let offset = 0; offset < sampleCount; offset += hopSize) {
50
51
  const windowSamples = samples.subarray(offset, offset + windowSize)
@@ -58,13 +59,15 @@ export async function* stftrGenerator(samples: Float32Array, fftOrder: number, w
58
59
 
59
60
  binsBufferRef.clear()
60
61
 
61
- m._kiss_fftr(statePtr, frameBufferRef.address, binsBufferRef.address)
62
+ m._pffft_transform_ordered(statePtr, frameBufferRef.address, binsBufferRef.address, workBufferRef.address, 0)
62
63
 
63
64
  const bins = binsBufferRef.view.slice(0, fftOrder + 2)
64
65
 
65
66
  yield bins
66
67
  }
67
68
 
69
+ m._pffft_destroy_setup(statePtr)
70
+
68
71
  wasmMemory.freeAll()
69
72
  }
70
73
 
@@ -96,14 +99,18 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
96
99
 
97
100
  const outSamples = new Float32Array(outSampleCount)
98
101
 
99
- const m = await getKissFFTInstance()
100
- const wasmMemory = new WasmMemoryManager(m)
102
+ const m = await getPFFFTInstance(isPffftSimdSupportedForFFTOrder(fftOrder))
101
103
 
102
- const statePtr = m._kiss_fftr_alloc(fftOrder, 1, 0, 0)
103
- wasmMemory.wrapPointer(statePtr)
104
+ const wasmMemory = new WasmMemoryManager(m, {
105
+ wasmAlloc: m._pffft_aligned_malloc,
106
+ wasmFree: m._pffft_aligned_free
107
+ })
108
+
109
+ const statePtr = m._pffft_new_setup(fftOrder, 0)
104
110
 
105
111
  const frameBufferRef = wasmMemory.allocFloat32Array(fftOrder)
106
112
  const binsRef = wasmMemory.allocFloat32Array(fftOrder * 2)
113
+ const workBufferRef = wasmMemory.allocFloat32Array(fftOrder * 2)
107
114
 
108
115
  const sumOfSquaredWeightsForSample = new Float32Array(outSampleCount)
109
116
 
@@ -114,7 +121,7 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
114
121
 
115
122
  frameBufferRef.clear()
116
123
 
117
- m._kiss_fftri(statePtr, binsRef.address, frameBufferRef.address)
124
+ m._pffft_transform_ordered(statePtr, binsRef.address, frameBufferRef.address, workBufferRef.address, 1)
118
125
 
119
126
  const frameSamples = frameBufferRef.view
120
127
 
@@ -132,6 +139,7 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
132
139
  }
133
140
  }
134
141
 
142
+ m._pffft_destroy_setup(statePtr)
135
143
  wasmMemory.freeAll()
136
144
 
137
145
  // Divide each output sample by the sum of squared weights
@@ -247,15 +255,33 @@ export function getWindowWeights(windowType: WindowType, windowSize: number) {
247
255
  return weights
248
256
  }
249
257
 
250
- // Get KISS FFT instance (initialize new if not exists)
251
- export async function getKissFFTInstance() {
252
- if (!kissFFTInstance) {
253
- const { default: initializer } = await import('@echogarden/kissfft-wasm')
258
+ export function isPffftSimdSupportedForFFTOrder(fftOrder: number) {
259
+ return fftOrder % 32 === 0
260
+ }
254
261
 
255
- kissFFTInstance = await initializer()
256
- }
262
+ let pffftNonSimdInstance: any
263
+ let pffftSimdInstance: any
264
+
265
+ export async function getPFFFTInstance(enableSimd: boolean) {
266
+ return enableSimd ? getSimdPFFFTInstance() : getNonSimdPFFFTInstance()
267
+ }
268
+
269
+ // Get non-SIMD PFFFT instance (initialize new if not exists)
270
+ async function getNonSimdPFFFTInstance() {
271
+ const { default: initializer } = await import('@echogarden/pffft-wasm')
272
+
273
+ pffftNonSimdInstance = await initializer()
274
+
275
+ return pffftNonSimdInstance
276
+ }
277
+
278
+ // Get SIMD PFFFT instance (initialize new if not exists)
279
+ async function getSimdPFFFTInstance() {
280
+ const { default: initializer } = await import('@echogarden/pffft-wasm/simd')
281
+
282
+ pffftSimdInstance = await initializer()
257
283
 
258
- return kissFFTInstance
284
+ return pffftSimdInstance
259
285
  }
260
286
 
261
287
  export type WindowType = 'hann' | 'hamming' | 'povey'
@@ -459,7 +459,7 @@ export class Whisper {
459
459
  // Find alignment path
460
460
  let alignmentHeads: number[] | undefined = undefined
461
461
 
462
- if (this.modelName.startsWith('small') || this.modelName.startsWith('medium') || this.modelName.startsWith('large')) {
462
+ if (options.useOptimizedAlignmentHeads) {
463
463
  alignmentHeads = this.alignmentHeadIndexes
464
464
  }
465
465
 
@@ -497,7 +497,7 @@ export class Whisper {
497
497
 
498
498
  logger.end()
499
499
 
500
- return { transcript, timeline }
500
+ return { transcript, timeline, allDecodedTokens }
501
501
  }
502
502
 
503
503
  async align(rawAudio: RawAudio, transcript: string, sourceLanguage: string, task: 'transcribe' | 'translate', whisperAlignmentOptions: WhisperAlignmentOptions) {
@@ -523,8 +523,10 @@ export class Whisper {
523
523
  }
524
524
  } else {
525
525
  let words = await splitToWords(transcript, targetLanguage)
526
+
526
527
  words = words.map(word => word.trim())
527
528
  words = words.filter(word => isWord(word))
529
+
528
530
  simplifiedTranscript = words.join(' ')
529
531
  }
530
532
 
@@ -566,17 +568,64 @@ export class Whisper {
566
568
  topCandidateCount: 1,
567
569
  punctuationThreshold: Infinity,
568
570
  autoPromptParts: false,
569
- maxTokensPerPart: Infinity,
571
+ maxTokensPerPart: whisperAlignmentOptions.maxTokensPerPart!,
570
572
  suppressRepetition: false,
571
573
  repetitionThreshold: Infinity,
572
574
  decodeTimestampTokens: true,
573
- endTokenThreshold: whisperAlignmentOptions!.endTokenThreshold!,
575
+ endTokenThreshold: whisperAlignmentOptions.endTokenThreshold!,
574
576
  includeEndTokenInCandidates: false,
577
+ useOptimizedAlignmentHeads: whisperAlignmentOptions.useOptimizedAlignmentHeads!,
578
+ encoderProvider: whisperAlignmentOptions.encoderProvider!,
579
+ decoderProvider: whisperAlignmentOptions.decoderProvider!,
575
580
  seed: undefined,
576
581
  }
577
582
 
578
583
  // Recognize
579
- const { timeline } = await this.recognize(rawAudio, task, sourceLanguage, options, logitFilter)
584
+ const { timeline, allDecodedTokens } = await this.recognize(rawAudio, task, sourceLanguage, options, logitFilter)
585
+
586
+ {
587
+ // If not all tokens were decoded, add the remaining ones to the timeline
588
+ const lastKnownWordStartTime = timeline.length > 0 ? timeline[timeline.length - 1].startTime : 0
589
+
590
+ const allDecodedTextTokens = allDecodedTokens.filter(token => this.isTextToken(token))
591
+
592
+ while (allDecodedTextTokens.length < simplifiedTranscriptTokens.length) {
593
+ const token = simplifiedTranscriptTokens[allDecodedTextTokens.length]
594
+ const tokenText = this.tokenToText(token)
595
+
596
+ allDecodedTextTokens.push(token)
597
+
598
+ const newTokenEntry: TimelineEntry = {
599
+ type: 'token',
600
+ text: tokenText,
601
+
602
+ startTime: lastKnownWordStartTime,
603
+ endTime: lastKnownWordStartTime,
604
+
605
+ id: token,
606
+ confidence: 0,
607
+ }
608
+
609
+ if (tokenText.startsWith(' ') || timeline.length === 0) {
610
+ timeline.push({
611
+ type: 'word',
612
+ text: tokenText.trim(),
613
+
614
+ startTime: lastKnownWordStartTime,
615
+ endTime: lastKnownWordStartTime,
616
+
617
+ timeline: [newTokenEntry],
618
+
619
+ confidence: 0,
620
+ })
621
+ } else {
622
+ const lastWordEntry = timeline[timeline.length - 1]
623
+
624
+ lastWordEntry.timeline!.push(newTokenEntry)
625
+ lastWordEntry.text += tokenText
626
+ }
627
+ }
628
+ }
580
629
 
581
630
  return timeline
582
631
  }
@@ -1103,7 +1152,8 @@ export class Whisper {
1103
1152
  const sampleRate = rawAudio.sampleRate
1104
1153
 
1105
1154
  const fftOrder = 400
1106
- const hopLength = 160
1155
+ const fftWindowSize = 400
1156
+ const fftHopLength = 160
1107
1157
 
1108
1158
  const filterbankCount = this.filterbankCount
1109
1159
  const filterbanks = this.filterbanks
@@ -1128,7 +1178,7 @@ export class Whisper {
1128
1178
 
1129
1179
  const rawAudioPart: RawAudio = { audioChannels: [paddedAudioSamples], sampleRate }
1130
1180
 
1131
- const { melSpectogram } = await computeMelSpectogramUsingFilterbanks(rawAudioPart, fftOrder, fftOrder, hopLength, filterbanks)
1181
+ const { melSpectogram } = await computeMelSpectogramUsingFilterbanks(rawAudioPart, fftOrder, fftWindowSize, fftHopLength, filterbanks)
1132
1182
 
1133
1183
  await logger.startAsync('Normalize mel spectogram')
1134
1184
 
@@ -2440,6 +2490,7 @@ export interface WhisperOptions {
2440
2490
  decodeTimestampTokens?: boolean
2441
2491
  endTokenThreshold?: number
2442
2492
  includeEndTokenInCandidates?: boolean
2493
+ useOptimizedAlignmentHeads?: boolean,
2443
2494
  encoderProvider?: OnnxExecutionProvider
2444
2495
  decoderProvider?: OnnxExecutionProvider
2445
2496
  seed?: number
@@ -2458,6 +2509,7 @@ export const defaultWhisperOptions: WhisperOptions = {
2458
2509
  decodeTimestampTokens: true,
2459
2510
  endTokenThreshold: 0.9,
2460
2511
  includeEndTokenInCandidates: true,
2512
+ useOptimizedAlignmentHeads: true,
2461
2513
  encoderProvider: undefined,
2462
2514
  decoderProvider: undefined,
2463
2515
  seed: undefined,
@@ -2467,6 +2519,9 @@ export const defaultWhisperOptions: WhisperOptions = {
2467
2519
  export interface WhisperAlignmentOptions {
2468
2520
  model?: WhisperModelName
2469
2521
  endTokenThreshold?: number
2522
+ maxTokensPerPart?: number
2523
+ useOptimizedAlignmentHeads?: boolean
2524
+
2470
2525
  encoderProvider?: OnnxExecutionProvider
2471
2526
  decoderProvider?: OnnxExecutionProvider
2472
2527
  }
@@ -2474,8 +2529,11 @@ export interface WhisperAlignmentOptions {
2474
2529
  export const defaultWhisperAlignmentOptions: WhisperAlignmentOptions = {
2475
2530
  model: undefined,
2476
2531
  endTokenThreshold: 0.9,
2532
+ maxTokensPerPart: 250,
2533
+ useOptimizedAlignmentHeads: true,
2534
+
2477
2535
  encoderProvider: undefined,
2478
- decoderProvider: undefined
2536
+ decoderProvider: undefined,
2479
2537
  }
2480
2538
 
2481
2539
  // Language detection options