echogarden 1.6.2 → 1.8.0

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 (115) hide show
  1. package/data/schemas/options.json +50 -12
  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 -5
  9. package/dist/api/SourceSeparation.js +14 -13
  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 +8 -6
  16. package/dist/api/Synthesis.js.map +1 -1
  17. package/dist/api/TextTranslation.d.ts +3 -1
  18. package/dist/api/TextTranslation.js +12 -6
  19. package/dist/api/TextTranslation.js.map +1 -1
  20. package/dist/api/TranslationAlignment.js +2 -2
  21. package/dist/api/TranslationAlignment.js.map +1 -1
  22. package/dist/audio/AudioUtilities.d.ts +5 -3
  23. package/dist/audio/AudioUtilities.js +39 -25
  24. package/dist/audio/AudioUtilities.js.map +1 -1
  25. package/dist/build-tools/MakeTarballsForInstalledPackages.d.ts +1 -0
  26. package/dist/build-tools/MakeTarballsForInstalledPackages.js +20 -0
  27. package/dist/build-tools/MakeTarballsForInstalledPackages.js.map +1 -0
  28. package/dist/cli/CLI.js +4 -3
  29. package/dist/cli/CLI.js.map +1 -1
  30. package/dist/dsp/FFT.d.ts +3 -3
  31. package/dist/dsp/FFT.js +55 -34
  32. package/dist/dsp/FFT.js.map +1 -1
  33. package/dist/nlp/IPA.d.ts +3 -3
  34. package/dist/nlp/Segmentation.js +2 -1
  35. package/dist/nlp/Segmentation.js.map +1 -1
  36. package/dist/nlp/TextNormalizer.d.ts +2 -0
  37. package/dist/nlp/TextNormalizer.js +51 -0
  38. package/dist/nlp/TextNormalizer.js.map +1 -1
  39. package/dist/recognition/WhisperSTT.d.ts +5 -0
  40. package/dist/recognition/WhisperSTT.js +19 -11
  41. package/dist/recognition/WhisperSTT.js.map +1 -1
  42. package/dist/source-separation/MDXNetSourceSeparation.d.ts +26 -3
  43. package/dist/source-separation/MDXNetSourceSeparation.js +159 -79
  44. package/dist/source-separation/MDXNetSourceSeparation.js.map +1 -1
  45. package/dist/subtitles/Subtitles.js +2 -1
  46. package/dist/subtitles/Subtitles.js.map +1 -1
  47. package/dist/synthesis/EspeakTTS.js +5 -5
  48. package/dist/synthesis/EspeakTTS.js.map +1 -1
  49. package/dist/tests/Test.js +0 -1
  50. package/dist/tests/Test.js.map +1 -1
  51. package/dist/text-translation/GoogleTranslateTextTranslation.d.ts +9 -3
  52. package/dist/text-translation/GoogleTranslateTextTranslation.js +59 -29
  53. package/dist/text-translation/GoogleTranslateTextTranslation.js.map +1 -1
  54. package/dist/utilities/OnnxUtilities.d.ts +1 -0
  55. package/dist/utilities/OnnxUtilities.js +5 -5
  56. package/dist/utilities/OnnxUtilities.js.map +1 -1
  57. package/dist/utilities/PackageManager.js +6 -6
  58. package/dist/utilities/PackageManager.js.map +1 -1
  59. package/dist/utilities/StringUtilities.d.ts +28 -0
  60. package/dist/utilities/StringUtilities.js +150 -0
  61. package/dist/utilities/StringUtilities.js.map +1 -0
  62. package/dist/utilities/TarballMaker.d.ts +3 -4
  63. package/dist/utilities/TarballMaker.js +6 -19
  64. package/dist/utilities/TarballMaker.js.map +1 -1
  65. package/dist/utilities/Timeline.js +2 -1
  66. package/dist/utilities/Timeline.js.map +1 -1
  67. package/dist/utilities/Utilities.d.ts +1 -33
  68. package/dist/utilities/Utilities.js +3 -176
  69. package/dist/utilities/Utilities.js.map +1 -1
  70. package/dist/utilities/WasmMemoryManager.d.ts +9 -1
  71. package/dist/utilities/WasmMemoryManager.js +24 -3
  72. package/dist/utilities/WasmMemoryManager.js.map +1 -1
  73. package/dist/utilities/WebReader.js +1 -1
  74. package/dist/utilities/WebReader.js.map +1 -1
  75. package/dist/utilities/WikipediaReader.js +1 -1
  76. package/dist/utilities/WikipediaReader.js.map +1 -1
  77. package/dist/utilities/WindowedList.d.ts +10 -0
  78. package/dist/utilities/WindowedList.js +36 -0
  79. package/dist/utilities/WindowedList.js.map +1 -0
  80. package/docs/Engines.md +1 -1
  81. package/docs/Options.md +16 -10
  82. package/package.json +11 -11
  83. package/src/alignment/SpeechAlignment.ts +1 -1
  84. package/src/api/Alignment.ts +2 -2
  85. package/src/api/Denoising.ts +5 -4
  86. package/src/api/Recognition.ts +3 -3
  87. package/src/api/SourceSeparation.ts +20 -19
  88. package/src/api/SpeechLanguageDetection.ts +2 -2
  89. package/src/api/SpeechTranslation.ts +2 -2
  90. package/src/api/Synthesis.ts +8 -6
  91. package/src/api/TextTranslation.ts +17 -9
  92. package/src/api/TranslationAlignment.ts +2 -2
  93. package/src/audio/AudioUtilities.ts +53 -33
  94. package/src/build-tools/MakeTarballsForInstalledPackages.ts +25 -0
  95. package/src/cli/CLI.ts +4 -3
  96. package/src/dsp/FFT.ts +65 -42
  97. package/src/nlp/Segmentation.ts +2 -1
  98. package/src/nlp/TextNormalizer.ts +60 -0
  99. package/src/recognition/WhisperSTT.ts +25 -12
  100. package/src/source-separation/MDXNetSourceSeparation.ts +218 -88
  101. package/src/subtitles/Subtitles.ts +2 -1
  102. package/src/synthesis/EspeakTTS.ts +5 -5
  103. package/src/tests/Test.ts +0 -4
  104. package/src/text-translation/GoogleTranslateTextTranslation.ts +91 -30
  105. package/src/typings/Fillers.d.ts +2 -0
  106. package/src/utilities/OnnxUtilities.ts +7 -8
  107. package/src/utilities/PackageManager.ts +6 -7
  108. package/src/utilities/StringUtilities.ts +203 -0
  109. package/src/utilities/TarballMaker.ts +6 -23
  110. package/src/utilities/Timeline.ts +2 -1
  111. package/src/utilities/Utilities.ts +4 -237
  112. package/src/utilities/WasmMemoryManager.ts +38 -4
  113. package/src/utilities/WebReader.ts +1 -1
  114. package/src/utilities/WikipediaReader.ts +1 -1
  115. package/src/utilities/WindowedList.ts +45 -0
