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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/data/lexicons/heteronyms.en.json +45 -3
  3. package/data/schemas/options.json +64 -0
  4. package/dist/api/Synthesis.d.ts +10 -1
  5. package/dist/api/Synthesis.js +76 -9
  6. package/dist/api/Synthesis.js.map +1 -1
  7. package/dist/recognition/WhisperSTT.js +7 -4
  8. package/dist/recognition/WhisperSTT.js.map +1 -1
  9. package/dist/synthesis/GnuSpeechTTS.d.ts +12 -0
  10. package/dist/synthesis/GnuSpeechTTS.js +36 -0
  11. package/dist/synthesis/GnuSpeechTTS.js.map +1 -0
  12. package/dist/synthesis/KokoroTTS.d.ts +22 -0
  13. package/dist/synthesis/KokoroTTS.js +595 -0
  14. package/dist/synthesis/KokoroTTS.js.map +1 -0
  15. package/dist/synthesis/OpenAICloudTTS.js +15 -0
  16. package/dist/synthesis/OpenAICloudTTS.js.map +1 -1
  17. package/dist/synthesis/VitsTTS.js +6 -1
  18. package/dist/synthesis/VitsTTS.js.map +1 -1
  19. package/dist/utilities/FileReader.js +1 -1
  20. package/dist/utilities/PackageManager.js +4 -0
  21. package/dist/utilities/PackageManager.js.map +1 -1
  22. package/dist/utilities/Utilities.d.ts +1 -0
  23. package/dist/utilities/Utilities.js +8 -0
  24. package/dist/utilities/Utilities.js.map +1 -1
  25. package/dist/utilities/WasmMemoryManager.d.ts +1 -1
  26. package/docs/Development.md +1 -1
  27. package/docs/Engines.md +4 -1
  28. package/docs/Licenses.md +2 -1
  29. package/docs/Options.md +9 -5
  30. package/docs/Tasklist.md +1 -1
  31. package/package.json +14 -13
  32. package/src/api/Synthesis.ts +132 -10
  33. package/src/recognition/WhisperSTT.ts +8 -4
  34. package/src/synthesis/GnuSpeechTTS.ts +40 -0
  35. package/src/synthesis/KokoroTTS.ts +693 -0
  36. package/src/synthesis/OpenAICloudTTS.ts +15 -0
  37. package/src/synthesis/VitsTTS.ts +6 -1
  38. package/src/utilities/FileReader.ts +1 -1
  39. package/src/utilities/PackageManager.ts +5 -0
  40. package/src/utilities/Utilities.ts +10 -0
@@ -159,10 +159,10 @@ async function synthesizeSegments(segments: string[], options: SynthesisOptions,
159
159
 
160
160
  const sentenceStartTime = timeOffset
161
161
 
162
- let sentencetSynthesisOptions: SynthesisOptions = { postProcessing: { normalizeAudio: false } }
163
- sentencetSynthesisOptions = extendDeep(options, sentencetSynthesisOptions)
162
+ let sentenceSynthesisOptions: SynthesisOptions = { postProcessing: { normalizeAudio: false } }
163
+ sentenceSynthesisOptions = extendDeep(options, sentenceSynthesisOptions)
164
164
 
165
- const { synthesizedAudio: sentenceRawAudio, timeline: sentenceTimeline } = await synthesizeSegment(sentenceText, sentencetSynthesisOptions)
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('Segment synthesis time', startTimestamp, chalk.magentaBright)
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 = 'vits' | 'pico' | 'flite' | 'espeak' | 'sam' | 'sapi' | 'msspeech' | 'coqui-server' | 'google-cloud' | 'microsoft-azure' | 'amazon-polly' | 'openai-cloud' | 'elevenlabs' | 'google-translate' | 'microsoft-edge' | 'streamlabs-polly'
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 synthesis engine.',
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 synthesis engine.',
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' sounding formant-based synthesizer.`,
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 < timestampTokensCount; 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
- return (timestampToken - this.tokenConfig.timestampTokensStart) * 0.02
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
+ ]