echogarden 0.12.2 → 1.0.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 (334) hide show
  1. package/README.md +15 -14
  2. package/data/schemas/options.json +398 -111
  3. package/dist/alignment/DTWMfccSequenceAlignment.d.ts +1 -1
  4. package/dist/alignment/DTWMfccSequenceAlignment.js +8 -8
  5. package/dist/alignment/DTWSequenceAlignment.d.ts +1 -1
  6. package/dist/alignment/DTWSequenceAlignment.js +1 -1
  7. package/dist/alignment/DTWSequenceAlignmentWindowed.d.ts +1 -1
  8. package/dist/alignment/DTWSequenceAlignmentWindowed.js +2 -2
  9. package/dist/alignment/LevenshteinSequenceAlignment.d.ts +1 -1
  10. package/dist/alignment/LevenshteinSequenceAlignment.js +1 -1
  11. package/dist/alignment/SpeechAlignment.d.ts +9 -10
  12. package/dist/alignment/SpeechAlignment.js +136 -105
  13. package/dist/alignment/SpeechAlignment.js.map +1 -1
  14. package/dist/api/API.d.ts +13 -12
  15. package/dist/api/API.js +14 -13
  16. package/dist/api/API.js.map +1 -1
  17. package/dist/api/APIOptions.d.ts +5 -4
  18. package/dist/api/Alignment.d.ts +15 -9
  19. package/dist/api/Alignment.js +88 -74
  20. package/dist/api/Alignment.js.map +1 -1
  21. package/dist/api/Common.js +1 -1
  22. package/dist/api/Denoising.d.ts +6 -6
  23. package/dist/api/Denoising.js +23 -23
  24. package/dist/api/Denoising.js.map +1 -1
  25. package/dist/api/LanguageDetection.d.ts +19 -12
  26. package/dist/api/LanguageDetection.js +88 -38
  27. package/dist/api/LanguageDetection.js.map +1 -1
  28. package/dist/api/Recognition.d.ts +16 -6
  29. package/dist/api/Recognition.js +129 -55
  30. package/dist/api/Recognition.js.map +1 -1
  31. package/dist/api/SourceSeparation.d.ts +17 -0
  32. package/dist/api/SourceSeparation.js +61 -0
  33. package/dist/api/SourceSeparation.js.map +1 -0
  34. package/dist/api/Synthesis.d.ts +18 -18
  35. package/dist/api/Synthesis.js +191 -164
  36. package/dist/api/Synthesis.js.map +1 -1
  37. package/dist/api/Translation.d.ts +19 -8
  38. package/dist/api/Translation.js +132 -35
  39. package/dist/api/Translation.js.map +1 -1
  40. package/dist/api/Vad.d.ts +10 -5
  41. package/dist/api/Vad.js +76 -38
  42. package/dist/api/Vad.js.map +1 -1
  43. package/dist/audio/AudioBufferConversion.d.ts +1 -1
  44. package/dist/audio/AudioBufferConversion.js +4 -4
  45. package/dist/audio/AudioPlayer.d.ts +1 -1
  46. package/dist/audio/AudioPlayer.js +26 -26
  47. package/dist/audio/AudioPlayer.js.map +1 -1
  48. package/dist/audio/AudioRecorder.d.ts +1 -1
  49. package/dist/audio/AudioRecorder.js +5 -5
  50. package/dist/audio/AudioUtilities.d.ts +13 -9
  51. package/dist/audio/AudioUtilities.js +86 -24
  52. package/dist/audio/AudioUtilities.js.map +1 -1
  53. package/dist/cli/CLI.d.ts +3 -3
  54. package/dist/cli/CLI.js +271 -162
  55. package/dist/cli/CLI.js.map +1 -1
  56. package/dist/cli/CLIConfigFile.js +8 -8
  57. package/dist/cli/CLILauncher.js +6 -6
  58. package/dist/cli/CLIOptionsSchema.js +2 -2
  59. package/dist/cli/CLIParser.js +5 -5
  60. package/dist/cli/CLIStarter.js +4 -4
  61. package/dist/codecs/FFMpegTranscoder.d.ts +2 -2
  62. package/dist/codecs/FFMpegTranscoder.js +37 -37
  63. package/dist/codecs/FFMpegTranscoder.js.map +1 -1
  64. package/dist/codecs/TIMITCodec.js +5 -5
  65. package/dist/codecs/WaveCodec.d.ts +1 -1
  66. package/dist/codecs/WaveCodec.js +22 -22
  67. package/dist/denoising/RNNoise.d.ts +1 -1
  68. package/dist/denoising/RNNoise.js +9 -9
  69. package/dist/dsp/BiquadFilter.d.ts +3 -2
  70. package/dist/dsp/BiquadFilter.js +18 -11
  71. package/dist/dsp/BiquadFilter.js.map +1 -1
  72. package/dist/dsp/DecayingPeakEstimator.d.ts +16 -0
  73. package/dist/dsp/DecayingPeakEstimator.js +23 -0
  74. package/dist/dsp/DecayingPeakEstimator.js.map +1 -0
  75. package/dist/dsp/FFT.d.ts +8 -4
  76. package/dist/dsp/FFT.js +76 -30
  77. package/dist/dsp/FFT.js.map +1 -1
  78. package/dist/dsp/KWeightingFilter.d.ts +9 -0
  79. package/dist/dsp/KWeightingFilter.js +40 -0
  80. package/dist/dsp/KWeightingFilter.js.map +1 -0
  81. package/dist/dsp/LoudnessEstimator.d.ts +21 -0
  82. package/dist/dsp/LoudnessEstimator.js +47 -0
  83. package/dist/dsp/LoudnessEstimator.js.map +1 -0
  84. package/dist/dsp/MFCC.d.ts +2 -2
  85. package/dist/dsp/MFCC.js +15 -15
  86. package/dist/dsp/MelSpectogram.d.ts +1 -1
  87. package/dist/dsp/MelSpectogram.js +6 -6
  88. package/dist/dsp/Rubberband.d.ts +11 -11
  89. package/dist/dsp/Rubberband.js +27 -27
  90. package/dist/dsp/Sonic.d.ts +1 -1
  91. package/dist/dsp/Sonic.js +3 -3
  92. package/dist/dsp/SpeexResampler.d.ts +1 -1
  93. package/dist/dsp/SpeexResampler.js +2 -2
  94. package/dist/math/VectorMath.d.ts +12 -8
  95. package/dist/math/VectorMath.js +35 -32
  96. package/dist/math/VectorMath.js.map +1 -1
  97. package/dist/nlp/ChineseSegmentation.js +2 -2
  98. package/dist/nlp/CompromiseNLP.js +3 -3
  99. package/dist/nlp/EspeakPhonemizer.js +30 -30
  100. package/dist/nlp/IPA.js +20 -20
  101. package/dist/nlp/JapaneseSegmentation.js +6 -6
  102. package/dist/nlp/Lexicon.d.ts +1 -1
  103. package/dist/nlp/Lexicon.js +7 -7
  104. package/dist/nlp/Segmentation.d.ts +3 -0
  105. package/dist/nlp/Segmentation.js +21 -14
  106. package/dist/nlp/Segmentation.js.map +1 -1
  107. package/dist/nlp/TextNormalizer.js +16 -16
  108. package/dist/recognition/AmazonTranscribeSTT.d.ts +2 -2
  109. package/dist/recognition/AmazonTranscribeSTT.js +13 -14
  110. package/dist/recognition/AmazonTranscribeSTT.js.map +1 -1
  111. package/dist/recognition/AzureCognitiveServicesSTT.js +5 -6
  112. package/dist/recognition/AzureCognitiveServicesSTT.js.map +1 -1
  113. package/dist/recognition/GoogleCloudSTT.d.ts +3 -3
  114. package/dist/recognition/GoogleCloudSTT.js +18 -18
  115. package/dist/recognition/OpenAICloudSTT.d.ts +19 -0
  116. package/dist/recognition/OpenAICloudSTT.js +81 -0
  117. package/dist/recognition/OpenAICloudSTT.js.map +1 -0
  118. package/dist/recognition/SileroSTT.d.ts +2 -2
  119. package/dist/recognition/SileroSTT.js +25 -25
  120. package/dist/recognition/VoskSTT.d.ts +2 -2
  121. package/dist/recognition/VoskSTT.js +8 -8
  122. package/dist/recognition/WhisperCppSTT.d.ts +88 -0
  123. package/dist/recognition/WhisperCppSTT.js +332 -0
  124. package/dist/recognition/WhisperCppSTT.js.map +1 -0
  125. package/dist/recognition/WhisperSTT.d.ts +49 -25
  126. package/dist/recognition/WhisperSTT.js +626 -481
  127. package/dist/recognition/WhisperSTT.js.map +1 -1
  128. package/dist/server/Client.d.ts +1 -1
  129. package/dist/server/Client.js +22 -22
  130. package/dist/server/Server.js +9 -9
  131. package/dist/server/Server.js.map +1 -1
  132. package/dist/server/Worker.d.ts +22 -22
  133. package/dist/server/Worker.js +36 -36
  134. package/dist/server/Worker.js.map +1 -1
  135. package/dist/server/WorkerStarter.js +2 -2
  136. package/dist/source-separation/MDXNetSourceSeparation.d.ts +11 -0
  137. package/dist/source-separation/MDXNetSourceSeparation.js +161 -0
  138. package/dist/source-separation/MDXNetSourceSeparation.js.map +1 -0
  139. package/dist/speech-language-detection/SileroLanguageDetection.d.ts +1 -1
  140. package/dist/speech-language-detection/SileroLanguageDetection.js +7 -7
  141. package/dist/subtitles/Subtitles.d.ts +10 -0
  142. package/dist/subtitles/Subtitles.js +2 -2
  143. package/dist/subtitles/Subtitles.js.map +1 -1
  144. package/dist/synthesis/AwsPollyTTS.d.ts +1 -1
  145. package/dist/synthesis/AwsPollyTTS.js +12 -12
  146. package/dist/synthesis/AzureCognitiveServicesTTS.js +7 -7
  147. package/dist/synthesis/CoquiServerTTS.js +10 -10
  148. package/dist/synthesis/CoquiServerTTS.js.map +1 -1
  149. package/dist/synthesis/ElevenlabsTTS.d.ts +23 -0
  150. package/dist/synthesis/ElevenlabsTTS.js +103 -0
  151. package/dist/synthesis/ElevenlabsTTS.js.map +1 -0
  152. package/dist/synthesis/EspeakTTS.d.ts +6 -5
  153. package/dist/synthesis/EspeakTTS.js +81 -69
  154. package/dist/synthesis/EspeakTTS.js.map +1 -1
  155. package/dist/synthesis/FliteTTS.d.ts +3 -3
  156. package/dist/synthesis/FliteTTS.js +154 -154
  157. package/dist/synthesis/FliteTTS.js.map +1 -1
  158. package/dist/synthesis/GoogleCloudTTS.d.ts +3 -3
  159. package/dist/synthesis/GoogleCloudTTS.js +17 -17
  160. package/dist/synthesis/GoogleCloudTTS.js.map +1 -1
  161. package/dist/synthesis/GoogleTranslateTTS.d.ts +1 -1
  162. package/dist/synthesis/GoogleTranslateTTS.js +103 -103
  163. package/dist/synthesis/MicrosoftEdgeTTS.d.ts +2 -2
  164. package/dist/synthesis/MicrosoftEdgeTTS.js +74 -74
  165. package/dist/synthesis/OpenAICloudTTS.d.ts +13 -0
  166. package/dist/synthesis/OpenAICloudTTS.js +169 -0
  167. package/dist/synthesis/OpenAICloudTTS.js.map +1 -0
  168. package/dist/synthesis/SamTTS.js +3 -3
  169. package/dist/synthesis/SapiTTS.d.ts +3 -3
  170. package/dist/synthesis/SapiTTS.js +26 -26
  171. package/dist/synthesis/StreamlabsPollyTTS.d.ts +2 -2
  172. package/dist/synthesis/StreamlabsPollyTTS.js +27 -27
  173. package/dist/synthesis/SvoxPicoTTS.d.ts +2 -2
  174. package/dist/synthesis/SvoxPicoTTS.js +65 -65
  175. package/dist/synthesis/SvoxPicoTTS.js.map +1 -1
  176. package/dist/synthesis/VitsTTS.d.ts +3 -3
  177. package/dist/synthesis/VitsTTS.js +378 -378
  178. package/dist/synthesis/VitsTTS.js.map +1 -1
  179. package/dist/tests/Test.js +2 -2
  180. package/dist/utilities/Compression.d.ts +5 -0
  181. package/dist/utilities/Compression.js +29 -13
  182. package/dist/utilities/Compression.js.map +1 -1
  183. package/dist/utilities/FileDownloader.d.ts +1 -1
  184. package/dist/utilities/FileDownloader.js +16 -16
  185. package/dist/utilities/FileSystem.js +7 -7
  186. package/dist/utilities/Locale.d.ts +7 -7
  187. package/dist/utilities/Locale.js +15 -15
  188. package/dist/utilities/Logger.js +3 -3
  189. package/dist/utilities/ObjectUtilities.js +19 -19
  190. package/dist/utilities/OpenPromise.js +2 -2
  191. package/dist/utilities/OpenPromise.js.map +1 -1
  192. package/dist/utilities/PackageManager.js +31 -0
  193. package/dist/utilities/PackageManager.js.map +1 -1
  194. package/dist/utilities/PathUtilities.js +8 -8
  195. package/dist/utilities/RandomGenerator.js +2 -2
  196. package/dist/utilities/SmoothEstimator.d.ts +8 -0
  197. package/dist/utilities/SmoothEstimator.js +25 -0
  198. package/dist/utilities/SmoothEstimator.js.map +1 -0
  199. package/dist/utilities/TarballMaker.js +8 -8
  200. package/dist/utilities/Timeline.d.ts +3 -2
  201. package/dist/utilities/Timeline.js +11 -11
  202. package/dist/utilities/Timeline.js.map +1 -1
  203. package/dist/utilities/Timer.js +4 -4
  204. package/dist/utilities/Utilities.d.ts +4 -0
  205. package/dist/utilities/Utilities.js +38 -15
  206. package/dist/utilities/Utilities.js.map +1 -1
  207. package/dist/utilities/WasmMemoryManager.js +7 -7
  208. package/dist/utilities/WebReader.js +23 -23
  209. package/dist/utilities/WikipediaReader.js +2 -2
  210. package/dist/voice-activity-detection/AdaptiveGateVAD.d.ts +28 -0
  211. package/dist/voice-activity-detection/AdaptiveGateVAD.js +138 -0
  212. package/dist/voice-activity-detection/AdaptiveGateVAD.js.map +1 -0
  213. package/dist/voice-activity-detection/SileroVAD.d.ts +1 -1
  214. package/dist/voice-activity-detection/SileroVAD.js +5 -5
  215. package/dist/voice-activity-detection/SileroVAD.js.map +1 -1
  216. package/dist/voice-activity-detection/WebRtcVAD.d.ts +1 -1
  217. package/dist/voice-activity-detection/WebRtcVAD.js +4 -4
  218. package/docs/API.md +29 -11
  219. package/docs/CLI.md +31 -7
  220. package/docs/Contributing.md +38 -0
  221. package/docs/Development.md +93 -19
  222. package/docs/Engines.md +28 -16
  223. package/docs/Licenses.md +4 -1
  224. package/docs/Options.md +158 -78
  225. package/docs/Releases.md +262 -0
  226. package/docs/Server.md +7 -7
  227. package/docs/Tasklist.md +95 -76
  228. package/docs/Technical.md +4 -4
  229. package/package.json +13 -14
  230. package/src/alignment/DTWMfccSequenceAlignment.ts +9 -9
  231. package/src/alignment/DTWSequenceAlignment.ts +2 -2
  232. package/src/alignment/DTWSequenceAlignmentWindowed.ts +3 -3
  233. package/src/alignment/LevenshteinSequenceAlignment.ts +2 -2
  234. package/src/alignment/SpeechAlignment.ts +204 -119
  235. package/src/api/API.ts +14 -13
  236. package/src/api/APIOptions.ts +12 -11
  237. package/src/api/Alignment.ts +147 -90
  238. package/src/api/Common.ts +1 -1
  239. package/src/api/Denoising.ts +28 -28
  240. package/src/api/LanguageDetection.ts +135 -48
  241. package/src/api/Recognition.ts +198 -59
  242. package/src/api/SourceSeparation.ts +99 -0
  243. package/src/api/Synthesis.ts +217 -181
  244. package/src/api/Translation.ts +193 -40
  245. package/src/api/Vad.ts +110 -41
  246. package/src/audio/AudioBufferConversion.ts +4 -4
  247. package/src/audio/AudioPlayer.ts +27 -27
  248. package/src/audio/AudioRecorder.ts +5 -5
  249. package/src/audio/AudioUtilities.ts +107 -24
  250. package/src/cli/CLI.ts +313 -164
  251. package/src/cli/CLIConfigFile.ts +8 -8
  252. package/src/cli/CLILauncher.ts +6 -6
  253. package/src/cli/CLIOptionsSchema.ts +2 -2
  254. package/src/cli/CLIParser.ts +5 -5
  255. package/src/cli/CLIStarter.ts +4 -4
  256. package/src/codecs/FFMpegTranscoder.ts +38 -38
  257. package/src/codecs/TIMITCodec.ts +5 -5
  258. package/src/codecs/WaveCodec.ts +22 -22
  259. package/src/denoising/RNNoise.ts +9 -9
  260. package/src/dsp/BiquadFilter.ts +19 -11
  261. package/src/dsp/DecayingPeakEstimator.ts +35 -0
  262. package/src/dsp/FFT.ts +103 -35
  263. package/src/dsp/KWeightingFilter.ts +43 -0
  264. package/src/dsp/LoudnessEstimator.ts +74 -0
  265. package/src/dsp/MFCC.ts +15 -15
  266. package/src/dsp/MelSpectogram.ts +7 -7
  267. package/src/dsp/Rubberband.ts +38 -38
  268. package/src/dsp/Sonic.ts +4 -4
  269. package/src/dsp/SpeexResampler.ts +2 -2
  270. package/src/math/VectorMath.ts +42 -33
  271. package/src/nlp/ChineseSegmentation.ts +3 -3
  272. package/src/nlp/CompromiseNLP.ts +3 -3
  273. package/src/nlp/EspeakPhonemizer.ts +30 -30
  274. package/src/nlp/IPA.ts +20 -20
  275. package/src/nlp/JapaneseSegmentation.ts +6 -6
  276. package/src/nlp/Lexicon.ts +8 -8
  277. package/src/nlp/Segmentation.ts +23 -14
  278. package/src/nlp/TextNormalizer.ts +16 -16
  279. package/src/recognition/AmazonTranscribeSTT.ts +16 -17
  280. package/src/recognition/AzureCognitiveServicesSTT.ts +8 -6
  281. package/src/recognition/GoogleCloudSTT.ts +21 -21
  282. package/src/recognition/OpenAICloudSTT.ts +142 -0
  283. package/src/recognition/SileroSTT.ts +26 -26
  284. package/src/recognition/VoskSTT.ts +10 -10
  285. package/src/recognition/WhisperCppSTT.ts +555 -0
  286. package/src/recognition/WhisperSTT.ts +760 -507
  287. package/src/server/Client.ts +23 -23
  288. package/src/server/Server.ts +9 -9
  289. package/src/server/Worker.ts +53 -53
  290. package/src/server/WorkerStarter.ts +2 -2
  291. package/src/source-separation/MDXNetSourceSeparation.ts +228 -0
  292. package/src/speech-language-detection/SileroLanguageDetection.ts +8 -8
  293. package/src/subtitles/Subtitles.ts +3 -3
  294. package/src/synthesis/AwsPollyTTS.ts +14 -14
  295. package/src/synthesis/AzureCognitiveServicesTTS.ts +10 -10
  296. package/src/synthesis/CoquiServerTTS.ts +10 -10
  297. package/src/synthesis/ElevenlabsTTS.ts +137 -0
  298. package/src/synthesis/EspeakTTS.ts +90 -71
  299. package/src/synthesis/FliteTTS.ts +157 -157
  300. package/src/synthesis/GoogleCloudTTS.ts +19 -19
  301. package/src/synthesis/GoogleTranslateTTS.ts +104 -104
  302. package/src/synthesis/MicrosoftEdgeTTS.ts +80 -80
  303. package/src/synthesis/OpenAICloudTTS.ts +196 -0
  304. package/src/synthesis/SamTTS.ts +3 -3
  305. package/src/synthesis/SapiTTS.ts +29 -29
  306. package/src/synthesis/StreamlabsPollyTTS.ts +29 -29
  307. package/src/synthesis/SvoxPicoTTS.ts +67 -67
  308. package/src/synthesis/VitsTTS.ts +380 -380
  309. package/src/tests/Test.ts +4 -4
  310. package/src/utilities/Compression.ts +34 -13
  311. package/src/utilities/FileDownloader.ts +19 -19
  312. package/src/utilities/FileSystem.ts +7 -7
  313. package/src/utilities/Locale.ts +22 -22
  314. package/src/utilities/Logger.ts +4 -4
  315. package/src/utilities/ObjectUtilities.ts +19 -19
  316. package/src/utilities/OpenPromise.ts +2 -2
  317. package/src/utilities/PackageManager.ts +40 -0
  318. package/src/utilities/PathUtilities.ts +8 -8
  319. package/src/utilities/RandomGenerator.ts +3 -3
  320. package/src/utilities/SmoothEstimator.ts +35 -0
  321. package/src/utilities/TarballMaker.ts +9 -9
  322. package/src/utilities/Timeline.ts +15 -13
  323. package/src/utilities/Timer.ts +4 -4
  324. package/src/utilities/Utilities.ts +49 -15
  325. package/src/utilities/WasmMemoryManager.ts +7 -7
  326. package/src/utilities/WebReader.ts +23 -23
  327. package/src/utilities/WikipediaReader.ts +2 -2
  328. package/src/voice-activity-detection/AdaptiveGateVAD.ts +202 -0
  329. package/src/voice-activity-detection/SileroVAD.ts +5 -5
  330. package/src/voice-activity-detection/WebRtcVAD.ts +5 -5
  331. package/dist/synthesis/ElevenLabsTTS.d.ts +0 -8
  332. package/dist/synthesis/ElevenLabsTTS.js +0 -82
  333. package/dist/synthesis/ElevenLabsTTS.js.map +0 -1
  334. package/src/synthesis/ElevenLabsTTS.ts +0 -104