@@ -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
@@ -0,0 +1,25 @@
1
+ import path from 'path'
2
+ import { existsSync, readdir } from '../utilities/FileSystem.js'
3
+ import { ensureAndGetPackagesDir } from '../utilities/PackageManager.js'
4
+ import { createNamedTarball } from '../utilities/TarballMaker.js'
5
+ import { setupProgramTerminationListeners } from '../utilities/Utilities.js'
6
+
7
+ async function makeTarballsForInstalledPackages(tarballDir: string, skipIfExists = false) {
8
+ const packagesDir = await ensureAndGetPackagesDir()
9
+ const packageList = await readdir(packagesDir)
10
+
11
+ for (const packageName of packageList) {
12
+ if (skipIfExists && existsSync(path.join(tarballDir, `${packageName}.tar.gz`))) {
13
+ continue
14
+ }
15
+
16
+ const packagePath = path.join(packagesDir, packageName)
17
+ await createNamedTarball(packagePath, packageName, tarballDir)
18
+ }
19
+ }
20
+
21
+ setupProgramTerminationListeners()
22
+
23
+ await makeTarballsForInstalledPackages('../resources/tarballs', true)
24
+
25
+ process.exit(0)
package/src/cli/CLI.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as API from '../api/API.js'
2
2
  import { parseCLIArguments } from './CLIParser.js'
