echogarden 1.0.4 → 1.1.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 (122) hide show
  1. package/README.md +26 -23
  2. package/data/schemas/options.json +177 -36
  3. package/dist/alignment/SpeechAlignment.d.ts +1 -1
  4. package/dist/alignment/SpeechAlignment.js +1 -1
  5. package/dist/alignment/SpeechAlignment.js.map +1 -1
  6. package/dist/api/API.d.ts +1 -0
  7. package/dist/api/API.js +1 -0
  8. package/dist/api/API.js.map +1 -1
  9. package/dist/api/APIOptions.d.ts +1 -0
  10. package/dist/api/Alignment.d.ts +3 -3
  11. package/dist/api/Alignment.js +5 -10
  12. package/dist/api/Alignment.js.map +1 -1
  13. package/dist/api/LanguageDetection.d.ts +5 -7
  14. package/dist/api/LanguageDetection.js +3 -2
  15. package/dist/api/LanguageDetection.js.map +1 -1
  16. package/dist/api/Recognition.d.ts +4 -5
  17. package/dist/api/Recognition.js +5 -8
  18. package/dist/api/Recognition.js.map +1 -1
  19. package/dist/api/SourceSeparation.d.ts +2 -0
  20. package/dist/api/SourceSeparation.js +4 -2
  21. package/dist/api/SourceSeparation.js.map +1 -1
  22. package/dist/api/Synthesis.d.ts +3 -1
  23. package/dist/api/Synthesis.js +9 -10
  24. package/dist/api/Synthesis.js.map +1 -1
  25. package/dist/api/Translation.d.ts +1 -1
  26. package/dist/api/Translation.js +4 -8
  27. package/dist/api/Translation.js.map +1 -1
  28. package/dist/api/TranslationAlignment.d.ts +31 -0
  29. package/dist/api/TranslationAlignment.js +121 -0
  30. package/dist/api/TranslationAlignment.js.map +1 -0
  31. package/dist/api/VoiceActivityDetection.d.ts +5 -1
  32. package/dist/api/VoiceActivityDetection.js +38 -2
  33. package/dist/api/VoiceActivityDetection.js.map +1 -1
  34. package/dist/audio/AudioPlayer.js +6 -1
  35. package/dist/audio/AudioPlayer.js.map +1 -1
  36. package/dist/cli/CLI.js +85 -0
  37. package/dist/cli/CLI.js.map +1 -1
  38. package/dist/dsp/FFT.js.map +1 -1
  39. package/dist/math/MedianFilter.d.ts +5 -0
  40. package/dist/math/MedianFilter.js +102 -0
  41. package/dist/math/MedianFilter.js.map +1 -0
  42. package/dist/math/VectorMath.d.ts +0 -2
  43. package/dist/math/VectorMath.js +1 -25
  44. package/dist/math/VectorMath.js.map +1 -1
  45. package/dist/recognition/OpenAICloudSTT.d.ts +1 -1
  46. package/dist/recognition/OpenAICloudSTT.js.map +1 -1
  47. package/dist/recognition/SileroSTT.d.ts +22 -1
  48. package/dist/recognition/SileroSTT.js +122 -95
  49. package/dist/recognition/SileroSTT.js.map +1 -1
  50. package/dist/recognition/WhisperCppSTT.js +1 -1
  51. package/dist/recognition/WhisperCppSTT.js.map +1 -1
  52. package/dist/recognition/WhisperSTT.d.ts +52 -19
  53. package/dist/recognition/WhisperSTT.js +645 -494
  54. package/dist/recognition/WhisperSTT.js.map +1 -1
  55. package/dist/server/Server.js.map +1 -1
  56. package/dist/source-separation/MDXNetSourceSeparation.d.ts +5 -3
  57. package/dist/source-separation/MDXNetSourceSeparation.js +26 -19
  58. package/dist/source-separation/MDXNetSourceSeparation.js.map +1 -1
  59. package/dist/speech-language-detection/SileroLanguageDetection.d.ts +15 -9
  60. package/dist/speech-language-detection/SileroLanguageDetection.js +23 -16
  61. package/dist/speech-language-detection/SileroLanguageDetection.js.map +1 -1
  62. package/dist/synthesis/EspeakTTS.js +4 -0
  63. package/dist/synthesis/EspeakTTS.js.map +1 -1
  64. package/dist/synthesis/GoogleCloudTTS.js.map +1 -1
  65. package/dist/synthesis/VitsTTS.d.ts +8 -6
  66. package/dist/synthesis/VitsTTS.js +36 -31
  67. package/dist/synthesis/VitsTTS.js.map +1 -1
  68. package/dist/tests/Test.js.map +1 -1
  69. package/dist/utilities/OnnxUtilities.d.ts +14 -0
  70. package/dist/utilities/OnnxUtilities.js +43 -0
  71. package/dist/utilities/OnnxUtilities.js.map +1 -0
  72. package/dist/utilities/Utilities.d.ts +4 -8
  73. package/dist/utilities/Utilities.js +35 -58
  74. package/dist/utilities/Utilities.js.map +1 -1
  75. package/dist/voice-activity-detection/SileroVAD.d.ts +5 -3
  76. package/dist/voice-activity-detection/SileroVAD.js +9 -11
  77. package/dist/voice-activity-detection/SileroVAD.js.map +1 -1
  78. package/docs/API.md +54 -34
  79. package/docs/CLI.md +25 -13
  80. package/docs/Contributing.md +4 -2
  81. package/docs/Engines.md +43 -32
  82. package/docs/Licenses.md +3 -4
  83. package/docs/Options.md +47 -11
  84. package/docs/Releases.md +4 -0
  85. package/docs/Server.md +8 -6
  86. package/docs/Tasklist.md +39 -52
  87. package/docs/Technical.md +1 -1
  88. package/package.json +8 -12
  89. package/src/alignment/SpeechAlignment.ts +1 -1
  90. package/src/api/API.ts +1 -0
  91. package/src/api/APIOptions.ts +1 -0
  92. package/src/api/Alignment.ts +10 -14
  93. package/src/api/LanguageDetection.ts +14 -10
  94. package/src/api/Recognition.ts +17 -10
  95. package/src/api/SourceSeparation.ts +7 -2
  96. package/src/api/Synthesis.ts +26 -11
  97. package/src/api/Translation.ts +14 -8
  98. package/src/api/TranslationAlignment.ts +213 -0
  99. package/src/api/VoiceActivityDetection.ts +66 -3
  100. package/src/audio/AudioPlayer.ts +6 -2
  101. package/src/cli/CLI.ts +121 -2
  102. package/src/dsp/FFT.ts +3 -0
  103. package/src/math/MedianFilter.ts +124 -0
  104. package/src/math/VectorMath.ts +1 -36
  105. package/src/recognition/OpenAICloudSTT.ts +27 -27
  106. package/src/recognition/SileroSTT.ts +149 -102
  107. package/src/recognition/WhisperCppSTT.ts +1 -1
  108. package/src/recognition/WhisperSTT.ts +961 -684
  109. package/src/server/Server.ts +1 -1
  110. package/src/source-separation/MDXNetSourceSeparation.ts +35 -19
  111. package/src/speech-language-detection/SileroLanguageDetection.ts +53 -33
  112. package/src/synthesis/EspeakTTS.ts +8 -0
  113. package/src/synthesis/GoogleCloudTTS.ts +12 -1
  114. package/src/synthesis/VitsTTS.ts +57 -46
  115. package/src/tests/Test.ts +1 -1
  116. package/src/utilities/OnnxUtilities.ts +68 -0
  117. package/src/utilities/Utilities.ts +38 -66
  118. package/src/voice-activity-detection/SileroVAD.ts +15 -15
  119. package/dist/utilities/NdArrayUtilities.d.ts +0 -3
  120. package/dist/utilities/NdArrayUtilities.js +0 -23
  121. package/dist/utilities/NdArrayUtilities.js.map +0 -1
  122. package/src/utilities/NdArrayUtilities.ts +0 -31
