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.
- package/README.md +26 -23
- package/data/schemas/options.json +177 -36
- package/dist/alignment/SpeechAlignment.d.ts +1 -1
- package/dist/alignment/SpeechAlignment.js +1 -1
- package/dist/alignment/SpeechAlignment.js.map +1 -1
- package/dist/api/API.d.ts +1 -0
- package/dist/api/API.js +1 -0
- package/dist/api/API.js.map +1 -1
- package/dist/api/APIOptions.d.ts +1 -0
- package/dist/api/Alignment.d.ts +3 -3
- package/dist/api/Alignment.js +5 -10
- package/dist/api/Alignment.js.map +1 -1
- package/dist/api/LanguageDetection.d.ts +5 -7
- package/dist/api/LanguageDetection.js +3 -2
- package/dist/api/LanguageDetection.js.map +1 -1
- package/dist/api/Recognition.d.ts +4 -5
- package/dist/api/Recognition.js +5 -8
- package/dist/api/Recognition.js.map +1 -1
- package/dist/api/SourceSeparation.d.ts +2 -0
- package/dist/api/SourceSeparation.js +4 -2
- package/dist/api/SourceSeparation.js.map +1 -1
- package/dist/api/Synthesis.d.ts +3 -1
- package/dist/api/Synthesis.js +9 -10
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/api/Translation.d.ts +1 -1
- package/dist/api/Translation.js +4 -8
- package/dist/api/Translation.js.map +1 -1
- package/dist/api/TranslationAlignment.d.ts +31 -0
- package/dist/api/TranslationAlignment.js +121 -0
- package/dist/api/TranslationAlignment.js.map +1 -0
- package/dist/api/VoiceActivityDetection.d.ts +5 -1
- package/dist/api/VoiceActivityDetection.js +38 -2
- package/dist/api/VoiceActivityDetection.js.map +1 -1
- package/dist/audio/AudioPlayer.js +6 -1
- package/dist/audio/AudioPlayer.js.map +1 -1
- package/dist/cli/CLI.js +85 -0
- package/dist/cli/CLI.js.map +1 -1
- package/dist/dsp/FFT.js.map +1 -1
- package/dist/math/MedianFilter.d.ts +5 -0
- package/dist/math/MedianFilter.js +102 -0
- package/dist/math/MedianFilter.js.map +1 -0
- package/dist/math/VectorMath.d.ts +0 -2
- package/dist/math/VectorMath.js +1 -25
- package/dist/math/VectorMath.js.map +1 -1
- package/dist/recognition/OpenAICloudSTT.d.ts +1 -1
- package/dist/recognition/OpenAICloudSTT.js.map +1 -1
- package/dist/recognition/SileroSTT.d.ts +22 -1
- package/dist/recognition/SileroSTT.js +122 -95
- package/dist/recognition/SileroSTT.js.map +1 -1
- package/dist/recognition/WhisperCppSTT.js +1 -1
- package/dist/recognition/WhisperCppSTT.js.map +1 -1
- package/dist/recognition/WhisperSTT.d.ts +52 -19
- package/dist/recognition/WhisperSTT.js +645 -494
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/server/Server.js.map +1 -1
- package/dist/source-separation/MDXNetSourceSeparation.d.ts +5 -3
- package/dist/source-separation/MDXNetSourceSeparation.js +26 -19
- package/dist/source-separation/MDXNetSourceSeparation.js.map +1 -1
- package/dist/speech-language-detection/SileroLanguageDetection.d.ts +15 -9
- package/dist/speech-language-detection/SileroLanguageDetection.js +23 -16
- package/dist/speech-language-detection/SileroLanguageDetection.js.map +1 -1
- package/dist/synthesis/EspeakTTS.js +4 -0
- package/dist/synthesis/EspeakTTS.js.map +1 -1
- package/dist/synthesis/GoogleCloudTTS.js.map +1 -1
- package/dist/synthesis/VitsTTS.d.ts +8 -6
- package/dist/synthesis/VitsTTS.js +36 -31
- package/dist/synthesis/VitsTTS.js.map +1 -1
- package/dist/tests/Test.js.map +1 -1
- package/dist/utilities/OnnxUtilities.d.ts +14 -0
- package/dist/utilities/OnnxUtilities.js +43 -0
- package/dist/utilities/OnnxUtilities.js.map +1 -0
- package/dist/utilities/Utilities.d.ts +4 -8
- package/dist/utilities/Utilities.js +35 -58
- package/dist/utilities/Utilities.js.map +1 -1
- package/dist/voice-activity-detection/SileroVAD.d.ts +5 -3
- package/dist/voice-activity-detection/SileroVAD.js +9 -11
- package/dist/voice-activity-detection/SileroVAD.js.map +1 -1
- package/docs/API.md +54 -34
- package/docs/CLI.md +25 -13
- package/docs/Contributing.md +4 -2
- package/docs/Engines.md +43 -32
- package/docs/Licenses.md +3 -4
- package/docs/Options.md +47 -11
- package/docs/Releases.md +4 -0
- package/docs/Server.md +8 -6
- package/docs/Tasklist.md +39 -52
- package/docs/Technical.md +1 -1
- package/package.json +8 -12
- package/src/alignment/SpeechAlignment.ts +1 -1
- package/src/api/API.ts +1 -0
- package/src/api/APIOptions.ts +1 -0
- package/src/api/Alignment.ts +10 -14
- package/src/api/LanguageDetection.ts +14 -10
- package/src/api/Recognition.ts +17 -10
- package/src/api/SourceSeparation.ts +7 -2
- package/src/api/Synthesis.ts +26 -11
- package/src/api/Translation.ts +14 -8
- package/src/api/TranslationAlignment.ts +213 -0
- package/src/api/VoiceActivityDetection.ts +66 -3
- package/src/audio/AudioPlayer.ts +6 -2
- package/src/cli/CLI.ts +121 -2
- package/src/dsp/FFT.ts +3 -0
- package/src/math/MedianFilter.ts +124 -0
- package/src/math/VectorMath.ts +1 -36
- package/src/recognition/OpenAICloudSTT.ts +27 -27
- package/src/recognition/SileroSTT.ts +149 -102
- package/src/recognition/WhisperCppSTT.ts +1 -1
- package/src/recognition/WhisperSTT.ts +961 -684
- package/src/server/Server.ts +1 -1
- package/src/source-separation/MDXNetSourceSeparation.ts +35 -19
- package/src/speech-language-detection/SileroLanguageDetection.ts +53 -33
- package/src/synthesis/EspeakTTS.ts +8 -0
- package/src/synthesis/GoogleCloudTTS.ts +12 -1
- package/src/synthesis/VitsTTS.ts +57 -46
- package/src/tests/Test.ts +1 -1
- package/src/utilities/OnnxUtilities.ts +68 -0
- package/src/utilities/Utilities.ts +38 -66
- package/src/voice-activity-detection/SileroVAD.ts +15 -15
- package/dist/utilities/NdArrayUtilities.d.ts +0 -3
- package/dist/utilities/NdArrayUtilities.js +0 -23
- package/dist/utilities/NdArrayUtilities.js.map +0 -1
- package/src/utilities/NdArrayUtilities.ts +0 -31
package/src/server/Server.ts
CHANGED
|
@@ -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(
|
|
11
|
-
|
|
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(
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 =
|
|
153
|
+
const bin = binsForOutputChannel[frameIndex][binIndex]
|
|
139
154
|
|
|
140
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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(
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
40
|
+
session?: Onnx.InferenceSession
|
|
56
41
|
|
|
57
|
-
|
|
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(
|
|
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
|
|
package/src/synthesis/VitsTTS.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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(
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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 = {
|
|
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.
|
|
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 {
|
|
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'
|