echogarden 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/data/schemas/options.json +3 -3
  2. package/dist/alignment/SpeechAlignment.d.ts +1 -1
  3. package/dist/alignment/SpeechAlignment.js +2 -1
  4. package/dist/alignment/SpeechAlignment.js.map +1 -1
  5. package/dist/api/API.d.ts +1 -1
  6. package/dist/api/API.js +1 -1
  7. package/dist/api/API.js.map +1 -1
  8. package/dist/api/Alignment.d.ts +1 -1
  9. package/dist/api/Alignment.js +11 -7
  10. package/dist/api/Alignment.js.map +1 -1
  11. package/dist/api/LanguageDetection.d.ts +1 -1
  12. package/dist/api/LanguageDetection.js +1 -1
  13. package/dist/api/LanguageDetection.js.map +1 -1
  14. package/dist/api/Recognition.d.ts +1 -1
  15. package/dist/api/Synthesis.d.ts +2 -2
  16. package/dist/api/Synthesis.js.map +1 -1
  17. package/dist/api/Translation.d.ts +3 -3
  18. package/dist/api/Translation.js.map +1 -1
  19. package/dist/api/{Vad.js → VoiceActivityDetection.js} +1 -1
  20. package/dist/api/VoiceActivityDetection.js.map +1 -0
  21. package/dist/dsp/FFT.js +1 -1
  22. package/dist/dsp/FFT.js.map +1 -1
  23. package/dist/nlp/Segmentation.d.ts +2 -0
  24. package/dist/nlp/Segmentation.js +8 -3
  25. package/dist/nlp/Segmentation.js.map +1 -1
  26. package/dist/recognition/SileroSTT.js +1 -1
  27. package/dist/recognition/SileroSTT.js.map +1 -1
  28. package/dist/recognition/WhisperCppSTT.js +3 -2
  29. package/dist/recognition/WhisperCppSTT.js.map +1 -1
  30. package/dist/recognition/WhisperSTT.d.ts +13 -4
  31. package/dist/recognition/WhisperSTT.js +113 -32
  32. package/dist/recognition/WhisperSTT.js.map +1 -1
  33. package/dist/source-separation/MDXNetSourceSeparation.d.ts +1 -1
  34. package/dist/source-separation/MDXNetSourceSeparation.js +2 -1
  35. package/dist/source-separation/MDXNetSourceSeparation.js.map +1 -1
  36. package/dist/speech-language-detection/SileroLanguageDetection.d.ts +1 -1
  37. package/dist/speech-language-detection/SileroLanguageDetection.js +2 -1
  38. package/dist/speech-language-detection/SileroLanguageDetection.js.map +1 -1
  39. package/dist/subtitles/Subtitles.js +2 -2
  40. package/dist/subtitles/Subtitles.js.map +1 -1
  41. package/dist/synthesis/VitsTTS.d.ts +2 -2
  42. package/dist/synthesis/VitsTTS.js.map +1 -1
  43. package/dist/tests/Test.js +1 -1
  44. package/dist/tests/Test.js.map +1 -1
  45. package/dist/voice-activity-detection/SileroVAD.d.ts +5 -5
  46. package/dist/voice-activity-detection/SileroVAD.js +13 -12
  47. package/dist/voice-activity-detection/SileroVAD.js.map +1 -1
  48. package/docs/CLI.md +7 -0
  49. package/docs/Development.md +1 -1
  50. package/docs/Engines.md +1 -1
  51. package/docs/Options.md +4 -10
  52. package/docs/Releases.md +41 -30
  53. package/docs/Tasklist.md +5 -2
  54. package/package.json +8 -8
  55. package/src/alignment/SpeechAlignment.ts +2 -1
  56. package/src/api/API.ts +1 -1
  57. package/src/api/Alignment.ts +15 -9
  58. package/src/api/LanguageDetection.ts +2 -2
  59. package/src/api/Recognition.ts +1 -1
  60. package/src/api/Synthesis.ts +3 -3
  61. package/src/api/Translation.ts +3 -3
  62. package/src/dsp/FFT.ts +1 -1
  63. package/src/nlp/Segmentation.ts +11 -3
  64. package/src/recognition/SileroSTT.ts +3 -1
  65. package/src/recognition/WhisperCppSTT.ts +3 -2
  66. package/src/recognition/WhisperSTT.ts +154 -45
  67. package/src/source-separation/MDXNetSourceSeparation.ts +5 -1
  68. package/src/speech-language-detection/SileroLanguageDetection.ts +5 -1
  69. package/src/subtitles/Subtitles.ts +1 -1
  70. package/src/synthesis/VitsTTS.ts +3 -3
  71. package/src/tests/Test.ts +5 -4
  72. package/src/voice-activity-detection/SileroVAD.ts +23 -19
  73. package/dist/api/Vad.js.map +0 -1
  74. /package/dist/api/{Vad.d.ts → VoiceActivityDetection.d.ts} +0 -0
  75. /package/src/api/{Vad.ts → VoiceActivityDetection.ts} +0 -0
