echogarden 1.3.2 → 1.3.3
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/data/schemas/options.json +3 -0
- package/dist/api/LanguageDetection.js +1 -1
- package/dist/api/LanguageDetection.js.map +1 -1
- package/dist/api/Recognition.js +1 -1
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/Translation.js +1 -1
- package/dist/api/Translation.js.map +1 -1
- package/dist/api/TranslationAlignment.js +1 -1
- package/dist/api/TranslationAlignment.js.map +1 -1
- package/dist/audio/AudioBufferConversion.d.ts +2 -2
- package/dist/audio/AudioBufferConversion.js +46 -36
- package/dist/audio/AudioBufferConversion.js.map +1 -1
- package/dist/cli/CLI.js +1 -0
- package/dist/cli/CLI.js.map +1 -1
- package/dist/codecs/FFMpegTranscoder.js +2 -1
- package/dist/codecs/FFMpegTranscoder.js.map +1 -1
- package/dist/codecs/WaveCodec.js +8 -3
- package/dist/codecs/WaveCodec.js.map +1 -1
- package/dist/recognition/WhisperSTT.d.ts +1 -0
- package/dist/recognition/WhisperSTT.js +13 -6
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/utilities/Compression.js +1 -1
- package/dist/utilities/Compression.js.map +1 -1
- package/docs/Options.md +1 -0
- package/docs/Tasklist.md +4 -3
- package/package.json +6 -6
- package/src/api/LanguageDetection.ts +1 -1
- package/src/api/Recognition.ts +1 -1
- package/src/api/Translation.ts +1 -1
- package/src/api/TranslationAlignment.ts +1 -1
- package/src/audio/AudioBufferConversion.ts +46 -36
- package/src/cli/CLI.ts +1 -0
- package/src/codecs/FFMpegTranscoder.ts +3 -1
- package/src/codecs/WaveCodec.ts +11 -3
- package/src/recognition/WhisperSTT.ts +14 -7
- package/src/utilities/Compression.ts +1 -1
|
@@ -8,34 +8,36 @@ import { BitDepth, SampleFormat } from '../codecs/WaveCodec.js'
|
|
|
8
8
|
export function encodeToAudioBuffer(audioChannels: Float32Array[], targetBitDepth: BitDepth = 16, targetSampleFormat: SampleFormat = SampleFormat.PCM) {
|
|
9
9
|
const interleavedChannels = interleaveChannels(audioChannels)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
audioChannels = [] // Zero the array references to allow the GC to free up memory, if possible
|
|
12
|
+
|
|
13
|
+
if (targetSampleFormat === SampleFormat.PCM) {
|
|
14
|
+
if (targetBitDepth === 8) {
|
|
15
|
+
return BinaryArrayConversion.int8ToBuffer(float32ToInt8Pcm(interleavedChannels))
|
|
16
|
+
} else if (targetBitDepth === 16) {
|
|
15
17
|
return BinaryArrayConversion.int16ToBufferLE(float32ToInt16Pcm(interleavedChannels))
|
|
16
|
-
} else if (targetBitDepth
|
|
18
|
+
} else if (targetBitDepth === 24) {
|
|
17
19
|
return BinaryArrayConversion.int24ToBufferLE(float32ToInt24Pcm(interleavedChannels))
|
|
18
|
-
} else if (targetBitDepth
|
|
20
|
+
} else if (targetBitDepth === 32) {
|
|
19
21
|
return BinaryArrayConversion.int32ToBufferLE(float32ToInt32Pcm(interleavedChannels))
|
|
20
22
|
} else {
|
|
21
23
|
throw new Error(`Unsupported PCM bit depth: ${targetBitDepth}`)
|
|
22
24
|
}
|
|
23
|
-
} else if (targetSampleFormat
|
|
24
|
-
if (targetBitDepth
|
|
25
|
+
} else if (targetSampleFormat === SampleFormat.Float) {
|
|
26
|
+
if (targetBitDepth === 32) {
|
|
25
27
|
return BinaryArrayConversion.float32ToBufferLE(interleavedChannels)
|
|
26
|
-
} else if (targetBitDepth
|
|
28
|
+
} else if (targetBitDepth === 64) {
|
|
27
29
|
return BinaryArrayConversion.float64ToBufferLE(BinaryArrayConversion.float32Tofloat64(interleavedChannels))
|
|
28
30
|
} else {
|
|
29
31
|
throw new Error(`Unsupported float bit depth: ${targetBitDepth}`)
|
|
30
32
|
}
|
|
31
|
-
} else if (targetSampleFormat
|
|
32
|
-
if (targetBitDepth
|
|
33
|
+
} else if (targetSampleFormat === SampleFormat.Alaw) {
|
|
34
|
+
if (targetBitDepth === 8) {
|
|
33
35
|
return Buffer.from(AlawMulaw.alaw.encode(float32ToInt16Pcm(interleavedChannels)))
|
|
34
36
|
} else {
|
|
35
37
|
throw new Error(`Unsupported alaw bit depth: ${targetBitDepth}`)
|
|
36
38
|
}
|
|
37
|
-
} else if (targetSampleFormat
|
|
38
|
-
if (targetBitDepth
|
|
39
|
+
} else if (targetSampleFormat === SampleFormat.Mulaw) {
|
|
40
|
+
if (targetBitDepth === 8) {
|
|
39
41
|
return Buffer.from(AlawMulaw.mulaw.encode(float32ToInt16Pcm(interleavedChannels)))
|
|
40
42
|
} else {
|
|
41
43
|
throw new Error(`Unsupported mulaw bit depth: ${targetBitDepth}`)
|
|
@@ -48,34 +50,34 @@ export function encodeToAudioBuffer(audioChannels: Float32Array[], targetBitDept
|
|
|
48
50
|
export function decodeToChannels(audioBuffer: Buffer, channelCount: number, sourceBitDepth: number, sourceSampleFormat: SampleFormat) {
|
|
49
51
|
let interleavedChannels: Float32Array
|
|
50
52
|
|
|
51
|
-
if (sourceSampleFormat
|
|
52
|
-
if (sourceBitDepth
|
|
53
|
-
interleavedChannels =
|
|
54
|
-
} else if (sourceBitDepth
|
|
53
|
+
if (sourceSampleFormat === SampleFormat.PCM) {
|
|
54
|
+
if (sourceBitDepth === 8) {
|
|
55
|
+
interleavedChannels = int8PcmToFloat32(BinaryArrayConversion.bufferToInt8(audioBuffer))
|
|
56
|
+
} else if (sourceBitDepth === 16) {
|
|
55
57
|
interleavedChannels = int16PcmToFloat32(BinaryArrayConversion.bufferLEToInt16(audioBuffer))
|
|
56
|
-
} else if (sourceBitDepth
|
|
58
|
+
} else if (sourceBitDepth === 24) {
|
|
57
59
|
interleavedChannels = int24PcmToFloat32(BinaryArrayConversion.bufferLEToInt24(audioBuffer))
|
|
58
|
-
} else if (sourceBitDepth
|
|
60
|
+
} else if (sourceBitDepth === 32) {
|
|
59
61
|
interleavedChannels = int32PcmToFloat32(BinaryArrayConversion.bufferLEToInt32(audioBuffer))
|
|
60
62
|
} else {
|
|
61
63
|
throw new Error(`Unsupported PCM bit depth: ${sourceBitDepth}`)
|
|
62
64
|
}
|
|
63
|
-
} else if (sourceSampleFormat
|
|
64
|
-
if (sourceBitDepth
|
|
65
|
+
} else if (sourceSampleFormat === SampleFormat.Float) {
|
|
66
|
+
if (sourceBitDepth === 32) {
|
|
65
67
|
interleavedChannels = BinaryArrayConversion.bufferLEToFloat32(audioBuffer)
|
|
66
|
-
} else if (sourceBitDepth
|
|
68
|
+
} else if (sourceBitDepth === 64) {
|
|
67
69
|
interleavedChannels = BinaryArrayConversion.float64Tofloat32(BinaryArrayConversion.bufferLEToFloat64(audioBuffer))
|
|
68
70
|
} else {
|
|
69
71
|
throw new Error(`Unsupported float bit depth: ${sourceBitDepth}`)
|
|
70
72
|
}
|
|
71
|
-
} else if (sourceSampleFormat
|
|
72
|
-
if (sourceBitDepth
|
|
73
|
+
} else if (sourceSampleFormat === SampleFormat.Alaw) {
|
|
74
|
+
if (sourceBitDepth === 8) {
|
|
73
75
|
interleavedChannels = int16PcmToFloat32(AlawMulaw.alaw.decode(audioBuffer))
|
|
74
76
|
} else {
|
|
75
77
|
throw new Error(`Unsupported alaw bit depth: ${sourceBitDepth}`)
|
|
76
78
|
}
|
|
77
|
-
} else if (sourceSampleFormat
|
|
78
|
-
if (sourceBitDepth
|
|
79
|
+
} else if (sourceSampleFormat === SampleFormat.Mulaw) {
|
|
80
|
+
if (sourceBitDepth === 8) {
|
|
79
81
|
interleavedChannels = int16PcmToFloat32(AlawMulaw.mulaw.decode(audioBuffer))
|
|
80
82
|
} else {
|
|
81
83
|
throw new Error(`Unsupported mulaw bit depth: ${sourceBitDepth}`)
|
|
@@ -84,27 +86,29 @@ export function decodeToChannels(audioBuffer: Buffer, channelCount: number, sour
|
|
|
84
86
|
throw new Error(`Unsupported audio format: ${sourceSampleFormat}`)
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
audioBuffer = Buffer.from([]) // Zero the buffer reference to allow the GC to free up memory, if possible
|
|
90
|
+
|
|
87
91
|
return deInterleaveChannels(interleavedChannels, channelCount)
|
|
88
92
|
}
|
|
89
93
|
|
|
90
94
|
// Int8 PCM <-> Float32 conversion
|
|
91
|
-
export function
|
|
95
|
+
export function int8PcmToFloat32(input: Int8Array) {
|
|
92
96
|
const output = new Float32Array(input.length)
|
|
93
97
|
|
|
94
98
|
for (let i = 0; i < input.length; i++) {
|
|
95
|
-
const sample = input[i]
|
|
99
|
+
const sample = input[i]
|
|
96
100
|
output[i] = sample < 0 ? sample / 128 : sample / 127
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
return output
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
export function
|
|
103
|
-
const output = new
|
|
106
|
+
export function float32ToInt8Pcm(input: Float32Array) {
|
|
107
|
+
const output = new Int8Array(input.length)
|
|
104
108
|
|
|
105
109
|
for (let i = 0; i < input.length; i++) {
|
|
106
110
|
const sample = clampFloatSample(input[i])
|
|
107
|
-
output[i] = (
|
|
111
|
+
output[i] = (sample < 0 ? sample * 128 : sample * 127) | 0
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
return output
|
|
@@ -185,11 +189,11 @@ export function float32ToInt32Pcm(input: Float32Array) {
|
|
|
185
189
|
export function interleaveChannels(channels: Float32Array[]) {
|
|
186
190
|
const channelCount = channels.length
|
|
187
191
|
|
|
188
|
-
if (channelCount
|
|
192
|
+
if (channelCount === 0) {
|
|
189
193
|
throw new Error('Empty channel array received')
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
if (channelCount
|
|
196
|
+
if (channelCount === 1) {
|
|
193
197
|
return channels[0]
|
|
194
198
|
}
|
|
195
199
|
|
|
@@ -209,11 +213,11 @@ export function interleaveChannels(channels: Float32Array[]) {
|
|
|
209
213
|
}
|
|
210
214
|
|
|
211
215
|
export function deInterleaveChannels(interleavedChannels: Float32Array, channelCount: number) {
|
|
212
|
-
if (channelCount
|
|
216
|
+
if (channelCount === 0) {
|
|
213
217
|
throw new Error('0 channel count received')
|
|
214
218
|
}
|
|
215
219
|
|
|
216
|
-
if (channelCount
|
|
220
|
+
if (channelCount === 1) {
|
|
217
221
|
return [interleavedChannels]
|
|
218
222
|
}
|
|
219
223
|
|
|
@@ -244,5 +248,11 @@ export function deInterleaveChannels(interleavedChannels: Float32Array, channelC
|
|
|
244
248
|
// Utilities
|
|
245
249
|
/////////////////////////////////////////////////////////////////////////////////////////////
|
|
246
250
|
export function clampFloatSample(floatSample: number) {
|
|
247
|
-
|
|
251
|
+
if (floatSample < -1.0) {
|
|
252
|
+
return -1.0
|
|
253
|
+
} else if (floatSample > 1.0) {
|
|
254
|
+
return 1.0
|
|
255
|
+
} else {
|
|
256
|
+
return floatSample
|
|
257
|
+
}
|
|
248
258
|
}
|
package/src/cli/CLI.ts
CHANGED
|
@@ -425,6 +425,7 @@ async function speak(operationData: CLIOperationData) {
|
|
|
425
425
|
} else if (sourceFileExtension == 'srt' || sourceFileExtension == 'vtt') {
|
|
426
426
|
const fileContent = await readFile(sourceFile, { encoding: 'utf-8' })
|
|
427
427
|
textSegments = subtitlesToTimeline(fileContent).map(entry => entry.text)
|
|
428
|
+
//textSegments = [subtitlesToText(fileContent)]
|
|
428
429
|
} else if (sourceFileExtension == 'xml' || sourceFileExtension == 'ssml') {
|
|
429
430
|
options.ssml = true
|
|
430
431
|
textSegments = [fileContent]
|
|
@@ -88,7 +88,9 @@ async function transcode_CLI(ffmpegCommand: string, input: string | Buffer, outp
|
|
|
88
88
|
|
|
89
89
|
process.on('close', (exitCode) => {
|
|
90
90
|
if (exitCode == 0) {
|
|
91
|
-
|
|
91
|
+
const concatenatedChunks = Buffer.concat(stdoutChunks)
|
|
92
|
+
|
|
93
|
+
resolve(concatenatedChunks)
|
|
92
94
|
} else {
|
|
93
95
|
reject(`ffmpeg exited with code ${exitCode}`)
|
|
94
96
|
log(stderrOutput)
|
package/src/codecs/WaveCodec.ts
CHANGED
|
@@ -87,13 +87,15 @@ export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = true, ignor
|
|
|
87
87
|
throw new Error('A data subchunk was encountered before a format subchunk')
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// If the data chunk is truncated or extended beyond 4 GiB,
|
|
91
|
+
// the data would be read up to the end of the buffer
|
|
90
92
|
if (ignoreOverflowingDataChunks && subChunkSize === 4294967295) {
|
|
91
93
|
subChunkSize = waveData.length - readOffset
|
|
92
94
|
}
|
|
93
95
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
dataBuffers.push(
|
|
96
|
+
const subChunkData = waveData.subarray(readOffset, readOffset + subChunkSize)
|
|
97
|
+
|
|
98
|
+
dataBuffers.push(subChunkData)
|
|
97
99
|
}
|
|
98
100
|
// All sub chunks other than 'data' (e.g. 'LIST', 'fact', 'plst', 'junk' etc.) are ignored
|
|
99
101
|
|
|
@@ -111,6 +113,10 @@ export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = true, ignor
|
|
|
111
113
|
throw new Error('No format subchunk was found in the wave file')
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
if (dataBuffers.length === 0) {
|
|
117
|
+
throw new Error('No data subchunks were found in the wave file')
|
|
118
|
+
}
|
|
119
|
+
|
|
114
120
|
const waveFormat = WaveFormat.deserializeFrom(formatSubChunkBodyBuffer)
|
|
115
121
|
|
|
116
122
|
const sampleFormat = waveFormat.sampleFormat
|
|
@@ -120,6 +126,8 @@ export function decodeWave(waveData: Buffer, ignoreTruncatedChunks = true, ignor
|
|
|
120
126
|
const speakerPositionMask = waveFormat.speakerPositionMask
|
|
121
127
|
|
|
122
128
|
const concatenatedDataBuffers = Buffer.concat(dataBuffers)
|
|
129
|
+
dataBuffers.length = 0 // Allow the garbage collector to free up memory held by the data buffers
|
|
130
|
+
|
|
123
131
|
const audioChannels = AudioBufferConversion.decodeToChannels(concatenatedDataBuffers, channelCount, bitDepth, sampleFormat)
|
|
124
132
|
|
|
125
133
|
return {
|
|
@@ -442,11 +442,6 @@ export class Whisper {
|
|
|
442
442
|
partTokensConfidence = partTokensConfidence.slice(initialTokens.length)
|
|
443
443
|
partCrossAttentionQKs = partCrossAttentionQKs.slice(initialTokens.length)
|
|
444
444
|
|
|
445
|
-
// Compute compression ratio for part (disabled for now)
|
|
446
|
-
if (false) {
|
|
447
|
-
const compressionRatioForPart = (await getDeflateCompressionMetricsForString(this.tokensToText(partTokens))).ratio
|
|
448
|
-
}
|
|
449
|
-
|
|
450
445
|
// Find alignment path
|
|
451
446
|
const alignmentPath = await this.findAlignmentPathFromQKs(partCrossAttentionQKs, partTokens, 0, segmentFrameCount) //, alignmentHeadsIndexes[this.modelName])
|
|
452
447
|
|
|
@@ -457,8 +452,17 @@ export class Whisper {
|
|
|
457
452
|
allDecodedTokens.push(...partTokens)
|
|
458
453
|
timeline.push(...partTimeline)
|
|
459
454
|
|
|
460
|
-
//
|
|
461
|
-
|
|
455
|
+
// Determine compression ratio for recognized text (normalized to lowercase) of this part
|
|
456
|
+
const compressionRatioForPart = (await getDeflateCompressionMetricsForString(this.tokensToText(partTokens).toLocaleLowerCase())).ratio
|
|
457
|
+
|
|
458
|
+
// If the recognized text isn't too repetitive
|
|
459
|
+
if (compressionRatioForPart < options.repetitionThreshold!) {
|
|
460
|
+
// Set current part tokens as the previous part text tokens
|
|
461
|
+
previousPartTextTokens = partTokens.filter(token => this.isTextToken(token))
|
|
462
|
+
} else {
|
|
463
|
+
// Otherwise, set previous part tokens to an empty array
|
|
464
|
+
previousPartTextTokens = []
|
|
465
|
+
}
|
|
462
466
|
|
|
463
467
|
audioOffset = audioEndOffset
|
|
464
468
|
|
|
@@ -544,6 +548,7 @@ export class Whisper {
|
|
|
544
548
|
autoPromptParts: false,
|
|
545
549
|
maxTokensPerPart: Infinity,
|
|
546
550
|
suppressRepetition: false,
|
|
551
|
+
repetitionThreshold: Infinity,
|
|
547
552
|
decodeTimestampTokens: true,
|
|
548
553
|
endTokenThreshold: whisperAlignmentOptions!.endTokenThreshold!,
|
|
549
554
|
includeEndTokenInCandidates: false,
|
|
@@ -2111,6 +2116,7 @@ export interface WhisperOptions {
|
|
|
2111
2116
|
autoPromptParts?: boolean
|
|
2112
2117
|
maxTokensPerPart?: number
|
|
2113
2118
|
suppressRepetition?: boolean
|
|
2119
|
+
repetitionThreshold?: number
|
|
2114
2120
|
decodeTimestampTokens?: boolean
|
|
2115
2121
|
endTokenThreshold?: number
|
|
2116
2122
|
includeEndTokenInCandidates?: boolean
|
|
@@ -2128,6 +2134,7 @@ export const defaultWhisperOptions: WhisperOptions = {
|
|
|
2128
2134
|
autoPromptParts: true,
|
|
2129
2135
|
maxTokensPerPart: 250,
|
|
2130
2136
|
suppressRepetition: true,
|
|
2137
|
+
repetitionThreshold: 2.4,
|
|
2131
2138
|
decodeTimestampTokens: true,
|
|
2132
2139
|
endTokenThreshold: 0.9,
|
|
2133
2140
|
includeEndTokenInCandidates: true,
|
|
@@ -107,6 +107,6 @@ export async function getDeflateCompressionMetricsForString(str: string) {
|
|
|
107
107
|
return {
|
|
108
108
|
originalSize: originalStringBytes.length,
|
|
109
109
|
compressedSize: compressedStringBytes.length,
|
|
110
|
-
ratio:
|
|
110
|
+
ratio: originalStringBytes.length / compressedStringBytes.length
|
|
111
111
|
}
|
|
112
112
|
}
|