3
- import { convertHtmlToText, formatIntegerWithLeadingZeros, formatListWithQuotedElements, getWithDefault, logToStderr, setupUnhandledExceptionListeners, splitFilenameOnExtendedExtension, stringifyAndFormatJson } from '../utilities/Utilities.js'
3
+ import { getWithDefault, logToStderr, setupUnhandledExceptionListeners, splitFilenameOnExtendedExtension, stringifyAndFormatJson } from '../utilities/Utilities.js'
4
4
  import { getOptionTypeFromSchema, SchemaTypeDefinition } from './CLIOptionsSchema.js'
5
5
  import { ParsedConfigFile, parseConfigFile, parseJSONConfigFile } from './CLIConfigFile.js'
6
6
 
@@ -26,6 +26,7 @@ import { OpenPromise } from '../utilities/OpenPromise.js'
26
26
  import JSON5 from 'json5'
27
27
  import { getLowercaseFileExtension, resolveToModuleRootDir } from '../utilities/PathUtilities.js'
28
28
  import { CLIOptions, CLIOptionsKeys } from './CLIOptions.js'
29
+ import { convertHtmlToText, formatIntegerWithLeadingZeros, formatListWithQuotedElements } from '../utilities/StringUtilities.js'
29
30
 
30
31
  //const log = logToStderr
31
32
 
@@ -1049,7 +1050,7 @@ export async function translateText(operationData: CLIOperationData) {
1049
1050
  } else if (inputFileExtension == 'srt' || inputFileExtension == 'vtt') {
1050
1051
  inputText = subtitlesToText(inputFileContent)
1051
1052
  } else {
1052
- throw new Error(`align only supports reference files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`)
1053
+ throw new Error(`translate-text only supports input files with extensions 'txt', 'html', 'htm', 'srt' or 'vtt'`)
1053
1054
  }
1054
1055
 
1055
1056
  const options = await optionsLookupToTypedObject(operationOptionsLookup, 'TextTranslationOptions')
@@ -1066,7 +1067,7 @@ export async function translateText(operationData: CLIOperationData) {
1066
1067
 
1067
1068
  sourceLanguage,
1068
1069
  targetLanguage,
1069
- } = await API.translateText(inputFileContent, options)
1070
+ } = await API.translateText(inputText, options)
1070
1071
 
