echogarden 2.8.6 → 2.8.7

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.
@@ -3,7 +3,7 @@ import type * as Onnx from 'onnxruntime-node'
3
3
  import { Logger } from '../utilities/Logger.js'
4
4
  import { computeMelSpectogramUsingFilterbanks, Filterbank } from '../dsp/MelSpectogram.js'
5
5
  import { clip, getIntegerRange, getTopKIndexes, splitFloat32Array, yieldToEventLoop } from '../utilities/Utilities.js'
6
- import { indexOfMax, logOfVector, logSumExp, meanOfVector, softmax, sumOfSquaresForVector, sumVector } from '../math/VectorMath.js'
6
+ import { indexOfMax, logOfVector, logSumExp, meanOfVector, medianOfVector, softmax, sumOfSquaresForVector, sumVector } from '../math/VectorMath.js'
7
7
 
8
8
  import { alignDTWWindowed } from '../alignment/DTWSequenceAlignmentWindowed.js'
9
9
  import { extendDeep } from '../utilities/ObjectUtilities.js'
@@ -11,7 +11,7 @@ import { Timeline, TimelineEntry } from '../utilities/Timeline.js'
11
11
  import { AlignmentPath } from '../alignment/SpeechAlignment.js'
12
12
  import { getRawAudioDuration, RawAudio, sliceRawAudio } from '../audio/AudioUtilities.js'
13
13
  import { readFileAsUtf8 } from '../utilities/FileSystem.js'
14
- import type { LanguageDetectionResults } from '../api/API.js'
14
+ import { logLevelGreaterOrEqualTo, type LanguageDetectionResults } from '../api/API.js'
15
15
  import { formatLanguageCodeWithName, getShortLanguageCode, languageCodeToName } from '../utilities/Locale.js'
16
16
  import { loadPackage } from '../utilities/PackageManager.js'
17
17
  import chalk from 'chalk'
