echogarden 2.8.7 → 2.9.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 (42) hide show
  1. package/dist/api/Recognition.d.ts +3 -3
  2. package/dist/api/Recognition.js +5 -5
  3. package/dist/api/Recognition.js.map +1 -1
  4. package/dist/api/SpeechTranslation.d.ts +4 -4
  5. package/dist/api/SpeechTranslation.js +4 -4
  6. package/dist/api/SpeechTranslation.js.map +1 -1
  7. package/dist/denoising/NSNet2.js +8 -8
  8. package/dist/denoising/NSNet2.js.map +1 -1
  9. package/dist/dsp/FFT.d.ts +2 -2
  10. package/dist/dsp/FFT.js +19 -15
  11. package/dist/dsp/FFT.js.map +1 -1
  12. package/dist/dsp/MFCC.d.ts +1 -1
  13. package/dist/dsp/MFCC.js +8 -8
  14. package/dist/dsp/MFCC.js.map +1 -1
  15. package/dist/dsp/{MelSpectogram.d.ts → MelSpectrogram.d.ts} +5 -5
  16. package/dist/dsp/{MelSpectogram.js → MelSpectrogram.js} +9 -9
  17. package/dist/dsp/MelSpectrogram.js.map +1 -0
  18. package/dist/math/VectorMath.d.ts +5 -1
  19. package/dist/math/VectorMath.js +12 -1
  20. package/dist/math/VectorMath.js.map +1 -1
  21. package/dist/nlp/IPA.d.ts +3 -3
  22. package/dist/recognition/WhisperSTT.d.ts +5 -5
  23. package/dist/recognition/WhisperSTT.js +38 -32
  24. package/dist/recognition/WhisperSTT.js.map +1 -1
  25. package/dist/source-separation/MDXNetSourceSeparation.js +5 -5
  26. package/dist/source-separation/MDXNetSourceSeparation.js.map +1 -1
  27. package/dist/speech-embeddings/WavToVec2BertFeatureEmbeddings.js +14 -14
  28. package/dist/speech-embeddings/WavToVec2BertFeatureEmbeddings.js.map +1 -1
  29. package/dist/utilities/Utilities.d.ts +1 -1
  30. package/docs/Tasklist.md +8 -0
  31. package/package.json +13 -13
  32. package/src/api/Recognition.ts +10 -7
  33. package/src/api/SpeechTranslation.ts +16 -6
  34. package/src/denoising/NSNet2.ts +8 -8
  35. package/src/dsp/FFT.ts +23 -17
  36. package/src/dsp/MFCC.ts +8 -8
  37. package/src/dsp/{MelSpectogram.ts → MelSpectrogram.ts} +9 -8
  38. package/src/math/VectorMath.ts +17 -1
  39. package/src/recognition/WhisperSTT.ts +61 -35
  40. package/src/source-separation/MDXNetSourceSeparation.ts +5 -5
  41. package/src/speech-embeddings/WavToVec2BertFeatureEmbeddings.ts +14 -14
  42. package/dist/dsp/MelSpectogram.js.map +0 -1
