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.
Files changed (36) hide show
  1. package/data/schemas/options.json +3 -0
  2. package/dist/api/LanguageDetection.js +1 -1
  3. package/dist/api/LanguageDetection.js.map +1 -1
  4. package/dist/api/Recognition.js +1 -1
  5. package/dist/api/Recognition.js.map +1 -1
  6. package/dist/api/Translation.js +1 -1
  7. package/dist/api/Translation.js.map +1 -1
  8. package/dist/api/TranslationAlignment.js +1 -1
  9. package/dist/api/TranslationAlignment.js.map +1 -1
  10. package/dist/audio/AudioBufferConversion.d.ts +2 -2
  11. package/dist/audio/AudioBufferConversion.js +46 -36
  12. package/dist/audio/AudioBufferConversion.js.map +1 -1
  13. package/dist/cli/CLI.js +1 -0
  14. package/dist/cli/CLI.js.map +1 -1
  15. package/dist/codecs/FFMpegTranscoder.js +2 -1
  16. package/dist/codecs/FFMpegTranscoder.js.map +1 -1
  17. package/dist/codecs/WaveCodec.js +8 -3
  18. package/dist/codecs/WaveCodec.js.map +1 -1
  19. package/dist/recognition/WhisperSTT.d.ts +1 -0
  20. package/dist/recognition/WhisperSTT.js +13 -6
  21. package/dist/recognition/WhisperSTT.js.map +1 -1
  22. package/dist/utilities/Compression.js +1 -1
  23. package/dist/utilities/Compression.js.map +1 -1
  24. package/docs/Options.md +1 -0
  25. package/docs/Tasklist.md +4 -3
  26. package/package.json +6 -6
  27. package/src/api/LanguageDetection.ts +1 -1
  28. package/src/api/Recognition.ts +1 -1
  29. package/src/api/Translation.ts +1 -1
  30. package/src/api/TranslationAlignment.ts +1 -1
  31. package/src/audio/AudioBufferConversion.ts +46 -36
  32. package/src/cli/CLI.ts +1 -0
  33. package/src/codecs/FFMpegTranscoder.ts +3 -1
  34. package/src/codecs/WaveCodec.ts +11 -3
  35. package/src/recognition/WhisperSTT.ts +14 -7
  36. 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
- if (targetSampleFormat == SampleFormat.PCM) {
12
- if (targetBitDepth == 8) {
13
- return Buffer.from(float32ToUint8Pcm(interleavedChannels).buffer)
14
- } else if (targetBitDepth == 16) {
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 == 24) {
18
+ } else if (targetBitDepth === 24) {
17
19
  return BinaryArrayConversion.int24ToBufferLE(float32ToInt24Pcm(interleavedChannels))
18
- } else if (targetBitDepth == 32) {
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 == SampleFormat.Float) {
24
- if (targetBitDepth == 32) {
25
+ } else if (targetSampleFormat === SampleFormat.Float) {
26
+ if (targetBitDepth === 32) {
25
27
  return BinaryArrayConversion.float32ToBufferLE(interleavedChannels)
26
- } else if (targetBitDepth == 64) {
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 == SampleFormat.Alaw) {
32
- if (targetBitDepth == 8) {
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 == SampleFormat.Mulaw) {
38
- if (targetBitDepth == 8) {
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 == SampleFormat.PCM) {
52
- if (sourceBitDepth == 8) {
53
- interleavedChannels = uint8PcmToFloat32(audioBuffer)
54
- } else if (sourceBitDepth == 16) {
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 == 24) {
58
+ } else if (sourceBitDepth === 24) {
57
59
  interleavedChannels = int24PcmToFloat32(BinaryArrayConversion.bufferLEToInt24(audioBuffer))
58
- } else if (sourceBitDepth == 32) {
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 == SampleFormat.Float) {
64
- if (sourceBitDepth == 32) {
65
+ } else if (sourceSampleFormat === SampleFormat.Float) {
66
+ if (sourceBitDepth === 32) {
65
67
  interleavedChannels = BinaryArrayConversion.bufferLEToFloat32(audioBuffer)
66
- } else if (sourceBitDepth == 64) {
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 == SampleFormat.Alaw) {
72
- if (sourceBitDepth == 8) {
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 == SampleFormat.Mulaw) {
78
- if (sourceBitDepth == 8) {
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 uint8PcmToFloat32(input: Uint8Array) {
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] - 128
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 float32ToUint8Pcm(input: Float32Array) {
103
- const output = new Uint8Array(input.length)
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] = ((sample < 0 ? sample * 128 : sample * 127) | 0) + 128
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 == 0) {
192
+ if (channelCount === 0) {
189
193
  throw new Error('Empty channel array received')
190
194
  }
191
195
 
192
- if (channelCount == 1) {
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 == 0) {
216
+ if (channelCount === 0) {
213
217
  throw new Error('0 channel count received')
214
218
  }
215
219
 
216
- if (channelCount == 1) {
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
- return Math.max(-1, Math.min(floatSample, 1))
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
- resolve(Buffer.concat(stdoutChunks))
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)
@@ -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
- // If the data chunk is truncated, but truncations are ignored,
95
- // it would be read up to the end of the buffer
96
- dataBuffers.push(waveData.subarray(readOffset, readOffset + subChunkSize))
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
- // Update previous text tokens
461
- previousPartTextTokens = partTokens.filter(token => this.isTextToken(token))
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: compressedStringBytes.length / originalStringBytes.length
110
+ ratio: originalStringBytes.length / compressedStringBytes.length
111
111
  }
112
112
  }