@@ -25,6 +25,7 @@ import { dmlProviderAvailable, getOnnxSessionOptions, makeOnnxLikeFloat32Tensor,
25
25
  import { murmurHash3_int32Input } from '../utilities/Hashing.js'
26
26
  import { containsInvalidCodepoint, getTokenRepetitionScore } from '../utilities/StringUtilities.js'
27
27
  import { joinPath } from '../utilities/PathUtilities.js'
28
+ import { Timer } from '../utilities/Timer.js'
28
29
 
29
30
  export async function recognize(
30
31
  sourceRawAudio: RawAudio,
@@ -36,7 +37,7 @@ export async function recognize(
36
37
 
37
38
  options = extendDeep(defaultWhisperOptions, options)
38
39
 
39
- if (sourceRawAudio.sampleRate != 16000) {
40
+ if (sourceRawAudio.sampleRate !== 16000) {
40
41
  throw new Error('Source audio must have a sample rate of 16000 Hz')
41
42
  }
42
43
 
@@ -46,7 +47,7 @@ export async function recognize(
46
47
  throw new Error(`The language ${formatLanguageCodeWithName(sourceLanguage)} is not supported by the Whisper engine.`)
47
48
  }
48
49
 
49
- if (isEnglishOnlyModel(modelName) && sourceLanguage != 'en') {
50
+ if (isEnglishOnlyModel(modelName) && sourceLanguage !== 'en') {
50
51
  throw new Error(`The model '${modelName}' can only be used with English inputs. However, the given source language was ${languageCodeToName(sourceLanguage)}.`)
51
52
  }
52
53
 
@@ -60,7 +61,7 @@ export async function recognize(
60
61
 
61
62
  // Workaround issue with large-v3-turbo that produces invalid results when a prompt is passed to it.
62
63
  // Always disable autoprompting for that model.
63
- if (options.autoPromptParts && modelName == 'large-v3-turbo') {
64
+ if (options.autoPromptParts && modelName === 'large-v3-turbo') {
64
65
  options.autoPromptParts = false
65
66
  }
66
67
 
@@ -96,7 +97,7 @@ export async function align(
96
97
 
97
98
  options = extendDeep(defaultWhisperAlignmentOptions, options)
98
99
 
99
- if (sourceRawAudio.sampleRate != 16000) {
100
+ if (sourceRawAudio.sampleRate !== 16000) {
100
101
  throw new Error('Source audio must have a sample rate of 16000 Hz')
101
102
  }
102
103
 
@@ -106,7 +107,7 @@ export async function align(
106
107
  throw new Error(`The language ${formatLanguageCodeWithName(sourceLanguage)} is not supported by the Whisper engine.`)
107
108
  }
108
109
 
109
- if (isEnglishOnlyModel(modelName) && sourceLanguage != 'en') {
110
+ if (isEnglishOnlyModel(modelName) && sourceLanguage !== 'en') {
110
111
  throw new Error(`The model '${modelName}' can only be used with English inputs. However, the given source language was ${languageCodeToName(sourceLanguage)}.`)
111
112
  }
112
113
 
@@ -139,7 +140,7 @@ export async function alignEnglishTranslation(
139
140
 
140
141
  options = extendDeep(defaultWhisperAlignmentOptions, options)
141
142
 
142
- if (sourceRawAudio.sampleRate != 16000) {
143
+ if (sourceRawAudio.sampleRate !== 16000) {
143
144
  throw new Error('Source audio must have a sample rate of 16000 Hz')
144
145
  }
145
146
 
@@ -184,7 +185,7 @@ export async function detectLanguage(
184
185
 
185
186
  options = extendDeep(defaultWhisperLanguageDetectionOptions, options)
186
187
 
187
- if (sourceRawAudio.sampleRate != 16000) {
188
+ if (sourceRawAudio.sampleRate !== 16000) {
188
189
  throw new Error('Source audio must have a sample rate of 16000 Hz')
189
190
  }
190
191
 
@@ -232,7 +233,7 @@ export async function detectVoiceActivity(
232
233
 
233
234
  options = extendDeep(defaultWhisperVADOptions, options)
234
235
 
235
- if (sourceRawAudio.sampleRate != 16000) {
236
+ if (sourceRawAudio.sampleRate !== 16000) {
236
237
  throw new Error('Source audio must have a sample rate of 16000 Hz')
237
238
  }
238
239
 
@@ -441,7 +442,11 @@ export class Whisper {
441
442
  let {
442
443
  decodedTokens: partTokens,
443
444
  decodedTokensConfidence: partTokensConfidence,
444
- decodedTokensCrossAttentionQKs: partCrossAttentionQKs,
445
+ decodedTokensCrossAttentionQKs: partTokensCrossAttentionQKs,
446
+
447
+ decodedTokensDecodingTime: partTokensDecodingTime,
448
+ decodedTokensInferenceTime: partTokensInferenceTime,
449
+ decodedTokensOverheadTime: partTokensOverheadTime,
445
450
  } = await this.decodeTokens(
446
451
  audioPartFeatures,
447
452
  initialTokens,
@@ -470,19 +475,19 @@ export class Whisper {
470
475
 
471
476
  await logger.startAsync(`Extract timeline for part (timestamp accuracy: ${options.timestampAccuracy!})`)
472
477
 
473
- if (partTokens.length != partCrossAttentionQKs.length) {
474
- throw new Error('Unexpected: partTokens.length != partCrossAttentionQKs.length')
478
+ if (partTokens.length !== partTokensCrossAttentionQKs.length) {
479
+ throw new Error('Unexpected: partTokens.length !== partCrossAttentionQKs.length')
475
480
  }
476
481
 
477
482
  // Prepare tokens
478
483
  partTokens = partTokens.slice(initialTokens.length)
479
484
  partTokensConfidence = partTokensConfidence.slice(initialTokens.length)
480
- partCrossAttentionQKs = partCrossAttentionQKs.slice(initialTokens.length)
485
+ partTokensCrossAttentionQKs = partTokensCrossAttentionQKs.slice(initialTokens.length)
481
486
 
482
487
  // Find alignment path
483
488
  let alignmentHeads: number[] | undefined
484
489
 
485
- if (options.timestampAccuracy === 'medium' || options.model == 'large-v3-turbo') {
490
+ if (options.timestampAccuracy === 'medium' || options.model === 'large-v3-turbo') {
486
491
  alignmentHeads = this.alignmentHeadIndexes
487
492
  } else if (options.timestampAccuracy === 'high') {
488
493
  alignmentHeads = undefined
@@ -490,7 +495,7 @@ export class Whisper {
490
495
  throw new Error(`Unsupported timestamp accuracy '${options.timestampAccuracy}', can only be 'medium' or 'high'.`)
491
496
  }
492
497
 
493
- const alignmentPath = await this.findAlignmentPathFromQKs(partCrossAttentionQKs, partTokens, 0, segmentFrameCount, alignmentHeads)
498
+ const alignmentPath = await this.findAlignmentPathFromQKs(partTokensCrossAttentionQKs, partTokens, 0, segmentFrameCount, alignmentHeads)
494
499
 
495
500
  // Generate timeline from alignment path
496
501
  const partTimeline = await this.getTokenTimelineFromAlignmentPath(alignmentPath, partTokens, segmentStartTime, segmentEndTime, partTokensConfidence)
@@ -514,6 +519,15 @@ export class Whisper {
514
519
  audioOffset = audioEndOffset
515
520
 
516
521
  logger.end()
522
+
523
+ if (logLevelGreaterOrEqualTo('trace')) {
524
+ const promptDecodingTime = partTokensDecodingTime[0]
525
+ const medianTokenDecodingTime = medianOfVector(partTokensDecodingTime.slice(1))
526
+ const medianTokenInferenceTime = medianOfVector(partTokensInferenceTime.slice(1))
527
+ const medianOverheadTime = medianOfVector(partTokensOverheadTime.slice(1))
528
+
529
+ logger.log(`${chalk.blueBright('Context')}: ${initialTokens.length + partTokens.length} tokens (${initialTokens.length} prompt, ${partTokens.length} decoded)\n${chalk.blueBright('Prompt decode time')}: ${promptDecodingTime.toFixed(1)}ms\n${chalk.blueBright('Median token decode time')}: ${medianTokenDecodingTime.toFixed(1)}ms (${medianTokenInferenceTime.toFixed(1)}ms inference, ${medianOverheadTime.toFixed(2)}ms overhead)`, 'trace')
530
+ }
517
531
  }
518
532
 
519
533
  // Convert token timeline to word timeline
@@ -812,8 +826,17 @@ export class Whisper {
812
826
 
813
827
  const maxTokensPerPart = Math.min(options.maxTokensPerPart!, largestMaximumTokensPerPart)
814
828
 
829
+ let decodedTokensInferenceTime: number[] = []
830
+ let decodedTokensDecodingTime: number[] = []
831
+
832
+ const tokenDecodingTimeTimer = new Timer()
833
+
815
834
  // Start decoding loop
816
835
  for (let decodedTokenCount = 0; decodedTokenCount < maxTokensPerPart; decodedTokenCount++) {
836
+ if (decodedTokenCount > 0) {
837
+ decodedTokensDecodingTime.push(tokenDecodingTimeTimer.getElapsedTimeAndRestart())
838
+ }
839
+
817
840
  const isInitialState = decodedTokens.length === initialTokens.length
818
841
  const atLeastOneTextTokenDecoded = decodedTokens.slice(initialTokens.length).some(token => this.isTextToken(token))
819
842
 
@@ -847,9 +870,13 @@ export class Whisper {
847
870
  offset: offsetTensor
848
871
  }
849
872
 
850
- // Run decoder model
873
+ //// Infer with ONNX decoder model
874
+ const tokenInferenceTimeTimer = new Timer()
875
+
851
876
  const decoderOutputs = await this.textDecoder!.run(decoderInputs)
852
877
 
878
+ decodedTokensInferenceTime.push(tokenInferenceTimeTimer.elapsedTime)
879
+
853
880
  // Extract decoder model results
854
881
  const logitsBuffer = decoderOutputs['logits'].data as Float32Array
855
882
  kvCacheTensor = decoderOutputs['output_kv_cache'] as any
@@ -1142,6 +1169,10 @@ export class Whisper {
1142
1169
  await yieldToEventLoop()
1143
1170
  }
1144
1171
 
1172
+ if (decodedTokensDecodingTime.length === decodedTokensInferenceTime.length - 1) {
1173
+ decodedTokensDecodingTime.push(tokenDecodingTimeTimer.getElapsedTimeAndRestart())
1174
+ }
1175
+
1145
1176
  // If at least two timestamp tokens were decoded and it's not the final part,
1146
1177
  // truncate up to the last timestamp token
1147
1178
  if (timestampTokenSeenCount >= 2 && !isFinalPart) {
@@ -1151,8 +1182,13 @@ export class Whisper {
1151
1182
  decodedTokensTimestampLogits = decodedTokensTimestampLogits.slice(0, sliceEndTokenIndex)
1152
1183
  decodedTokensCrossAttentionQKs = decodedTokensCrossAttentionQKs.slice(0, sliceEndTokenIndex)
1153
1184
  decodedTokensConfidence = decodedTokensConfidence.slice(0, sliceEndTokenIndex)
1185
+
1186
+ decodedTokensDecodingTime = decodedTokensDecodingTime.slice(0, sliceEndTokenIndex)
1187
+ decodedTokensInferenceTime = decodedTokensInferenceTime.slice(0, sliceEndTokenIndex)
1154
1188
  }
1155
1189
 
1190
+ const decodedTokensOverheadTime = decodedTokensDecodingTime.map((time, index) => time - decodedTokensInferenceTime[index])
1191
+
1156
1192
  logger.write('\n')
1157
1193
  logger.end()
1158
1194
 
@@ -1162,6 +1198,10 @@ export class Whisper {
1162
1198
  decodedTokensTimestampLogits,
1163
1199
  decodedTokensConfidence,
1164
1200
  decodedTokensCrossAttentionQKs,
1201
+
1202
+ decodedTokensDecodingTime,
1203
+ decodedTokensInferenceTime,
1204
+ decodedTokensOverheadTime,
1165
1205
  }
1166
1206
  }
1167
1207
 
@@ -1186,7 +1226,7 @@ export class Whisper {
1186
1226
  const maxAudioSamples = sampleRate * 30
1187
1227
  const maxAudioFrames = 3000
1188
1228
 
1189
- if (sampleRate != 16000) {
1229
+ if (sampleRate !== 16000) {
1190
1230
  throw new Error('Audio must have a sample rate of 16000 Hz')
1191
1231
  }
1192
1232
 
@@ -1267,7 +1307,7 @@ export class Whisper {
1267
1307
  return isSeparatorCharacter(text[text.length - 1])
1268
1308
  }
1269
1309
 
1270
- if (language != 'zh' && language != 'ja') {
1310
+ if (language !== 'zh' && language !== 'ja') {
1271
1311
  tokenTimeline = tokenTimeline.filter(entry => this.isTextToken(entry.id!))
1272
1312
  }
1273
1313
 
@@ -1282,7 +1322,7 @@ export class Whisper {
1282
1322
  const text = entry.text
1283
1323
  const previousEntryText = previousEntry?.text
1284
1324
 
1285
- if (groups.length == 0 ||
1325
+ if (groups.length === 0 ||
1286
1326
  text === '' ||
1287
1327
  startsWithSeparatorCharacter(text) ||
1288
1328
  (previousEntryText != null && endsWithSeparatorCharacter(previousEntryText))) {
@@ -1346,14 +1386,14 @@ export class Whisper {
1346
1386
  }
1347
1387
 
1348
1388
  async getTokenTimelineFromAlignmentPath(alignmentPath: AlignmentPath, tokens: number[], startTimeOffset: number, endTimeOffset: number, tokensConfidence?: number[], correctionAmount = 0.0) {
1349
- if (alignmentPath.length == 0) {
1389
+ if (alignmentPath.length === 0) {
1350
1390
  return []
1351
1391
  }
1352
1392
 
1353
1393
  const tokenTimeline: Timeline = []
1354
1394
 
1355
1395
  for (let pathIndex = 0; pathIndex < alignmentPath.length; pathIndex++) {
1356
- if (pathIndex != 0 && alignmentPath[pathIndex].source == alignmentPath[pathIndex - 1].source) {
1396
+ if (pathIndex !== 0 && alignmentPath[pathIndex].source === alignmentPath[pathIndex - 1].source) {
1357
1397
  continue
1358
1398
  }
1359
1399
 
@@ -1604,7 +1644,7 @@ export class Whisper {
1604
1644
 
1605
1645
  const onnxSessionOptions = getOnnxSessionOptions({ executionProviders: this.encoderExecutionProviders })
1606
1646
 
1607
- const onnxProvidersString = onnxSessionOptions.executionProviders!.length > 0 ? `${ onnxSessionOptions.executionProviders!.join(', ') }` : `default`
1647
+ const onnxProvidersString = onnxSessionOptions.executionProviders!.length > 0 ? `${onnxSessionOptions.executionProviders!.join(', ')}` : `default`
1608
1648
 
1609
1649
  await logger.startAsync(`Create encoder inference session for model '${this.modelName}' (ONNX provider: ${onnxProvidersString})`)
1610
1650
 
@@ -1642,15 +1682,15 @@ export class Whisper {
1642
1682
  getKvDimensions(groupCount: number, length: number) {
1643
1683
  const modelName = this.modelName
1644
1684
 
1645
- if (modelName == 'tiny' || modelName == 'tiny.en') {
1685
+ if (modelName === 'tiny' || modelName === 'tiny.en') {
1646
1686
  return [8, groupCount, length, 384]
1647
- } else if (modelName == 'base' || modelName == 'base.en') {
1687
+ } else if (modelName === 'base' || modelName === 'base.en') {
1648
1688
  return [12, groupCount, length, 512]
1649
- } else if (modelName == 'small' || modelName == 'small.en') {
1689
+ } else if (modelName === 'small' || modelName === 'small.en') {
1650
1690
  return [24, groupCount, length, 768]
1651
- } else if (modelName == 'medium' || modelName == 'medium.en') {
1691
+ } else if (modelName === 'medium' || modelName === 'medium.en') {
1652
1692
  return [48, groupCount, length, 1024]
1653
- } else if (modelName == 'large-v1' || modelName == 'large-v2' || modelName == 'large-v3' || modelName == 'large-v3-turbo') {
1693
+ } else if (modelName === 'large-v1' || modelName === 'large-v2' || modelName === 'large-v3' || modelName === 'large-v3-turbo') {
1654
1694
  return [64, groupCount, length, 1280]
1655
1695
  } else {
1656
1696
  throw new Error(`Unsupported model: ${modelName}`)
@@ -1664,7 +1704,7 @@ export class Whisper {
1664
1704
 
1665
1705
  if (this.isMultiligualModel) {
1666
1706
  const languageToken = this.tokenConfig.languageTokensStart + languageIdLookup[language]
1667
- const taskToken = task == 'translate' ? this.tokenConfig.translateTaskToken : this.tokenConfig.transcribeTaskToken
1707
+ const taskToken = task === 'translate' ? this.tokenConfig.translateTaskToken : this.tokenConfig.transcribeTaskToken
1668
1708
 
1669
1709
  tokens = [startOfTextToken, languageToken, taskToken]
1670
1710
  } else {
@@ -1900,7 +1940,7 @@ export async function loadPackagesAndGetPaths(modelName: WhisperModelName | unde
1900
1940
  if (languageCode) {
1901
1941
  const shortLanguageCode = getShortLanguageCode(languageCode)
1902
1942
 
1903
- modelName = shortLanguageCode == 'en' ? 'tiny.en' : 'tiny'
1943
+ modelName = shortLanguageCode === 'en' ? 'tiny.en' : 'tiny'
1904
1944
  } else {
1905
1945
  modelName = 'tiny'
1906
1946
  }
@@ -1918,7 +1958,7 @@ export async function loadPackagesAndGetPaths(modelName: WhisperModelName | unde
1918
1958
  }
1919
1959
 
1920
1960
  export function normalizeWhisperModelName(modelName: WhisperModelName, languageCode: string | undefined): WhisperModelName {
1921
- if (languageCode != 'en' && modelName.endsWith('.en')) {
1961
+ if (languageCode !== 'en' && modelName.endsWith('.en')) {
1922
1962
  const originalModelName = modelName
1923
1963
  modelName = modelName.slice(0, modelName.length - 3) as WhisperModelName
1924
1964
 
@@ -130,13 +130,15 @@ export function sleep(timeMs: number) {
130
130
  return new Promise<void>((resolve) => {
131
131
  const tickCallback = () => {
132
132
  if (timer.elapsedTime < timeMs) {
133
- setImmediate(tickCallback)
133
+ //setImmediate(tickCallback)
134
+ setTimeout(tickCallback, 0)
134
135
  } else {
135
136
  resolve()
136
137
  }
137
138
  }
138
139
 
139
- setImmediate(tickCallback)
140
+ //setImmediate(tickCallback)
141
+ setTimeout(tickCallback, 0)
140
142
  })
141
143
  }
142
144