@@ -7,8 +7,8 @@ import { extendDeep } from '../utilities/ObjectUtilities.js'
7
7
  import { sendMessageToWorker, addListenerToWorkerMessages, startNewWorkerThread, startMessageChannel } from './Worker.js'
8
8
  import { Worker } from 'node:worker_threads'
9
9
  import { IncomingMessage, ServerResponse } from 'node:http'
10
- import { Logger } from '../utilities/Logger.js'
11
10
  import chalk from 'chalk'
11
+ import { Logger } from '../utilities/Logger.js'
12
12
 
13
13
  const log = logToStderr
14
14
 
@@ -4,11 +4,16 @@ import { binBufferToComplex, complexToBinBuffer, getWindowWeights, stftr, stiftr
4
4
  import { ComplexNumber } from '../math/VectorMath.js';
5
5
  import { logToStderr } from '../utilities/Utilities.js';
6
6
  import { Logger } from '../utilities/Logger.js';
7
+ import { OnnxExecutionProvider, getOnnxSessionOptions } from '../utilities/OnnxUtilities.js';
7
8
 
8
9
  const log = logToStderr
9
10
 
10
- export async function isolate(rawAudio: RawAudio, modelFilePath: string) {
11
- const model = new MDXNet(modelFilePath)
11
+ export async function isolate(
12
+ rawAudio: RawAudio,
13
+ modelFilePath: string,
14
+ executionProviders: OnnxExecutionProvider[]) {
15
+
16
+ const model = new MDXNet(modelFilePath, executionProviders)
12
17
 
13
18
  return model.processAudio(rawAudio)
14
19
  }
@@ -16,7 +21,9 @@ export async function isolate(rawAudio: RawAudio, modelFilePath: string) {
16
21
  export class MDXNet {
17
22
  session?: Onnx.InferenceSession
18
23
 
19
- constructor(public readonly modelFilePath: string) {
24
+ constructor(
25
+ public readonly modelFilePath: string,
26
+ public readonly executionProviders: OnnxExecutionProvider[]) {
20
27
  }
21
28
 
22
29
  async processAudio(rawAudio: RawAudio) {
@@ -28,9 +35,7 @@ export class MDXNet {
28
35
  throw new Error(`Input audio must have a 44100 Hz sampling rate`)
29
36
  }
30
37
 
31
- if (!this.session) {
32
- await this.initializeSession(this.modelFilePath)
33
- }
38
+ await this.initializeSessionIfNeeded()
34
39
 
35
40
  const Onnx = await import('onnxruntime-node')
36
41
 
@@ -75,6 +80,9 @@ export class MDXNet {
75
80
  let writePosition = 0
76
81
 
77
82
  for (let tensorChannelIndex = 0; tensorChannelIndex < 4; tensorChannelIndex++) {
83
+ const isEvenTensorChannelIndex = tensorChannelIndex % 2 === 0
84
+ const inChannelIndex = tensorChannelIndex < 2 ? 0 : 1
85
+
78
86
  for (let binIndex = 0; binIndex < fftCount; binIndex++) {
79
87
  for (let frameIndex = 0; frameIndex < segmentSize; frameIndex++) {
80
88
  let value = 0
@@ -82,7 +90,7 @@ export class MDXNet {
82
90
  if (frameIndex < segmentLength && binIndex >= 0) {
83
91
  let frame: ComplexNumber[]
84
92
 
85
- if (tensorChannelIndex < 2) {
93
+ if (inChannelIndex === 0) {
86
94
  frame = fftFramesLeftComplexForSegment[frameIndex]
87
95
  } else {
88
96
  frame = fftFramesRightComplexForSegment[frameIndex]
@@ -90,7 +98,7 @@ export class MDXNet {
90
98
 
91
99
  const bin = frame[binIndex]
92
100
 
93
- if (tensorChannelIndex % 2 === 0) {
101
+ if (isEvenTensorChannelIndex) {
94
102
  value = bin.real
95
103
  } else {
96
104
  value = bin.imaginary
@@ -105,8 +113,12 @@ export class MDXNet {
105
113
 
106
114
  const inputTensor = new Onnx.Tensor('float32', flattenedInputTensor, [1, 4, 2048, 256])
107
115
 
116
+ logger.start('Run MDXNet model')
117
+
108
118
  const { output: outputTensor } = await session.run({ input: inputTensor })
109
119
 
120
+ logger.start('Process MDXNet model output')
121
+
110
122
  const flattenedOutputTensor = outputTensor.data as Float32Array
111
123
 
112
124
  const outputChannelComplexFrames: ComplexNumber[][][] = []
@@ -131,13 +143,16 @@ export class MDXNet {
131
143
  let readPosition = 0
132
144
 
133
145
  for (let tensorChannelIndex = 0; tensorChannelIndex < 4; tensorChannelIndex++) {
146
+ const isEvenTensorChannelIndex = tensorChannelIndex % 2 === 0
134
147
  const outChannelIndex = tensorChannelIndex < 2 ? 0 : 1
135
148
 
149
+ const binsForOutputChannel = outputChannelComplexFrames[outChannelIndex]
150
+
136
151
  for (let binIndex = 0; binIndex < 2048; binIndex++) {
137
152
  for (let frameIndex = 0; frameIndex < 256; frameIndex++) {
138
- const bin = outputChannelComplexFrames[outChannelIndex][frameIndex][binIndex]
153
+ const bin = binsForOutputChannel[frameIndex][binIndex]
139
154
 
140
- if (tensorChannelIndex % 2 === 0) {
155
+ if (isEvenTensorChannelIndex) {
141
156
  bin.real = flattenedOutputTensor[readPosition++]
142
157
  } else {
143
158
  bin.imaginary = flattenedOutputTensor[readPosition++]
@@ -149,10 +164,12 @@ export class MDXNet {
149
164
 
150
165
  const outputAudioChannels: Float32Array[] = []
151
166
 
152
- //logger.start(`Compute inverse STFT for segment`)
167
+ logger.start(`Compute inverse STFT for segment`)
153
168
  for (let channelIndex = 0; channelIndex < 2; channelIndex++) {
169
+ const fftSizeReciprocal = 1 / fftSize
170
+
154
171
  let outputChannelFlattenedFrames = outputChannelComplexFrames[channelIndex]
155
- .map(frame => complexToBinBuffer(frame).map(value => value / fftSize))
172
+ .map(frame => complexToBinBuffer(frame).map(value => value * fftSizeReciprocal))
156
173
 
157
174
  const samples = await stiftr(
158
175
  outputChannelFlattenedFrames,
@@ -164,9 +181,6 @@ export class MDXNet {
164
181
  outputAudioChannels.push(samples)
165
182
  }
166
183
 
167
- //logger.log(`Reconstructed waveform peak: ${getAudioPeakDecibels(outputAudioChannels).toFixed(3)}dB`)
168
- //await playAudioSamples({ audioChannels: outputAudioChannels, sampleRate })
169
-
170
184
  audioForSegments.push(outputAudioChannels)
171
185
  }
172
186
 
@@ -220,13 +234,15 @@ export class MDXNet {
220
234
  return isolatedRawAudio
221
235
  }
222
236
 
223
- private async initializeSession(modelPath: string) {
224
- const onnxOptions: Onnx.InferenceSession.SessionOptions = {
225
- logSeverityLevel: 3
237
+ private async initializeSessionIfNeeded() {
238
+ if (this.session) {
239
+ return
226
240
  }
227
241
 
228
242
  const Onnx = await import('onnxruntime-node')
229
243
 
230
- this.session = await Onnx.InferenceSession.create(modelPath, onnxOptions)
244
+ const onnxSessionOptions = getOnnxSessionOptions({ executionProviders: this.executionProviders })
245
+
246
+ this.session = await Onnx.InferenceSession.create(this.modelFilePath, onnxSessionOptions)
231
247
  }
232
248
  }
@@ -5,10 +5,20 @@ import { RawAudio } from '../audio/AudioUtilities.js'
5
5
  import { readAndParseJsonFile } from '../utilities/FileSystem.js'
6
6
  import { detectSpeechLanguageByParts, type LanguageDetectionResults } from '../api/LanguageDetection.js'
7
7
  import { languageCodeToName } from '../utilities/Locale.js'
8
+ import { OnnxExecutionProvider, getOnnxSessionOptions } from '../utilities/OnnxUtilities.js'
8
9
 
9
- export async function detectLanguage(rawAudio: RawAudio, modelPath: string, languageDictionaryPath: string, languageGroupDictionaryPath: string) {
10
- const languageDetection = new SileroLanguageDetection(modelPath, languageDictionaryPath, languageGroupDictionaryPath)
11
- await languageDetection.initialize()
10
+ export async function detectLanguage(
11
+ rawAudio: RawAudio,
12
+ modelPath: string,
13
+ languageDictionaryPath: string,
14
+ languageGroupDictionaryPath: string,
15
+ onnxExecutionProviders: OnnxExecutionProvider[]) {
16
+
17
+ const languageDetection = new SileroLanguageDetection(
18
+ modelPath,
19
+ languageDictionaryPath,
20
+ languageGroupDictionaryPath,
21
+ onnxExecutionProviders)
12
22
 
13
23
  async function detectLanguageForPart(partAudio: RawAudio) {
14
24
  const { languageResults } = await languageDetection.detectLanguage(partAudio)
@@ -24,40 +34,21 @@ export async function detectLanguage(rawAudio: RawAudio, modelPath: string, lang
24
34
  }
25
35
 
26
36
  export class SileroLanguageDetection {
27
- modelPath: string
28
- languageDictionaryPath: string
29
- languageGroupDictionaryPath: string
30
-
31
- languageDictionary: any
32
- languageGroupDictionary: any
33
-
34
- session: Onnx.InferenceSession | undefined
35
-
36
- constructor(modelPath: string, languageDictionaryPath: string, languageGroupDictionaryPath: string) {
37
- this.modelPath = modelPath
38
- this.languageDictionaryPath = languageDictionaryPath
39
- this.languageGroupDictionaryPath = languageGroupDictionaryPath
40
- }
41
-
42
- async initialize() {
43
- const logger = new Logger()
44
- logger.start('Initialize ONNX inference session')
45
-
46
- this.languageDictionary = await readAndParseJsonFile(this.languageDictionaryPath)
47
- this.languageGroupDictionary = await readAndParseJsonFile(this.languageGroupDictionaryPath)
48
-
49
- const onnxOptions: Onnx.InferenceSession.SessionOptions = {
50
- logSeverityLevel: 3
51
- }
52
-
53
- const Onnx = await import('onnxruntime-node')
37
+ languageDictionary?: any
38
+ languageGroupDictionary?: any
54
39
 
55
- this.session = await Onnx.InferenceSession.create(this.modelPath, onnxOptions)
40
+ session?: Onnx.InferenceSession
56
41
 
57
- logger.end()
42
+ constructor(
43
+ public readonly modelPath: string,
44
+ public readonly languageDictionaryPath: string,
45
+ public readonly languageGroupDictionaryPath: string,
46
+ public readonly onnxExecutionProviders: OnnxExecutionProvider[]) {
58
47
  }
59
48
 
60
49
  async detectLanguage(rawAudio: RawAudio) {
50
+ await this.initializeIfNeeded()
51
+
61
52
  const logger = new Logger()
62
53
 
63
54
  logger.start('Detect language with Silero')
@@ -65,7 +56,7 @@ export class SileroLanguageDetection {
65
56
  const audioSamples = rawAudio.audioChannels[0]
66
57
 
67
58
  const Onnx = await import('onnxruntime-node')
68
-
59
+
69
60
  const inputTensor = new Onnx.Tensor('float32', audioSamples, [1, audioSamples.length])
70
61
 
71
62
  const inputs = { input: inputTensor }
@@ -106,4 +97,33 @@ export class SileroLanguageDetection {
106
97
 
107
98
  return { languageResults, languageGroupResults }
108
99
  }
100
+
101
+ async initializeIfNeeded() {
102
+ if (this.session) {
103
+ return
104
+ }
105
+
106
+ const logger = new Logger()
107
+
108
+ logger.start('Initialize ONNX inference session for Silero language detection')
109
+
110
+ this.languageDictionary = await readAndParseJsonFile(this.languageDictionaryPath)
111
+ this.languageGroupDictionary = await readAndParseJsonFile(this.languageGroupDictionaryPath)
112
+
113
+ const Onnx = await import('onnxruntime-node')
114
+
115
+ const onnxSessionOptions = getOnnxSessionOptions({ executionProviders: this.onnxExecutionProviders })
116
+
117
+ this.session = await Onnx.InferenceSession.create(this.modelPath, onnxSessionOptions)
118
+
119
+ logger.end()
120
+ }
121
+ }
122
+
123
+ export interface SileroLanguageDetectionOptions {
124
+ provider?: OnnxExecutionProvider
125
+ }
126
+
127
+ export const defaultSileroLanguageDetectionOptions: SileroLanguageDetectionOptions = {
128
+ provider: undefined
109
129
  }
@@ -10,6 +10,7 @@ import { splitToWords, wordCharacterPattern } from '../nlp/Segmentation.js'
10
10
  import { Lexicon, tryGetFirstLexiconSubstitution } from '../nlp/Lexicon.js'
11
11
  import { phonemizeSentence } from '../nlp/EspeakPhonemizer.js'
12
12
  import { Timeline, TimelineEntry } from '../utilities/Timeline.js'
13
+ import { extendDeep } from '../utilities/ObjectUtilities.js'
13
14
 
14
15
  const log = logToStderr
15
16
 
@@ -19,6 +20,8 @@ let espeakModule: any
19
20
  export async function preprocessAndSynthesize(text: string, language: string, espeakOptions: EspeakOptions, lexicons: Lexicon[] = []) {
20
21
  const logger = new Logger()
21
22
 
23
+ espeakOptions = extendDeep(defaultEspeakOptions, espeakOptions)
24
+
22
25
  await logger.startAsync('Tokenize and analyze text')
23
26
 
24
27
  let lowerCaseLanguageCode = language.toLowerCase()
@@ -135,6 +138,8 @@ export async function preprocessAndSynthesize(text: string, language: string, es
135
138
  export async function synthesizeFragments(fragments: string[], espeakOptions: EspeakOptions) {
136
139
  const logger = new Logger()
137
140
 
141
+ espeakOptions = extendDeep(defaultEspeakOptions, espeakOptions)
142
+
138
143
  const sampleRate = await getSampleRate()
139
144
 
140
145
  //fragments = fragments.filter(fragment => fragment.trim() != '')
@@ -399,6 +404,9 @@ export async function synthesizeFragments(fragments: string[], espeakOptions: Es
399
404
 
400
405
  export async function synthesize(text: string, espeakOptions: EspeakOptions) {
401
406
  const logger = new Logger()
407
+
408
+ espeakOptions = extendDeep(defaultEspeakOptions, espeakOptions)
409
+
402
410
  logger.start('Get eSpeak Emscripten instance')
403
411
 
404
412
  if (!espeakOptions.ssml) {
@@ -4,7 +4,18 @@ import { logToStderr } from '../utilities/Utilities.js'
4
4
 
5
5
  const log = logToStderr
6
6
 
7
- export async function synthesize(text: string, apiKey: string, languageCode = 'en-US', voice = 'en-US-Wavenet-C', speakingRate = 1.0, pitchDeltaSemitones = 0.0, volumeGainDecibels = 0.0, ssml = false, audioEncoding: AudioEncoding = 'MP3_64_KBPS', sampleRate = 24000) {
7
+ export async function synthesize(
8
+ text: string,
9
+ apiKey: string,
10
+ languageCode = 'en-US',
11
+ voice = 'en-US-Wavenet-C',
12
+ speakingRate = 1.0,
13
+ pitchDeltaSemitones = 0.0,
14
+ volumeGainDecibels = 0.0,
15
+ ssml = false,
16
+ audioEncoding: AudioEncoding = 'MP3_64_KBPS',
17
+ sampleRate = 24000) {
18
+
8
19
  const logger = new Logger()
9
20
  logger.start('Request synthesis from Google Cloud')
10
21
 
@@ -7,17 +7,25 @@ import { Timeline } from '../utilities/Timeline.js'
7
7
  import { readAndParseJsonFile, readdir } from '../utilities/FileSystem.js'
8
8
  import path from 'node:path'
9
9
  import { EspeakOptions, defaultEspeakOptions } from '../synthesis/EspeakTTS.js'
10
+ import { OnnxExecutionProvider, getOnnxSessionOptions } from '../utilities/OnnxUtilities.js'
10
11
 
11
12
  const cachedInstanceLookup = new Map<string, VitsTTS>()
12
13
 
13
- export async function synthesizeSentence(text: string, voiceName: string, modelPath: string, lengthScale: number, speakerId = 0, lexicons?: Lexicon[]) {
14
+ export async function synthesizeSentence(
15
+ text: string,
16
+ voiceName: string,
17
+ modelPath: string,
18
+ lengthScale: number,
19
+ speakerId: number,
20
+ lexicons: Lexicon[],
21
+ executionProviders: OnnxExecutionProvider[]) {
22
+
14
23
  const cacheLookupKey = modelPath
15
24
 
16
25
  let vitsTTS: VitsTTS | undefined = cachedInstanceLookup.get(cacheLookupKey)
17
26
 
18
27
  if (!vitsTTS) {
19
- vitsTTS = new VitsTTS(voiceName, modelPath)
20
- await vitsTTS.initialize()
28
+ vitsTTS = new VitsTTS(voiceName, modelPath, executionProviders)
21
29
 
22
30
  cachedInstanceLookup.clear()
23
31
  cachedInstanceLookup.set(cacheLookupKey, vitsTTS)
@@ -29,55 +37,20 @@ export async function synthesizeSentence(text: string, voiceName: string, modelP
29
37
  }
30
38
 
31
39
  export class VitsTTS {
32
- voiceName: string
33
- modelPath: string
34
-
35
- modelSession?: Onnx.InferenceSession
40
+ session?: Onnx.InferenceSession
36
41
  metadata?: any
37
42
  phonemeMap?: Map<string, number[]>
38
43
 
39
- constructor(voiceName: string, modelPath: string) {
40
- this.voiceName = voiceName
41
- this.modelPath = modelPath
42
- }
43
-
44
- async initialize() {
45
- const logger = new Logger()
46
- await logger.startAsync('Initialize VITS ONNX synthesis model')
47
-
48
- const onnxOptions: Onnx.InferenceSession.SessionOptions = {
49
- logSeverityLevel: 3
50
- }
51
-
52
- const { default: Onnx } = await import('onnxruntime-node')
53
-
54
- const filesInModelPath = await readdir(this.modelPath)
55
- const onnxModelFilename = filesInModelPath.find(filename => filename.endsWith('.onnx'))
56
-
57
- if (!onnxModelFilename) {
58
- throw new Error(`Couldn't file any ONNX model file in ${this.modelPath}`)
59
- }
60
-
61
- const onnxModelFilepath = path.join(this.modelPath, onnxModelFilename)
62
-
63
- this.modelSession = await Onnx.InferenceSession.create(onnxModelFilepath, onnxOptions)
64
- this.metadata = await readAndParseJsonFile(`${onnxModelFilepath}.json`)
65
-
66
- this.phonemeMap = new Map<string, number[]>()
67
-
68
- for (const key in this.metadata.phoneme_id_map) {
69
- this.phonemeMap.set(key, this.metadata.phoneme_id_map[key])
70
- }
71
-
72
- logger.end()
44
+ constructor(
45
+ public readonly voiceName: string,
46
+ public readonly modelPath: string,
47
+ public readonly executionProviders: OnnxExecutionProvider[]) {
73
48
  }
74
49
 
75
50
  async synthesizeSentence(sentence: string, lengthScale: number, speakerId = 0, lexicons?: Lexicon[]) {
76
51
  const logger = new Logger()
77
52
 
78
- if (!this.modelSession) {
79
- throw new Error('Model has not been initialized')
80
- }
53
+ await this.initializeIfNeeded()
81
54
 
82
55
  await logger.startAsync('Prepare for synthesis')
83
56
 
@@ -100,7 +73,12 @@ export class VitsTTS {
100
73
 
101
74
  logger.end()
102
75
 
103
- const espeakOptions: EspeakOptions = { ...defaultEspeakOptions, voice: espeakVoice, useKlatt: false }
76
+ const espeakOptions: EspeakOptions = {
77
+ ...defaultEspeakOptions,
78
+ voice: espeakVoice,
79
+ useKlatt: false
80
+ }
81
+
104
82
  const { referenceSynthesizedAudio, referenceTimeline, fragments, phonemizedFragmentsSubstitutions, phonemizedSentence } = await Espeak.preprocessAndSynthesize(sentence, languageCode, espeakOptions, lexicons)
105
83
 
106
84
  if (phonemizedSentence.length == 0) {
@@ -180,7 +158,7 @@ export class VitsTTS {
180
158
 
181
159
  const modelInputs = { input: inputTensor, input_lengths: inputLengthsTensor, scales: scalesTensor, sid: speakerIdTensor }
182
160
 
183
- const modelResults = await this.modelSession.run(modelInputs)
161
+ const modelResults = await this.session!.run(modelInputs)
184
162
  const modelOutput = modelResults['output']
185
163
 
186
164
  const modelOutputAudioSamples = modelOutput['data'] as Float32Array
@@ -200,6 +178,39 @@ export class VitsTTS {
200
178
 
201
179
  return { rawAudio: synthesizedAudio, timeline: mappedTimeline, referenceSynthesizedAudio, referenceTimeline }
202
180
  }
181
+
182
+ async initializeIfNeeded() {
183
+ if (this.session) {
184
+ return
185
+ }
186
+
187
+ const logger = new Logger()
188
+ await logger.startAsync('Initialize VITS ONNX synthesis model')
189
+
190
+ const { default: Onnx } = await import('onnxruntime-node')
191
+
192
+ const filesInModelPath = await readdir(this.modelPath)
193
+ const onnxModelFilename = filesInModelPath.find(filename => filename.endsWith('.onnx'))
194
+
195
+ if (!onnxModelFilename) {
196
+ throw new Error(`Couldn't file any ONNX model file in ${this.modelPath}`)
197
+ }
198
+
199
+ const onnxModelFilepath = path.join(this.modelPath, onnxModelFilename)
200
+
201
+ const onnxSessionOptions = getOnnxSessionOptions({ executionProviders: this.executionProviders })
202
+
203
+ this.session = await Onnx.InferenceSession.create(onnxModelFilepath, onnxSessionOptions)
204
+ this.metadata = await readAndParseJsonFile(`${onnxModelFilepath}.json`)
205
+
206
+ this.phonemeMap = new Map<string, number[]>()
207
+
208
+ for (const key in this.metadata.phoneme_id_map) {
209
+ this.phonemeMap.set(key, this.metadata.phoneme_id_map[key])
210
+ }
211
+
212
+ logger.end()
213
+ }
203
214
  }
204
215
 
205
216
  export const voiceList: SynthesisVoice[] = [
package/src/tests/Test.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { getRepetitionScoreRelativeToFirstSubstring, logToStderr, setupProgramTerminationListeners, writeToStderr } from '../utilities/Utilities.js'
1
+ import { getTokenRepetitionScore, 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
 
@@ -0,0 +1,68 @@
1
+ import type * as Onnx from 'onnxruntime-node'
2
+
3
+ export function getOnnxSessionOptions(options: OnnxSessionOptions) {
4
+
5
+ const onnxOptions: Onnx.InferenceSession.SessionOptions = {
6
+ executionProviders: ['cpu'],
7
+ logSeverityLevel: 3,
8
+ }
9
+
10
+ function dmlProviderAvailable() {
11
+ const platform = process.platform
12
+ const arch = process.arch
13
+
14
+ return platform === 'win32' && arch === 'x64'
15
+ }
16
+
17
+ if (options) {
18
+ if (options.executionProviders != null) {
19
+ let executionProviders = options.executionProviders.filter(provider => {
20
+ if (!provider) {
21
+ return false
22
+ }
23
+
24
+ if (provider === 'dml' && !dmlProviderAvailable()) {
25
+ return false
26
+ }
27
+
28
+ return true
29
+ })
30
+
31
+ if (!executionProviders.includes('cpu')) {
32
+ executionProviders.push('cpu')
33
+ }
34
+
35
+ executionProviders = Array.from(new Set(executionProviders))
36
+
37
+ onnxOptions.executionProviders = executionProviders as any
38
+ } else if (options.enableGPU === true && dmlProviderAvailable()) {
39
+ onnxOptions.executionProviders = ['dml', 'cpu']
40
+ }
41
+
42
+ if (options.logSeverityLevel != null) {
43
+ onnxOptions.logSeverityLevel = options.logSeverityLevel
44
+ }
45
+ }
46
+
47
+ return onnxOptions
48
+ }
49
+
50
+ export function makeOnnxLikeFloat32Tensor(onnxTensor: Onnx.Tensor): OnnxLikeFloat32Tensor {
51
+ return {
52
+ data: (onnxTensor.data as Float32Array).slice(),
53
+ dims: onnxTensor.dims.slice()
54
+ }
55
+ }
56
+
57
+ export interface OnnxLikeFloat32Tensor {
58
+ readonly data: Float32Array
59
+ readonly dims: number[]
60
+ }
61
+
62
+ export interface OnnxSessionOptions {
63
+ enableGPU?: boolean
64
+ executionProviders?: OnnxExecutionProvider[]
65
+ logSeverityLevel?: 0 | 1 | 2 | 3 | 4
66
+ }
67
+
68
+ export type OnnxExecutionProvider = 'cpu' | 'dml'