1071
1072
  if (outputFilenames.length > 0) {
1072
1073
  logger.start('\nWrite output files')
package/src/dsp/FFT.ts CHANGED
@@ -1,13 +1,22 @@
1
- import { RawAudio } from '../audio/AudioUtilities.js'
2
- import { ComplexNumber, sumVector } from '../math/VectorMath.js'
1
+ import { ComplexNumber } from '../math/VectorMath.js'
3
2
  import { concatFloat32Arrays } from '../utilities/Utilities.js'
4
3
  import { WasmMemoryManager } from '../utilities/WasmMemoryManager.js'
5
4
 
6
- let kissFFTInstance: any
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) {
7
+ const frames: Float32Array[] = []
8
+
9
+ for await (const frame of stftrGenerator(samples, fftOrder, windowSize, hopSize, windowType)) {
10
+ frames.push(frame)
11
+ }
12
+
13
+ return frames
14
+ }
15
+
16
+ // Incrementally generate short-term Fourier transform frames (real-valued)
17
+ export async function* stftrGenerator(samples: Float32Array, fftOrder: number, windowSize: number, hopSize: number, windowType: WindowType) {
9
18
  if (fftOrder % 2 != 0 || windowSize % 2 != 0) {
10
- throw new Error('FFT order and window size must multiples of 2')
19
+ throw new Error('FFT order and window size must be multiples of 2')
11
20
  }
12
21
 
13
22
  if (windowSize > fftOrder) {
@@ -25,17 +34,18 @@ export async function stftr(samples: Float32Array, fftOrder: number, windowSize:
25
34
 
26
35
  const windowWeights = getWindowWeights(windowType, windowSize)
27
36
 
28
- const m = await getKissFFTInstance()
29
- 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
+ })
30
42
 
31
- const statePtr = m._kiss_fftr_alloc(fftOrder, 0, 0, 0)
32
- wasmMemory.wrapPointer(statePtr)
43
+ const statePtr = m._pffft_new_setup(fftOrder, 0)
33
44
 
34
45
  const sampleCount = samples.length
35
46
  const frameBufferRef = wasmMemory.allocFloat32Array(fftOrder)
36
47
  const binsBufferRef = wasmMemory.allocFloat32Array(fftOrder * 2)
37
-
38
- const frames: Float32Array[] = []
48
+ const workBufferRef = wasmMemory.allocFloat32Array(fftOrder * 2)
39
49
 
40
50
  for (let offset = 0; offset < sampleCount; offset += hopSize) {
41
51
  const windowSamples = samples.subarray(offset, offset + windowSize)
@@ -49,17 +59,19 @@ export async function stftr(samples: Float32Array, fftOrder: number, windowSize:
49
59
 
50
60
  binsBufferRef.clear()
51
61
 
52
- m._kiss_fftr(statePtr, frameBufferRef.address, binsBufferRef.address)
62
+ m._pffft_transform_ordered(statePtr, frameBufferRef.address, binsBufferRef.address, workBufferRef.address, 0)
53
63
 
54
64
  const bins = binsBufferRef.view.slice(0, fftOrder + 2)
55
- frames.push(bins)
65
+
66
+ yield bins
56
67
  }
57
68
 
58
- wasmMemory.freeAll()
69
+ m._pffft_destroy_setup(statePtr)
59
70
 
60
- return frames
71
+ wasmMemory.freeAll()
61
72
  }
62
73
 
74
+ // Compute short-term inverse Fourier transform (real-valued)
63
75
  export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, windowSize: number, hopSize: number, windowType: WindowType, expectedOutputLength?: number) {
64
76
  if (fftOrder % 2 != 0 || windowSize % 2 != 0) {
65
77
  throw new Error('FFT order and window size must multiples of 2')
@@ -87,14 +99,18 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
87
99
 
88
100
  const outSamples = new Float32Array(outSampleCount)
89
101
 
90
- const m = await getKissFFTInstance()
91
- const wasmMemory = new WasmMemoryManager(m)
102
+ const m = await getPFFFTInstance(isPffftSimdSupportedForFFTOrder(fftOrder))
103
+
104
+ const wasmMemory = new WasmMemoryManager(m, {
105
+ wasmAlloc: m._pffft_aligned_malloc,
106
+ wasmFree: m._pffft_aligned_free
107
+ })
92
108
 
93
- const statePtr = m._kiss_fftr_alloc(fftOrder, 1, 0, 0)
94
- wasmMemory.wrapPointer(statePtr)
109
+ const statePtr = m._pffft_new_setup(fftOrder, 0)
95
110
 
96
111
  const frameBufferRef = wasmMemory.allocFloat32Array(fftOrder)
97
112
  const binsRef = wasmMemory.allocFloat32Array(fftOrder * 2)
113
+ const workBufferRef = wasmMemory.allocFloat32Array(fftOrder * 2)
98
114
 
99
115
  const sumOfSquaredWeightsForSample = new Float32Array(outSampleCount)
100
116
 
@@ -105,7 +121,7 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
105
121
 
106
122
  frameBufferRef.clear()
107
123
 
108
- m._kiss_fftri(statePtr, binsRef.address, frameBufferRef.address)
124
+ m._pffft_transform_ordered(statePtr, binsRef.address, frameBufferRef.address, workBufferRef.address, 1)
109
125
 
110
126
  const frameSamples = frameBufferRef.view
111
127
 
@@ -123,6 +139,7 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
123
139
  }
124
140
  }