@@ -1,17 +1,16 @@
1
1
  import Onnx from 'onnxruntime-node'
2
2
 
3
3
  import { Logger } from '../utilities/Logger.js'
4
- import { computeMelSpectogramUsingFilterbanks, Filterbank } from "../dsp/MelSpectogram.js"
5
- import { clip, splitFloat32Array, writeToStderr, yieldToEventLoop } from '../utilities/Utilities.js'
4
+ import { computeMelSpectogramUsingFilterbanks, Filterbank } from '../dsp/MelSpectogram.js'
5
+ import { clip, getIntegerRange, getRepetitionScoreRelativeToFirstSubstring, splitFloat32Array, yieldToEventLoop } from '../utilities/Utilities.js'
6
6
  import { indexOfMax, logOfVector, logSumExp, meanOfVector, medianFilter, softmax, stdDeviationOfVector } from '../math/VectorMath.js'
7
- import { isWordOrSymbolWord, splitToWords } from '../nlp/Segmentation.js'
8
7
 
9
8
  import { alignDTWWindowed } from '../alignment/DTWSequenceAlignmentWindowed.js'
10
- import { deepClone, extendDeep } from '../utilities/ObjectUtilities.js'
9
+ import { extendDeep } from '../utilities/ObjectUtilities.js'
11
10
  import { Timeline, TimelineEntry } from '../utilities/Timeline.js'
12
11
  import { AlignmentPath } from '../alignment/SpeechAlignment.js'
13
12
  import { getRawAudioDuration, RawAudio } from '../audio/AudioUtilities.js'
14
- import { readAndParseJsonFile, readFile } from '../utilities/FileSystem.js'
13
+ import { readFile } from '../utilities/FileSystem.js'
15
14
  import path from 'path'
16
15
  import type { LanguageDetectionResults } from '../api/API.js'
17
16
  import { getShortLanguageCode, languageCodeToName } from '../utilities/Locale.js'
@@ -19,10 +18,11 @@ import { loadPackage } from '../utilities/PackageManager.js'
19
18
  import chalk from 'chalk'
20
19
  import { XorShift32RNG } from '../utilities/RandomGenerator.js'
21
20
  import { detectSpeechLanguageByParts } from '../api/LanguageDetection.js'
21
+ import { type Tiktoken } from 'tiktoken/lite'
22
22
 
