echogarden 2.0.13 → 2.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 +1 -1
- package/data/lexicons/heteronyms.en.json +45 -3
- package/data/schemas/options.json +64 -0
- package/dist/api/Synthesis.d.ts +10 -1
- package/dist/api/Synthesis.js +76 -9
- package/dist/api/Synthesis.js.map +1 -1
- package/dist/recognition/WhisperSTT.js +7 -4
- package/dist/recognition/WhisperSTT.js.map +1 -1
- package/dist/synthesis/GnuSpeechTTS.d.ts +12 -0
- package/dist/synthesis/GnuSpeechTTS.js +36 -0
- package/dist/synthesis/GnuSpeechTTS.js.map +1 -0
- package/dist/synthesis/KokoroTTS.d.ts +22 -0
- package/dist/synthesis/KokoroTTS.js +595 -0
- package/dist/synthesis/KokoroTTS.js.map +1 -0
- package/dist/synthesis/OpenAICloudTTS.js +15 -0
- package/dist/synthesis/OpenAICloudTTS.js.map +1 -1
- package/dist/synthesis/VitsTTS.js +6 -1
- package/dist/synthesis/VitsTTS.js.map +1 -1
- package/dist/utilities/FileReader.js +1 -1
- package/dist/utilities/PackageManager.js +4 -0
- package/dist/utilities/PackageManager.js.map +1 -1
- package/dist/utilities/Utilities.d.ts +1 -0
- package/dist/utilities/Utilities.js +8 -0
- package/dist/utilities/Utilities.js.map +1 -1
- package/dist/utilities/WasmMemoryManager.d.ts +1 -1
- package/docs/Development.md +1 -1
- package/docs/Engines.md +4 -1
- package/docs/Licenses.md +2 -1
- package/docs/Options.md +9 -5
- package/docs/Tasklist.md +1 -1
- package/package.json +14 -13
- package/src/api/Synthesis.ts +132 -10
- package/src/recognition/WhisperSTT.ts +8 -4
- package/src/synthesis/GnuSpeechTTS.ts +40 -0
- package/src/synthesis/KokoroTTS.ts +693 -0
- package/src/synthesis/OpenAICloudTTS.ts +15 -0
- package/src/synthesis/VitsTTS.ts +6 -1
- package/src/utilities/FileReader.ts +1 -1
- package/src/utilities/PackageManager.ts +5 -0
- package/src/utilities/Utilities.ts +10 -0
package/src/api/Synthesis.ts
CHANGED
|
@@ -159,10 +159,10 @@ async function synthesizeSegments(segments: string[], options: SynthesisOptions,
|
|
|
159
159
|
|
|
160
160
|
const sentenceStartTime = timeOffset
|
|
161
161
|
|
|
162
|
-
let
|
|
163
|
-
|
|
162
|
+
let sentenceSynthesisOptions: SynthesisOptions = { postProcessing: { normalizeAudio: false } }
|
|
163
|
+
sentenceSynthesisOptions = extendDeep(options, sentenceSynthesisOptions)
|
|
164
164
|
|
|
165
|
-
const { synthesizedAudio: sentenceRawAudio, timeline: sentenceTimeline } = await synthesizeSegment(sentenceText,
|
|
165
|
+
const { synthesizedAudio: sentenceRawAudio, timeline: sentenceTimeline } = await synthesizeSegment(sentenceText, sentenceSynthesisOptions)
|
|
166
166
|
|
|
167
167
|
const endPause = sentenceIndex == sentences.length - 1 ? options.segmentEndPause! : options.sentenceEndPause!
|
|
168
168
|
sentenceRawAudio.audioChannels[0] = trimAudioEnd(sentenceRawAudio.audioChannels[0], endPause * sentenceRawAudio.sampleRate)
|
|
@@ -414,6 +414,47 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
|
|
|
414
414
|
break
|
|
415
415
|
}
|
|
416
416
|
|
|
417
|
+
case 'kokoro': {
|
|
418
|
+
if (inputIsSSML) {
|
|
419
|
+
throw new Error(`The Kokoro engine doesn't currently support SSML inputs`)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const kokoroOptions = options.kokoro!
|
|
423
|
+
|
|
424
|
+
const kokoroTTS = await import('../synthesis/KokoroTTS.js')
|
|
425
|
+
|
|
426
|
+
const lexicons = await loadLexiconsForLanguage(language, options.customLexiconPaths)
|
|
427
|
+
const onnxExecutionProviders: OnnxExecutionProvider[] = kokoroOptions.provider ? [kokoroOptions.provider] : []
|
|
428
|
+
const modelName = kokoroOptions.model!
|
|
429
|
+
const modelPackageName = `kokoro-${modelName}`
|
|
430
|
+
|
|
431
|
+
const modelPath = await loadPackage(modelPackageName)
|
|
432
|
+
const voicesPath = await loadPackage('kokoro-82m-v1.0-voices')
|
|
433
|
+
|
|
434
|
+
logger.end()
|
|
435
|
+
|
|
436
|
+
logger.logTitledMessage(`Using model`, modelPackageName)
|
|
437
|
+
|
|
438
|
+
const { rawAudio, timeline: outTimeline } = await kokoroTTS.synthesizeSentence(
|
|
439
|
+
text,
|
|
440
|
+
selectedVoice,
|
|
441
|
+
speed,
|
|
442
|
+
lexicons,
|
|
443
|
+
modelPath,
|
|
444
|
+
voicesPath,
|
|
445
|
+
onnxExecutionProviders
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
synthesizedAudio = rawAudio
|
|
449
|
+
timeline = outTimeline
|
|
450
|
+
|
|
451
|
+
shouldPostprocessPitch = true
|
|
452
|
+
|
|
453
|
+
logger.end()
|
|
454
|
+
|
|
455
|
+
break
|
|
456
|
+
}
|
|
457
|
+
|
|
417
458
|
case 'pico': {
|
|
418
459
|
if (inputIsSSML) {
|
|
419
460
|
throw new Error(`The SVOX Pico engine doesn't currently support SSML inputs`)
|
|
@@ -459,6 +500,35 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
|
|
|
459
500
|
break
|
|
460
501
|
}
|
|
461
502
|
|
|
503
|
+
case 'gnuspeech': {
|
|
504
|
+
if (inputIsSSML) {
|
|
505
|
+
throw new Error(`The Gnuspeech engine doesn't currently support SSML inputs`)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const engineOptions = options.gnuspeech!
|
|
509
|
+
|
|
510
|
+
const GnuSpeech = await import('../synthesis/GnuSpeechTTS.js')
|
|
511
|
+
const { defaultGnuSpeechOptions } = await import('@echogarden/gnuspeech-wasm')
|
|
512
|
+
|
|
513
|
+
const gnuSpeechOptions = extendDeep(defaultGnuSpeechOptions, engineOptions)
|
|
514
|
+
|
|
515
|
+
if (!engineOptions.tempo) {
|
|
516
|
+
gnuSpeechOptions.tempo = speed
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
await logger.startAsync(`Synthesize with Gnuspeech`)
|
|
520
|
+
|
|
521
|
+
const { rawAudio } = await GnuSpeech.synthesize(simplifiedText, gnuSpeechOptions)
|
|
522
|
+
|
|
523
|
+
synthesizedAudio = rawAudio
|
|
524
|
+
|
|
525
|
+
shouldPostprocessPitch = true
|
|
526
|
+
|
|
527
|
+
logger.end()
|
|
528
|
+
|
|
529
|
+
break
|
|
530
|
+
}
|
|
531
|
+
|
|
462
532
|
case 'espeak': {
|
|
463
533
|
const EspeakTTS = await import('../synthesis/EspeakTTS.js')
|
|
464
534
|
|
|
@@ -959,7 +1029,7 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
|
|
|
959
1029
|
|
|
960
1030
|
logger.end()
|
|
961
1031
|
|
|
962
|
-
logger.logDuration('
|
|
1032
|
+
logger.logDuration('Part synthesis time', startTimestamp, chalk.magentaBright)
|
|
963
1033
|
|
|
964
1034
|
return { synthesizedAudio, timeline }
|
|
965
1035
|
}
|
|
@@ -997,7 +1067,12 @@ function convertPitchScaleToSSMLValueString(pitch: number, voiceGender: VoiceGen
|
|
|
997
1067
|
}
|
|
998
1068
|
}
|
|
999
1069
|
|
|
1000
|
-
export type SynthesisEngine =
|
|
1070
|
+
export type SynthesisEngine =
|
|
1071
|
+
'vits' | 'kokoro' | 'pico' | 'flite' | 'gnuspeech' |
|
|
1072
|
+
'espeak' | 'sam' | 'sapi' | 'msspeech' | 'coqui-server' |
|
|
1073
|
+
'google-cloud' | 'microsoft-azure' | 'amazon-polly' |
|
|
1074
|
+
'openai-cloud' | 'elevenlabs' | 'google-translate' |
|
|
1075
|
+
'microsoft-edge' | 'streamlabs-polly'
|
|
1001
1076
|
|
|
1002
1077
|
export type TimePitchShiftingMethod = 'sonic' | 'rubberband'
|
|
1003
1078
|
|
|
@@ -1051,12 +1126,23 @@ export interface SynthesisOptions {
|
|
|
1051
1126
|
provider?: OnnxExecutionProvider
|
|
1052
1127
|
}
|
|
1053
1128
|
|
|
1129
|
+
kokoro?: {
|
|
1130
|
+
provider?: OnnxExecutionProvider
|
|
1131
|
+
model?: '82m-v1.0-fp32' | '82m-v1.0-quantized'
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1054
1134
|
pico?: {
|
|
1055
1135
|
}
|
|
1056
1136
|
|
|
1057
1137
|
flite?: {
|
|
1058
1138
|
}
|
|
1059
1139
|
|
|
1140
|
+
gnuspeech?: {
|
|
1141
|
+
tempo?: number
|
|
1142
|
+
controlRate?: number
|
|
1143
|
+
debug?: boolean
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1060
1146
|
espeak?: {
|
|
1061
1147
|
rate?: number
|
|
1062
1148
|
pitch?: number
|
|
@@ -1186,17 +1272,25 @@ export const defaultSynthesisOptions: SynthesisOptions = {
|
|
|
1186
1272
|
provider: undefined,
|
|
1187
1273
|
},
|
|
1188
1274
|
|
|
1275
|
+
kokoro: {
|
|
1276
|
+
model: '82m-v1.0-fp32'
|
|
1277
|
+
},
|
|
1278
|
+
|
|
1189
1279
|
pico: {
|
|
1190
1280
|
},
|
|
1191
1281
|
|
|
1192
1282
|
flite: {
|
|
1193
1283
|
},
|
|
1194
1284
|
|
|
1285
|
+
gnuspeech: {
|
|
1286
|
+
debug: false,
|
|
1287
|
+
},
|
|
1288
|
+
|
|
1195
1289
|
espeak: {
|
|
1196
1290
|
rate: undefined,
|
|
1197
1291
|
pitch: undefined,
|
|
1198
1292
|
pitchRange: undefined,
|
|
1199
|
-
useKlatt: false
|
|
1293
|
+
useKlatt: false,
|
|
1200
1294
|
},
|
|
1201
1295
|
|
|
1202
1296
|
sam: {
|
|
@@ -1330,6 +1424,14 @@ export async function requestVoiceList(options: VoiceListRequestOptions): Promis
|
|
|
1330
1424
|
break
|
|
1331
1425
|
}
|
|
1332
1426
|
|
|
1427
|
+
case 'gnuspeech': {
|
|
1428
|
+
const GnuSpeech = await import('../synthesis/GnuSpeechTTS.js')
|
|
1429
|
+
|
|
1430
|
+
voiceList = GnuSpeech.voiceList
|
|
1431
|
+
|
|
1432
|
+
break
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1333
1435
|
case 'sam': {
|
|
1334
1436
|
voiceList.push({
|
|
1335
1437
|
name: 'sam',
|
|
@@ -1350,6 +1452,14 @@ export async function requestVoiceList(options: VoiceListRequestOptions): Promis
|
|
|
1350
1452
|
break
|
|
1351
1453
|
}
|
|
1352
1454
|
|
|
1455
|
+
case 'kokoro': {
|
|
1456
|
+
const KokoroTTS = await import('../synthesis/KokoroTTS.js')
|
|
1457
|
+
|
|
1458
|
+
voiceList = KokoroTTS.voiceList
|
|
1459
|
+
|
|
1460
|
+
break
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1353
1463
|
case 'sapi': {
|
|
1354
1464
|
const SapiTTS = await import('../synthesis/SapiTTS.js')
|
|
1355
1465
|
|
|
@@ -1701,28 +1811,40 @@ export const synthesisEngines: EngineMetadata[] = [
|
|
|
1701
1811
|
description: 'A high-quality end-to-end neural speech synthesis architecture.',
|
|
1702
1812
|
type: 'local'
|
|
1703
1813
|
},
|
|
1814
|
+
{
|
|
1815
|
+
id: 'kokoro',
|
|
1816
|
+
name: 'Kokoro',
|
|
1817
|
+
description: 'A high-quality neural speech synthesis model based on the StyleTTS 2 architecture.',
|
|
1818
|
+
type: 'local'
|
|
1819
|
+
},
|
|
1704
1820
|
{
|
|
1705
1821
|
id: 'pico',
|
|
1706
1822
|
name: 'SVOX Pico',
|
|
1707
|
-
description: 'A legacy diphone-based
|
|
1823
|
+
description: 'A legacy diphone-based speech synthesizer.',
|
|
1708
1824
|
type: 'local'
|
|
1709
1825
|
},
|
|
1710
1826
|
{
|
|
1711
1827
|
id: 'flite',
|
|
1712
1828
|
name: 'Flite',
|
|
1713
|
-
description: 'A legacy diphone-based
|
|
1829
|
+
description: 'A legacy diphone-based speech synthesizer.',
|
|
1830
|
+
type: 'local'
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
id: 'gnuspeech',
|
|
1834
|
+
name: 'Gnuspeech',
|
|
1835
|
+
description: 'A legacy articulatory speech synthesizer.',
|
|
1714
1836
|
type: 'local'
|
|
1715
1837
|
},
|
|
1716
1838
|
{
|
|
1717
1839
|
id: 'espeak',
|
|
1718
1840
|
name: 'eSpeak NG',
|
|
1719
|
-
description: `A lightweight 'robot'
|
|
1841
|
+
description: `A lightweight, highly multilingual, 'robot'-like formant-based speech synthesizer.`,
|
|
1720
1842
|
type: 'local'
|
|
1721
1843
|
},
|
|
1722
1844
|
{
|
|
1723
1845
|
id: 'sam',
|
|
1724
1846
|
name: 'SAM (Software Automatic Mouth)',
|
|
1725
|
-
description: `A classic 'robot' speech synthesizer from 1982.`,
|
|
1847
|
+
description: `A classic 'robot'-like speech synthesizer from 1982.`,
|
|
1726
1848
|
type: 'local'
|
|
1727
1849
|
},
|
|
1728
1850
|
{
|
|
@@ -792,7 +792,7 @@ export class Whisper {
|
|
|
792
792
|
let decodedTokensCrossAttentionQKs: OnnxLikeFloat32Tensor[] = []
|
|
793
793
|
|
|
794
794
|
for (let i = 0; i < decodedTokens.length; i++) {
|
|
795
|
-
decodedTokensTimestampLogits.push(new Array(1501))
|
|
795
|
+
decodedTokensTimestampLogits.push(new Array(1501)) // Should the length be 1500 instead?
|
|
796
796
|
decodedTokensConfidence.push(1.0)
|
|
797
797
|
decodedTokensCrossAttentionQKs.push(undefined as any)
|
|
798
798
|
}
|
|
@@ -1569,7 +1569,7 @@ export class Whisper {
|
|
|
1569
1569
|
|
|
1570
1570
|
const timestampTokensCount = 1501
|
|
1571
1571
|
|
|
1572
|
-
for (let i = 0; i
|
|
1572
|
+
for (let i = 0; i <= timestampTokensCount; i++) {
|
|
1573
1573
|
const tokenIndex = this.tokenConfig.timestampTokensStart + i
|
|
1574
1574
|
const tokenTime = this.timestampTokenToSeconds(tokenIndex)
|
|
1575
1575
|
|
|
@@ -1727,11 +1727,15 @@ export class Whisper {
|
|
|
1727
1727
|
throw new Error(`Invalid timestamp token: ${timestampToken}`)
|
|
1728
1728
|
}
|
|
1729
1729
|
|
|
1730
|
-
|
|
1730
|
+
let seconds = (timestampToken - this.tokenConfig.timestampTokensStart) * 0.02
|
|
1731
|
+
seconds = clip(seconds, 0.0, 30.0)
|
|
1732
|
+
|
|
1733
|
+
return seconds
|
|
1731
1734
|
}
|
|
1732
1735
|
|
|
1733
1736
|
isValidToken(token: number) {
|
|
1734
|
-
return token < this.tokenConfig.timestampTokensEnd
|
|
1737
|
+
//return token < this.tokenConfig.timestampTokensEnd
|
|
1738
|
+
return token <= this.tokenConfig.timestampTokensEnd
|
|
1735
1739
|
}
|
|
1736
1740
|
|
|
1737
1741
|
assertIsValidToken(token: number) {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defaultGnuSpeechOptions, GnuSpeechOptions, synthesize as gnuSpeechSynthesize } from '@echogarden/gnuspeech-wasm'
|
|
2
|
+
import { SynthesisVoice } from '../api/Synthesis.js'
|
|
3
|
+
import { decodeWaveToRawAudio } from '../audio/AudioUtilities.js'
|
|
4
|
+
import { extendDeep } from '../utilities/ObjectUtilities.js'
|
|
5
|
+
|
|
6
|
+
export async function synthesize(text: string, options: GnuSpeechOptions) {
|
|
7
|
+
const gnuSpeechOptions = extendDeep(defaultGnuSpeechOptions, options)
|
|
8
|
+
|
|
9
|
+
const { audioData, params } = await gnuSpeechSynthesize(text, gnuSpeechOptions)
|
|
10
|
+
|
|
11
|
+
return decodeWaveToRawAudio(audioData)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const voiceList: SynthesisVoice[] = [
|
|
15
|
+
{
|
|
16
|
+
name: 'male',
|
|
17
|
+
languages: ['en-US', 'en'],
|
|
18
|
+
gender: 'male',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'female',
|
|
22
|
+
languages: ['en-US', 'en'],
|
|
23
|
+
gender: 'female',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'large_child',
|
|
27
|
+
languages: ['en-US', 'en'],
|
|
28
|
+
gender: 'unknown',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'small_child',
|
|
32
|
+
languages: ['en-US', 'en'],
|
|
33
|
+
gender: 'unknown',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'baby',
|
|
37
|
+
languages: ['en-US', 'en'],
|
|
38
|
+
gender: 'unknown',
|
|
39
|
+
},
|
|
40
|
+
]
|