125
141
 
142
+ m._pffft_destroy_setup(statePtr)
126
143
  wasmMemory.freeAll()
127
144
 
128
145
  // Divide each output sample by the sum of squared weights
@@ -135,6 +152,7 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
135
152
  return outSamplesTrimmed
136
153
  }
137
154
 
155
+ // Get bin frequency thresholds for a particular bin count and maximum frequency
138
156
  export function getBinFrequencies(binCount: number, maxFrequency: number) {
139
157
  const binFrequencies = new Float32Array(binCount)
140
158
  const frequencyStep = maxFrequency / (binCount - 1)
@@ -146,10 +164,12 @@ export function getBinFrequencies(binCount: number, maxFrequency: number) {
146
164
  return binFrequencies
147
165
  }
148
166
 
167
+ // Convert an array of raw FFT frames to a power spectrum
149
168
  export function fftFramesToPowerSpectogram(fftFrames: Float32Array[]) {
150
169
  return fftFrames.map(fftFrame => fftFrameToPowerSpectrum(fftFrame))
151
170
  }
152
171
 
172
+ // Convert a raw FFT frame to a power spectrum
153
173
  export function fftFrameToPowerSpectrum(fftFrame: Float32Array) {
154
174
  const powerSpectrum = new Float32Array(fftFrame.length / 2)
155
175
 
@@ -165,6 +185,7 @@ export function fftFrameToPowerSpectrum(fftFrame: Float32Array) {
165
185
  return powerSpectrum
166
186
  }
167
187
 
188
+ // Convert raw FFT frames to an array of complex numbers
168
189
  export function binBufferToComplex(bins: Float32Array, extendAndMirror = false) {
169
190
  const complexBins: ComplexNumber[] = []
170
191
 
@@ -191,6 +212,7 @@ export function binBufferToComplex(bins: Float32Array, extendAndMirror = false)
191
212
  return complexBins
192
213
  }
193
214
 
215
+ // Convert an array of complex numbers to raw FFT frames
194
216
  export function complexToBinBuffer(complexBins: ComplexNumber[]) {
195
217
  const binBuffer = new Float32Array(complexBins.length * 2)
196
218
 
@@ -204,16 +226,7 @@ export function complexToBinBuffer(complexBins: ComplexNumber[]) {
204
226
  return binBuffer
205
227
  }
206
228
 
207
- export async function getKissFFTInstance() {
208
- if (!kissFFTInstance) {
209
- const { default: initializer } = await import('@echogarden/kissfft-wasm')
210
-
211
- kissFFTInstance = await initializer()
212
- }
213
-
214
- return kissFFTInstance
215
- }
216
-
229
+ // Get window weights for a particular window function
217
230
  export function getWindowWeights(windowType: WindowType, windowSize: number) {
218
231
  const weights = new Float32Array(windowSize)
219
232
 
@@ -242,23 +255,33 @@ export function getWindowWeights(windowType: WindowType, windowSize: number) {
242
255
  return weights
243
256
  }
244
257
 
245
- export async function testFFT1(rawAudio: RawAudio) {
246
- const { resampleAudioSpeex } = await import('./SpeexResampler.js')
258
+ export function isPffftSimdSupportedForFFTOrder(fftOrder: number) {
259
+ return fftOrder % 32 === 0
260
+ }
261
+
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')
247
272
 
248
- const samples = (await resampleAudioSpeex(rawAudio, 16000)).audioChannels[0]
273
+ pffftNonSimdInstance = await initializer()
249
274
 
250
- const fftOrder = 512
251
- const windowSize = 320
252
- const hopLength = windowSize / 2
253
- const windowType: WindowType = 'hann'
275
+ return pffftNonSimdInstance
276
+ }
254
277
 
255
- const bins = await stftr(samples, fftOrder, windowSize, hopLength, windowType)
256
- const normalizedBins = bins.map(bin => bin.map(x => x / fftOrder))
257
- const recoveredSamples = await stiftr(normalizedBins, fftOrder, windowSize, hopLength, windowType, samples.length)
278
+ // Get SIMD PFFFT instance (initialize new if not exists)
279
+ async function getSimdPFFFTInstance() {
280
+ const { default: initializer } = await import('@echogarden/pffft-wasm/simd')
258
281
 
259
- const recoveredRawAudio: RawAudio = { audioChannels: [recoveredSamples], sampleRate: 16000 }
282
+ pffftSimdInstance = await initializer()
260
283
 
261
- return recoveredRawAudio
284
+ return pffftSimdInstance
262
285
  }
263
286
 
264
287
  export type WindowType = 'hann' | 'hamming' | 'povey'
@@ -1,10 +1,11 @@
1
1
  import * as CldrSegmentation from 'cldr-segmentation'
2
2
  import { splitChineseTextToWords_Jieba } from './ChineseSegmentation.js'
3
3
 
4
- import { sumArray, includesAnyOf, indexOfAnyOf, logToStderr } from '../utilities/Utilities.js'
4
+ import { sumArray, logToStderr } from '../utilities/Utilities.js'
5
5
  import { getShortLanguageCode } from '../utilities/Locale.js'
6
6
  import { splitJapaneseTextToWords_Kuromoji } from './JapaneseSegmentation.js'
7
7
  import { ParagraphBreakType, WhitespaceProcessing } from '../api/Common.js'
8
+ import { includesAnyOf, indexOfAnyOf } from '../utilities/StringUtilities.js'
8
9
 
9
10
  const log = logToStderr
10
11
 
@@ -1,4 +1,5 @@
1
1
  import { getShortLanguageCode } from '../utilities/Locale.js'
2
+ import { substituteCharactersUsingLookup } from '../utilities/StringUtilities.js'
2
3
 
3
4
  export function getNormalizedFragmentsForSpeech(words: string[], language: string) {
4
5
  language = getShortLanguageCode(language)
@@ -158,3 +159,62 @@ export function normalizeFourDigitDecadeString(decadeString: string) {
158
159
 
159
160
  return normalizedString
160
161
  }
162
+
163
+ export function simplifyPunctuationCharacters(text: string) {
164
+ return substituteCharactersUsingLookup(text, punctuationSubstitutionLookup)
165
+ }
166
+
167
+ export const punctuationSubstitutionLookup: Record<string, string> = {
168
+ '“': `"`,
169
+ '”': `"`,
170
+ '‟': `"`,
171
+ 'ˮ': `"`,
172
+ '„': `"`,
173
+ '‹': `"`,
174
+ '›': `"`,
175
+ '❮': `"`,
176
+ '❯': '"',
177
+ '«': `"`,
178
+ '»': `"`,
179
+ '״': `"`,
180
+ '❝': `"`,
181
+ '❞': `"`,
182
+ '🙶': `"`,
183
+ '🙷': `"`,
184
+ '⹂': `"`,
185
+ '〝': `"`,
186
+ '〞': `"`,
187
+ '〟': `"`,
188
+ '"': `"`,
189
+ '❠': `"`,
190
+ '🙸': `"`,
191
+
192
+ 'ߵ': `'`,
193
+ 'ߴ': `'`,
194
+ '’': `'`,
195
+ '‘': `'`,
196
+ 'ʹ': `'`,
197
+ 'ʼ': `'`,
198
+ ''': `'`,
199
+ 'ʻ': `'`,
200
+ '՚': `'`,
201
+ '՛': `'`,
202
+ '❛': `'`,
203
+ '❜': `'`,
204
+ '❟': `'`,
205
+
206
+ ',': `,`,
207
+ '、': `,`,
208
+
209
+ ':': `:`,
210
+
211
+ ';': `;`,
212
+
213
+ '。': `.`,
214
+
215
+ '?': `?`,
216
+ '؟': `?`,
217
+
218
+ '!': `!`,
219
+ '¡': `!`,
220
+ }
@@ -2,7 +2,7 @@ import type * as Onnx from 'onnxruntime-node'
2
2
 
3
3
  import { Logger } from '../utilities/Logger.js'
4
4
  import { computeMelSpectogramUsingFilterbanks, Filterbank } from '../dsp/MelSpectogram.js'
5
- import { clip, containsInvalidCodepoint, getIntegerRange, getTokenRepetitionScore, splitFloat32Array, yieldToEventLoop } from '../utilities/Utilities.js'
5
+ import { clip, getIntegerRange, splitFloat32Array, yieldToEventLoop } from '../utilities/Utilities.js'
6
6
  import { indexOfMax, logOfVector, logSumExp, meanOfVector, softmax, stdDeviationOfVector } from '../math/VectorMath.js'
7
7
 
8
8
  import { alignDTWWindowed } from '../alignment/DTWSequenceAlignmentWindowed.js'
@@ -22,8 +22,9 @@ import { type Tiktoken } from 'tiktoken/lite'
22
22
  import { isPunctuation, isWhitespace, isWord, splitToSentences, splitToWords } from '../nlp/Segmentation.js'
23
23
  import { medianOf5Filter } from '../math/MedianFilter.js'
24
24
  import { getDeflateCompressionMetricsForString } from '../utilities/Compression.js'
25
- import { getOnnxSessionOptions, makeOnnxLikeFloat32Tensor, OnnxExecutionProvider, OnnxLikeFloat32Tensor } from '../utilities/OnnxUtilities.js'
25
+ import { dmlProviderAvailable, getOnnxSessionOptions, makeOnnxLikeFloat32Tensor, OnnxExecutionProvider, OnnxLikeFloat32Tensor } from '../utilities/OnnxUtilities.js'
26
26
  import { murmurHash3_int32Input } from '../utilities/Hashing.js'
27
+ import { containsInvalidCodepoint, getTokenRepetitionScore } from '../utilities/StringUtilities.js'
27
28
 
28
29
  export async function recognize(
29
30
  sourceRawAudio: RawAudio,
@@ -458,7 +459,7 @@ export class Whisper {
458
459
  // Find alignment path
459
460
  let alignmentHeads: number[] | undefined = undefined
460
461
 
461
- if (this.modelName.startsWith('small') || this.modelName.startsWith('medium') || this.modelName.startsWith('large')) {
462
+ if (options.useOptimizedAlignmentHeads) {
462
463
  alignmentHeads = this.alignmentHeadIndexes
463
464
  }
464
465
 
@@ -565,12 +566,15 @@ export class Whisper {
565
566
  topCandidateCount: 1,
566
567
  punctuationThreshold: Infinity,
567
568
  autoPromptParts: false,
568
- maxTokensPerPart: Infinity,
569
+ maxTokensPerPart: whisperAlignmentOptions.maxTokensPerPart!,
569
570
  suppressRepetition: false,
570
571
  repetitionThreshold: Infinity,
571
572
  decodeTimestampTokens: true,
572
- endTokenThreshold: whisperAlignmentOptions!.endTokenThreshold!,
573
+ endTokenThreshold: whisperAlignmentOptions.endTokenThreshold!,
573
574
  includeEndTokenInCandidates: false,
575
+ useOptimizedAlignmentHeads: whisperAlignmentOptions.useOptimizedAlignmentHeads!,
576
+ encoderProvider: whisperAlignmentOptions.encoderProvider!,
577
+ decoderProvider: whisperAlignmentOptions.decoderProvider!,
574
578
  seed: undefined,
575
579
  }
576
580
 
@@ -1102,7 +1106,8 @@ export class Whisper {
1102
1106
  const sampleRate = rawAudio.sampleRate
1103
1107
 
1104
1108
  const fftOrder = 400
1105
- const hopLength = 160
1109
+ const fftWindowSize = 400
1110
+ const fftHopLength = 160
1106
1111
 
1107
1112
  const filterbankCount = this.filterbankCount
1108
1113
  const filterbanks = this.filterbanks
@@ -1127,7 +1132,7 @@ export class Whisper {
1127
1132
 
1128
1133
  const rawAudioPart: RawAudio = { audioChannels: [paddedAudioSamples], sampleRate }
1129
1134
 
1130
- const { melSpectogram } = await computeMelSpectogramUsingFilterbanks(rawAudioPart, fftOrder, fftOrder, hopLength, filterbanks)
1135
+ const { melSpectogram } = await computeMelSpectogramUsingFilterbanks(rawAudioPart, fftOrder, fftWindowSize, fftHopLength, filterbanks)
1131
1136
 
1132
1137
  await logger.startAsync('Normalize mel spectogram')
1133
1138
 
@@ -1841,17 +1846,17 @@ export function isEnglishOnlyModel(modelName: WhisperModelName) {
1841
1846
  return modelName.endsWith('.en')
1842
1847
  }
1843
1848
 
1844
- function getDefaultEncoderProvidersForModel(modelName: WhisperModelName): OnnxExecutionProvider[] {
1845
- if (process.platform === 'win32') {
1849
+ export function getDefaultEncoderProvidersForModel(modelName: WhisperModelName): OnnxExecutionProvider[] {
1850
+ if (dmlProviderAvailable()) {
1846
1851
  return ['dml', 'cpu']
1847
1852
  } else {
1848
1853
  return []
1849
1854
  }
1850
1855
  }
1851
1856
 
1852
- function getDefaultDecoderProvidersForModel(modelName: WhisperModelName): OnnxExecutionProvider[] {
1857
+ export function getDefaultDecoderProvidersForModel(modelName: WhisperModelName): OnnxExecutionProvider[] {
1853
1858
  if (modelName.startsWith('small') || modelName.startsWith('medium') || modelName.startsWith('large')) {
1854
- if (process.platform === 'win32') {
1859
+ if (dmlProviderAvailable()) {
1855
1860
  return ['dml', 'cpu']
1856
1861
  } else {
1857
1862
  return []
@@ -2439,6 +2444,7 @@ export interface WhisperOptions {
2439
2444
  decodeTimestampTokens?: boolean
2440
2445
  endTokenThreshold?: number
2441
2446
  includeEndTokenInCandidates?: boolean
2447
+ useOptimizedAlignmentHeads?: boolean,
2442
2448
  encoderProvider?: OnnxExecutionProvider
2443
2449
  decoderProvider?: OnnxExecutionProvider
2444
2450
  seed?: number
@@ -2457,6 +2463,7 @@ export const defaultWhisperOptions: WhisperOptions = {
2457
2463
  decodeTimestampTokens: true,
2458
2464
  endTokenThreshold: 0.9,
2459
2465
  includeEndTokenInCandidates: true,
2466
+ useOptimizedAlignmentHeads: true,
2460
2467
  encoderProvider: undefined,
2461
2468
  decoderProvider: undefined,
2462
2469
  seed: undefined,
@@ -2466,6 +2473,9 @@ export const defaultWhisperOptions: WhisperOptions = {
2466
2473
  export interface WhisperAlignmentOptions {
2467
2474
  model?: WhisperModelName
2468
2475
  endTokenThreshold?: number
2476
+ maxTokensPerPart?: number
2477
+ useOptimizedAlignmentHeads?: boolean
2478
+
2469
2479
  encoderProvider?: OnnxExecutionProvider
2470
2480
  decoderProvider?: OnnxExecutionProvider
2471
2481
  }
@@ -2473,8 +2483,11 @@ export interface WhisperAlignmentOptions {
2473
2483
  export const defaultWhisperAlignmentOptions: WhisperAlignmentOptions = {
2474
2484
  model: undefined,
2475
2485
  endTokenThreshold: 0.9,
2486
+ maxTokensPerPart: 250,
2487
+ useOptimizedAlignmentHeads: true,
2488
+
2476
2489
  encoderProvider: undefined,
2477
- decoderProvider: undefined
2490
+ decoderProvider: undefined,
2478
2491
  }
2479
2492
 
2480
2493
  // Language detection options