package/package.json CHANGED
@@ -1,21 +1,21 @@
1
1
  {
2
2
  "name": "echogarden",
3
- "version": "2.8.7",
3
+ "version": "2.9.0",
4
4
  "description": "An easy-to-use speech toolset. Includes tools for synthesis, recognition, alignment, speech translation, language detection, source separation and more.",
5
5
  "author": "Rotem Dan",
6
6
  "license": "GPL-3.0",
7
7
  "keywords": [
8
8
  "speech",
9
9
  "text-to-speech",
10
- "speech synthesis",
10
+ "speech-synthesis",
11
11
  "speech-to-text",
12
- "speech recognition",
13
- "speech alignment",
14
- "forced alignment",
15
- "speech translation",
16
- "language identification",
17
- "language detection",
18
- "source separation"
12
+ "speech-recognition",
13
+ "speech-alignment",
14
+ "forced-alignment",
15
+ "speech-translation",
16
+ "language-identification",
17
+ "language-detection",
18
+ "source-separation"
19
19
  ],
20
20
  "repository": {
21
21
  "type": "git",
@@ -52,8 +52,8 @@
52
52
  "echogarden": "./dist/cli/CLILauncher.js"
53
53
  },
54
54
  "dependencies": {
55
- "@aws-sdk/client-polly": "~3.812.0",
56
- "@aws-sdk/client-transcribe-streaming": "~3.812.0",
55
+ "@aws-sdk/client-polly": "~3.817.0",
56
+ "@aws-sdk/client-transcribe-streaming": "~3.817.0",
57
57
  "@echogarden/audio-io": "~0.4.2",
58
58
  "@echogarden/espeak-ng-emscripten": "~0.3.3",
59
59
  "@echogarden/fasttext-wasm": "~0.1.0",
@@ -85,7 +85,7 @@
85
85
  "microsoft-cognitiveservices-speech-sdk": "~1.44.0",
86
86
  "msgpack-lite": "~0.1.26",
87
87
  "onnxruntime-node": "~1.21.1",
88
- "openai": "~4.102.0",
88
+ "openai": "~4.103.0",
89
89
  "regexp-composer": "~0.3.0",
90
90
  "sam-js": "~0.3.1",
91
91
  "strip-ansi": "~7.1.0",
@@ -114,7 +114,7 @@
114
114
  "@types/graceful-fs": "~4.1.9",
115
115
  "@types/jsdom": "~21.1.7",
116
116
  "@types/msgpack-lite": "~0.1.11",
117
- "@types/node": "~22.15.21",
117
+ "@types/node": "~22.15.23",
118
118
  "@types/tar": "~6.1.13",
119
119
  "@types/ws": "~8.18.1",
120
120
  "ts-json-schema-generator": "~2.4.0",
@@ -1,26 +1,28 @@
1
+ import chalk from 'chalk'
2
+
3
+ import * as API from './API.js'
4
+
1
5
  import { extendDeep } from '../utilities/ObjectUtilities.js'
2
6
 
3
7
  import { logToStderr } from '../utilities/Utilities.js'
4
8
  import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevelInPlace, trimAudioEnd } from '../audio/AudioUtilities.js'
5
9
  import { Logger } from '../utilities/Logger.js'
6
10
 
7
- import * as API from './API.js'
8
11
  import { Timeline, addWordTextOffsetsToTimelineInPlace, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
9
12
  import { formatLanguageCodeWithName, parseLangIdentifier } from '../utilities/Locale.js'
10
13
  import { loadPackage } from '../utilities/PackageManager.js'
11
- import chalk from 'chalk'
12
14
 
13
- import { type WhisperOptions } from '../recognition/WhisperSTT.js'
15
+ import { type WhisperPartCallback, type WhisperOptions } from '../recognition/WhisperSTT.js'
14
16
  import { type SubtitlesConfig } from '../subtitles/Subtitles.js'
15
17
  import { type OpenAICloudSTTOptions } from '../recognition/OpenAICloudSTT.js'
16
18
  import { type WhisperCppOptions } from '../recognition/WhisperCppSTT.js'
17
19
  import { type SileroRecognitionOptions } from '../recognition/SileroSTT.js'
18
20
  import { type DeepgramSTTOptions } from '../recognition/DeepgramSTT.js'
19
- import { OnnxExecutionProvider } from '../utilities/OnnxUtilities.js'
21
+ import { type OnnxExecutionProvider } from '../utilities/OnnxUtilities.js'
20
22
 
21
23
  const log = logToStderr
22
24
 
23
- export async function recognize(input: AudioSourceParam, options: RecognitionOptions): Promise<RecognitionResult> {
25
+ export async function recognize(input: AudioSourceParam, options: RecognitionOptions, onPart?: WhisperPartCallback): Promise<RecognitionResult> {
24
26
  const logger = new Logger()
25
27
 
26
28
  const startTimestamp = logger.getTimestamp()
@@ -110,7 +112,8 @@ export async function recognize(input: AudioSourceParam, options: RecognitionOpt
110
112
  modelDir,
111
113
  'transcribe',
112
114
  shortLanguageCode,
113
- whisperOptions
115
+ whisperOptions,
116
+ onPart,
114
117
  ))
115
118
 
116
119
  break
@@ -318,7 +321,7 @@ export async function recognize(input: AudioSourceParam, options: RecognitionOpt
318
321
  const { segmentTimeline } = await wordTimelineToSegmentSentenceTimeline(timeline, transcript, languageCode, 'single', 'preserve')
319
322
 
320
323
  logger.end()
321
- logger.logDuration('Total recognition time', startTimestamp, chalk.magentaBright)
324
+ logger.logDuration('\nTotal recognition time', startTimestamp, chalk.magentaBright)
322
325
 
323
326
  return {
324
327
  transcript,
@@ -1,3 +1,7 @@
1
+ import chalk from 'chalk'
2
+
3
+ import * as API from './API.js'
4
+
1
5
  import { extendDeep } from '../utilities/ObjectUtilities.js'
2
6
 
3
7
  import { logToStderr } from '../utilities/Utilities.js'
@@ -5,14 +9,12 @@ import { AudioSourceParam, RawAudio, ensureRawAudio, normalizeAudioLevelInPlace,
5
9
  import { Logger } from '../utilities/Logger.js'
6
10
 
7
11
  import { Timeline, addWordTextOffsetsToTimelineInPlace, wordTimelineToSegmentSentenceTimeline } from '../utilities/Timeline.js'
8
- import { type WhisperOptions } from '../recognition/WhisperSTT.js'
12
+ import { WhisperPartCallback, type WhisperOptions } from '../recognition/WhisperSTT.js'
9
13
  import { formatLanguageCodeWithName, getShortLanguageCode, normalizeIdentifierToLanguageCode, parseLangIdentifier } from '../utilities/Locale.js'
10
- import { EngineMetadata } from './Common.js'
14
+ import { type EngineMetadata } from './Common.js'
11
15
  import { type SpeechLanguageDetectionOptions, detectSpeechLanguage } from './API.js'
12
- import chalk from 'chalk'
13
16
  import { type SubtitlesConfig } from '../subtitles/Subtitles.js'
14
17
 
15
- import * as API from './API.js'
16
18
  import { type OpenAICloudSTTOptions } from '../recognition/OpenAICloudSTT.js'
17
19
  import { type WhisperCppOptions } from '../recognition/WhisperCppSTT.js'
18
20
 
@@ -21,7 +23,7 @@ const log = logToStderr
21
23
  /////////////////////////////////////////////////////////////////////////////////////////////
22
24
  // Speech translation
23
25
  /////////////////////////////////////////////////////////////////////////////////////////////
24
- export async function translateSpeech(input: AudioSourceParam, options: SpeechTranslationOptions): Promise<SpeechTranslationResult> {
26
+ export async function translateSpeech(input: AudioSourceParam, options: SpeechTranslationOptions, onPart?: WhisperPartCallback): Promise<SpeechTranslationResult> {
25
27
  const logger = new Logger()
26
28
 
27
29
  const startTimestamp = logger.getTimestamp()
@@ -122,7 +124,15 @@ export async function translateSpeech(input: AudioSourceParam, options: SpeechTr
122
124
 
123
125
  logger.end();
124
126
 
125
- ({ transcript, timeline: wordTimeline } = await WhisperSTT.recognize(sourceRawAudio, modelName, modelDir, 'translate', sourceLanguage, whisperOptions))
127
+ ({ transcript, timeline: wordTimeline } = await WhisperSTT.recognize(
128
+ sourceRawAudio,
129
+ modelName,
130
+ modelDir,
131
+ 'translate',
132
+ sourceLanguage,
133
+ whisperOptions,
134
+ onPart,
135
+ ))
126
136
 
127
137
  break
128
138
  }
@@ -58,8 +58,8 @@ export class NSNet2 {
58
58
  logger.start('Compute STFT frames')
59
59
  const stftrFrames = await stftr(rawAudio.audioChannels[0], fftSize, fftSize, fftHopSize, 'hann')
60
60
 
61
- logger.start('Compute log-power spectogram')
62
- let logPowerSpectogram: Float32Array[] = []
61
+ logger.start('Compute log-power spectrogram')
62
+ let logPowerSpectrogram: Float32Array[] = []
63
63
 
64
64
  {
65
65
  for (const frame of stftrFrames) {
@@ -79,19 +79,19 @@ export class NSNet2 {
79
79
  logPowerSpectrum[writeOffset++] = logPowerValue
80
80
  }
81
81
 
82
- logPowerSpectogram.push(logPowerSpectrum)
82
+ logPowerSpectrogram.push(logPowerSpectrum)
83
83
  }
84
84
  }
85
85
 
86
- logger.start('Process log-power spectogram using ONNX model')
86
+ logger.start('Process log-power spectrogram using ONNX model')
87
87
 
88
- const frameCount = logPowerSpectogram.length
88
+ const frameCount = logPowerSpectrogram.length
89
89
  let flattenedOutputTensor: Float32Array
90
90
 
91
91
  {
92
92
  const Onnx = await import('onnxruntime-node')
93
93
 
94
- const flattenedFeatures = concatFloat32Arrays(logPowerSpectogram)
94
+ const flattenedFeatures = concatFloat32Arrays(logPowerSpectrogram)
95
95
 
96
96
  const inputTensor = new Onnx.Tensor('float32', flattenedFeatures, [1, frameCount, fftRealBinCount])
97
97
  const inputs = { input: inputTensor }
@@ -126,8 +126,8 @@ export class NSNet2 {
126
126
  }
127
127
  }
128
128
 
129
- // Allow logPowerSpectogram to be garbage collected
130
- logPowerSpectogram = undefined as any
129
+ // Allow logPowerSpectrogram to be garbage collected
130
+ logPowerSpectrogram = undefined as any
131
131
 
132
132
  logger.start('Reconstruct filtered signal using inverse STFT')
133
133
  const filteredSignal = await stiftr(stftrFrames, fftSize, fftSize, fftHopSize, 'hann')
package/src/dsp/FFT.ts CHANGED
@@ -6,7 +6,9 @@ import { createWasmHeapManager } from 'wasm-heap-manager'
6
6
  export async function stftr(samples: Float32Array, fftOrder: number, windowSize: number, hopSize: number, windowType: WindowType) {
7
7
  const frames: Float32Array[] = []
8
8
 
9
- for await (const frame of stftrGenerator(samples, fftOrder, windowSize, hopSize, windowType)) {
9
+ const stftrGenerator = await createStftrGenerator(samples, fftOrder, windowSize, hopSize, windowType)
10
+
11
+ for (const frame of stftrGenerator) {
10
12
  frames.push(frame)
11
13
  }
12
14
 
@@ -14,7 +16,7 @@ export async function stftr(samples: Float32Array, fftOrder: number, windowSize:
14
16
  }
15
17
 
16
18
  // Incrementally generate short-term Fourier transform frames (real-valued)
17
- export async function* stftrGenerator(samples: Float32Array, fftOrder: number, windowSize: number, hopSize: number, windowType: WindowType) {
19
+ export async function createStftrGenerator(samples: Float32Array, fftOrder: number, windowSize: number, hopSize: number, windowType: WindowType) {
18
20
  if (fftOrder % 2 !== 0 || windowSize % 2 !== 0) {
19
21
  throw new Error('FFT order and window size must be multiples of 2')
20
22
  }
@@ -49,28 +51,32 @@ export async function* stftrGenerator(samples: Float32Array, fftOrder: number, w
49
51
  const binsBufferRef = wasmHeap.allocFloat32Array(fftOrder * 2)
50
52
  const workBufferRef = wasmHeap.allocFloat32Array(fftOrder * 2)
51
53
 
52
- for (let offset = 0; offset < sampleCount; offset += hopSize) {
53
- const windowSamples = samples.subarray(offset, offset + windowSize)
54
- frameBufferRef.clear()
54
+ function* stftrGenerator() {
55
+ for (let offset = 0; offset < sampleCount; offset += hopSize) {
56
+ const windowSamples = samples.subarray(offset, offset + windowSize)
57
+ frameBufferRef.clear()
55
58
 
56
- const frameBufferView = frameBufferRef.view
59
+ const frameBufferView = frameBufferRef.view
57
60
 
58
- for (let i = 0; i < windowSamples.length; i++) {
59
- frameBufferView[i] = windowSamples[i] * windowWeights[i]
60
- }
61
+ for (let i = 0; i < windowSamples.length; i++) {
62
+ frameBufferView[i] = windowSamples[i] * windowWeights[i]
63
+ }
61
64
 
62
- binsBufferRef.clear()
65
+ binsBufferRef.clear()
63
66
 
64
- m._pffft_transform_ordered(statePtr, frameBufferRef.address, binsBufferRef.address, workBufferRef.address, 0)
67
+ m._pffft_transform_ordered(statePtr, frameBufferRef.address, binsBufferRef.address, workBufferRef.address, 0)
65
68
 
66
- const bins = binsBufferRef.view.slice(0, fftOrder + 2)
69
+ const bins = binsBufferRef.view.slice(0, fftOrder + 2)
67
70
 
68
- yield bins
69
- }
71
+ yield bins
72
+ }
70
73
 
71
- m._pffft_destroy_setup(statePtr)
74
+ m._pffft_destroy_setup(statePtr)
72
75
 
73
- wasmHeap.freeAll()
76
+ wasmHeap.freeAll()
77
+ }
78
+
79
+ return stftrGenerator()
74
80
  }
75
81
 
76
82
  // Compute short-term inverse Fourier transform (real-valued)
@@ -168,7 +174,7 @@ export function getBinFrequencies(binCount: number, maxFrequency: number) {
168
174
  }
169
175
 
170
176
  // Convert an array of raw FFT frames to a power spectrum
171
- export function fftFramesToPowerSpectogram(fftFrames: Float32Array[]) {
177
+ export function fftFramesToPowerSpectrogram(fftFrames: Float32Array[]) {
172
178
  return fftFrames.map(fftFrame => fftFrameToPowerSpectrum(fftFrame))
173
179
  }
174
180
 
package/src/dsp/MFCC.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { extendDeep } from '../utilities/ObjectUtilities.js'
2
2
  import { Logger } from '../utilities/Logger.js'
3
3
  import { resampleAudioSpeex } from './SpeexResampler.js'
4
- import { computeMelSpectogram } from './MelSpectogram.js'
4
+ import { computeMelSpectrogram } from './MelSpectrogram.js'
5
5
  import { RawAudio, powerToDecibels } from '../audio/AudioUtilities.js'
6
6
  import { normalizeVectors } from '../math/VectorMath.js'
7
7
 
@@ -41,11 +41,11 @@ export async function computeMFCCs(monoAudio: RawAudio, options: MfccOptions = {
41
41
  resampledAudio.audioChannels[0] = applyEmphasis(resampledAudio.audioChannels[0], emphasisFactor)
42
42
  }
43
43
 
44
- logger.start('Compute Mel spectogram')
45
- const { melSpectogram } = await computeMelSpectogram(resampledAudio, fftOrder, windowSize, hopLength, filterbankCount, lowerFrequencyHz, upperFrequencyHz)
44
+ logger.start('Compute Mel spectrogram')
45
+ const { melSpectrogram } = await computeMelSpectrogram(resampledAudio, fftOrder, windowSize, hopLength, filterbankCount, lowerFrequencyHz, upperFrequencyHz)
46
46
 
47
- logger.start('Extract MFCCs from Mel spectogram')
48
- let mfccs = melSpectogramToMFCCs(melSpectogram, featureCount)
47
+ logger.start('Extract MFCCs from Mel spectrogram')
48
+ let mfccs = melSpectrogramToMFCCs(melSpectrogram, featureCount)
49
49
 
50
50
  if (options.normalize!) {
51
51
  logger.start('Normalize MFCCs')
@@ -72,11 +72,11 @@ export async function computeMFCCs(monoAudio: RawAudio, options: MfccOptions = {
72
72
  return mfccs
73
73
  }
74
74
 
75
- export function melSpectogramToMFCCs(melSpectogram: ArrayLike<number>[], mfccFeatureCount: number) {
76
- const melBandCount = melSpectogram[0].length
75
+ export function melSpectrogramToMFCCs(melSpectrogram: ArrayLike<number>[], mfccFeatureCount: number) {
76
+ const melBandCount = melSpectrogram[0].length
77
77
  const dctMatrix = createDCTType2CoefficientMatrix(mfccFeatureCount, melBandCount)
78
78
 
79
- const mfccs = melSpectogram.map(frame => melSpectrumToMFCC(frame, mfccFeatureCount, dctMatrix))
79
+ const mfccs = melSpectrogram.map(frame => melSpectrumToMFCC(frame, mfccFeatureCount, dctMatrix))
80
80
 
81
81
  return mfccs
82
82
  }
@@ -2,10 +2,10 @@ import { RawAudio } from '../audio/AudioUtilities.js'
2
2
  import { Logger } from '../utilities/Logger.js'
3
3
  import * as FFT from './FFT.js'
4
4
 
5
- export async function computeMelSpectogram(rawAudio: RawAudio, fftOrder: number, windowSize: number, hopLength: number, filterbankCount: number, lowerFrequencyHz: number, upperFrequencyHz: number, windowType: FFT.WindowType = 'hann') {
5
+ export async function computeMelSpectrogram(rawAudio: RawAudio, fftOrder: number, windowSize: number, hopLength: number, filterbankCount: number, lowerFrequencyHz: number, upperFrequencyHz: number, windowType: FFT.WindowType = 'hann') {
6
6
  const logger = new Logger()
7
7
 
8
- logger.start('Compute mel filterbank')
8
+ logger.start('Compute Mel filterbank')
9
9
  const binCount = (fftOrder / 2) + 2
10
10
  const nyquistFrequency = rawAudio.sampleRate / 2
11
11
  const binFrequencies = FFT.getBinFrequencies(binCount, nyquistFrequency)
@@ -18,27 +18,28 @@ export async function computeMelSpectogram(rawAudio: RawAudio, fftOrder: number,
18
18
 
19
19
  logger.end()
20
20
 
21
- return computeMelSpectogramUsingFilterbanks(rawAudio, fftOrder, windowSize, hopLength, melFilterbanks, windowType)
21
+ return computeMelSpectrogramUsingFilterbanks(rawAudio, fftOrder, windowSize, hopLength, melFilterbanks, windowType)
22
22
  }
23
23
 
24
- export async function computeMelSpectogramUsingFilterbanks(rawAudio: RawAudio, fftOrder: number, windowSize: number, hopLength: number, filterbanks: Filterbank[], windowType: FFT.WindowType = 'hann') {
24
+ export async function computeMelSpectrogramUsingFilterbanks(rawAudio: RawAudio, fftOrder: number, windowSize: number, hopLength: number, filterbanks: Filterbank[], windowType: FFT.WindowType = 'hann') {
25
25
  const logger = new Logger()
26
26
 
27
27
  logger.start('Compute short-time FFTs')
28
28
  const audioSamples = rawAudio.audioChannels[0]
29
29
  const fftFrames = await FFT.stftr(audioSamples, fftOrder, windowSize, hopLength, windowType)
30
30
 
31
- logger.start('Convert FFT frames to a mel spectogram')
32
- const melSpectogram = fftFramesToMelSpectogram(fftFrames, filterbanks)
31
+ logger.start('Convert FFT frames to Mel spectrogram')
32
+ const melSpectrogram = fftFramesToMelSpectrogram(fftFrames, filterbanks)
33
33
 
34
34
  logger.end()
35
35
 
36
- return { melSpectogram, fftFrames }
36
+ return { melSpectrogram, fftFrames }
37
37
  }
38
38
 
39
- export function fftFramesToMelSpectogram(fftFrames: Float32Array[], melFilterbanks: Filterbank[]) {
39
+ export function fftFramesToMelSpectrogram(fftFrames: Float32Array[], melFilterbanks: Filterbank[]) {
40
40
  return fftFrames.map(fftFrame => {
41
41
  const powerSpectrum = FFT.fftFrameToPowerSpectrum(fftFrame)
42
+
42
43
  return powerSpectrumToMelSpectrum(powerSpectrum, melFilterbanks)
43
44
  })
44
45
  }
@@ -643,7 +643,7 @@ export function sumVector(vector: ArrayLike<number>) {
643
643
  return result
644
644
  }
645
645
 
646
- export function sumOfSquaresForVector(vector: ArrayLike<number>) {
646
+ export function sumOfSquaresOfVector(vector: ArrayLike<number>) {
647
647
  const elementCount = vector.length
648
648
 
649
649
  let result = 0.0
@@ -655,6 +655,22 @@ export function sumOfSquaresForVector(vector: ArrayLike<number>) {
655
655
  return result
656
656
  }
657
657
 
658
+ export function sumAndSumOfSquaresOfVector(vector: ArrayLike<number>) {
659
+ const elementCount = vector.length
660
+
661
+ let sum = 0.0
662
+ let sumOfSquares = 0.0
663
+
664
+ for (let i = 0; i < elementCount; i++) {
665
+ const value = vector[i]
666
+
667
+ sum += value
668
+ sumOfSquares += value ** 2
669
+ }
670
+
671
+ return { sum, sumOfSquares }
672
+ }
673
+
658
674
  export function dotProduct(vector1: ArrayLike<number>, vector2: ArrayLike<number>) {
659
675
  if (vector1.length != vector2.length) {
660
676
  throw new Error('Vectors are not the same length')
@@ -1,9 +1,10 @@
1
+ import chalk from 'chalk'
1
2
  import type * as Onnx from 'onnxruntime-node'
2
3
 
3
4
  import { Logger } from '../utilities/Logger.js'
4
- import { computeMelSpectogramUsingFilterbanks, Filterbank } from '../dsp/MelSpectogram.js'
5
+ import { computeMelSpectrogramUsingFilterbanks, Filterbank } from '../dsp/MelSpectrogram.js'
5
6
  import { clip, getIntegerRange, getTopKIndexes, splitFloat32Array, yieldToEventLoop } from '../utilities/Utilities.js'
6
- import { indexOfMax, logOfVector, logSumExp, meanOfVector, medianOfVector, softmax, sumOfSquaresForVector, sumVector } from '../math/VectorMath.js'
7
+ import { indexOfMax, logOfVector, logSumExp, meanOfVector, medianOfVector, softmax, sumAndSumOfSquaresOfVector, sumOfSquaresOfVector, sumVector } from '../math/VectorMath.js'
7
8
 
8
9
  import { alignDTWWindowed } from '../alignment/DTWSequenceAlignmentWindowed.js'
9
10
  import { extendDeep } from '../utilities/ObjectUtilities.js'
@@ -14,7 +15,6 @@ import { readFileAsUtf8 } from '../utilities/FileSystem.js'
14
15
  import { logLevelGreaterOrEqualTo, type LanguageDetectionResults } from '../api/API.js'
15
16
  import { formatLanguageCodeWithName, getShortLanguageCode, languageCodeToName } from '../utilities/Locale.js'
16
17
  import { loadPackage } from '../utilities/PackageManager.js'
17
- import chalk from 'chalk'
18
18
  import { XorShift32PRNG } from '../utilities/RandomGenerator.js'
19
19
  import { detectSpeechLanguageByParts } from '../api/SpeechLanguageDetection.js'
20
20
  import { type Tiktoken } from 'tiktoken/lite'
@@ -33,7 +33,8 @@ export async function recognize(
33
33
  modelDir: string,
34
34
  task: WhisperTask,
35
35
  sourceLanguage: string,
36
- options: WhisperOptions) {
36
+ options: WhisperOptions,
37
+ onPart?: WhisperPartCallback) {
37
38
 
38
39
  options = extendDeep(defaultWhisperOptions, options)
39
40
 
@@ -82,7 +83,7 @@ export async function recognize(
82
83
  decoderProviders,
83
84
  seed)
84
85
 
85
- const result = await whisper.recognize(sourceRawAudio, task, sourceLanguage, options)
86
+ const result = await whisper.recognize(sourceRawAudio, task, sourceLanguage, options, undefined, onPart)
86
87
 
87
88
  return result
88
89
  }
@@ -366,6 +367,7 @@ export class Whisper {
366
367
  language: string,
367
368
  options: WhisperOptions,
368
369
  logitFilter?: WhisperLogitFilter,
370
+ onPart?: WhisperPartCallback,
369
371
  ) {
370
372
  await this.initializeIfNeeded()
371
373
 
@@ -500,6 +502,13 @@ export class Whisper {
500
502
  // Generate timeline from alignment path
501
503
  const partTimeline = await this.getTokenTimelineFromAlignmentPath(alignmentPath, partTokens, segmentStartTime, segmentEndTime, partTokensConfidence)
502
504
 
505
+ if (onPart) {
506
+ const partWordTimeline = this.tokenTimelineToWordTimeline(partTimeline, language)
507
+ const partTranscript = this.tokensToText(partTokens)
508
+
509
+ onPart(partTranscript, partTimeline, partWordTimeline)
510
+ }
511
+
503
512
  // Add tokens to output
504
513
  allDecodedTokens.push(...partTokens)
505
514
  timeline.push(...partTimeline)
@@ -1234,8 +1243,7 @@ export class Whisper {
1234
1243
  throw new Error(`Audio part is longer than 30 seconds`)
1235
1244
  }
1236
1245
 
1237
- // Compute a mel spectogram
1238
- await logger.startAsync('Extract mel spectogram from audio part')
1246
+ await logger.startAsync('Extract Mel spectrogram from audio part')
1239
1247
 
1240
1248
  // Pad audio samples to ensure that have a duration of 30 seconds
1241
1249
  const paddedAudioSamples = new Float32Array(maxAudioSamples)
@@ -1243,40 +1251,40 @@ export class Whisper {
1243
1251
 
1244
1252
  const rawAudioPart: RawAudio = { audioChannels: [paddedAudioSamples], sampleRate }
1245
1253
 
1246
- const { melSpectogram } = await computeMelSpectogramUsingFilterbanks(rawAudioPart, fftOrder, fftWindowSize, fftHopLength, filterbanks)
1254
+ // Compute Mel spectrogram
1255
+ const { melSpectrogram } = await computeMelSpectrogramUsingFilterbanks(rawAudioPart, fftOrder, fftWindowSize, fftHopLength, filterbanks)
1247
1256
 
1248
- await logger.startAsync('Normalize mel spectogram')
1257
+ // Flatten, transpose, apply logarithm and normalize Mel spectrogram
1258
+ await logger.startAsync('Process Mel spectrogram')
1249
1259
 
1250
- const logMelSpectogram = melSpectogram.map(spectrum => spectrum.map(mel => Math.log10(Math.max(mel, 1e-10))))
1260
+ const flattenedLogMelSpectrogram = new Float32Array(maxAudioFrames * filterbankCount)
1251
1261
 
1252
- // Find maximum log mel value in the spectrum
1253
1262
  let maxLogMel = -Infinity
1254
1263
 
1255
- for (const spectrum of logMelSpectogram) {
1256
- for (const mel of spectrum) {
1257
- if (mel > maxLogMel) {
1258
- maxLogMel = mel
1264
+ for (let i = 0; i < filterbankCount; i++) {
1265
+ for (let j = 0; j < maxAudioFrames; j++) {
1266
+ const mel = melSpectrogram[j][i]
1267
+ const logMel = Math.log10(Math.max(mel, 1e-10))
1268
+
1269
+ if (logMel > maxLogMel) {
1270
+ maxLogMel = logMel
1259
1271
  }
1272
+
1273
+ flattenedLogMelSpectrogram[(i * maxAudioFrames) + j] = logMel
1260
1274
  }
1261
1275
  }
1262
1276
 
1263
- // Normalize log mel spectogram (based on Python reference code)
1264
- const normalizedLogMelSpectogram = logMelSpectogram.map(spectrum => spectrum.map(
1265
- logMel => (Math.max(logMel, maxLogMel - 8) + 4) / 4))
1277
+ for (let i = 0; i < flattenedLogMelSpectrogram.length; i++) {
1278
+ const logMel = flattenedLogMelSpectrogram[i]
1279
+ const normalizedLogMel = (Math.max(logMel, maxLogMel - 8) + 4) / 4
1266
1280
 
1267
- // Flatten the normalized log mel spectogram
1268
- const flattenedNormalizedLogMelSpectogram = new Float32Array(maxAudioFrames * filterbankCount)
1269
-
1270
- for (let i = 0; i < filterbankCount; i++) {
1271
- for (let j = 0; j < maxAudioFrames; j++) {
1272
- flattenedNormalizedLogMelSpectogram[(i * maxAudioFrames) + j] = normalizedLogMelSpectogram[j][i]
1273
- }
1281
+ flattenedLogMelSpectrogram[i] = normalizedLogMel
1274
1282
  }
1275
1283
 
1276
1284
  // Run the encoder model
1277
- await logger.startAsync('Encode mel spectogram with Whisper encoder model')
1285
+ await logger.startAsync('Encode Mel spectrogram with Whisper encoder model')
1278
1286
 
1279
- const inputTensor = new Onnx.Tensor('float32', flattenedNormalizedLogMelSpectogram, [1, filterbankCount, maxAudioFrames])
1287
+ const inputTensor = new Onnx.Tensor('float32', flattenedLogMelSpectrogram, [1, filterbankCount, maxAudioFrames])
1280
1288
 
1281
1289
  const encoderInputs = { mel: inputTensor }
1282
1290
 
@@ -1488,8 +1496,10 @@ export class Whisper {
1488
1496
  let countOfAllWeightsForHead = 0
1489
1497
 
1490
1498
  for (const tokenFrames of head) {
1491
- sumOfAllWeightsForHead += sumVector(tokenFrames)
1492
- sumOfAllSquaredWeightsForHead += sumOfSquaresForVector(tokenFrames)
1499
+ const { sum, sumOfSquares } = sumAndSumOfSquaresOfVector(tokenFrames)
1500
+
1501
+ sumOfAllWeightsForHead += sum
1502
+ sumOfAllSquaredWeightsForHead += sumOfSquares
1493
1503
  countOfAllWeightsForHead += tokenFrames.length
1494
1504
  }
1495
1505
 
@@ -2004,7 +2014,20 @@ export type WhisperTokenData = {
2004
2014
 
2005
2015
  export type WhisperLogitFilter = (logits: number[], decodedTokens: number[], isFirstPart: boolean, isFinalPart: boolean) => number[]
2006
2016
 
2007
- export type WhisperModelName = 'tiny' | 'tiny.en' | 'base' | 'base.en' | 'small' | 'small.en' | 'medium' | 'medium.en' | 'large-v1' | 'large-v2' | 'large-v3' | 'large-v3-turbo'
2017
+ export type WhisperModelName =
2018
+ 'tiny' |
2019
+ 'tiny.en' |
2020
+ 'base' |
2021
+ 'base.en' |
2022
+ 'small' |
2023
+ 'small.en' |
2024
+ 'medium' |
2025
+ 'medium.en' |
2026
+ 'large-v1' |
2027
+ 'large-v2' |
2028
+ 'large-v3' |
2029
+ 'large-v3-turbo'
2030
+
2008
2031
  export type WhisperTask = 'transcribe' | 'translate' | 'detect-language'
2009
2032
 
2010
2033
  export const modelNameToPackageName: { [modelName in WhisperModelName]: string } = {
@@ -2124,17 +2147,18 @@ const languageIdLookup: { [s: string]: number } = {
2124
2147
  'ba': 96,
2125
2148
  'jw': 97,
2126
2149
  'su': 98,
2150
+ //'yue': 99
2127
2151
  }
2128
2152
 
2129
2153
  const alignmentHeadsIndexes: { [name in WhisperModelName]: number[] } = {
2130
- 'tiny.en': [6, 12, 17, 18, 19, 20, 21, 22,],
2131
2154
  'tiny': [14, 18, 20, 21, 22, 23,],
2132
- 'base.en': [27, 39, 41, 45, 47,],
2155
+ 'tiny.en': [6, 12, 17, 18, 19, 20, 21, 22,],
2133
2156
  'base': [25, 34, 35, 39, 41, 42, 44, 46,],
2134
- 'small.en': [78, 84, 87, 92, 98, 101, 103, 108, 112, 116, 118, 120, 121, 122, 123, 126, 131, 134, 136,],
2157
+ 'base.en': [27, 39, 41, 45, 47,],
2135
2158
  'small': [63, 69, 96, 100, 103, 104, 108, 115, 117, 125,],
2136
- 'medium.en': [180, 225, 236, 238, 244, 256, 260, 265, 284, 286, 295, 298, 303, 320, 323, 329, 334, 348,],
2159
+ 'small.en': [78, 84, 87, 92, 98, 101, 103, 108, 112, 116, 118, 120, 121, 122, 123, 126, 131, 134, 136,],
2137
2160
  'medium': [223, 244, 255, 257, 320, 372,],
2161
+ 'medium.en': [180, 225, 236, 238, 244, 256, 260, 265, 284, 286, 295, 298, 303, 320, 323, 329, 334, 348,],
2138
2162
  'large-v1': [199, 222, 224, 237, 447, 451, 457, 462, 475,],
2139
2163
  'large-v2': [212, 277, 331, 332, 333, 355, 356, 364, 371, 379, 391, 422, 423, 443, 449, 452, 465, 467, 473, 505, 521, 532, 555,],
2140
2164
  'large-v3': [140, 217, 258, 272, 321, 354, 391, 424, 481, 506,],
@@ -2653,4 +2677,6 @@ export const defaultWhisperVADOptions: WhisperVADOptions = {
2653
2677
  decoderProvider: undefined,
2654
2678
  }
2655
2679
 
2656
- type WhisperTimestampAccuracy = 'medium' | 'high'
2680
+ export type WhisperTimestampAccuracy = 'medium' | 'high'
2681
+
2682
+ export type WhisperPartCallback = (partTranscript: string, partTokenTimeline: Timeline, partWordTimeline: Timeline) => void
@@ -1,6 +1,6 @@
1
1
  import type * as Onnx from 'onnxruntime-node'
2
2
  import { getEmptyRawAudio, RawAudio } from '../audio/AudioUtilities.js'
3
- import { getWindowWeights, stftrGenerator, stiftr, WindowType } from '../dsp/FFT.js'
3
+ import { getWindowWeights, createStftrGenerator, stiftr, WindowType } from '../dsp/FFT.js'
4
4
  import { logToStderr } from '../utilities/Utilities.js'
5
5
  import { Logger } from '../utilities/Logger.js'
6
6
  import { OnnxExecutionProvider, dmlProviderAvailable, getOnnxSessionOptions } from '../utilities/OnnxUtilities.js'
@@ -74,8 +74,8 @@ export class MDXNet {
74
74
  const fftSizeReciprocal = 1 / fftSize
75
75
 
76
76
  // Initialize generators for STFT frames for each channel
77
- const fftFramesLeftGenerator = stftrGenerator(rawAudio.audioChannels[0], fftSize, fftWindowSize, fftHopSize, fftWindowType)
78
- const fftFramesRightGenerator = stftrGenerator(rawAudio.audioChannels[1], fftSize, fftWindowSize, fftHopSize, fftWindowType)
77
+ const fftFramesLeftGenerator = await createStftrGenerator(rawAudio.audioChannels[0], fftSize, fftWindowSize, fftHopSize, fftWindowType)
78
+ const fftFramesRightGenerator = await createStftrGenerator(rawAudio.audioChannels[1], fftSize, fftWindowSize, fftHopSize, fftWindowType)
79
79
 
80
80
  // Initial windowed lists to store recently computed STFT frames
81
81
  const fftFramesLeftWindowedList = new WindowedList<Float32Array>(segmentSize)
@@ -95,13 +95,13 @@ export class MDXNet {
95
95
  }
96
96
 
97
97
  while (fftFramesLeftWindowedList.endOffset < segmentEndFrameOffset) {
98
- const nextLeftFrameResult = await fftFramesLeftGenerator.next()
98
+ const nextLeftFrameResult = fftFramesLeftGenerator.next()
99
99
 
100
100
  if (nextLeftFrameResult.done) {
101
101
  break
102
102
  }
103
103
 
104
- const nextRightFrameResult = await fftFramesRightGenerator.next()
104
+ const nextRightFrameResult = fftFramesRightGenerator.next()
105
105
 
106
106
  if (nextRightFrameResult.done) {
107
107
  break