23
- export async function recognize(sourceRawAudio: RawAudio, modelName: WhisperModelName, modelDir: string, tokenizerDir: string, task: WhisperTask, sourceLanguage: string, options: WhisperOptions) {
23
+ export async function recognize(sourceRawAudio: RawAudio, modelName: WhisperModelName, modelDir: string, task: WhisperTask, sourceLanguage: string, options: WhisperOptions) {
24
24
  if (sourceRawAudio.sampleRate != 16000) {
25
- throw new Error("Source audio must have a sampling rate of 16000")
25
+ throw new Error('Source audio must have a sampling rate of 16000')
26
26
  }
27
27
 
28
28
  sourceLanguage = getShortLanguageCode(sourceLanguage)
@@ -31,21 +31,30 @@ export async function recognize(sourceRawAudio: RawAudio, modelName: WhisperMode
31
31
  throw new Error(`The language ${languageCodeToName(sourceLanguage)} is not supported by the Whisper engine.`)
32
32
  }
33
33
 
34
- if (!isMultiligualModel(modelName) && sourceLanguage != 'en') {
34
+ if (isEnglishOnlyModel(modelName) && sourceLanguage != 'en') {
35
35
  throw new Error(`The model '${modelName}' can only be used with English inputs. However, the given source language was ${languageCodeToName(sourceLanguage)}.`)
36
36
  }
37
37
 
38
- const whisper = new Whisper(modelName, modelDir, tokenizerDir)
39
- await whisper.initialize()
38
+ if (options.temperature && options.temperature < 0) {
39
+ throw new Error(`Temperature can't be negative`)
40
+ }
41
+
42
+ let seed = options.seed
43
+
44
+ if (seed) {
45
+ seed = Math.max(Math.floor(seed), 1) | 0
46
+ }
47
+
48
+ const whisper = new Whisper(modelName, modelDir, seed)
40
49
 
41
50
  const result = await whisper.recognize(sourceRawAudio, task, sourceLanguage, options)
42
51
 
43
52
  return result
44
53
  }
45
54
 
46
- export async function align(sourceRawAudio: RawAudio, referenceText: string, modelName: WhisperModelName, modelDir: string, tokenizerDir: string, sourceLanguage: string) {
55
+ export async function align(sourceRawAudio: RawAudio, referenceText: string, modelName: WhisperModelName, modelDir: string, sourceLanguage: string) {
47
56
  if (sourceRawAudio.sampleRate != 16000) {
48
- throw new Error("Source audio must have a sampling rate of 16000")
57
+ throw new Error('Source audio must have a sampling rate of 16000')
49
58
  }
50
59
 
51
60
  sourceLanguage = getShortLanguageCode(sourceLanguage)
@@ -54,29 +63,35 @@ export async function align(sourceRawAudio: RawAudio, referenceText: string, mod
54
63
  throw new Error(`The language ${languageCodeToName(sourceLanguage)} is not supported by the Whisper engine.`)
55
64
  }
56
65
 
57
- if (!isMultiligualModel(modelName) && sourceLanguage != 'en') {
66
+ if (isEnglishOnlyModel(modelName) && sourceLanguage != 'en') {
58
67
  throw new Error(`The model '${modelName}' can only be used with English inputs. However, the given source language was ${languageCodeToName(sourceLanguage)}.`)
59
68
  }
60
69
 
61
- const whisper = new Whisper(modelName, modelDir, tokenizerDir)
62
- await whisper.initialize()
70
+ const whisper = new Whisper(modelName, modelDir)
63
71
 
64
72
  const timeline = await whisper.align(sourceRawAudio, referenceText, sourceLanguage)
65
73
 
66
74
  return timeline
67
75
  }
68
76
 
69
- export async function detectLanguage(sourceRawAudio: RawAudio, modelName: WhisperModelName, modelDir: string, tokenizerDir: string) {
77
+ export async function detectLanguage(sourceRawAudio: RawAudio, modelName: WhisperModelName, modelDir: string, temperature: number) {
70
78
  if (sourceRawAudio.sampleRate != 16000) {
71
- throw new Error("Source audio must have a sampling rate of 16000")
79
+ throw new Error('Source audio must have a sampling rate of 16000')
80
+ }
81
+
82
+ if (!isMultilingualModel(modelName)) {
83
+ throw new Error(`Language detection is only supported with multilingual models.`)
72
84
  }
73
85
 
74
- const whisper = new Whisper(modelName, modelDir, tokenizerDir)
75
- await whisper.initialize()
86
+ if (temperature < 0) {
87
+ throw new Error(`Temperature cannot be negative`)
88
+ }
89
+
90
+ const whisper = new Whisper(modelName, modelDir)
76
91
 
77
92
  async function detectLanguageForPart(partAudio: RawAudio) {
78
93
  const audioFeatures = await whisper.encodeAudio(partAudio)
79
- const partResults = await whisper.detectLanguage(audioFeatures)
94
+ const partResults = await whisper.detectLanguage(audioFeatures, temperature)
80
95
 
81
96
  return partResults
82
97
  }
@@ -91,17 +106,13 @@ export async function detectLanguage(sourceRawAudio: RawAudio, modelName: Whispe
91
106
  export class Whisper {
92
107
  modelName: WhisperModelName
93
108
  modelDir: string
94
- tokenizerDir: string
95
109
 
96
110
  isMultiligualModel: boolean
97
111
 
98
112
  audioEncoder?: Onnx.InferenceSession
99
113
  textDecoder?: Onnx.InferenceSession
100
114
 
101
- textToTokenLookup = new Map<string, number>()
102
- tokenToTextLookup = new Map<number, string>()
103
-
104
- merges: [string, string][] = []
115
+ tiktoken?: Tiktoken
105
116
 
106
117
  onnxOptions: Onnx.InferenceSession.SessionOptions = {
107
118
  logSeverityLevel: 2,
@@ -109,109 +120,203 @@ export class Whisper {
109
120
  }
110
121
 
111
122
  tokenConfig: {
112
- suppressedTokens: number[]
113
- sotToken: number
114
- sotPrevToken: number
115
- eotToken: number
123
+ endOfTextToken: number
124
+ startOfTextToken: number
125
+
126
+ languageTokensStart: number
127
+ languageTokensEnd: number
128
+
129
+ translateTaskToken: number
130
+ transcribeTaskToken: number
131
+ startOfPromptToken: number
132
+ nonSpeechToken: number
116
133
  noTimestampsToken: number
117
- noSpeechToken: number
134
+
118
135
  timestampTokensStart: number
136
+ timestampTokensEnd: number
137
+
138
+ suppressedTokens: Set<number>
119
139
  }
120
140
 
121
- randomGen = new XorShift32RNG(23948203)
141
+ randomGen: XorShift32RNG
122
142
 
123
- constructor(modelName: WhisperModelName, modelDir: string, tokenizerDir: string) {
124
- this.modelDir = modelDir
143
+ constructor(modelName: WhisperModelName, modelDir: string, rngSeed = 461845907) {
125
144
  this.modelName = modelName
126
- this.tokenizerDir = tokenizerDir
145
+ this.modelDir = modelDir
127
146
 
128
- this.isMultiligualModel = isMultiligualModel(this.modelName)
147
+ this.isMultiligualModel = isMultilingualModel(this.modelName)
129
148
 
130
149
  if (this.isMultiligualModel) {
131
150
  this.tokenConfig = {
132
- sotToken: 50258,
133
- sotPrevToken: 50361,
134
- eotToken: 50257,
135
- noSpeechToken: 50362,
151
+ endOfTextToken: 50257,
152
+ startOfTextToken: 50258,
153
+
154
+ languageTokensStart: 50259,
155
+ languageTokensEnd: 50358,
156
+
157
+ translateTaskToken: 50358,
158
+ transcribeTaskToken: 50359,
159
+ startOfPromptToken: 50361,
160
+ nonSpeechToken: 50362,
136
161
  noTimestampsToken: 50363,
162
+
137
163
  timestampTokensStart: 50364,
138
- suppressedTokens: [1, 2, 6, 7, 8, 9, 10, 12, 14, 25, 26, 27, 28, 29, 31, 58, 59, 60, 61, 62, 63, 90, 91, 92, 93, 359, 503, 522, 542, 873, 893, 902, 918, 922, 931, 1350, 1853, 1982, 2460, 2627, 3246, 3253, 3268, 3536, 3846, 3961, 4183, 4667, 6585, 6647, 7273, 9061, 9383, 10428, 10929, 11938, 12033, 12331, 12562, 13793, 14157, 14635, 15265, 15618, 16553, 16604, 18362, 18956, 20075, 21675, 22520, 26130, 26161, 26435, 28279, 29464, 31650, 32302, 32470, 36865, 42863, 47425, 49870, 50254, 50258, 50360, 50361, 50362]
164
+ timestampTokensEnd: 50364 + 1501,
165
+
166
+ suppressedTokens: new Set()
139
167
  }
168
+
169
+ const suppressedTextTokens = [1, 2, 6, 7, 8, 9, 10, 12, 14, 25, 26, 27, 28, 29, 31, 58, 59, 60, 61, 62, 63, 90, 91, 92, 93, 359, 503, 522, 542, 873, 893, 902, 918, 922, 931, 1350, 1853, 1982, 2460, 2627, 3246, 3253, 3268, 3536, 3846, 3961, 4183, 4667, 6585, 6647, 7273, 9061, 9383, 10428, 10929, 11938, 12033, 12331, 12562, 13793, 14157, 14635, 15265, 15618, 16553, 16604, 18362, 18956, 20075, 21675, 22520, 26130, 26161, 26435, 28279, 29464, 31650, 32302, 32470, 36865, 42863, 47425, 49870, 50254]
170
+ const suppressedMetadataTokens = [50256, ...getIntegerRange(50258, 50364)]
171
+
172
+ const suppressedTokens = [...suppressedTextTokens, ...suppressedMetadataTokens]
173
+
174
+ this.tokenConfig.suppressedTokens = new Set(suppressedTokens)
140
175
  } else {
141
176
  this.tokenConfig = {
142
- sotToken: 50257,
143
- sotPrevToken: 50360,
144
- eotToken: 50256,
145
- noSpeechToken: 50361,
177
+ endOfTextToken: 50256,
178
+ startOfTextToken: 50257,
179
+
180
+ languageTokensStart: 50258,
181
+ languageTokensEnd: 50358,
182
+
183
+ translateTaskToken: 50358,
184
+ transcribeTaskToken: 50359,
185
+ startOfPromptToken: 50360,
186
+ nonSpeechToken: 50361,
146
187
  noTimestampsToken: 50362,
188
+
147
189
  timestampTokensStart: 50363,
148
- suppressedTokens: [1, 2, 7, 8, 9, 10, 14, 25, 26, 27, 28, 29, 31, 58, 59, 60, 61, 62, 63, 90, 91, 92, 93, 357, 366, 438, 532, 685, 705, 796, 930, 1058, 1220, 1267, 1279, 1303, 1343, 1377, 1391, 1635, 1782, 1875, 2162, 2361, 2488, 3467, 4008, 4211, 4600, 4808, 5299, 5855, 6329, 7203, 9609, 9959, 10563, 10786, 11420, 11709, 11907, 13163, 13697, 13700, 14808, 15306, 16410, 16791, 17992, 19203, 19510, 20724, 22305, 22935, 27007, 30109, 30420, 33409, 34949, 40283, 40493, 40549, 47282, 49146, 50257, 50359, 50360, 50361]
190
+ timestampTokensEnd: 50363 + 1501,
191
+
192
+ suppressedTokens: new Set()
149
193
  }
194
+
195
+ const suppressedTextTokens = [1, 2, 7, 8, 9, 10, 14, 25, 26, 27, 28, 29, 31, 58, 59, 60, 61, 62, 63, 90, 91, 92, 93, 357, 366, 438, 532, 685, 705, 796, 930, 1058, 1220, 1267, 1279, 1303, 1343, 1377, 1391, 1635, 1782, 1875, 2162, 2361, 2488, 3467, 4008, 4211, 4600, 4808, 5299, 5855, 6329, 7203, 9609, 9959, 10563, 10786, 11420, 11709, 11907, 13163, 13697, 13700, 14808, 15306, 16410, 16791, 17992, 19203, 19510, 20724, 22305, 22935, 27007, 30109, 30420, 33409, 34949, 40283, 40493, 40549, 47282, 49146]
196
+ const suppressedMetadataTokens = [...getIntegerRange(50257, 50363)]
197
+
198
+ const suppressedTokens = [...suppressedTextTokens, ...suppressedMetadataTokens]
199
+
200
+ this.tokenConfig.suppressedTokens = new Set(suppressedTokens)
150
201
  }
202
+
203
+ this.randomGen = new XorShift32RNG(rngSeed)
204
+ }
205
+
206
+ async initializeIfNeeded() {
207
+ await this.initializeTokenizerIfNeeded()
208
+ await this.initializeEncoderSessionIfNeeded()
209
+ await this.initializeDecoderSessionIfNeeded()
151
210
  }
152
211
 
153
- async initialize() {
212
+ async initializeTokenizerIfNeeded() {
213
+ if (this.tiktoken) {
214
+ return
215
+ }
216
+
154
217
  const logger = new Logger()
155
- await logger.startAsync("Load tokenizer data")
218
+ await logger.startAsync('Load tokenizer data')
219
+
220
+ const tiktokenModulePackagePath = await loadPackage('whisper-tiktoken-data')
156
221
 
157
- const encoderFilePath = path.join(this.modelDir, "encoder.onnx")
158
- const decoderFilePath = path.join(this.modelDir, "decoder.onnx")
222
+ const tiktokenDataFilePath = path.join(tiktokenModulePackagePath, this.isMultiligualModel ? 'multilingual.tiktoken' : 'gpt2.tiktoken')
223
+ let tiktokenData = await readFile(tiktokenDataFilePath, { encoding: 'utf8' })
159
224
 
160
- const vocabFilePath = path.join(this.tokenizerDir, "vocab.json")
161
- const mergesFilePath = path.join(this.tokenizerDir, "merges.txt")
225
+ const tokenConfig = this.tokenConfig
162
226
 
163
- const vocabObject = await readAndParseJsonFile(vocabFilePath)
227
+ const metadataTokens: Record<number, string> = {
228
+ [tokenConfig.endOfTextToken]: '[EndOfText]',
229
+ [tokenConfig.startOfTextToken]: '[StartOfText]',
230
+ [tokenConfig.translateTaskToken]: '[TranslateTask]',
231
+ [tokenConfig.transcribeTaskToken]: '[TranscribeTask]',
232
+ [tokenConfig.startOfPromptToken]: '[StartOfPrompt]',
233
+ [tokenConfig.nonSpeechToken]: '[NonSpeech]',
234
+ [tokenConfig.noTimestampsToken]: '[NoTimestamps]',
235
+ }
164
236
 
165
- function bpeEncodedStrToString(str: string) {
166
- const decodedChars = []
237
+ if (this.isMultiligualModel) {
238
+ metadataTokens[50256] = '[Unused_50256]'
239
+ metadataTokens[50360] = '[Unused_50360]'
240
+ }
167
241
 
168
- for (const char of str) {
169
- const decodedChar = vocabCharacterSetLookup[char]
242
+ const languageTokenCount = tokenConfig.languageTokensEnd - tokenConfig.languageTokensStart
170
243
 
171
- if (decodedChar == undefined) {
172
- throw new Error(`Invalid char: '${char}'`)
173
- }
244
+ for (let i = 0; i < languageTokenCount; i++) {
245
+ const tokenIndex = this.tokenConfig.languageTokensStart + i
174
246
 
175
- decodedChars.push(decodedChar)
176
- }
247
+ metadataTokens[tokenIndex] = `[Language_${i}]`
248
+ }
249
+
250
+ const timestampTokensCount = 1501
177
251
 
178
- return Buffer.from(decodedChars).toString("utf-8")
252
+ for (let i = 0; i < timestampTokensCount; i++) {
253
+ const tokenIndex = this.tokenConfig.timestampTokensStart + i
254
+ const tokenTime = this.timestampTokenToSeconds(tokenIndex)
255
+
256
+ metadataTokens[tokenIndex] = `[Timestamp_${tokenTime.toFixed(2)}]`
257
+ }
258
+
259
+ const inverseMetadataTokensLookup: Record<string, number> = {}
260
+
261
+ for (const [key, value] of Object.entries(metadataTokens)) {
262
+ inverseMetadataTokensLookup[value] = parseInt(key)
179
263
  }
180
264
 
181
- for (const key in vocabObject) {
182
- const value = vocabObject[key]
265
+ const patternString = `'s|'t|'re|'ve|'m|'ll|'d| ?\\p{L}+| ?\\p{N}+| ?[^\\s\\p{L}\\p{N}]+|\\s+(?!\\S)|\\s+`
183
266
 
184
- const decodedKey = bpeEncodedStrToString(key)
267
+ const { Tiktoken } = await import('tiktoken/lite')
268
+
269
+ this.tiktoken = new Tiktoken(tiktokenData, inverseMetadataTokensLookup, patternString)
270
+
271
+ logger.end()
272
+ }
185
273
 
186
- this.textToTokenLookup.set(decodedKey, value)
187
- this.tokenToTextLookup.set(value, decodedKey)
274
+ async initializeEncoderSessionIfNeeded() {
275
+ if (this.audioEncoder) {
276
+ return
188
277
  }
189
278
 
190
- const mergesFileRawLines = (await readFile(mergesFilePath, "utf8")).trim().split(/\r?\n/g)
191
- const mergesFileRawEntries = mergesFileRawLines.map(line => line.trim().split(" "))
192
- this.merges = mergesFileRawEntries.map(entry => [bpeEncodedStrToString(entry[0]), bpeEncodedStrToString(entry[1])])
279
+ const logger = new Logger()
280
+
281
+ await logger.startAsync(`Create encoder model inference session for model '${this.modelName}'`)
193
282
 
194
- await logger.startAsync(`Create ONNX inference session for model '${this.modelName}'`)
283
+ const encoderFilePath = path.join(this.modelDir, 'encoder.onnx')
195
284
 
196
285
  this.audioEncoder = await Onnx.InferenceSession.create(encoderFilePath, this.onnxOptions)
286
+
287
+ logger.end()
288
+ }
289
+
290
+ async initializeDecoderSessionIfNeeded() {
291
+ if (this.textDecoder) {
292
+ return
293
+ }
294
+
295
+ const logger = new Logger()
296
+
297
+ await logger.startAsync(`Create decoder model inference session for model '${this.modelName}'`)
298
+
299
+ const decoderFilePath = path.join(this.modelDir, 'decoder.onnx')
300
+
197
301
  this.textDecoder = await Onnx.InferenceSession.create(decoderFilePath, this.onnxOptions)
198
302
 
199
303
  logger.end()
200
304
  }
201
305
 
202
306
  async recognize(rawAudio: RawAudio, task: WhisperTask, language: string, options: WhisperOptions) {
203
- const logger = new Logger()
307
+ await this.initializeIfNeeded()
204
308
 
205
- const timestampTokensStart = this.tokenConfig.timestampTokensStart
309
+ const logger = new Logger()
206
310
 
207
311
  const audioSamples = rawAudio.audioChannels[0]
208
312
  const sampleRate = rawAudio.sampleRate
209
- const audioDuration = getRawAudioDuration(rawAudio)
210
313
  const prompt = options.prompt
211
314
 
212
- const maxAudioSamples = sampleRate * 30
315
+ const maxAudioSamplesPerPart = sampleRate * 30
213
316
 
214
- let previousPartTokens: number[] = []
317
+ const decodeTimestampTokens = options.decodeTimestampTokens!
318
+
319
+ let previousPartTextTokens: number[] = []
215
320
 
216
321
  let timeline: Timeline = []
217
322
  let allDecodedTokens: number[] = []
@@ -221,7 +326,7 @@ export class Whisper {
221
326
 
222
327
  await logger.startAsync(`\nPrepare audio part at time position ${segmentStartTime.toFixed(2)}`, undefined, chalk.magentaBright)
223
328
 
224
- const audioPartSamples = audioSamples.slice(audioOffset, audioOffset + maxAudioSamples)
329
+ const audioPartSamples = audioSamples.slice(audioOffset, audioOffset + maxAudioSamplesPerPart)
225
330
  const audioPartRawAudio: RawAudio = { audioChannels: [audioPartSamples], sampleRate }
226
331
  const audioPartDuration = getRawAudioDuration(audioPartRawAudio)
227
332
 
@@ -229,77 +334,73 @@ export class Whisper {
229
334
 
230
335
  const audioPartFeatures = await this.encodeAudio(audioPartRawAudio)
231
336
 
232
- const isFirstPart = audioOffset == 0
233
- const isFinalPart = audioOffset + maxAudioSamples > audioSamples.length
337
+ const isFirstPart = audioOffset === 0
338
+ const isFinalPart = audioOffset + maxAudioSamplesPerPart >= audioSamples.length
234
339
 
235
340
  let initialTokens: number[] = []
236
341
 
237
342
  if (isFirstPart && prompt) {
238
- const promptTokens = await this.textToTokens(prompt, language)
343
+ const promptTokens = this.textToTokens(prompt)
239
344
 
240
- initialTokens = [this.tokenConfig.sotPrevToken, ...promptTokens]
241
- } else if (options.autoPromptParts && previousPartTokens.length > 0) {
242
- initialTokens = [this.tokenConfig.sotPrevToken, ...previousPartTokens]
345
+ initialTokens = [this.tokenConfig.startOfPromptToken, ...promptTokens]
346
+ } else if (options.autoPromptParts && previousPartTextTokens.length > 0) {
347
+ initialTokens = [this.tokenConfig.startOfPromptToken, ...previousPartTextTokens]
243
348
  }
244
349
 
245
- initialTokens = [...initialTokens, ...this.getInitialTokens(language, task)]
350
+ initialTokens = [...initialTokens, ...this.getTextStartTokens(language, task, !decodeTimestampTokens)]
246
351
 
247
352
  logger.end()
248
353
 
249
- let { decodedTokens: partTokens, crossAttentionQKs: partCrossAttentionQKs, decodedTokensConfidence } = await this.decodeTokens(audioPartFeatures, initialTokens, audioPartDuration, isFirstPart, isFinalPart, options)
354
+ let {
355
+ decodedTokens: partTokens,
356
+ crossAttentionQKs: partCrossAttentionQKs,
357
+ decodedTokensConfidence
358
+ } = await this.decodeTokens(audioPartFeatures, initialTokens, audioPartDuration, isFirstPart, isFinalPart, options)
250
359
 
251
360
  const lastToken = partTokens[partTokens.length - 1]
252
- const lastTokenIsTimestamp = lastToken >= timestampTokensStart
361
+ const lastTokenIsTimestamp = this.isTimestampToken(lastToken)
253
362
 
254
363
  let audioEndOffset: number
255
364
 
256
365
  if (!isFinalPart && lastTokenIsTimestamp) {
257
- const timePosition = (lastToken - timestampTokensStart) * 0.02
366
+ const timePosition = this.timestampTokenToSeconds(lastToken)
258
367
 
259
368
  audioEndOffset = audioOffset + Math.floor(timePosition * sampleRate)
260
369
  } else {
261
- audioEndOffset = Math.min(audioOffset + maxAudioSamples, audioSamples.length)
370
+ audioEndOffset = Math.min(audioOffset + maxAudioSamplesPerPart, audioSamples.length)
262
371
  }
263
372
 
264
373
  const segmentEndTime = audioEndOffset / sampleRate
265
- const segmentFrameCount = Math.floor((segmentEndTime - segmentStartTime) / 0.02)
374
+ const segmentFrameCount = this.secondsRangeToFrameCount(segmentStartTime, segmentEndTime)
266
375
 
267
376
  await logger.startAsync(`Extract timeline for part`)
268
377
 
269
378
  if (partTokens.length != partCrossAttentionQKs.length) {
270
- throw new Error("Unexpected: partTokens.length != partCrossAttentionQKs.length")
379
+ throw new Error('Unexpected: partTokens.length != partCrossAttentionQKs.length')
271
380
  }
272
381
 
273
- //partTokens = partTokens.filter(token => token < timestampTokensStart)
274
- //partCrossAttentionQKs = await this.inferCrossAttentionQKs(partTokens, audioPartFeatures)
275
-
276
382
  partTokens = partTokens.slice(initialTokens.length)
277
- partCrossAttentionQKs = partCrossAttentionQKs.slice(initialTokens.length)
278
383
 
279
- //await this.addWordsToTimeline(timeline, partTokens, audioPartRawAudio, partCrossAttentionQKs, initialAudioTimeOffset, audioPartSamples.length / sampleRate)
384
+ //const compressionRatioForPart = (await getDeflateCompressionMetricsForString(this.tokensToText(partTokens))).ratio
280
385
 
281
- const alignmentPath = await this.findAlignmentPathFromQKs(partCrossAttentionQKs, partTokens, 0, segmentFrameCount) //, alignmentHeadsIndexes[this.modelName])
282
- const partTimeline = await this.getWordTimelineFromAlignmentPath(alignmentPath, partTokens, segmentStartTime, segmentEndTime, decodedTokensConfidence)
386
+ partCrossAttentionQKs = partCrossAttentionQKs.slice(initialTokens.length)
283
387
 
284
- timeline.push(...partTimeline)
388
+ const alignmentPath = await this.findAlignmentPathFromQKs(partCrossAttentionQKs, partTokens, 0, segmentFrameCount) //, alignmentHeadsIndexes[this.modelName])
389
+ const partTimeline = await this.getTokenTimelineFromAlignmentPath(alignmentPath, partTokens, segmentStartTime, segmentEndTime, decodedTokensConfidence)
285
390
 
286
391
  audioOffset = audioEndOffset
287
392
 
288
- previousPartTokens = partTokens.filter(token => token < this.tokenConfig.eotToken)
393
+ allDecodedTokens.push(...partTokens)
394
+ timeline.push(...partTimeline)
289
395
 
290
- allDecodedTokens.push(...previousPartTokens)
396
+ previousPartTextTokens = partTokens.filter(token => this.isTextToken(token))
291
397
 
292
398
  logger.end()
293
399
  }
294
400
 
295
- if (timeline.length > 0) {
296
- timeline[timeline.length - 1].endTime = audioDuration
297
- }
401
+ timeline = this.tokenTimelineToWordTimeline(timeline)
298
402
 
299
- timeline = this.mergeSuccessiveWordFragmentsInTimeline(timeline)
300
- timeline.forEach(entry => { entry.text = entry.text.trim() })
301
-
302
- const transcript = this.tokensToText(allDecodedTokens)
403
+ const transcript = this.tokensToText(allDecodedTokens).trim()
303
404
 
304
405
  logger.end()
305
406
 
@@ -307,51 +408,56 @@ export class Whisper {
307
408
  }
308
409
 
309
410
  async align(rawAudio: RawAudio, referenceText: string, language: string) {
411
+ await this.initializeIfNeeded()
412
+
310
413
  const logger = new Logger()
311
414
 
312
- await logger.startAsync("Prepare for alignment")
415
+ await logger.startAsync('Prepare for alignment')
416
+
417
+ referenceText = referenceText.replaceAll(/\s+/g, ' ')
418
+
313
419
  const audioDuration = Math.min(getRawAudioDuration(rawAudio), 30)
314
- const audioFrameCount = Math.floor(audioDuration / 0.02)
420
+ const audioFrameCount = this.secondsToFrame(audioDuration)
315
421
 
316
- const initialTokens = this.getInitialTokens(language, "transcribe", true)
317
- const timestampTokensStart = this.tokenConfig.timestampTokensStart
318
- const eotToken = this.tokenConfig.eotToken
422
+ const initialTokens = this.getTextStartTokens(language, 'transcribe', true)
319
423
 
320
- let tokens = [...initialTokens, ...await this.textToTokens(referenceText, language), eotToken]
424
+ const endOfTextToken = this.tokenConfig.endOfTextToken
425
+
426
+ let tokens = [...initialTokens, ...this.textToTokens(referenceText), endOfTextToken]
321
427
 
322
428
  logger.end()
323
429
  const audioFeatures = await this.encodeAudio(rawAudio)
324
430
 
325
- await logger.startAsync("Infer cross-attention QKs")
431
+ await logger.startAsync('Infer cross-attention QKs')
326
432
  let crossAttentionQKs = await this.inferCrossAttentionQKs(tokens, audioFeatures)
327
433
 
328
434
  tokens = tokens.slice(initialTokens.length, tokens.length - 1)
329
435
  crossAttentionQKs = crossAttentionQKs.slice(initialTokens.length, crossAttentionQKs.length - 1)
330
436
 
331
- await logger.startAsync("Extract word timeline")
437
+ await logger.startAsync('Extract word timeline')
332
438
  const alignmentPath = await this.findAlignmentPathFromQKs(crossAttentionQKs, tokens, 0, audioFrameCount)//, this.getAlignmentHeadIndexes())
333
- let timeline = await this.getWordTimelineFromAlignmentPath(alignmentPath, tokens, 0, audioDuration)
439
+ let timeline = await this.getTokenTimelineFromAlignmentPath(alignmentPath, tokens, 0, audioDuration)
334
440
 
335
- timeline = this.mergeSuccessiveWordFragmentsInTimeline(timeline)
336
- timeline.forEach(entry => { entry.text = entry.text.trim() })
337
- //timeline = timeline.filter(entry => isWordOrSymbolWord(entry.text))
441
+ timeline = this.tokenTimelineToWordTimeline(timeline)
338
442
 
339
443
  logger.end()
340
444
 
341
445
  return timeline
342
446
  }
343
447
 
344
- async detectLanguage(audioFeatures: Onnx.Tensor): Promise<LanguageDetectionResults> {
345
- const logger = new Logger()
346
-
448
+ async detectLanguage(audioFeatures: Onnx.Tensor, temperature: number): Promise<LanguageDetectionResults> {
347
449
  if (!this.isMultiligualModel) {
348
- throw new Error("Language detection only works for a multilingual model")
450
+ throw new Error('Language detection is only supported with multilingual models')
349
451
  }
350
452
 
453
+ await this.initializeTokenizerIfNeeded()
454
+ await this.initializeDecoderSessionIfNeeded()
455
+
351
456
  // Prepare and run decoder
352
- await logger.startAsync("Detect language with Whisper model")
457
+ const logger = new Logger()
458
+ await logger.startAsync('Detect language with Whisper model')
353
459
 
354
- const sotToken = this.tokenConfig.sotToken
460
+ const sotToken = this.tokenConfig.startOfTextToken
355
461
 
356
462
  const initialTokens = [sotToken]
357
463
  const offset = 0
@@ -362,13 +468,20 @@ export class Whisper {
362
468
  const tokensTensor = new Onnx.Tensor('int64', new BigInt64Array(initialTokens.map(token => BigInt(token))), [1, initialTokens.length])
363
469
  const offsetTensor = new Onnx.Tensor('int64', new BigInt64Array([BigInt(offset)]), [])
364
470
 
365
- const decoderInputs = { tokens: tokensTensor, audio_features: audioFeatures, kv_cache: kvCacheTensor, offset: offsetTensor }
471
+ const decoderInputs = {
472
+ tokens: tokensTensor,
473
+ audio_features: audioFeatures,
474
+ kv_cache: kvCacheTensor,
475
+ offset: offsetTensor
476
+ }
366
477
 
367
478
  const decoderOutputs = await this.textDecoder!.run(decoderInputs)
368
- const logitsBuffer = decoderOutputs["logits"].data as Float32Array
479
+ const logitsBuffer = decoderOutputs['logits'].data as Float32Array
369
480
 
370
- const languageTokensLogits = Array.from(logitsBuffer.slice(sotToken + 1, sotToken + 1 + 99))
371
- const languageTokensProbabilities = softmax(languageTokensLogits, 1.0)
481
+ const tokenConfig = this.tokenConfig
482
+
483
+ const languageTokensLogits = Array.from(logitsBuffer.slice(tokenConfig.languageTokensStart, tokenConfig.languageTokensEnd))
484
+ const languageTokensProbabilities = softmax(languageTokensLogits, temperature)
372
485
 
373
486
  const results: LanguageDetectionResults = []
374
487
 
@@ -388,24 +501,33 @@ export class Whisper {
388
501
  return results
389
502
  }
390
503
 
391
- async decodeTokens(audioFeatures: Onnx.Tensor, initialTokens: number[], audioDuration: number, isFirstPart: boolean, isFinalPart: boolean, options: WhisperOptions) {
504
+ async decodeTokens(
505
+ audioFeatures: Onnx.Tensor,
506
+ initialTokens: number[],
507
+ audioDuration: number,
508
+ isFirstPart: boolean,
509
+ isFinalPart: boolean,
510
+ options: WhisperOptions) {
511
+
512
+ await this.initializeTokenizerIfNeeded()
513
+ await this.initializeDecoderSessionIfNeeded()
514
+
392
515
  const logger = new Logger()
393
- await logger.startAsync("Decode text tokens with Whisper decoder model")
394
516
 
395
- options = extendDeep(whisperOptionsDefaults, options)
517
+ await logger.startAsync('Decode text tokens with Whisper decoder model')
396
518
 
397
- const noSpeechThreshold = 0.6
519
+ options = extendDeep(defaultWhisperOptions, options)
398
520
 
399
- const blankToken = this.textToTokenLookup.get(" ")
521
+ const endOfTextToken = this.tokenConfig.endOfTextToken
400
522
 
401
- const suppressedTokens = this.tokenConfig.suppressedTokens
402
- const sotToken = this.tokenConfig.sotToken
403
- const eotToken = this.tokenConfig.eotToken
404
- const noTimestampsToken = this.tokenConfig.noTimestampsToken
405
- const noSpeechToken = this.tokenConfig.noSpeechToken
406
523
  const timestampTokensStart = this.tokenConfig.timestampTokensStart
524
+ const suppressedTokens = this.tokenConfig.suppressedTokens
407
525
 
408
- const maxDecodedTokenCount = 250
526
+ const spaceToken = this.textToTokens(' ')[0]
527
+
528
+ const maxDecodedTokenCount = options.maxTokensPerPart!
529
+
530
+ //const suppressedTokensText = Array.from(this.tokenConfig.suppressedTokens).map(token => this.tokenToText(token, true))
409
531
 
410
532
  let decodedTokens = initialTokens.slice()
411
533
  const initialKvDimensions = this.getKvDimensions(1, decodedTokens.length)
@@ -455,147 +577,196 @@ export class Whisper {
455
577
 
456
578
  const decoderOutputs = await this.textDecoder!.run(decoderInputs)
457
579
 
458
- const logitsBuffer = decoderOutputs["logits"].data as Float32Array
459
- kvCacheTensor = decoderOutputs["output_kv_cache"] as any
580
+ const logitsBuffer = decoderOutputs['logits'].data as Float32Array
581
+ kvCacheTensor = decoderOutputs['output_kv_cache'] as any
460
582
 
461
583
  // Compute logits
462
- const resultLogits = splitFloat32Array(logitsBuffer, logitsBuffer.length / decoderOutputs["logits"].dims[1])
463
- const tokenLogits = resultLogits[resultLogits.length - 1]
464
- const tokenTimestampLogits = Array.from(tokenLogits.slice(timestampTokensStart))
584
+ const resultLogits = splitFloat32Array(logitsBuffer, logitsBuffer.length / decoderOutputs['logits'].dims[1])
585
+ const allTokenLogits = Array.from(resultLogits[resultLogits.length - 1])
586
+ const timestampTokenLogits = allTokenLogits.slice(timestampTokensStart)
465
587
 
466
588
  // Suppress tokens
467
- for (let logitIndex = 0; logitIndex < tokenLogits.length; logitIndex++) {
468
- const isWrongTokenForInitialState = isInitialState && (logitIndex == blankToken || logitIndex == eotToken)
469
- const isInSupressedList = suppressedTokens.includes(logitIndex)
470
- const isNoTimestampsToken = logitIndex == noTimestampsToken
589
+ for (let logitIndex = 0; logitIndex < allTokenLogits.length; logitIndex++) {
590
+ const isWrongTokenForInitialState =
591
+ isInitialState &&
592
+ (logitIndex === spaceToken || logitIndex === endOfTextToken)
471
593
 
472
- const shouldSupressToken = isWrongTokenForInitialState || isInSupressedList || isNoTimestampsToken
594
+ const isInSuppressedList = suppressedTokens.has(logitIndex)
473
595
 
474
- if (shouldSupressToken) {
475
- tokenLogits[logitIndex] = -Infinity
596
+ const shouldSuppressToken = isWrongTokenForInitialState || isInSuppressedList
597
+
598
+ if (shouldSuppressToken) {
599
+ allTokenLogits[logitIndex] = -Infinity
476
600
  }
477
601
  }
478
602
 
479
- // Find token distributions and best token
480
- const probs = softmax(tokenLogits as any)
481
- const logProbs = logOfVector(probs)
603
+ // Derive token probabilities
604
+ let bufferedTokensToPrint: number[] = []
605
+
606
+ // Add best token
607
+ function addToken(tokenToAdd: number, timestampLogits: number[], confidence: number) {
608
+ decodedTokens.push(tokenToAdd)
609
+ decodedTokensTimestampLogits.push(timestampLogits)
610
+ decodedTokensCrossAttentionQKs.push(decoderOutputs['cross_attention_qks'])
611
+ decodedTokensConfidence.push(confidence)
612
+ }
613
+
614
+ let shouldDecodeNonTimestampToken = true
482
615
 
483
- const textTokenLogProbs = logProbs.slice(0, timestampTokensStart)
484
- const timestampTokenLogProbs = logProbs.slice(timestampTokensStart)
616
+ if (options.decodeTimestampTokens) {
617
+ const probabilities = softmax(allTokenLogits as any, 1.0)
618
+ const logProbabilities = logOfVector(probabilities)
485
619
 
486
- const indexOfMaxTextLogProb = indexOfMax(textTokenLogProbs)
487
- const valueOfMaxTextLogProb = textTokenLogProbs[indexOfMaxTextLogProb]
620
+ const nonTimestampTokenLogProbs = logProbabilities.slice(0, timestampTokensStart)
488
621
 
489
- const indexOfMaxTimestampLogProb = indexOfMax(timestampTokenLogProbs)
622
+ const indexOfMaxNonTimestampLogProb = indexOfMax(nonTimestampTokenLogProbs)
623
+ const valueOfMaxNonTimestampLogProb = nonTimestampTokenLogProbs[indexOfMaxNonTimestampLogProb]
490
624
 
491
- const logSumExpOfTimestampTokenLogProbs = logSumExp(timestampTokenLogProbs)
625
+ const timestampTokenLogProbs = logProbabilities.slice(timestampTokensStart)
626
+ const indexOfMaxTimestampLogProb = indexOfMax(timestampTokenLogProbs)
492
627
 
493
- const isTimestampToken = logSumExpOfTimestampTokenLogProbs > valueOfMaxTextLogProb
494
- const previousTokenWasTimestamp = decodedTokens[decodedTokens.length - 1] >= timestampTokensStart
495
- const secondPreviousTokenWasTimestamp = decodedTokens.length < 2 || decodedTokens[decodedTokens.length - 2] >= timestampTokensStart
628
+ const logSumExpOfTimestampTokenLogProbs = logSumExp(timestampTokenLogProbs)
496
629
 
497
- if (isTimestampToken && !previousTokenWasTimestamp) {
498
- timestampsSeenCount += 1
499
- }
630
+ const shouldDecodeTimestampToken = logSumExpOfTimestampTokenLogProbs > valueOfMaxNonTimestampLogProb
500
631
 
501
- //
502
- //const topLogits = [...tokenLogits].map((logit, index) => ({ index, logit, token: this.tokenToTextLookup.get(index) || "", prob: probs[index] }))
503
- //topLogits.sort((a, b) => b.logit - a.logit)
504
- ///
632
+ const previousTokenWasTimestamp = this.isTimestampToken(decodedTokens[decodedTokens.length - 1])
633
+ const secondPreviousTokenWasTimestamp = decodedTokens.length < 2 || this.isTimestampToken(decodedTokens[decodedTokens.length - 2])
505
634
 
506
- // Add best token
507
- function addToken(tokenToAdd: number, timestampLogits: number[], confidence: number) {
508
- decodedTokens.push(tokenToAdd)
509
- decodedTokensTimestampLogits.push(timestampLogits)
510
- decodedTokensCrossAttentionQKs.push(decoderOutputs["cross_attention_qks"])
511
- decodedTokensConfidence.push(confidence)
512
- }
635
+ if (shouldDecodeTimestampToken && !previousTokenWasTimestamp) {
636
+ timestampsSeenCount += 1
637
+ }
513
638
 
514
- if (isTimestampToken || (previousTokenWasTimestamp && !secondPreviousTokenWasTimestamp)) {
515
- if (previousTokenWasTimestamp) {
516
- const previousToken = decodedTokens[decodedTokens.length - 1]
517
- const previousTokenTimestampLogits = decodedTokensTimestampLogits[decodedTokensTimestampLogits.length - 1]
518
- const previousTokenConfidence = decodedTokensConfidence[decodedTokensConfidence.length - 1]
639
+ if (shouldDecodeTimestampToken || (previousTokenWasTimestamp && !secondPreviousTokenWasTimestamp)) {
640
+ if (previousTokenWasTimestamp) {
641
+ const previousToken = decodedTokens[decodedTokens.length - 1]
642
+ const previousTokenTimestampLogits = decodedTokensTimestampLogits[decodedTokensTimestampLogits.length - 1]
643
+ const previousTokenConfidence = decodedTokensConfidence[decodedTokensConfidence.length - 1]
644
+
645
+ addToken(previousToken, previousTokenTimestampLogits, previousTokenConfidence)
519
646
 
520
- addToken(previousToken, previousTokenTimestampLogits, previousTokenConfidence)
647
+ lastTimestampTokenIndex = decodedTokens.length
521
648
 
522
- lastTimestampTokenIndex = decodedTokens.length
649
+ const previousTokenTimestamp = this.timestampTokenToSeconds(previousToken)
523
650
 
524
- const previousTokenTimestamp = (previousToken - timestampTokensStart) * 0.02
651
+ if (previousTokenTimestamp >= audioDuration) {
652
+ break
653
+ }
654
+ } else {
655
+ const timestampToken = timestampTokensStart + indexOfMaxTimestampLogProb
656
+ const confidence = probabilities[timestampToken]
525
657
 
526
- if (previousTokenTimestamp >= audioDuration) {
527
- break
658
+ addToken(timestampToken, timestampTokenLogits, confidence)
528
659
  }
529
- } else {
530
- const timestampToken = timestampTokensStart + indexOfMaxTimestampLogProb
531
- const confidence = probs[timestampToken]
532
660
 
533
- addToken(timestampToken, tokenTimestampLogits, confidence)
661
+ shouldDecodeNonTimestampToken = false
534
662
  }
535
- } else if (indexOfMaxTextLogProb == eotToken) {
536
- break
537
- } else {
538
- let chosenTokenIndex: number
663
+ }
539
664
 
540
- if (options.temperature == 0.0) {
541
- chosenTokenIndex = indexOfMaxTextLogProb
542
- } else {
543
- const topLogitCount = options.topCandidateCount!
544
-
545
- const textTokenLogits = tokenLogits.slice(0, timestampTokensStart)
546
- const sortedTextTokenLogitsWithIndexes = Array.from(textTokenLogits).map((logit, index) => ({ logit, index }))
547
- sortedTextTokenLogitsWithIndexes.sort((a, b) => b.logit - a.logit)
548
- let topLogitsWithIndexes = sortedTextTokenLogitsWithIndexes.slice(0, topLogitCount)
549
-
550
- ////
551
- /*
552
- topLogitsWithIndexes = topLogitsWithIndexes.filter(entry => {
553
- const lastDecodedTextTokens = decodedTokens.filter(token => token < eotToken).reverse().slice(0, 20)
554
- const { maxScore } = getRepetitionScoreRelativeToFirstSubstring([entry.index, ...lastDecodedTextTokens])
555
-
556
- if (maxScore < 4) {
557
- return true
558
- } else {
559
- return false
560
- }
561
- })
562
- */
563
- ////
665
+ if (shouldDecodeNonTimestampToken) {
666
+ const topLogitCount = options.topCandidateCount!
564
667
 
565
- const topLogits = topLogitsWithIndexes.map(a => a.logit)
566
- const textTokenProbs = softmax(topLogits, options.temperature)
668
+ const nonTimestampTokenLogits = allTokenLogits.slice(0, timestampTokensStart)
567
669
 
568
- const topIndexOfPromisingPunctuationLogit = topLogitsWithIndexes.findIndex(entry => {
569
- const tokenText = (this.tokenToTextLookup.get(entry.index) || "").trim()
570
- const tokenProb = probs[entry.index]
670
+ const sortedNonTimestampTokenLogitsWithIndexes =
671
+ Array.from(nonTimestampTokenLogits).map((logit, index) => ({ token: index, logit }))
571
672
 
572
- return tokenProb >= options.punctuationThreshold! && [',', ',', '.', '。', '!', '?'].includes(tokenText)
673
+ sortedNonTimestampTokenLogitsWithIndexes.sort((a, b) => b.logit - a.logit)
674
+
675
+ let topCandidates = sortedNonTimestampTokenLogitsWithIndexes.slice(0, topLogitCount)
676
+ .map(entry => ({
677
+ token: entry.token,
678
+ logit: entry.logit,
679
+ text: this.tokenToText(entry.token, true)
680
+ }))
681
+
682
+ //// Repetition suppression code
683
+ if (options.suppressRepetition) {
684
+ const topCandidatesRepetitionScores = topCandidates.map(entry => {
685
+ const lastDecodedTextTokens = decodedTokens.filter(token => this.isTextToken(token)).reverse().slice(0, 20)
686
+ const { maxScore } = getRepetitionScoreRelativeToFirstSubstring([entry.token, ...lastDecodedTextTokens])
687
+
688
+ return maxScore
573
689
  })
574
690
 
575
- let chosenTokenIndexInTopLogits: number
691
+ const thresholdRepetitionScore = 3
576
692
 
577
- if (topIndexOfPromisingPunctuationLogit >= 0) {
578
- chosenTokenIndexInTopLogits = topIndexOfPromisingPunctuationLogit
693
+ if (topCandidatesRepetitionScores.every(score => score >= thresholdRepetitionScore)) {
694
+ const indexOfMaxScore = topCandidatesRepetitionScores.indexOf(Math.max(...topCandidatesRepetitionScores))
695
+ topCandidates = [topCandidates[indexOfMaxScore]]
579
696
  } else {
580
- chosenTokenIndexInTopLogits = this.randomGen.selectRandomIndexFromDistribution(textTokenProbs)
697
+ topCandidates = topCandidates.filter((candidate, index) => topCandidatesRepetitionScores[index] < thresholdRepetitionScore)
581
698
  }
699
+ }
700
+ ////
701
+
702
+ const topCandidateProbabilities = softmax(topCandidates.map(a => a.logit), options.temperature)
703
+
704
+ //// Remove end-of-text token from candidates if its probability isn't high enough
705
+ if (options.decodeTimestampTokens === false) {
706
+ topCandidates = topCandidates.filter((candidate, index) => {
707
+ if (candidate.token === endOfTextToken) {
708
+ return topCandidateProbabilities[index] >= 0.9
709
+ }
582
710
 
583
- chosenTokenIndex = sortedTextTokenLogitsWithIndexes[chosenTokenIndexInTopLogits].index
711
+ return true
712
+ })
584
713
  }
714
+ ////
715
+
716
+ const rankOfPromisingPunctuationToken = topCandidates.findIndex((entry, index) => {
717
+ const tokenText = this.tokenToText(entry.token).trim()
585
718
 
586
- if (chosenTokenIndex < eotToken) {
587
- let chosenTokenText = this.tokenToTextLookup.get(chosenTokenIndex) || ""
719
+ const isPunctuationToken = [',', ',', '、', '.', '。', '!', '?'].includes(tokenText)
588
720
 
589
- if (isFirstPart && decodedTokens.every(token => token >= eotToken)) {
590
- chosenTokenText = chosenTokenText.trimStart()
721
+ if (!isPunctuationToken) {
722
+ return false
591
723
  }
592
724
 
593
- writeToStderr(chosenTokenText)
725
+ const tokenProb = topCandidateProbabilities[index]
726
+
727
+ return tokenProb >= options.punctuationThreshold!
728
+ })
729
+
730
+ let rankOfSpaceToken = topCandidates.findIndex(candidate => candidate.token === spaceToken)
731
+
732
+ if (rankOfSpaceToken < 0) {
733
+ rankOfSpaceToken = Infinity
734
+ }
735
+
736
+ let chosenCandidateRank: number
737
+
738
+ if (rankOfPromisingPunctuationToken >= 0 &&
739
+ rankOfPromisingPunctuationToken < rankOfSpaceToken) {
740
+ chosenCandidateRank = rankOfPromisingPunctuationToken
741
+ } else {
742
+ chosenCandidateRank = this.randomGen.selectRandomIndexFromDistribution(topCandidateProbabilities)
594
743
  }
595
744
 
596
- const confidence = probs[chosenTokenIndex]
745
+ const chosenToken = topCandidates[chosenCandidateRank].token
746
+
747
+ if (this.isTextToken(chosenToken)) {
748
+ bufferedTokensToPrint.push(chosenToken)
749
+
750
+ let textToPrint = this.tokensToText(bufferedTokensToPrint)
751
+
752
+ if (textToPrint.codePointAt(0) !== 65533) {
753
+ if (isFirstPart && decodedTokens.every(token => this.isMetadataToken(token))) {
754
+ textToPrint = textToPrint.trimStart()
755
+ }
597
756
 
598
- addToken(chosenTokenIndex, tokenTimestampLogits, confidence)
757
+ logger.write(textToPrint)
758
+
759
+ bufferedTokensToPrint = []
760
+ }
761
+ }
762
+
763
+ const confidence = topCandidateProbabilities[chosenCandidateRank]
764
+
765
+ addToken(chosenToken, timestampTokenLogits, confidence)
766
+
767
+ if (chosenToken === endOfTextToken) {
768
+ break
769
+ }
599
770
  }
600
771
 
601
772
  await yieldToEventLoop()
@@ -607,11 +778,16 @@ export class Whisper {
607
778
  decodedTokensCrossAttentionQKs = decodedTokensCrossAttentionQKs.slice(0, lastTimestampTokenIndex)
608
779
  }
609
780
 
610
- writeToStderr("\n")
781
+ logger.write('\n')
611
782
  logger.end()
612
783
 
613
784
  // Return the tokens
614
- return { decodedTokens, decodedTokensTimestampLogits, crossAttentionQKs: decodedTokensCrossAttentionQKs, decodedTokensConfidence }
785
+ return {
786
+ decodedTokens,
787
+ decodedTokensTimestampLogits,
788
+ crossAttentionQKs: decodedTokensCrossAttentionQKs,
789
+ decodedTokensConfidence
790
+ }
615
791
  }
616
792
 
617
793
  async inferCrossAttentionQKs(tokens: number[], audioFeatures: Onnx.Tensor) {
@@ -627,7 +803,7 @@ export class Whisper {
627
803
 
628
804
  const decoderOutputs = await this.textDecoder!.run(decoderInputs)
629
805
 
630
- const crossAttentionQKsTensor = decoderOutputs["cross_attention_qks"]
806
+ const crossAttentionQKsTensor = decoderOutputs['cross_attention_qks']
631
807
 
632
808
  const tensorShape = crossAttentionQKsTensor.dims.slice()
633
809
 
@@ -663,6 +839,8 @@ export class Whisper {
663
839
  }
664
840
 
665
841
  async encodeAudio(rawAudio: RawAudio) {
842
+ await this.initializeEncoderSessionIfNeeded()
843
+
666
844
  const logger = new Logger()
667
845
 
668
846
  const audioSamples = rawAudio.audioChannels[0]
@@ -675,7 +853,7 @@ export class Whisper {
675
853
  const maxAudioSamples = sampleRate * 30
676
854
  const maxAudioFrames = 3000
677
855
 
678
- await logger.startAsync("Extract mel spectogram from audio part")
856
+ await logger.startAsync('Extract mel spectogram from audio part')
679
857
 
680
858
  const paddedAudioSamples = new Float32Array(maxAudioSamples)
681
859
  paddedAudioSamples.set(audioSamples.subarray(0, maxAudioSamples), 0)
@@ -684,7 +862,7 @@ export class Whisper {
684
862
 
685
863
  const { melSpectogram } = await computeMelSpectogramUsingFilterbanks(rawAudioPart, fftOrder, fftOrder, hopLength, filterbanks)
686
864
 
687
- await logger.startAsync("Normalize mel spectogram")
865
+ await logger.startAsync('Normalize mel spectogram')
688
866
 
689
867
  const logMelSpectogram = melSpectogram.map(spectrum => spectrum.map(mel => Math.log10(Math.max(mel, 1e-10))))
690
868
  let maxLogMel = -Infinity
@@ -708,14 +886,14 @@ export class Whisper {
708
886
  }
709
887
  }
710
888
 
711
- await logger.startAsync("Encode mel spectogram with Whisper encoder model")
889
+ await logger.startAsync('Encode mel spectogram with Whisper encoder model')
712
890
 
713
891
  const inputTensor = new Onnx.Tensor('float32', flattenedNormalizedLogMelSpectogram, [1, filterbankCount, maxAudioFrames])
714
892
 
715
893
  const encoderInputs = { mel: inputTensor }
716
894
 
717
895
  const encoderOutputs = await this.audioEncoder!.run(encoderInputs)
718
- const encodedAudioFeatures = encoderOutputs["output"]
896
+ const encodedAudioFeatures = encoderOutputs['output']
719
897
 
720
898
  logger.end()
721
899
 
@@ -728,7 +906,7 @@ export class Whisper {
728
906
  for (let i = 0; i < tokens.length; i++) {
729
907
  const token = tokens[i]
730
908
 
731
- if (token == this.tokenConfig.sotToken || token == this.tokenConfig.eotToken) {
909
+ if (token == this.tokenConfig.startOfTextToken || token == this.tokenConfig.endOfTextToken) {
732
910
  continue
733
911
  }
734
912
 
@@ -740,7 +918,7 @@ export class Whisper {
740
918
  continue
741
919
  }
742
920
 
743
- let startTime = initialTimeOffset + (token - timestampTokensStart) * 0.02
921
+ let startTime = initialTimeOffset + this.timestampTokenToSeconds(token)
744
922
 
745
923
  startTime = Math.min(startTime, audioDuration)
746
924
 
@@ -749,22 +927,22 @@ export class Whisper {
749
927
  }
750
928
 
751
929
  timeline.push({
752
- type: "segment",
753
- text: "",
930
+ type: 'segment',
931
+ text: '',
754
932
  startTime,
755
933
  endTime: -1,
756
934
  })
757
935
  } else {
758
936
  if (timeline.length == 0) {
759
937
  timeline.push({
760
- type: "segment",
761
- text: "",
938
+ type: 'segment',
939
+ text: '',
762
940
  startTime: initialTimeOffset,
763
941
  endTime: -1,
764
942
  })
765
943
  }
766
944
 
767
- const tokenText = this.tokenToTextLookup.get(token) || ""
945
+ const tokenText = this.tokenToText(token)
768
946
 
769
947
  timeline[timeline.length - 1].text += tokenText
770
948
  }
@@ -772,8 +950,6 @@ export class Whisper {
772
950
  }
773
951
 
774
952
  async addWordsToTimeline(timeline: Timeline, tokens: number[], rawAudio: RawAudio, crossAttentionQKs: Onnx.Tensor[], initialAudioTimeOffset: number, duration: number) {
775
- const timestampTokensStart = this.tokenConfig.timestampTokensStart
776
-
777
953
  let segmentStartTime = 0
778
954
  let segmentTokens: number[] = []
779
955
  let segmentCrossAttentionQKs: Onnx.Tensor[] = []
@@ -782,15 +958,15 @@ export class Whisper {
782
958
  const token = tokens[tokenIndex]
783
959
  const tokenCrossAttentionQKs = crossAttentionQKs[tokenIndex]
784
960
 
785
- const segmentTokensWithoutTimestamps = segmentTokens.filter(token => token < this.tokenConfig.timestampTokensStart)
961
+ const segmentTokensWithoutTimestamps = segmentTokens.filter(token => this.isNonTimestampToken(token))
786
962
 
787
- const isTimestamp = token >= timestampTokensStart
963
+ const isTimestamp = this.isTimestampToken(token)
788
964
 
789
965
  if (isTimestamp || tokenIndex == tokens.length - 1) {
790
966
  let tokenTime: number
791
967
 
792
968
  if (isTimestamp) {
793
- tokenTime = (token - timestampTokensStart) * 0.02
969
+ tokenTime = this.timestampTokenToSeconds(token)
794
970
  } else {
795
971
  tokenTime = duration
796
972
  }
@@ -798,8 +974,8 @@ export class Whisper {
798
974
  if (segmentTokensWithoutTimestamps.length > 0) {
799
975
  const segmentEndTime = tokenTime
800
976
 
801
- const segmentStartFrame = Math.floor(segmentStartTime / 0.02)
802
- let segmentEndFrame = Math.floor(segmentEndTime / 0.02)
977
+ const segmentStartFrame = this.secondsToFrame(segmentStartTime)
978
+ let segmentEndFrame = this.secondsToFrame(segmentEndTime)
803
979
 
804
980
  if (segmentStartFrame == segmentEndFrame) {
805
981
  segmentEndFrame += 1
@@ -810,7 +986,7 @@ export class Whisper {
810
986
  const reinferCrossAttentionQKs = true
811
987
 
812
988
  if (reinferCrossAttentionQKs) {
813
- const initialTokens = this.getInitialTokens('en', 'transcribe')
989
+ const initialTokens = this.getTextStartTokens('en', 'transcribe')
814
990
  const tokensToDecode = [...initialTokens, ...segmentTokensWithoutTimestamps]
815
991
 
816
992
  //const segmentAudioFeaturesBuffer = audioFeatures.data.slice(segmentStartFrame * audioFeatures.dims[2], segmentEndFrame * audioFeatures.dims[2])
@@ -825,14 +1001,14 @@ export class Whisper {
825
1001
  reinferredCrossAttentionQKs.slice(initialTokens.length)
826
1002
 
827
1003
  const alignmentPath = await this.findAlignmentPathFromQKs(reinferredCrossAttentionQKs, tokensToDecode, 0, segmentFrameCount)//, alignmentHeadsIndexes[modelName])
828
- const wordTimeline = await this.getWordTimelineFromAlignmentPath(alignmentPath, segmentTokensWithoutTimestamps, initialAudioTimeOffset + segmentStartTime, initialAudioTimeOffset + segmentEndTime)
1004
+ const tokenTimeline = await this.getTokenTimelineFromAlignmentPath(alignmentPath, segmentTokensWithoutTimestamps, initialAudioTimeOffset + segmentStartTime, initialAudioTimeOffset + segmentEndTime)
829
1005
 
830
- timeline.push(...wordTimeline)
1006
+ timeline.push(...tokenTimeline)
831
1007
  } else {
832
1008
  const alignmentPath = await this.findAlignmentPathFromQKs(segmentCrossAttentionQKs, segmentTokens, segmentStartFrame, segmentEndFrame)//, alignmentHeadsIndexes[modelName])
833
- const wordTimeline = await this.getWordTimelineFromAlignmentPath(alignmentPath, segmentTokens, initialAudioTimeOffset, initialAudioTimeOffset + segmentEndTime)
1009
+ const tokenTimeline = await this.getTokenTimelineFromAlignmentPath(alignmentPath, segmentTokens, initialAudioTimeOffset, initialAudioTimeOffset + segmentEndTime)
834
1010
 
835
- timeline.push(...wordTimeline)
1011
+ timeline.push(...tokenTimeline)
836
1012
  }
837
1013
  }
838
1014
 
@@ -846,17 +1022,34 @@ export class Whisper {
846
1022
  }
847
1023
  }
848
1024
 
849
- mergeSuccessiveWordFragmentsInTimeline(timeline: Timeline) {
1025
+ tokenTimelineToWordTimeline(tokenTimeline: Timeline) {
1026
+ const separatorChars =
1027
+ [' ', '–', '一', ',', '、', '|', '/', '\\', ';', '"', '“', '”', '…', '(', ')', '[', ']', '{', '}']
1028
+
1029
+ function startsWithSeparatingPunctuation(text: string) {
1030
+ return separatorChars.some(char => text.startsWith(char))
1031
+ }
1032
+
1033
+ function isSeparatorPunctuation(text: string) {
1034
+ return separatorChars.includes(text)
1035
+ }
1036
+
850
1037
  const resultTimeline: Timeline = []
851
1038
 
852
1039
  const groups: TimelineEntry[][] = []
853
1040
 
854
- for (const entry of timeline) {
855
- if (entry.type != "word") {
856
- continue
857
- }
1041
+ for (let i = 0; i < tokenTimeline.length; i++) {
1042
+ const entry = tokenTimeline[i]
1043
+ const previousEntry = i > 0 ? tokenTimeline[i - 1] : undefined
1044
+
1045
+ const text = entry.text
1046
+ const previousEntryText = previousEntry?.text
1047
+
1048
+ if (groups.length == 0 ||
1049
+ text === '' ||
1050
+ startsWithSeparatingPunctuation(text) ||
1051
+ (previousEntryText != null && isSeparatorPunctuation(previousEntryText))) {
858
1052
 
859
- if (groups.length == 0 || entry.text.startsWith(" ")) {
860
1053
  groups.push([entry])
861
1054
  } else {
862
1055
  groups[groups.length - 1].push(entry)
@@ -864,39 +1057,41 @@ export class Whisper {
864
1057
  }
865
1058
 
866
1059
  for (const group of groups) {
867
- if (group.length == 1) {
868
- resultTimeline.push(deepClone(group[0]))
869
- } else {
870
- const text = group.map(entry => entry.text).join("")
871
- const startTime = group[0].startTime
872
- const endTime = group[group.length - 1].endTime
873
- let confidence: number | undefined = undefined
1060
+ const groupText = this.tokensToText(group.map(entry => entry.id!))
874
1061
 
875
- if (group[0].confidence != null) {
876
- confidence = meanOfVector(group.map(entry => entry.confidence!))
877
- }
1062
+ if (groupText === '') {
1063
+ continue
1064
+ }
878
1065
 
879
- const newEntry: TimelineEntry = {
880
- type: "word",
881
- text,
882
- startTime,
883
- endTime,
884
- confidence
885
- }
1066
+ const startTime = group[0].startTime
1067
+ const endTime = group[group.length - 1].endTime
1068
+ let confidence: number | undefined = undefined
1069
+
1070
+ if (group[0].confidence != null) {
1071
+ confidence = meanOfVector(group.map(entry => entry.confidence!))
1072
+ }
886
1073
 
887
- resultTimeline.push(newEntry)
1074
+ const newEntry: TimelineEntry = {
1075
+ type: 'word',
1076
+ text: groupText.trim(),
1077
+ startTime,
1078
+ endTime,
1079
+ confidence,
1080
+ timeline: group,
888
1081
  }
1082
+
1083
+ resultTimeline.push(newEntry)
889
1084
  }
890
1085
 
891
1086
  return resultTimeline
892
1087
  }
893
1088
 
894
- async getWordTimelineFromAlignmentPath(alignmentPath: AlignmentPath, tokens: number[], startTimeOffset: number, endTimeOffset: number, tokensConfidence?: number[], correctionAmount = 0.0) {
1089
+ async getTokenTimelineFromAlignmentPath(alignmentPath: AlignmentPath, tokens: number[], startTimeOffset: number, endTimeOffset: number, tokensConfidence?: number[], correctionAmount = 0.0) {
895
1090
  if (alignmentPath.length == 0) {
896
1091
  return []
897
1092
  }
898
1093
 
899
- const wordTimeline: Timeline = []
1094
+ const tokenTimeline: Timeline = []
900
1095
 
901
1096
  for (let pathIndex = 0; pathIndex < alignmentPath.length; pathIndex++) {
902
1097
  if (pathIndex != 0 && alignmentPath[pathIndex].source == alignmentPath[pathIndex - 1].source) {
@@ -908,41 +1103,37 @@ export class Whisper {
908
1103
  const tokenIndex = tokenMappingEntry.source
909
1104
  const token = tokens[tokenIndex]
910
1105
  const tokenConfidence = tokensConfidence ? tokensConfidence[tokenIndex] : undefined
911
- const tokenText = this.tokenToTextLookup.get(token)
912
-
913
- if (token >= this.tokenConfig.eotToken || !tokenText) {
914
- continue
915
- }
1106
+ const tokenText = this.tokenToText(token, true)
916
1107
 
917
1108
  let startTime = startTimeOffset + (tokenMappingEntry.dest * 0.02)
918
1109
 
919
1110
  startTime = Math.max(startTime + correctionAmount, startTimeOffset)
920
1111
 
921
- if (wordTimeline.length > 0) {
922
- wordTimeline[wordTimeline.length - 1].endTime = startTime
1112
+ if (tokenTimeline.length > 0) {
1113
+ tokenTimeline[tokenTimeline.length - 1].endTime = startTime
923
1114
  }
924
1115
 
925
- wordTimeline.push({
926
- type: "word",
1116
+ tokenTimeline.push({
1117
+ type: 'token',
927
1118
  text: tokenText,
1119
+ id: token,
928
1120
  startTime,
929
1121
  endTime: -1,
930
1122
  confidence: tokenConfidence
931
1123
  })
932
1124
  }
933
1125
 
934
- if (wordTimeline.length > 0) {
935
- wordTimeline[wordTimeline.length - 1].endTime = endTimeOffset
1126
+ if (tokenTimeline.length > 0) {
1127
+ tokenTimeline[tokenTimeline.length - 1].endTime = endTimeOffset
936
1128
  }
937
1129
 
938
- return wordTimeline
1130
+ return tokenTimeline
939
1131
  }
940
1132
 
941
1133
  async findAlignmentPathFromQKs(qksTensors: Onnx.Tensor[], tokens: number[], segmentStartFrame: number, segmentEndFrame: number, headIndexes?: number[]) {
942
1134
  const segmentFrameCount = segmentEndFrame - segmentStartFrame
943
1135
 
944
- if (segmentFrameCount == 0) {
945
- //throw new Error("Segment has 0 frames")
1136
+ if (segmentFrameCount === 0 || tokens.length === 0 || qksTensors.length === 0) {
946
1137
  return []
947
1138
  }
948
1139
 
@@ -961,10 +1152,10 @@ export class Whisper {
961
1152
  }
962
1153
 
963
1154
  // Load attention head weights from tensors
964
- const attentionHeads: number[][][] = [] // [heads, tokens, frames]
1155
+ const attentionHeads: number[][][] = [] // structure: [heads, tokens, frames]
965
1156
 
966
1157
  for (const headIndex of headIndexes) {
967
- const attentionHead: number[][] = [] // [tokens, frames]
1158
+ const attentionHead: number[][] = [] // structure: [tokens, frames]
968
1159
 
969
1160
  for (let tokenIndex = 0; tokenIndex < tokenCount; tokenIndex++) {
970
1161
  const bufferOffset = headIndex * frameCount
@@ -1002,7 +1193,7 @@ export class Whisper {
1002
1193
  const allWeightsForHead = head.flatMap(tokenFrames => tokenFrames)
1003
1194
 
1004
1195
  const meanOfAllWeights = meanOfVector(allWeightsForHead)
1005
- const stdDeviationOfAllWeights = stdDeviationOfVector(allWeightsForHead)
1196
+ const stdDeviationOfAllWeights = stdDeviationOfVector(allWeightsForHead) + 1e-10
1006
1197
 
1007
1198
  for (const tokenFrames of head) {
1008
1199
  for (let frameIndex = 0; frameIndex < tokenFrames.length; frameIndex++) {
@@ -1045,11 +1236,15 @@ export class Whisper {
1045
1236
  }
1046
1237
 
1047
1238
  if (fixateTimestampTokens) {
1239
+ // Fixate timestamp tokens to the original ones detected
1048
1240
  const timestampTokensStart = this.tokenConfig.timestampTokensStart
1049
1241
 
1050
1242
  for (let tokenIndex = 0; tokenIndex < tokens.length; tokenIndex++) {
1051
- if (tokens[tokenIndex] >= timestampTokensStart) {
1052
- let timestampFrame = tokens[tokenIndex] - timestampTokensStart
1243
+ const token = tokens[tokenIndex]
1244
+
1245
+ if (this.isTimestampToken(token)) {
1246
+ let timestampFrame = token - timestampTokensStart
1247
+
1053
1248
  timestampFrame = clip(timestampFrame, segmentStartFrame, segmentEndFrame - 1)
1054
1249
 
1055
1250
  frameMeansForToken[tokenIndex][timestampFrame] = 100
@@ -1061,9 +1256,9 @@ export class Whisper {
1061
1256
  const tokenIndexes = [...Array(tokenCount).keys()]
1062
1257
  const frameIndexes = [...Array(segmentFrameCount).keys()]
1063
1258
 
1064
- let { path } = await alignDTWWindowed(tokenIndexes, frameIndexes, (tokenIndex, frameIndex) => {
1259
+ let { path } = alignDTWWindowed(tokenIndexes, frameIndexes, (tokenIndex, frameIndex) => {
1065
1260
  return -frameMeansForToken[tokenIndex][frameIndex]
1066
- }, 1000)
1261
+ }, segmentFrameCount)
1067
1262
 
1068
1263
  path = path.map(entry => ({ source: entry.source, dest: segmentStartFrame + entry.dest }))
1069
1264
 
@@ -1073,101 +1268,138 @@ export class Whisper {
1073
1268
  getKvDimensions(groupCount: number, length: number) {
1074
1269
  const modelName = this.modelName
1075
1270
 
1076
- if (modelName == "tiny" || modelName == "tiny.en") {
1271
+ if (modelName == 'tiny' || modelName == 'tiny.en') {
1077
1272
  return [8, groupCount, length, 384]
1078
- } else if (modelName == "base" || modelName == "base.en") {
1273
+ } else if (modelName == 'base' || modelName == 'base.en') {
1079
1274
  return [12, groupCount, length, 512]
1080
- } else if (modelName == "small" || modelName == "small.en") {
1275
+ } else if (modelName == 'small' || modelName == 'small.en') {
1081
1276
  return [24, groupCount, length, 768]
1082
- } else if (modelName == "medium" || modelName == "medium.en") {
1277
+ } else if (modelName == 'medium' || modelName == 'medium.en') {
1083
1278
  return [48, groupCount, length, 1024]
1084
- } else if (modelName == "large" || modelName == "large-v1" || modelName == "large-v2" || modelName == "large-v3") {
1279
+ } else if (modelName == 'large' || modelName == 'large-v1' || modelName == 'large-v2' || modelName == 'large-v3') {
1085
1280
  return [64, groupCount, length, 1280]
1086
1281
  } else {
1087
1282
  throw new Error(`Unsupported model: ${modelName}`)
1088
1283
  }
1089
1284
  }
1090
1285
 
1091
- getInitialTokens(language: string, task: WhisperTask, disableTimestamps = false) {
1092
- const sotToken = this.tokenConfig.sotToken
1286
+ getTextStartTokens(language: string, task: WhisperTask, disableTimestamps = false) {
1287
+ const startOfTextToken = this.tokenConfig.startOfTextToken
1093
1288
 
1094
- let initialTokens: number[]
1289
+ let tokens: number[]
1095
1290
 
1096
1291
  if (this.isMultiligualModel) {
1097
- const languageToken = sotToken + 1 + languageIdLookup[language]
1098
- const translateTaskToken = 50358
1099
- const transcribeTaskToken = 50359
1100
- const taskToken = task == "transcribe" ? transcribeTaskToken : translateTaskToken
1292
+ const languageToken = this.tokenConfig.languageTokensStart + languageIdLookup[language]
1293
+ const taskToken = task == 'translate' ? this.tokenConfig.translateTaskToken : this.tokenConfig.transcribeTaskToken
1101
1294
 
1102
- initialTokens = [sotToken, languageToken, taskToken]
1295
+ tokens = [startOfTextToken, languageToken, taskToken]
1103
1296
  } else {
1104
- initialTokens = [sotToken]
1297
+ tokens = [startOfTextToken]
1105
1298
  }
1106
1299
 
1107
1300
  if (disableTimestamps) {
1108
- initialTokens.push(this.tokenConfig.noTimestampsToken)
1301
+ tokens.push(this.tokenConfig.noTimestampsToken)
1109
1302
  }
1110
1303
 
1111
- return initialTokens
1304
+ return tokens
1112
1305
  }
1113
1306
 
1114
- getAlignmentHeadIndexes() {
1115
- return alignmentHeadsIndexes[this.modelName]
1307
+ tokenToText(token: number, includeMetadataTokens = false) {
1308
+ return this.tokensToText([token], includeMetadataTokens)
1116
1309
  }
1117
1310
 
1118
- tokensToText(tokens: number[]) {
1119
- return tokens.map(token => this.tokenToTextLookup.get(token) || "").join("").trim()
1311
+ tokensToText(tokens: number[], includeMetadataTokens = false) {
1312
+ tokens.forEach(token => this.assertIsValidToken(token))
1313
+
1314
+ if (includeMetadataTokens === false) {
1315
+ tokens = tokens.filter(token => this.isTextToken(token))
1316
+ }
1317
+
1318
+ if (tokens.length === 0) {
1319
+ return ''
1320
+ }
1321
+
1322
+ try {
1323
+ return Buffer.from(this.tiktoken!.decode(new Uint32Array(tokens))).toString('utf8')
1324
+ } catch {
1325
+ return '[TOKENIZER_FAILED]'
1326
+ }
1120
1327
  }
1121
1328
 
1122
- async textToTokens(text: string, language: string) {
1123
- const resultTokens: number[] = []
1329
+ textToTokens(text: string) {
1330
+ return Array.from(this.tiktoken!.encode(text))
1331
+ }
1124
1332
 
1125
- const words = (await splitToWords(text, language)).filter(w => w.trim().length > 0)
1333
+ isTextToken(token: number) {
1334
+ return token < this.tokenConfig.endOfTextToken
1335
+ }
1126
1336
 
1127
- //words = words.filter(word => wordCharacterPattern.test(word))
1337
+ isMetadataToken(token: number) {
1338
+ return token >= this.tokenConfig.endOfTextToken
1339
+ }
1128
1340
 
1129
- for (let i = 1; i < words.length; i++) {
1130
- words[i] = ` ${words[i]}`
1131
- }
1341
+ isLanguageToken(token: number) {
1342
+ return token >= this.tokenConfig.languageTokensStart && token < this.tokenConfig.languageTokensEnd
1343
+ }
1132
1344
 
1133
- const allResultingSubwords: string[][] = []
1345
+ isTimestampToken(token: number) {
1346
+ return token >= this.tokenConfig.timestampTokensStart
1347
+ }
1134
1348
 
1135
- for (const word of words) {
1136
- const tokenForEntireWord = this.textToTokenLookup.get(word)
1349
+ isNonTimestampToken(token: number) {
1350
+ return token < this.tokenConfig.timestampTokensStart
1351
+ }
1137
1352
 
1138
- if (tokenForEntireWord) {
1139
- resultTokens.push(tokenForEntireWord)
1140
- allResultingSubwords.push([word])
1141
- continue
1142
- }
1353
+ timestampTokenToSeconds(timestampToken: number) {
1354
+ this.assertIsValidToken(timestampToken)
1143
1355
 
1144
- const subwords = word.split("")
1356
+ if (this.isNonTimestampToken(timestampToken)) {
1357
+ throw new Error(`Invalid timestamp token: ${timestampToken}`)
1358
+ }
1145
1359
 
1146
- for (const mergeRule of this.merges) {
1147
- for (let i = 0; i < subwords.length - 1; i++) {
1148
- const currentSubword = subwords[i]
1149
- const nextSubword = subwords[i + 1]
1360
+ return (timestampToken - this.tokenConfig.timestampTokensStart) * 0.02
1361
+ }
1150
1362
 
1151
- if (currentSubword == mergeRule[0] && nextSubword == mergeRule[1]) {
1152
- subwords.splice(i, 2, mergeRule[0] + mergeRule[1])
1153
- }
1154
- }
1155
- }
1363
+ isValidToken(token: number) {
1364
+ return token < this.tokenConfig.timestampTokensEnd
1365
+ }
1156
1366
 
1157
- for (const subword of subwords) {
1158
- const tokenForSubword = this.textToTokenLookup.get(subword)
1367
+ assertIsValidToken(token: number) {
1368
+ if (!this.isValidToken(token)) {
1369
+ throw new Error(`Invalid token: ${token}`)
1370
+ }
1371
+ }
1159
1372
 
1160
- if (!tokenForSubword) {
1161
- throw new Error(`Failed tokenizing the given text. The word '${word}' contains a subword '${subword}' which is not in the vocabulary.`)
1162
- }
1373
+ secondsToFrame(seconds: number) {
1374
+ return Math.floor(seconds / 0.02)
1375
+ }
1163
1376
 
1164
- resultTokens.push(tokenForSubword)
1165
- }
1377
+ secondsRangeToFrameCount(startSeconds: number, endSeconds: number) {
1378
+ if (startSeconds > endSeconds) {
1379
+ throw new Error(`Invalid range: ${startSeconds} > ${endSeconds}`)
1380
+ }
1166
1381
 
1167
- allResultingSubwords.push(subwords)
1382
+ return this.secondsToFrame(endSeconds - startSeconds)
1383
+ }
1384
+
1385
+ languageTokenToLanguageIndex(languageToken: number) {
1386
+ if (!this.isLanguageToken(languageToken)) {
1387
+ throw new Error(`Invalid language token: ${languageToken}`)
1168
1388
  }
1169
1389
 
1170
- return resultTokens
1390
+ let languageIndex = languageToken - this.tokenConfig.languageTokensStart
1391
+
1392
+ if (this.isEnglishOnlyModel) {
1393
+ languageIndex += 1
1394
+ }
1395
+ }
1396
+
1397
+ get isEnglishOnlyModel() {
1398
+ return this.isMultiligualModel === false
1399
+ }
1400
+
1401
+ getAlignmentHeadIndexes() {
1402
+ return alignmentHeadsIndexes[this.modelName]
1171
1403
  }
1172
1404
  }
1173
1405
 
@@ -1334,171 +1566,184 @@ const filterbanks: Filterbank[] = [
1334
1566
  ]
1335
1567
 
1336
1568
  export async function loadPackagesAndGetPaths(modelName: WhisperModelName | undefined, languageCode: string | undefined) {
1337
- if (!modelName) {
1569
+ if (modelName) {
1570
+ modelName = normalizeWhisperModelName(modelName, languageCode)
1571
+ } else {
1338
1572
  if (languageCode) {
1339
1573
  const shortLanguageCode = getShortLanguageCode(languageCode)
1340
1574
 
1341
- modelName = shortLanguageCode == "en" ? "tiny.en" : "tiny"
1575
+ modelName = shortLanguageCode == 'en' ? 'tiny.en' : 'tiny'
1342
1576
  } else {
1343
- modelName = "tiny"
1577
+ modelName = 'tiny'
1344
1578
  }
1345
1579
  }
1346
1580
 
1581
+ if (modelName.startsWith('large')) {
1582
+ throw new Error(`Large models are not currently supported by the integrated Whisper engine due to model size restrictions of onnxruntime-node. To use large models, you can select the whisper.cpp engine instead.`)
1583
+ }
1584
+
1347
1585
  const packageName = modelNameToPackageName[modelName]
1348
1586
 
1349
1587
  const modelDir = await loadPackage(packageName)
1350
1588
 
1351
- const tokenizerPackagePath = await loadPackage(tokenizerPackageName)
1352
- const tokenizerDir = isMultiligualModel(modelName) ? path.join(tokenizerPackagePath, "multilingual") : path.join(tokenizerPackagePath, "gpt2")
1353
-
1354
- return { modelName, modelDir, tokenizerDir }
1589
+ return { modelName, modelDir }
1355
1590
  }
1356
1591
 
1357
- export function isMultiligualModel(modelName: WhisperModelName) {
1358
- return !modelName.endsWith(".en")
1592
+ export function normalizeWhisperModelName(modelName: WhisperModelName, languageCode: string | undefined): WhisperModelName {
1593
+ if (languageCode != 'en' && modelName.endsWith('.en')) {
1594
+ const originalModelName = modelName
1595
+ modelName = modelName.slice(0, modelName.length - 3) as WhisperModelName
1596
+
1597
+ const logger = new Logger()
1598
+ logger.logTitledMessage(`Warning`, `The model '${originalModelName}' is English only and cannot be used to transcribe language '${languageCode}'. using '${modelName}' instead.`, chalk.yellowBright)
1599
+ }
1600
+
1601
+ return modelName
1359
1602
  }
1360
1603
 
1361
- export type WhisperModelName = "tiny" | "tiny.en" | "base" | "base.en" | "small" | "small.en" | "medium" | "medium.en" | "large" | "large-v1" | "large-v2" | "large-v3"
1362
- export type WhisperTask = "transcribe" | "translate"
1604
+ export function isMultilingualModel(modelName: WhisperModelName) {
1605
+ return !isEnglishOnlyModel(modelName)
1606
+ }
1363
1607
 
1364
- export const modelNameToPackageName: { [modelName in WhisperModelName]: string } = {
1365
- "tiny": "whisper-tiny",
1366
- "tiny.en": "whisper-tiny.en",
1367
- "base": "whisper-base",
1368
- "base.en": "whisper-base.en",
1369
- "small": "whisper-small",
1370
- "small.en": "whisper-small.en",
1371
- "medium": "whisper-medium",
1372
- "medium.en": "whisper-medium.en",
1373
- "large": "whisper-large-v3",
1374
- "large-v1": "whisper-large-v1",
1375
- "large-v2": "whisper-large-v2",
1376
- "large-v3": "whisper-large-v3"
1608
+ export function isEnglishOnlyModel(modelName: WhisperModelName) {
1609
+ return modelName.endsWith('.en')
1377
1610
  }
1378
1611
 
1379
- export const tokenizerPackageName = "whisper-tokenizer"
1612
+ export type WhisperModelName = 'tiny' | 'tiny.en' | 'base' | 'base.en' | 'small' | 'small.en' | 'medium' | 'medium.en' | 'large' | 'large-v1' | 'large-v2' | 'large-v3'
1613
+ export type WhisperTask = 'transcribe' | 'translate' | 'detect-language'
1380
1614
 
1381
- const vocabCharacterSetLookup: { [s: string]: number } = {
1382
- "!": 33, "\"": 34, "#": 35, "$": 36, "%": 37, "&": 38, "'": 39, "(": 40, ")": 41, "*": 42, "+": 43, ",": 44, "-": 45, ".": 46, "/": 47, "0": 48, "1": 49, "2": 50, "3": 51, "4": 52, "5": 53, "6": 54,
1383
- "7": 55, "8": 56, "9": 57, ":": 58, ";": 59, "<": 60, "=": 61, ">": 62, "?": 63, "@": 64, "A": 65, "B": 66, "C": 67, "D": 68, "E": 69, "F": 70, "G": 71, "H": 72, "I": 73, "J": 74, "K": 75, "L": 76, "M": 77, "N": 78, "O": 79, "P": 80, "Q": 81, "R": 82, "S": 83, "T": 84, "U": 85, "V": 86, "W": 87, "X": 88, "Y": 89, "Z": 90, "[": 91, "\\": 92, "]": 93, "^": 94, "_": 95, "`": 96, "a": 97, "b": 98, "c": 99, "d": 100, "e": 101, "f": 102, "g": 103, "h": 104, "i": 105, "j": 106, "k": 107, "l": 108, "m": 109, "n": 110, "o": 111, "p": 112, "q": 113, "r": 114, "s": 115, "t": 116, "u": 117, "v": 118, "w": 119, "x": 120, "y": 121, "z": 122, "{": 123, "|": 124, "}": 125, "~": 126, "¡": 161, "¢": 162, "£": 163, "¤": 164, "¥": 165, "¦": 166, "§": 167, "¨": 168, "©": 169, "ª": 170, "«": 171, "¬": 172, "®": 174, "¯": 175, "°": 176, "±": 177, "²": 178, "³": 179, "´": 180, "µ": 181, "¶": 182, "·": 183, "¸": 184, "¹": 185, "º": 186, "»": 187, "¼": 188, "½": 189, "¾": 190, "¿": 191, "À": 192, "Á": 193, "Â": 194, "Ã": 195, "Ä": 196, "Å": 197, "Æ": 198, "Ç": 199, "È": 200, "É": 201, "Ê": 202, "Ë": 203, "Ì": 204, "Í": 205, "Î": 206, "Ï": 207, "Ð": 208, "Ñ": 209, "Ò": 210, "Ó": 211, "Ô": 212, "Õ": 213, "Ö": 214, "×": 215, "Ø": 216, "Ù": 217, "Ú": 218, "Û": 219, "Ü": 220, "Ý": 221, "Þ": 222, "ß": 223, "à": 224, "á": 225, "â": 226, "ã": 227, "ä": 228, "å": 229, "æ": 230, "ç": 231, "è": 232, "é": 233, "ê": 234, "ë": 235, "ì": 236, "í": 237, "î": 238, "ï": 239, "ð": 240, "ñ": 241, "ò": 242, "ó": 243, "ô": 244, "õ": 245, "ö": 246, "÷": 247, "ø": 248, "ù": 249, "ú": 250, "û": 251, "ü": 252, "ý": 253, "þ": 254, "ÿ": 255, "Ā": 0, "ā": 1, "Ă": 2, "ă": 3, "Ą": 4, "ą": 5, "Ć": 6, "ć": 7, "Ĉ": 8, "ĉ": 9, "Ċ": 10, "ċ": 11, "Č": 12, "č": 13, "Ď": 14, "ď": 15, "Đ": 16, "đ": 17, "Ē": 18, "ē": 19, "Ĕ": 20, "ĕ":
1384
- 21, "Ė": 22, "ė": 23, "Ę": 24, "ę": 25, "Ě": 26, "ě": 27, "Ĝ": 28, "ĝ": 29, "Ğ": 30, "ğ": 31, "Ġ": 32, "ġ": 127, "Ģ": 128, "ģ": 129, "Ĥ": 130, "ĥ": 131, "Ħ": 132, "ħ": 133, "Ĩ": 134, "ĩ": 135, "Ī": 136, "ī": 137, "Ĭ": 138, "ĭ": 139, "Į": 140, "į": 141, "İ": 142, "ı": 143, "IJ": 144, "ij": 145, "Ĵ": 146, "ĵ": 147, "Ķ": 148, "ķ": 149, "ĸ": 150, "Ĺ": 151, "ĺ": 152, "Ļ": 153, "ļ": 154, "Ľ": 155, "ľ": 156, "Ŀ": 157, "ŀ": 158, "Ł": 159, "ł": 160, "Ń": 173
1615
+ export const modelNameToPackageName: { [modelName in WhisperModelName]: string } = {
1616
+ 'tiny': 'whisper-tiny',
1617
+ 'tiny.en': 'whisper-tiny.en',
1618
+ 'base': 'whisper-base',
1619
+ 'base.en': 'whisper-base.en',
1620
+ 'small': 'whisper-small',
1621
+ 'small.en': 'whisper-small.en',
1622
+ 'medium': 'whisper-medium',
1623
+ 'medium.en': 'whisper-medium.en',
1624
+ 'large': 'whisper-large-v3',
1625
+ 'large-v1': 'whisper-large-v1',
1626
+ 'large-v2': 'whisper-large-v2',
1627
+ 'large-v3': 'whisper-large-v3'
1385
1628
  }
1386
1629
 
1630
+ export const tokenizerPackageName = 'whisper-tokenizer'
1631
+
1387
1632
  const languageIdLookup: { [s: string]: number } = {
1388
- "en": 0,
1389
- "zh": 1,
1390
- "de": 2,
1391
- "es": 3,
1392
- "ru": 4,
1393
- "ko": 5,
1394
- "fr": 6,
1395
- "ja": 7,
1396
- "pt": 8,
1397
- "tr": 9,
1398
- "pl": 10,
1399
- "ca": 11,
1400
- "nl": 12,
1401
- "ar": 13,
1402
- "sv": 14,
1403
- "it": 15,
1404
- "id": 16,
1405
- "hi": 17,
1406
- "fi": 18,
1407
- "vi": 19,
1408
- "iw": 20,
1409
- "uk": 21,
1410
- "el": 22,
1411
- "ms": 23,
1412
- "cs": 24,
1413
- "ro": 25,
1414
- "da": 26,
1415
- "hu": 27,
1416
- "ta": 28,
1417
- "no": 29,
1418
- "th": 30,
1419
- "ur": 31,
1420
- "hr": 32,
1421
- "bg": 33,
1422
- "lt": 34,
1423
- "la": 35,
1424
- "mi": 36,
1425
- "ml": 37,
1426
- "cy": 38,
1427
- "sk": 39,
1428
- "te": 40,
1429
- "fa": 41,
1430
- "lv": 42,
1431
- "bn": 43,
1432
- "sr": 44,
1433
- "az": 45,
1434
- "sl": 46,
1435
- "kn": 47,
1436
- "et": 48,
1437
- "mk": 49,
1438
- "br": 50,
1439
- "eu": 51,
1440
- "is": 52,
1441
- "hy": 53,
1442
- "ne": 54,
1443
- "mn": 55,
1444
- "bs": 56,
1445
- "kk": 57,
1446
- "sq": 58,
1447
- "sw": 59,
1448
- "gl": 60,
1449
- "mr": 61,
1450
- "pa": 62,
1451
- "si": 63,
1452
- "km": 64,
1453
- "sn": 65,
1454
- "yo": 66,
1455
- "so": 67,
1456
- "af": 68,
1457
- "oc": 69,
1458
- "ka": 70,
1459
- "be": 71,
1460
- "tg": 72,
1461
- "sd": 73,
1462
- "gu": 74,
1463
- "am": 75,
1464
- "yi": 76,
1465
- "lo": 77,
1466
- "uz": 78,
1467
- "fo": 79,
1468
- "ht": 80,
1469
- "ps": 81,
1470
- "tk": 82,
1471
- "nn": 83,
1472
- "mt": 84,
1473
- "sa": 85,
1474
- "lb": 86,
1475
- "my": 87,
1476
- "bo": 88,
1477
- "tl": 89,
1478
- "mg": 90,
1479
- "as": 91,
1480
- "tt": 92,
1481
- "haw": 93,
1482
- "ln": 94,
1483
- "ha": 95,
1484
- "ba": 96,
1485
- "jw": 97,
1486
- "su": 98,
1633
+ 'en': 0,
1634
+ 'zh': 1,
1635
+ 'de': 2,
1636
+ 'es': 3,
1637
+ 'ru': 4,
1638
+ 'ko': 5,
1639
+ 'fr': 6,
1640
+ 'ja': 7,
1641
+ 'pt': 8,
1642
+ 'tr': 9,
1643
+ 'pl': 10,
1644
+ 'ca': 11,
1645
+ 'nl': 12,
1646
+ 'ar': 13,
1647
+ 'sv': 14,
1648
+ 'it': 15,
1649
+ 'id': 16,
1650
+ 'hi': 17,
1651
+ 'fi': 18,
1652
+ 'vi': 19,
1653
+ 'iw': 20,
1654
+ 'uk': 21,
1655
+ 'el': 22,
1656
+ 'ms': 23,
1657
+ 'cs': 24,
1658
+ 'ro': 25,
1659
+ 'da': 26,
1660
+ 'hu': 27,
1661
+ 'ta': 28,
1662
+ 'no': 29,
1663
+ 'th': 30,
1664
+ 'ur': 31,
1665
+ 'hr': 32,
1666
+ 'bg': 33,
1667
+ 'lt': 34,
1668
+ 'la': 35,
1669
+ 'mi': 36,
1670
+ 'ml': 37,
1671
+ 'cy': 38,
1672
+ 'sk': 39,
1673
+ 'te': 40,
1674
+ 'fa': 41,
1675
+ 'lv': 42,
1676
+ 'bn': 43,
1677
+ 'sr': 44,
1678
+ 'az': 45,
1679
+ 'sl': 46,
1680
+ 'kn': 47,
1681
+ 'et': 48,
1682
+ 'mk': 49,
1683
+ 'br': 50,
1684
+ 'eu': 51,
1685
+ 'is': 52,
1686
+ 'hy': 53,
1687
+ 'ne': 54,
1688
+ 'mn': 55,
1689
+ 'bs': 56,
1690
+ 'kk': 57,
1691
+ 'sq': 58,
1692
+ 'sw': 59,
1693
+ 'gl': 60,
1694
+ 'mr': 61,
1695
+ 'pa': 62,
1696
+ 'si': 63,
1697
+ 'km': 64,
1698
+ 'sn': 65,
1699
+ 'yo': 66,
1700
+ 'so': 67,
1701
+ 'af': 68,
1702
+ 'oc': 69,
1703
+ 'ka': 70,
1704
+ 'be': 71,
1705
+ 'tg': 72,
1706
+ 'sd': 73,
1707
+ 'gu': 74,
1708
+ 'am': 75,
1709
+ 'yi': 76,
1710
+ 'lo': 77,
1711
+ 'uz': 78,
1712
+ 'fo': 79,
1713
+ 'ht': 80,
1714
+ 'ps': 81,
1715
+ 'tk': 82,
1716
+ 'nn': 83,
1717
+ 'mt': 84,
1718
+ 'sa': 85,
1719
+ 'lb': 86,
1720
+ 'my': 87,
1721
+ 'bo': 88,
1722
+ 'tl': 89,
1723
+ 'mg': 90,
1724
+ 'as': 91,
1725
+ 'tt': 92,
1726
+ 'haw': 93,
1727
+ 'ln': 94,
1728
+ 'ha': 95,
1729
+ 'ba': 96,
1730
+ 'jw': 97,
1731
+ 'su': 98,
1487
1732
  }
1488
1733
 
1489
1734
  const alignmentHeadsIndexes: { [name in WhisperModelName]: number[] } = {
1490
- "tiny.en": [6, 12, 17, 18, 19, 20, 21, 22],
1491
- "tiny": [14, 18, 20, 21, 22, 23],
1492
- "base.en": [27, 39, 41, 45, 47],
1493
- "base": [25, 34, 35, 39, 41, 42, 44, 46],
1494
- "small.en": [78, 84, 87, 92, 98, 101, 103, 108, 112, 116, 118, 120, 121, 122, 123, 126, 131, 134, 136],
1495
- "small": [63, 69, 96, 100, 103, 104, 108, 115, 117, 125],
1496
- "medium.en": [180, 225, 236, 238, 244, 256, 260, 265, 284, 286, 295, 298, 303, 320, 323, 329, 334, 348],
1497
- "medium": [223, 244, 255, 257, 320, 372],
1498
- "large-v1": [199, 222, 224, 237, 447, 451, 457, 462, 475],
1499
- "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],
1500
- "large-v3": [212, 277, 331, 332, 333, 355, 356, 364, 371, 379, 391, 422, 423, 443, 449, 452, 465, 467, 473, 505, 521, 532, 555], // Temporary (may not be correct)
1501
- "large": [212, 277, 331, 332, 333, 355, 356, 364, 371, 379, 391, 422, 423, 443, 449, 452, 465, 467, 473, 505, 521, 532, 555],
1735
+ 'tiny.en': [6, 12, 17, 18, 19, 20, 21, 22],
1736
+ 'tiny': [14, 18, 20, 21, 22, 23],
1737
+ 'base.en': [27, 39, 41, 45, 47],
1738
+ 'base': [25, 34, 35, 39, 41, 42, 44, 46],
1739
+ 'small.en': [78, 84, 87, 92, 98, 101, 103, 108, 112, 116, 118, 120, 121, 122, 123, 126, 131, 134, 136],
1740
+ 'small': [63, 69, 96, 100, 103, 104, 108, 115, 117, 125],
1741
+ 'medium.en': [180, 225, 236, 238, 244, 256, 260, 265, 284, 286, 295, 298, 303, 320, 323, 329, 334, 348],
1742
+ 'medium': [223, 244, 255, 257, 320, 372],
1743
+ 'large-v1': [199, 222, 224, 237, 447, 451, 457, 462, 475],
1744
+ '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],
1745
+ 'large-v3': [212, 277, 331, 332, 333, 355, 356, 364, 371, 379, 391, 422, 423, 443, 449, 452, 465, 467, 473, 505, 521, 532, 555], // Temporary (may not be correct)
1746
+ 'large': [212, 277, 331, 332, 333, 355, 356, 364, 371, 379, 391, 422, 423, 443, 449, 452, 465, 467, 473, 505, 521, 532, 555],
1502
1747
  }
1503
1748
 
1504
1749
  export interface WhisperOptions {
@@ -1508,13 +1753,21 @@ export interface WhisperOptions {
1508
1753
  topCandidateCount?: number
1509
1754
  punctuationThreshold?: number
1510
1755
  autoPromptParts?: boolean
1756
+ maxTokensPerPart?: number
1757
+ suppressRepetition?: boolean
1758
+ seed?: number
1759
+ decodeTimestampTokens?: boolean
1511
1760
  }
1512
1761
 
1513
- export const whisperOptionsDefaults: WhisperOptions = {
1762
+ export const defaultWhisperOptions: WhisperOptions = {
1514
1763
  model: undefined,
1515
1764
  temperature: 0.1,
1516
1765
  prompt: undefined,
1517
1766
  topCandidateCount: 5,
1518
1767
  punctuationThreshold: 0.2,
1519
- autoPromptParts: true
1768
+ autoPromptParts: true,
1769
+ maxTokensPerPart: 250,
1770
+ suppressRepetition: true,
1771
+ seed: undefined,
1772
+ decodeTimestampTokens: false,
1520
1773
  }