@@ -22,8 +22,8 @@ import { shouldCancelCurrentTask } from '../server/Worker.js'
22
22
  import chalk from 'chalk'
23
23
  import { SubtitlesConfig, defaultSubtitlesBaseConfig } from '../subtitles/Subtitles.js'
24
24
  import { type EspeakOptions } from '../synthesis/EspeakTTS.js'
25
- import { OpenAICloudTTSOptions, defaultOpenAICloudTTSOptions } from '../synthesis/OpenAICloudTTS.js'
26
- import { ElevenlabsTTSOptions, defaultElevenlabsTTSOptions } from '../synthesis/ElevenlabsTTS.js'
25
+ import { type OpenAICloudTTSOptions, defaultOpenAICloudTTSOptions } from '../synthesis/OpenAICloudTTS.js'
26
+ import { type ElevenlabsTTSOptions, defaultElevenlabsTTSOptions } from '../synthesis/ElevenlabsTTS.js'
27
27
 
28
28
  const log = logToStderr
29
29
 
@@ -1036,7 +1036,7 @@ export interface SynthesisOptions {
1036
1036
  rate?: number
1037
1037
  pitch?: number
1038
1038
  pitchRange?: number
1039
-
1039
+
1040
1040
  useKlatt?: boolean
1041
1041
  insertSeparators?: boolean
1042
1042
  }
