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.
- package/dist/api/Recognition.d.ts +3 -3
- package/dist/api/Recognition.js +5 -5
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/SpeechTranslation.d.ts +4 -4
- package/dist/api/SpeechTranslation.js +4 -4
- package/dist/api/SpeechTranslation.js.map +1 -1
- package/dist/denoising/NSNet2.js +8 -8
- package/dist/denoising/NSNet2.js.map +1 -1
- package/dist/dsp/FFT.d.ts +2 -2
- package/dist/dsp/FFT.js +19 -15
- package/dist/dsp/FFT.js.map +1 -1
- package/dist/dsp/MFCC.d.ts +1 -1
- package/dist/dsp/MFCC.js +8 -8
- package/dist/dsp/MFCC.js.map +1 -1
- package/dist/dsp/{MelSpectogram.d.ts → MelSpectrogram.d.ts} +5 -5
- package/dist/dsp/{MelSpectogram.js → MelSpectrogram.js} +9 -9
- package/dist/dsp/MelSpectrogram.js.map +1 -0
- package/dist/math/VectorMath.d.ts +5 -1
- package/dist/math/VectorMath.js +12 -1
- package/dist/math/VectorMath.js.map +1 -1
- package/dist/nlp/IPA.d.ts +3 -3
- package/dist/recognition/WhisperSTT.d.ts +5 -5
- package/dist/recognition/WhisperSTT.js +38 -32
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/source-separation/MDXNetSourceSeparation.js +5 -5
- package/dist/source-separation/MDXNetSourceSeparation.js.map +1 -1
- package/dist/speech-embeddings/WavToVec2BertFeatureEmbeddings.js +14 -14
- package/dist/speech-embeddings/WavToVec2BertFeatureEmbeddings.js.map +1 -1
- package/dist/utilities/Utilities.d.ts +1 -1
- package/docs/Tasklist.md +8 -0
- package/package.json +13 -13
- package/src/api/Recognition.ts +10 -7
- package/src/api/SpeechTranslation.ts +16 -6
- package/src/denoising/NSNet2.ts +8 -8
- package/src/dsp/FFT.ts +23 -17
- package/src/dsp/MFCC.ts +8 -8
- package/src/dsp/{MelSpectogram.ts → MelSpectrogram.ts} +9 -8
- package/src/math/VectorMath.ts +17 -1
- package/src/recognition/WhisperSTT.ts +61 -35
- package/src/source-separation/MDXNetSourceSeparation.ts +5 -5
- package/src/speech-embeddings/WavToVec2BertFeatureEmbeddings.ts +14 -14
- 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.
|
|
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
|
|
10
|
+
"speech-synthesis",
|
|
11
11
|
"speech-to-text",
|
|
12
|
-
"speech
|
|
13
|
-
"speech
|
|
14
|
-
"forced
|
|
15
|
-
"speech
|
|
16
|
-
"language
|
|
17
|
-
"language
|
|
18
|
-
"source
|
|
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.
|
|
56
|
-
"@aws-sdk/client-transcribe-streaming": "~3.
|
|
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.
|
|
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.
|
|
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",
|
package/src/api/Recognition.ts
CHANGED
|
@@ -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('
|
|
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(
|
|
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
|
}
|
package/src/denoising/NSNet2.ts
CHANGED
|
@@ -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
|
|
62
|
-
let
|
|
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
|
-
|
|
82
|
+
logPowerSpectrogram.push(logPowerSpectrum)
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
logger.start('Process log-power
|
|
86
|
+
logger.start('Process log-power spectrogram using ONNX model')
|
|
87
87
|
|
|
88
|
-
const frameCount =
|
|
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(
|
|
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
|
|
130
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
+
const frameBufferView = frameBufferRef.view
|
|
57
60
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
for (let i = 0; i < windowSamples.length; i++) {
|
|
62
|
+
frameBufferView[i] = windowSamples[i] * windowWeights[i]
|
|
63
|
+
}
|
|
61
64
|
|
|
62
|
-
|
|
65
|
+
binsBufferRef.clear()
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
m._pffft_transform_ordered(statePtr, frameBufferRef.address, binsBufferRef.address, workBufferRef.address, 0)
|
|
65
68
|
|
|
66
|
-
|
|
69
|
+
const bins = binsBufferRef.view.slice(0, fftOrder + 2)
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
yield bins
|
|
72
|
+
}
|
|
70
73
|
|
|
71
|
-
|
|
74
|
+
m._pffft_destroy_setup(statePtr)
|
|
72
75
|
|
|
73
|
-
|
|
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
|
|
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 {
|
|
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
|
|
45
|
-
const {
|
|
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
|
|
48
|
-
let mfccs =
|
|
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
|
|
76
|
-
const melBandCount =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
21
|
+
return computeMelSpectrogramUsingFilterbanks(rawAudio, fftOrder, windowSize, hopLength, melFilterbanks, windowType)
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function
|
|
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
|
|
32
|
-
const
|
|
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 {
|
|
36
|
+
return { melSpectrogram, fftFrames }
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function
|
|
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
|
}
|
package/src/math/VectorMath.ts
CHANGED
|
@@ -643,7 +643,7 @@ export function sumVector(vector: ArrayLike<number>) {
|
|
|
643
643
|
return result
|
|
644
644
|
}
|
|
645
645
|
|
|
646
|
-
export function
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
1254
|
+
// Compute Mel spectrogram
|
|
1255
|
+
const { melSpectrogram } = await computeMelSpectrogramUsingFilterbanks(rawAudioPart, fftOrder, fftWindowSize, fftHopLength, filterbanks)
|
|
1247
1256
|
|
|
1248
|
-
|
|
1257
|
+
// Flatten, transpose, apply logarithm and normalize Mel spectrogram
|
|
1258
|
+
await logger.startAsync('Process Mel spectrogram')
|
|
1249
1259
|
|
|
1250
|
-
const
|
|
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 (
|
|
1256
|
-
for (
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
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
|
|
1285
|
+
await logger.startAsync('Encode Mel spectrogram with Whisper encoder model')
|
|
1278
1286
|
|
|
1279
|
-
const inputTensor = new Onnx.Tensor('float32',
|
|
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
|
-
|
|
1492
|
-
|
|
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 =
|
|
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
|
-
'
|
|
2155
|
+
'tiny.en': [6, 12, 17, 18, 19, 20, 21, 22,],
|
|
2133
2156
|
'base': [25, 34, 35, 39, 41, 42, 44, 46,],
|
|
2134
|
-
'
|
|
2157
|
+
'base.en': [27, 39, 41, 45, 47,],
|
|
2135
2158
|
'small': [63, 69, 96, 100, 103, 104, 108, 115, 117, 125,],
|
|
2136
|
-
'
|
|
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,
|
|
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 =
|
|
78
|
-
const fftFramesRightGenerator =
|
|
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 =
|
|
98
|
+
const nextLeftFrameResult = fftFramesLeftGenerator.next()
|
|
99
99
|
|
|
100
100
|
if (nextLeftFrameResult.done) {
|
|
101
101
|
break
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const nextRightFrameResult =
|
|
104
|
+
const nextRightFrameResult = fftFramesRightGenerator.next()
|
|
105
105
|
|
|
106
106
|
if (nextRightFrameResult.done) {
|
|
107
107
|
break
|