@@ -8,13 +8,13 @@ import { Timeline, addWordTextOffsetsToTimeline, wordTimelineToSegmentSentenceTi
8
8
  import { defaultWhisperOptions, type WhisperOptions } from '../recognition/WhisperSTT.js'
9
9
  import { formatLanguageCodeWithName, getShortLanguageCode, normalizeLanguageCode } from '../utilities/Locale.js'
10
10
  import { EngineMetadata } from './Common.js'
11
- import { SpeechLanguageDetectionOptions, detectSpeechLanguage } from './API.js'
11
+ import { type SpeechLanguageDetectionOptions, detectSpeechLanguage } from './API.js'
12
12
  import chalk from 'chalk'
13
13
  import { SubtitlesConfig, defaultSubtitlesBaseConfig } from '../subtitles/Subtitles.js'
14
14
 
15
15
  import * as API from './API.js'
16
- import { OpenAICloudSTTOptions, defaultOpenAICloudSTTOptions } from '../recognition/OpenAICloudSTT.js'
17
- import { WhisperCppOptions, defaultWhisperCppOptions } from '../recognition/WhisperCppSTT.js'
16
+ import { type OpenAICloudSTTOptions, defaultOpenAICloudSTTOptions } from '../recognition/OpenAICloudSTT.js'
17
+ import { type WhisperCppOptions, defaultWhisperCppOptions } from '../recognition/WhisperCppSTT.js'
18
18
 
19
19
  const log = logToStderr
20
20
 
package/src/dsp/FFT.ts CHANGED
@@ -122,7 +122,7 @@ export async function stiftr(binsForFrames: Float32Array[], fftOrder: number, wi
122
122
 
123
123
  wasmMemory.freeAll()
124
124
 
125
- // Divide by sum of weight squares for each samples
125
+ // Divide each output sample by the sum of squared weights
126
126
  for (let i = 0; i < outSamples.length; i++) {
127
127
  outSamples[i] /= sumOfSquaredWeightsForSample[i] + 1e-8
128
128
  }
@@ -10,6 +10,7 @@ const log = logToStderr
10
10
 
11
11
  export const wordCharacterPattern = /[\p{Letter}\p{Number}]/u
12
12
  export const punctuationPattern = /[\p{Punctuation}]/u
13
+
13
14
  export const phraseSeparators = [',', ';', ':']
14
15
  export const sentenceSeparators = ['.', '?', '!']
15
16
  export const symbolWords = ['$', '€', '¢', '£', '¥', '©', '®', '™', '%', '&', '#', '~', '@', '+', '±', '÷', '/', '*', '=', '¼', '½', '¾']
@@ -18,13 +19,20 @@ export function isWordOrSymbolWord(str: string) {
18
19
  return isWord(str) || symbolWords.includes(str)
19
20
  }
20
21
 
22
+ export function isSymbolWord(str: string) {
23
+ return symbolWords.includes(str.trim())
24
+ }
25
+
21
26
  export function isWord(str: string) {
22
- str = str.trim()
23
- return wordCharacterPattern.test(str) || symbolWords.includes(str)
27
+ return wordCharacterPattern.test(str.trim())
24
28
  }
25
29
 
26
30
  export function isPunctuation(str: string) {
27
- return punctuationPattern.test(str)
31
+ return punctuationPattern.test(str.trim())
32
+ }
33
+
34
+ export function isWhitespace(str: string) {
35
+ return str.trim().length === 0
28
36
  }
29
37
 
30
38
  export class Sentence {
@@ -1,11 +1,11 @@
1
1
  import { indexOfMax } from '../math/VectorMath.js'
2
2
  import { wordCharacterPattern } from '../nlp/Segmentation.js'
3
- import Onnx from 'onnxruntime-node'
4
3
  import { Logger } from '../utilities/Logger.js'
5
4
  import { logToStderr } from '../utilities/Utilities.js'
6
5
  import { Timeline } from '../utilities/Timeline.js'
7
6
  import { RawAudio, getRawAudioDuration } from '../audio/AudioUtilities.js'
8
7
  import { readAndParseJsonFile, readFile } from '../utilities/FileSystem.js'
8
+ import type * as Onnx from 'onnxruntime-node'
9
9
  import path from 'path'
10
10
 
11
11
  const log = logToStderr
@@ -14,6 +14,8 @@ export async function recognize(rawAudio: RawAudio, modelDirectory: string) {
14
14
  const logger = new Logger()
15
15
  logger.start('Create ONNX inference session')
16
16
 
17
+ const Onnx = await import('onnxruntime-node')
18
+
17
19
  const modelPath = path.join(modelDirectory, 'model.onnx')
18
20
  const labelsPath = path.join(modelDirectory, 'labels.json')
19
21
 
@@ -288,13 +288,14 @@ async function parseResultObject(resultObject: WhisperCppVerboseResult, modelNam
288
288
 
289
289
  const allTokenIds = tokenTimeline.map(entry => entry.id!)
290
290
  const transcript = whisper.tokensToText(allTokenIds).trim()
291
+ const language = resultObject.result.language
291
292
 
292
- let timeline = whisper.tokenTimelineToWordTimeline(tokenTimeline)
293
+ const timeline = whisper.tokenTimelineToWordTimeline(tokenTimeline, language)
293
294
 
294
295
  return {
295
296
  transcript,
296
297
  timeline,
297
- language: resultObject.result.language
298
+ language
298
299
  }
299
300
  }
300
301
 
@@ -1,8 +1,8 @@
1
- import Onnx from 'onnxruntime-node'
1
+ import type * as Onnx from 'onnxruntime-node'
2
2
 
3
3
  import { Logger } from '../utilities/Logger.js'
4
4
  import { computeMelSpectogramUsingFilterbanks, Filterbank } from '../dsp/MelSpectogram.js'
5
- import { clip, getIntegerRange, getRepetitionScoreRelativeToFirstSubstring, splitFloat32Array, yieldToEventLoop } from '../utilities/Utilities.js'
5
+ import { clip, getIntegerRange, getRepetitionScoreRelativeToFirstSubstring, getUTF32Chars, logToStderr, splitFloat32Array, yieldToEventLoop } from '../utilities/Utilities.js'
6
6
  import { indexOfMax, logOfVector, logSumExp, meanOfVector, medianFilter, softmax, stdDeviationOfVector } from '../math/VectorMath.js'
7
7
 
8
8
  import { alignDTWWindowed } from '../alignment/DTWSequenceAlignmentWindowed.js'
@@ -19,6 +19,7 @@ import chalk from 'chalk'
19
19
  import { XorShift32RNG } from '../utilities/RandomGenerator.js'
20
20
  import { detectSpeechLanguageByParts } from '../api/LanguageDetection.js'
21
21
  import { type Tiktoken } from 'tiktoken/lite'
22
+ import { isPunctuation, isWhitespace } from '../nlp/Segmentation.js'
22
23
 
23
24
  export async function recognize(sourceRawAudio: RawAudio, modelName: WhisperModelName, modelDir: string, task: WhisperTask, sourceLanguage: string, options: WhisperOptions) {
24
25
  if (sourceRawAudio.sampleRate != 16000) {
@@ -134,8 +135,6 @@ export class Whisper {
134
135
 
135
136
  timestampTokensStart: number
136
137
  timestampTokensEnd: number
137
-
138
- suppressedTokens: Set<number>
139
138
  }
140
139
 
141
140
  randomGen: XorShift32RNG
@@ -162,16 +161,7 @@ export class Whisper {
162
161
 
163
162
  timestampTokensStart: 50364,
164
163
  timestampTokensEnd: 50364 + 1501,
165
-
166
- suppressedTokens: new Set()
167
164
  }
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)
175
165
  } else {
176
166
  this.tokenConfig = {
177
167
  endOfTextToken: 50256,
@@ -188,16 +178,7 @@ export class Whisper {
188
178
 
189
179
  timestampTokensStart: 50363,
190
180
  timestampTokensEnd: 50363 + 1501,
191
-
192
- suppressedTokens: new Set()
193
181
  }
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)
201
182
  }
202
183
 
203
184
  this.randomGen = new XorShift32RNG(rngSeed)
@@ -282,6 +263,8 @@ export class Whisper {
282
263
 
283
264
  const encoderFilePath = path.join(this.modelDir, 'encoder.onnx')
284
265
 
266
+ const Onnx = await import('onnxruntime-node')
267
+
285
268
  this.audioEncoder = await Onnx.InferenceSession.create(encoderFilePath, this.onnxOptions)
286
269
 
287
270
  logger.end()
@@ -298,6 +281,8 @@ export class Whisper {
298
281
 
299
282
  const decoderFilePath = path.join(this.modelDir, 'decoder.onnx')
300
283
 
284
+ const Onnx = await import('onnxruntime-node')
285
+
301
286
  this.textDecoder = await Onnx.InferenceSession.create(decoderFilePath, this.onnxOptions)
302
287
 
303
288
  logger.end()
@@ -398,7 +383,7 @@ export class Whisper {
398
383
  logger.end()
399
384
  }
400
385
 
401
- timeline = this.tokenTimelineToWordTimeline(timeline)
386
+ timeline = this.tokenTimelineToWordTimeline(timeline, language)
402
387
 
403
388
  const transcript = this.tokensToText(allDecodedTokens).trim()
404
389
 
@@ -438,7 +423,7 @@ export class Whisper {
438
423
  const alignmentPath = await this.findAlignmentPathFromQKs(crossAttentionQKs, tokens, 0, audioFrameCount)//, this.getAlignmentHeadIndexes())
439
424
  let timeline = await this.getTokenTimelineFromAlignmentPath(alignmentPath, tokens, 0, audioDuration)
440
425
 
441
- timeline = this.tokenTimelineToWordTimeline(timeline)
426
+ timeline = this.tokenTimelineToWordTimeline(timeline, language)
442
427
 
443
428
  logger.end()
444
429
 
@@ -462,6 +447,8 @@ export class Whisper {
462
447
  const initialTokens = [sotToken]
463
448
  const offset = 0
464
449
 
450
+ const Onnx = await import('onnxruntime-node')
451
+
465
452
  const initialKvDimensions = this.getKvDimensions(1, initialTokens.length)
466
453
  const kvCacheTensor = new Onnx.Tensor('float32', new Float32Array(initialKvDimensions[0] * initialKvDimensions[1] * initialKvDimensions[2] * initialKvDimensions[3]), initialKvDimensions)
467
454
 
@@ -514,21 +501,23 @@ export class Whisper {
514
501
 
515
502
  const logger = new Logger()
516
503
 
504
+ const allowedPunctuationMarks = this.getAllowedPunctuationMarks()
505
+
517
506
  await logger.startAsync('Decode text tokens with Whisper decoder model')
518
507
 
519
508
  options = extendDeep(defaultWhisperOptions, options)
520
509
 
510
+ const Onnx = await import('onnxruntime-node')
511
+
521
512
  const endOfTextToken = this.tokenConfig.endOfTextToken
522
513
 
523
514
  const timestampTokensStart = this.tokenConfig.timestampTokensStart
524
- const suppressedTokens = this.tokenConfig.suppressedTokens
515
+ const suppressedTokens = new Set(this.getSuppressedTokens())
525
516
 
526
517
  const spaceToken = this.textToTokens(' ')[0]
527
518
 
528
519
  const maxDecodedTokenCount = options.maxTokensPerPart!
529
520
 
530
- //const suppressedTokensText = Array.from(this.tokenConfig.suppressedTokens).map(token => this.tokenToText(token, true))
531
-
532
521
  let decodedTokens = initialTokens.slice()
533
522
  const initialKvDimensions = this.getKvDimensions(1, decodedTokens.length)
534
523
  let kvCacheTensor = new Onnx.Tensor('float32', new Float32Array(initialKvDimensions[0] * initialKvDimensions[1] * initialKvDimensions[2] * initialKvDimensions[3]), initialKvDimensions)
@@ -688,7 +677,7 @@ export class Whisper {
688
677
  return maxScore
689
678
  })
690
679
 
691
- const thresholdRepetitionScore = 3
680
+ const thresholdRepetitionScore = 4
692
681
 
693
682
  if (topCandidatesRepetitionScores.every(score => score >= thresholdRepetitionScore)) {
694
683
  const indexOfMaxScore = topCandidatesRepetitionScores.indexOf(Math.max(...topCandidatesRepetitionScores))
@@ -716,7 +705,7 @@ export class Whisper {
716
705
  const rankOfPromisingPunctuationToken = topCandidates.findIndex((entry, index) => {
717
706
  const tokenText = this.tokenToText(entry.token).trim()
718
707
 
719
- const isPunctuationToken = [',', ',', '、', '.', '。', '!', '?'].includes(tokenText)
708
+ const isPunctuationToken = allowedPunctuationMarks.includes(tokenText)
720
709
 
721
710
  if (!isPunctuationToken) {
722
711
  return false
@@ -793,6 +782,8 @@ export class Whisper {
793
782
  async inferCrossAttentionQKs(tokens: number[], audioFeatures: Onnx.Tensor) {
794
783
  const offset = 0
795
784
 
785
+ const Onnx = await import('onnxruntime-node')
786
+
796
787
  const tokensTensor = new Onnx.Tensor('int64', new BigInt64Array(tokens.map(token => BigInt(token))), [1, tokens.length])
797
788
  const offsetTensor = new Onnx.Tensor('int64', new BigInt64Array([BigInt(offset)]), [])
798
789
 
@@ -841,6 +832,8 @@ export class Whisper {
841
832
  async encodeAudio(rawAudio: RawAudio) {
842
833
  await this.initializeEncoderSessionIfNeeded()
843
834
 
835
+ const Onnx = await import('onnxruntime-node')
836
+
844
837
  const logger = new Logger()
845
838
 
846
839
  const audioSamples = rawAudio.audioChannels[0]
@@ -1022,33 +1015,40 @@ export class Whisper {
1022
1015
  }
1023
1016
  }
1024
1017
 
1025
- tokenTimelineToWordTimeline(tokenTimeline: Timeline) {
1026
- const separatorChars =
1027
- [' ', '–', '一', ',', '、', '|', '/', '\\', ';', '"', '“', '”', '…', '(', ')', '[', ']', '{', '}']
1018
+ tokenTimelineToWordTimeline(tokenTimeline: Timeline, language: string): Timeline {
1019
+ function isSeparatorCharacter(char: string) {
1020
+ const nonSeparatingPunctuation = [`'`, `-`, `.`, `·`, `•`]
1021
+
1022
+ if (nonSeparatingPunctuation.includes(char)) {
1023
+ return false
1024
+ }
1028
1025
 
1029
- function startsWithSeparatingPunctuation(text: string) {
1030
- return separatorChars.some(char => text.startsWith(char))
1026
+ return isWhitespace(char) || isPunctuation(char)
1031
1027
  }
1032
1028
 
1033
- function isSeparatorPunctuation(text: string) {
1034
- return separatorChars.includes(text)
1029
+ function startsWithSeparatorCharacter(text: string) {
1030
+ return isSeparatorCharacter(text[0])
1031
+ }
1032
+
1033
+ function endsWithSeparatorCharacter(text: string) {
1034
+ return isSeparatorCharacter(text[text.length - 1])
1035
1035
  }
1036
1036
 
1037
1037
  const resultTimeline: Timeline = []
1038
1038
 
1039
- const groups: TimelineEntry[][] = []
1039
+ let groups: TimelineEntry[][] = []
1040
1040
 
1041
- for (let i = 0; i < tokenTimeline.length; i++) {
1042
- const entry = tokenTimeline[i]
1043
- const previousEntry = i > 0 ? tokenTimeline[i - 1] : undefined
1041
+ for (let tokenIndex = 0; tokenIndex < tokenTimeline.length; tokenIndex++) {
1042
+ const entry = tokenTimeline[tokenIndex]
1043
+ const previousEntry = tokenIndex > 0 ? tokenTimeline[tokenIndex - 1] : undefined
1044
1044
 
1045
1045
  const text = entry.text
1046
1046
  const previousEntryText = previousEntry?.text
1047
1047
 
1048
1048
  if (groups.length == 0 ||
1049
1049
  text === '' ||
1050
- startsWithSeparatingPunctuation(text) ||
1051
- (previousEntryText != null && isSeparatorPunctuation(previousEntryText))) {
1050
+ startsWithSeparatorCharacter(text) ||
1051
+ (previousEntryText != null && endsWithSeparatorCharacter(previousEntryText))) {
1052
1052
 
1053
1053
  groups.push([entry])
1054
1054
  } else {
@@ -1056,8 +1056,28 @@ export class Whisper {
1056
1056
  }
1057
1057
  }
1058
1058
 
1059
+ const newGroups: TimelineEntry[][] = []
1060
+
1061
+ for (let groupIndex = 0; groupIndex < groups.length - 1; groupIndex++) {
1062
+ const group = groups[groupIndex]
1063
+ const nextGroup = groups[groupIndex + 1]
1064
+
1065
+ if (
1066
+ group.length > 1 &&
1067
+ group[group.length - 1].text === '.' &&
1068
+ [' ', '['].includes(nextGroup[0].text[0])) {
1069
+
1070
+ newGroups.push(group.slice(0, group.length - 1))
1071
+ newGroups.push(group.slice(group.length - 1))
1072
+ } else {
1073
+ newGroups.push(group)
1074
+ }
1075
+ }
1076
+
1077
+ groups = newGroups
1078
+
1059
1079
  for (const group of groups) {
1060
- const groupText = this.tokensToText(group.map(entry => entry.id!))
1080
+ let groupText = this.tokensToText(group.map(entry => entry.id!))
1061
1081
 
1062
1082
  if (groupText === '') {
1063
1083
  continue
@@ -1401,6 +1421,90 @@ export class Whisper {
1401
1421
  getAlignmentHeadIndexes() {
1402
1422
  return alignmentHeadsIndexes[this.modelName]
1403
1423
  }
1424
+
1425
+ getSuppressedTokens() {
1426
+ return [
1427
+ ...this.getSuppressedTextTokens(),
1428
+ ...this.getSuppressedMetadataTokens(),
1429
+ ]
1430
+ }
1431
+
1432
+ getSuppressedTextTokens() {
1433
+ const allowedPunctuationMarks = this.getAllowedPunctuationMarks()
1434
+
1435
+ const nonWordTokensData = this.getNonWordTokenData()
1436
+
1437
+ const suppressedTextTokens = nonWordTokensData
1438
+ .filter(entry => !allowedPunctuationMarks.includes(entry.text))
1439
+ .map(entry => entry.id)
1440
+
1441
+ return suppressedTextTokens
1442
+ }
1443
+
1444
+ getSuppressedMetadataTokens() {
1445
+ if (this.isMultiligualModel) {
1446
+ return [50256, ...getIntegerRange(50258, 50364)]
1447
+ } else {
1448
+ return [...getIntegerRange(50257, 50363)]
1449
+ }
1450
+ }
1451
+
1452
+ getAllowedPunctuationMarks() {
1453
+ const generalPunctuation = [`'`, ',', '.', '?', '!']
1454
+
1455
+ let allowedPunctuation: string[]
1456
+
1457
+ if (this.isMultiligualModel) {
1458
+ const spanish = ['¿', '¡']
1459
+ const chinese = ['、', ',', '。', '?', '!']
1460
+ const arabic = ['،', '؟']
1461
+ const various = ['·', '•', '・']
1462
+
1463
+ allowedPunctuation = [...generalPunctuation, ...arabic, ...chinese, ...spanish, ...various]
1464
+ } else {
1465
+ allowedPunctuation = generalPunctuation
1466
+ }
1467
+
1468
+ return allowedPunctuation
1469
+ }
1470
+
1471
+ getNonWordTokenData() {
1472
+ const nonWordTokenData: WhisperTokenData[] = []
1473
+
1474
+ const invalidUTF8Char = String.fromCharCode(65533)
1475
+
1476
+ for (let i = 0; i < this.tokenConfig.endOfTextToken; i++) {
1477
+ const tokenText = this.tokenToText(i, false)
1478
+ const tokenTextWithoutWhitespace = tokenText.replaceAll(/\s/g, '')
1479
+
1480
+ const isNonWordToken = /^[\p{Punctuation}\p{Symbol}]+$/u.test(tokenTextWithoutWhitespace)
1481
+
1482
+ const containsInvalidUTF8 = getUTF32Chars(tokenTextWithoutWhitespace).utf32chars.includes(invalidUTF8Char)
1483
+
1484
+ if (isNonWordToken && !containsInvalidUTF8) {
1485
+ nonWordTokenData.push({
1486
+ id: i,
1487
+ text: tokenText,
1488
+ })
1489
+ }
1490
+ }
1491
+
1492
+ return nonWordTokenData
1493
+ }
1494
+
1495
+
1496
+ getTokensData(tokens: number[]) {
1497
+ const tokensData: WhisperTokenData[] = []
1498
+
1499
+ for (const token of tokens) {
1500
+ tokensData.push({
1501
+ id: token,
1502
+ text: this.tokenToText(token, true),
1503
+ })
1504
+ }
1505
+
1506
+ return tokensData
1507
+ }
1404
1508
  }
1405
1509
 
1406
1510
  const filterbanks: Filterbank[] = [
@@ -1609,6 +1713,11 @@ export function isEnglishOnlyModel(modelName: WhisperModelName) {
1609
1713
  return modelName.endsWith('.en')
1610
1714
  }
1611
1715
 
1716
+ export type WhisperTokenData = {
1717
+ id: number
1718
+ text: string
1719
+ }
1720
+
1612
1721
  export type WhisperModelName = 'tiny' | 'tiny.en' | 'base' | 'base.en' | 'small' | 'small.en' | 'medium' | 'medium.en' | 'large' | 'large-v1' | 'large-v2' | 'large-v3'
1613
1722
  export type WhisperTask = 'transcribe' | 'translate' | 'detect-language'
1614
1723
 
@@ -1755,8 +1864,8 @@ export interface WhisperOptions {
1755
1864
  autoPromptParts?: boolean
1756
1865
  maxTokensPerPart?: number
1757
1866
  suppressRepetition?: boolean
1758
- seed?: number
1759
1867
  decodeTimestampTokens?: boolean
1868
+ seed?: number
1760
1869
  }
1761
1870
 
1762
1871
  export const defaultWhisperOptions: WhisperOptions = {
@@ -1768,6 +1877,6 @@ export const defaultWhisperOptions: WhisperOptions = {
1768
1877
  autoPromptParts: true,
1769
1878
  maxTokensPerPart: 250,
1770
1879
  suppressRepetition: true,
1880
+ decodeTimestampTokens: true,
1771
1881
  seed: undefined,
1772
- decodeTimestampTokens: false,
1773
1882
  }
@@ -1,4 +1,4 @@
1
- import Onnx from 'onnxruntime-node'
1
+ import type * as Onnx from 'onnxruntime-node'
2
2
  import { RawAudio } from '../audio/AudioUtilities.js';
3
3
  import { binBufferToComplex, complexToBinBuffer, getWindowWeights, stftr, stiftr } from '../dsp/FFT.js';
4
4
  import { ComplexNumber } from '../math/VectorMath.js';
@@ -32,6 +32,8 @@ export class MDXNet {
32
32
  await this.initializeSession(this.modelFilePath)
33
33
  }
34
34
 
35
+ const Onnx = await import('onnxruntime-node')
36
+
35
37
  const logger = new Logger()
36
38
 
37
39
  const session = this.session!
@@ -223,6 +225,8 @@ export class MDXNet {
223
225
  logSeverityLevel: 3
224
226
  }
225
227
 
228
+ const Onnx = await import('onnxruntime-node')
229
+
226
230
  this.session = await Onnx.InferenceSession.create(modelPath, onnxOptions)
227
231
  }
228
232
  }
@@ -1,4 +1,4 @@
1
- import Onnx from 'onnxruntime-node'
1
+ import type * as Onnx from 'onnxruntime-node'
2
2
  import { softmax } from '../math/VectorMath.js'
3
3
  import { Logger } from '../utilities/Logger.js'
4
4
  import { RawAudio } from '../audio/AudioUtilities.js'
@@ -50,6 +50,8 @@ export class SileroLanguageDetection {
50
50
  logSeverityLevel: 3
51
51
  }
52
52
 
53
+ const Onnx = await import('onnxruntime-node')
54
+
53
55
  this.session = await Onnx.InferenceSession.create(this.modelPath, onnxOptions)
54
56
 
55
57
  logger.end()
@@ -62,6 +64,8 @@ export class SileroLanguageDetection {
62
64
 
63
65
  const audioSamples = rawAudio.audioChannels[0]
64
66
 
67
+ const Onnx = await import('onnxruntime-node')
68
+
65
69
  const inputTensor = new Onnx.Tensor('float32', audioSamples, [1, audioSamples.length])
66
70
 
67
71
  const inputs = { input: inputTensor }
@@ -200,7 +200,7 @@ function getCuesFromTimeline_IsolateSegmentSentence(timeline: Timeline, config:
200
200
  continue
201
201
  }
202
202
 
203
- const wordTimeline = entry.timeline!.filter(entry => isWord(entry.text))
203
+ const wordTimeline = entry.timeline!.filter(entry => isWordOrSymbolWord(entry.text))
204
204
 
205
205
  // First, add word start and end offsets for all word entries
206
206
  let lastWordEndOffset = 0
@@ -1,4 +1,4 @@
1
- import type { InferenceSession } from 'onnxruntime-node'
1
+ import type * as Onnx from 'onnxruntime-node'
2
2
  import { SynthesisVoice } from '../api/API.js'
3
3
  import { Logger } from '../utilities/Logger.js'
4
4
  import { RawAudio, getEmptyRawAudio, getRawAudioDuration } from '../audio/AudioUtilities.js'
@@ -32,7 +32,7 @@ export class VitsTTS {
32
32
  voiceName: string
33
33
  modelPath: string
34
34
 
35
- modelSession?: InferenceSession
35
+ modelSession?: Onnx.InferenceSession
36
36
  metadata?: any
37
37
  phonemeMap?: Map<string, number[]>
38
38
 
@@ -45,7 +45,7 @@ export class VitsTTS {
45
45
  const logger = new Logger()
46
46
  await logger.startAsync('Initialize VITS ONNX synthesis model')
47
47
 
48
- const onnxOptions: InferenceSession.SessionOptions = {
48
+ const onnxOptions: Onnx.InferenceSession.SessionOptions = {
49
49
  logSeverityLevel: 3
50
50
  }
51
51
 
package/src/tests/Test.ts CHANGED
@@ -1,19 +1,17 @@
1
- import { getRepetitionScoreRelativeToFirstSubstring, logToStderr, setupProgramTerminationListeners } from '../utilities/Utilities.js'
1
+ import { getRepetitionScoreRelativeToFirstSubstring, logToStderr, setupProgramTerminationListeners, writeToStderr } from '../utilities/Utilities.js'
2
2
  import { makeTarballsForInstalledPackages } from '../utilities/TarballMaker.js'
3
3
  import { testEspeakSynthesisWithPrePhonemizedInputs, testKirshenbaumPhonemization } from '../synthesis/EspeakTTS.js'
4
4
 
5
5
  const log = logToStderr
6
6
 
7
7
  setupProgramTerminationListeners()
8
+
8
9
  //process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
9
10
  //process.env.http_proxy = 'http://localhost:8080'
10
11
 
11
12
  //const testText = `After a while, finding that nothing more happened, she decided on going into the garden at once; but, alas for poor Alice! when she got to the door, she found she had forgotten the little golden key, and when she went back to the table for it, she found she could not possibly reach it: she could see it quite plainly through the glass, and she tried her best to climb up one of the legs of the table, but it was too slippery; and when she had tired herself out with trying, the poor little thing sat down and cried.`
12
-
13
13
  //await testKirshenbaumPhonemization(testText)
14
14
 
15
- //await makeTarballsForInstalledPackages(true)
16
-
17
15
  //getRepetitionScoreRelativeToFirstSubstring(['a', 'b', 'c', 'a', 'b', 'c'])
18
16
  //getRepetitionScoreRelativeToFirstSubstring(['a', 'b', 'a', 'd', 'a', 'b', 'a', 'd'])
19
17
  //getRepetitionScoreRelativeToFirstSubstring(['a', 'b', 'a', 'b', 'c', 'a', 'b', 'a', 'b'])
@@ -21,4 +19,7 @@ setupProgramTerminationListeners()
21
19
  //getRepetitionScoreRelativeToFirstSubstring(['a', 'b', 'a', 'c', 'a', 'b', 'a', 'c', 'a'])
22
20
  //getRepetitionScoreRelativeToFirstSubstring(['a', 'a', 'a', 'b', 'b', 'a', 'a', 'a', 'b'])
23
21
 
22
+ //await makeTarballsForInstalledPackages(true)
23
+
24
24
  process.exit(0)
25
+
@@ -1,4 +1,4 @@
1
- import Onnx from 'onnxruntime-node'
1
+ import type * as Onnx from 'onnxruntime-node'
2
2
 
3
3
  import { concatFloat32Arrays } from '../utilities/Utilities.js'
4
4
  import { RawAudio } from '../audio/AudioUtilities.js'
@@ -38,33 +38,24 @@ export async function detectVoiceActivity(rawAudio: RawAudio, modelPath: string,
38
38
  export class SileroVAD {
39
39
  session?: Onnx.InferenceSession
40
40
 
41
- modelStateH: Onnx.Tensor
42
- modelStateC: Onnx.Tensor
43
- modelSampleRate: Onnx.Tensor
41
+ modelStateH?: Onnx.Tensor
42
+ modelStateC?: Onnx.Tensor
43
+ modelSampleRate?: Onnx.Tensor
44
44
 
45
45
  modelPath: string
46
46
 
47
47
  constructor(modelPath: string) {
48
- this.modelSampleRate = new Onnx.Tensor('int64', new BigInt64Array([BigInt(16000)]), [])
49
-
50
- const h = new Float32Array(2 * 1 * 64)
51
- const c = new Float32Array(2 * 1 * 64)
52
-
53
- this.modelStateH = new Onnx.Tensor('float32', h, [2, 1, 64])
54
- this.modelStateC = new Onnx.Tensor('float32', c, [2, 1, 64])
55
-
56
48
  this.modelPath = modelPath
57
49
  }
58
50
 
59
51
  async predictAudioFrame(frame: Float32Array) {
60
- if (!this.session) {
61
- await this.initializeSession(this.modelPath)
62
- }
52
+ await this.initializeIfNeeded()
53
+
54
+ const Onnx = await import('onnxruntime-node')
63
55
 
64
56
  const inputTensor = new Onnx.Tensor('float32', frame, [1, frame.length])
65
57
 
66
- //const inputs = { input: inputTensor, h0: this.modelStateH, c0: this.modelStateC }
67
- const inputs = { input: inputTensor, sr: this.modelSampleRate, h: this.modelStateH, c: this.modelStateC }
58
+ const inputs = { input: inputTensor, sr: this.modelSampleRate!, h: this.modelStateH!, c: this.modelStateC! }
68
59
 
69
60
  const results = await this.session!.run(inputs)
70
61
 
@@ -75,12 +66,25 @@ export class SileroVAD {
75
66
 
76
67
  return probability
77
68
  }
69
+ private async initializeIfNeeded() {
70
+ if (this.session) {
71
+ return
72
+ }
73
+
74
+ const Onnx = await import('onnxruntime-node')
75
+
76
+ const h = new Float32Array(2 * 1 * 64)
77
+ const c = new Float32Array(2 * 1 * 64)
78
+
79
+ this.modelStateH = new Onnx.Tensor('float32', h, [2, 1, 64])
80
+ this.modelStateC = new Onnx.Tensor('float32', c, [2, 1, 64])
81
+
82
+ this.modelSampleRate = new Onnx.Tensor('int64', new BigInt64Array([BigInt(16000)]), [])
78
83
 
79
- private async initializeSession(modelPath: string) {
80
84
  const onnxOptions: Onnx.InferenceSession.SessionOptions = {
81
85
  logSeverityLevel: 3
82
86
  }
83
87
 
84
- this.session = await Onnx.InferenceSession.create(modelPath, onnxOptions)
88
+ this.session = await Onnx.InferenceSession.create(this.modelPath, onnxOptions)
85
89
  }
86
90
  }