echogarden 2.1.2 → 2.2.1

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 (50) hide show
  1. package/README.md +6 -1
  2. package/data/lexicons/heteronyms.en.json +31 -6
  3. package/data/lexicons/words.en.json +60 -0
  4. package/data/schemas/options.json +20 -4
  5. package/dist/api/Recognition.d.ts +3 -1
  6. package/dist/api/Recognition.js +17 -0
  7. package/dist/api/Recognition.js.map +1 -1
  8. package/dist/api/Synthesis.d.ts +2 -2
  9. package/dist/api/Synthesis.js +3 -3
  10. package/dist/api/Synthesis.js.map +1 -1
  11. package/dist/codecs/FFMpegTranscoder.js +2 -0
  12. package/dist/codecs/FFMpegTranscoder.js.map +1 -1
  13. package/dist/nlp/EspeakPhonemizer.d.ts +2 -2
  14. package/dist/nlp/EspeakPhonemizer.js +36 -36
  15. package/dist/nlp/PhoneConversion.d.ts +1 -0
  16. package/dist/nlp/PhoneConversion.js +151 -222
  17. package/dist/nlp/PhoneConversion.js.map +1 -1
  18. package/dist/recognition/DeepgramSTT.d.ts +11 -0
  19. package/dist/recognition/DeepgramSTT.js +64 -0
  20. package/dist/recognition/DeepgramSTT.js.map +1 -0
  21. package/dist/recognition/GoogleCloudSTT.js.map +1 -1
  22. package/dist/synthesis/{ElevenlabsTTS.d.ts → ElevenLabsTTS.d.ts} +3 -3
  23. package/dist/synthesis/{ElevenlabsTTS.js → ElevenLabsTTS.js} +3 -3
  24. package/dist/synthesis/{ElevenlabsTTS.js.map → ElevenLabsTTS.js.map} +1 -1
  25. package/dist/synthesis/EspeakTTS.d.ts +1 -1
  26. package/dist/synthesis/EspeakTTS.js +11 -7
  27. package/dist/synthesis/EspeakTTS.js.map +1 -1
  28. package/dist/synthesis/KokoroTTS.js +14 -14
  29. package/dist/synthesis/VitsTTS.js +9 -9
  30. package/dist/utilities/Utilities.d.ts +1 -0
  31. package/dist/utilities/Utilities.js +5 -0
  32. package/dist/utilities/Utilities.js.map +1 -1
  33. package/dist/utilities/WasmMemoryManager.d.ts +1 -1
  34. package/docs/CUDA.md +6 -6
  35. package/docs/Development.md +1 -1
  36. package/docs/Engines.md +1 -0
  37. package/docs/Options.md +5 -1
  38. package/package.json +6 -6
  39. package/src/api/Recognition.ts +29 -1
  40. package/src/api/Synthesis.ts +7 -7
  41. package/src/codecs/FFMpegTranscoder.ts +2 -0
  42. package/src/nlp/EspeakPhonemizer.ts +36 -36
  43. package/src/nlp/PhoneConversion.ts +176 -225
  44. package/src/recognition/DeepgramSTT.ts +136 -0
  45. package/src/recognition/GoogleCloudSTT.ts +0 -1
  46. package/src/synthesis/{ElevenlabsTTS.ts → ElevenLabsTTS.ts} +4 -4
  47. package/src/synthesis/EspeakTTS.ts +12 -7
  48. package/src/synthesis/KokoroTTS.ts +15 -15
  49. package/src/synthesis/VitsTTS.ts +9 -9
  50. package/src/utilities/Utilities.ts +6 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echogarden",
3
- "version": "2.1.2",
3
+ "version": "2.2.1",
4
4
  "description": "An easy-to-use speech toolset. Includes tools for synthesis, recognition, alignment, speech translation, language detection, source separation and more.",
5
5
  "author": "Rotem Dan",
6
6
  "license": "GPL-3.0",
@@ -52,8 +52,8 @@
52
52
  "echogarden": "./dist/cli/CLILauncher.js"
53
53
  },
54
54
  "dependencies": {
55
- "@aws-sdk/client-polly": "^3.747.0",
56
- "@aws-sdk/client-transcribe-streaming": "^3.744.0",
55
+ "@aws-sdk/client-polly": "^3.750.0",
56
+ "@aws-sdk/client-transcribe-streaming": "^3.750.0",
57
57
  "@echogarden/audio-io": "^0.3.0",
58
58
  "@echogarden/espeak-ng-emscripten": "^0.3.3",
59
59
  "@echogarden/fasttext-wasm": "^0.1.0",
@@ -85,14 +85,14 @@
85
85
  "microsoft-cognitiveservices-speech-sdk": "^1.42.0",
86
86
  "msgpack-lite": "^0.1.26",
87
87
  "onnxruntime-node": "^1.20.1",
88
- "openai": "^4.85.1",
88
+ "openai": "^4.85.4",
89
89
  "sam-js": "^0.3.1",
90
90
  "strip-ansi": "^7.1.0",
91
91
  "tar": "^7.4.3",
92
92
  "tiktoken": "^1.0.20",
93
93
  "tinyld": "^1.3.4",
94
94
  "wasm-feature-detect": "^1.8.0",
95
- "ws": "^8.18.0",
95
+ "ws": "^8.18.1",
96
96
  "wtf_wikipedia": "^10.4.0"
97
97
  },
98
98
  "peerDependencies": {
@@ -112,7 +112,7 @@
112
112
  "@types/graceful-fs": "^4.1.9",
113
113
  "@types/jsdom": "^21.1.7",
114
114
  "@types/msgpack-lite": "^0.1.11",
115
- "@types/node": "^22.13.4",
115
+ "@types/node": "^22.13.5",
116
116
  "@types/tar": "^6.1.13",
117
117
  "@types/ws": "^8.5.14",
118
118
  "ts-json-schema-generator": "^2.3.0",
@@ -15,6 +15,7 @@ import { type SubtitlesConfig } from '../subtitles/Subtitles.js'
15
15
  import { type OpenAICloudSTTOptions } from '../recognition/OpenAICloudSTT.js'
16
16
  import { type WhisperCppOptions } from '../recognition/WhisperCppSTT.js'
17
17
  import { type SileroRecognitionOptions } from '../recognition/SileroSTT.js'
18
+ import { type DeepgramSTTOptions } from '../recognition/DeepgramSTT.js'
18
19
  import { OnnxExecutionProvider } from '../utilities/OnnxUtilities.js'
19
20
 
20
21
  const log = logToStderr
@@ -274,6 +275,22 @@ export async function recognize(input: AudioSourceParam, options: RecognitionOpt
274
275
  break
275
276
  }
276
277
 
278
+ case 'deepgram': {
279
+ const DeepgramSTT = await import('../recognition/DeepgramSTT.js')
280
+
281
+ const deepgramOptions = options.deepgram!
282
+
283
+ if (!deepgramOptions.apiKey) {
284
+ throw new Error(`No Deepgram API key provided`)
285
+ }
286
+
287
+ logger.end();
288
+
289
+ ({ transcript, timeline } = await DeepgramSTT.recognize(sourceRawAudio, options.language ? shortLanguageCode : undefined, deepgramOptions))
290
+
291
+ break
292
+ }
293
+
277
294
  default: {
278
295
  throw new Error(`Engine '${options.engine}' is not supported`)
279
296
  }
@@ -330,7 +347,7 @@ export interface RecognitionResult {
330
347
  backgroundRawAudio?: RawAudio
331
348
  }
332
349
 
333
- export type RecognitionEngine = 'whisper' | 'whisper.cpp' | 'vosk' | 'silero' | 'google-cloud' | 'microsoft-azure' | 'amazon-transcribe' | 'openai-cloud'
350
+ export type RecognitionEngine = 'whisper' | 'whisper.cpp' | 'vosk' | 'silero' | 'google-cloud' | 'microsoft-azure' | 'amazon-transcribe' | 'openai-cloud' | 'deepgram'
334
351
 
335
352
  export interface RecognitionOptions {
336
353
  engine?: RecognitionEngine
@@ -383,6 +400,8 @@ export interface RecognitionOptions {
383
400
  }
384
401
 
385
402
  openAICloud?: OpenAICloudSTTOptions
403
+
404
+ deepgram?: DeepgramSTTOptions
386
405
  }
387
406
 
388
407
  export const defaultRecognitionOptions: RecognitionOptions = {
@@ -443,6 +462,9 @@ export const defaultRecognitionOptions: RecognitionOptions = {
443
462
 
444
463
  openAICloud: {
445
464
  },
465
+
466
+ deepgram: {
467
+ }
446
468
  }
447
469
 
448
470
  export const recognitionEngines: API.EngineMetadata[] = [
@@ -494,4 +516,10 @@ export const recognitionEngines: API.EngineMetadata[] = [
494
516
  description: 'OpenAI cloud speech-to-text service.',
495
517
  type: 'cloud'
496
518
  },
519
+ {
520
+ id: 'deepgram',
521
+ name: 'Deepgram',
522
+ description: 'Deepgram cloud speech-to-text service.',
523
+ type: 'cloud'
524
+ },
497
525
  ]
@@ -20,8 +20,8 @@ import { shouldCancelCurrentTask } from '../server/Worker.js'
20
20
  import chalk from 'chalk'
21
21
  import { type SubtitlesConfig } from '../subtitles/Subtitles.js'
22
22
  import { type EspeakOptions } from '../synthesis/EspeakTTS.js'
23
- import { type OpenAICloudTTSOptions } from '../synthesis/OpenAICloudTTS.js'
24
- import { type ElevenlabsTTSOptions } from '../synthesis/ElevenlabsTTS.js'
23
+ import { type OpenAICloudTTSOptions } from '../synthesis/OpenAICloudTTS.js'
24
+ import { type ElevenLabsTTSOptions } from '../synthesis/ElevenLabsTTS.js'
25
25
  import { OnnxExecutionProvider } from '../utilities/OnnxUtilities.js'
26
26
  import { simplifyPunctuationCharacters } from '../nlp/TextNormalizer.js'
27
27
  import { convertHtmlToText } from '../utilities/StringUtilities.js'
@@ -63,7 +63,7 @@ async function synthesizeSegments(segments: string[], options: SynthesisOptions,
63
63
  let segmentsPlainText = segments
64
64
 
65
65
  if (options.ssml) {
66
- segmentsPlainText= []
66
+ segmentsPlainText = []
67
67
 
68
68
  for (const segment of segments) {
69
69
  segmentsPlainText.push(await convertHtmlToText(segment))
@@ -811,7 +811,7 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
811
811
  throw new Error(`The Elevenlabs engine doesn't support SSML inputs`)
812
812
  }
813
813
 
814
- const ElevenLabsTTS = await import('../synthesis/ElevenlabsTTS.js')
814
+ const ElevenLabsTTS = await import('../synthesis/ElevenLabsTTS.js')
815
815
 
816
816
  const engineOptions = options.elevenlabs!
817
817
 
@@ -1198,7 +1198,7 @@ export interface SynthesisOptions {
1198
1198
 
1199
1199
  openAICloud?: OpenAICloudTTSOptions
1200
1200
 
1201
- elevenlabs?: ElevenlabsTTSOptions,
1201
+ elevenlabs?: ElevenLabsTTSOptions,
1202
1202
 
1203
1203
  googleTranslate?: {
1204
1204
  tld?: string
@@ -1593,14 +1593,14 @@ export async function requestVoiceList(options: VoiceListRequestOptions): Promis
1593
1593
  }
1594
1594
 
1595
1595
  case 'elevenlabs': {
1596
- const ElevenLabsTTS = await import('../synthesis/ElevenlabsTTS.js')
1596
+ const ElevenLabsTTS = await import('../synthesis/ElevenLabsTTS.js')
1597
1597
 
1598
1598
  const engineOptions = options.elevenlabs!
1599
1599
 
1600
1600
  const apiKey = engineOptions.apiKey
1601
1601
 
1602
1602
  if (!apiKey) {
1603
- throw new Error(`No Elevenlabs API key provided`)
1603
+ throw new Error(`No ElevenLabs API key provided`)
1604
1604
  }
1605
1605
 
1606
1606
  voiceList = await ElevenLabsTTS.getVoiceList(apiKey)
@@ -223,6 +223,7 @@ export function getDefaultFFMpegOptionsForSpeech(fileExtension: string, customBi
223
223
  }
224
224
  } else if (fileExtension == 'opus') {
225
225
  ffmpegOptions = {
226
+ format: 'ogg',
226
227
  codec: 'libopus',
227
228
  bitrate: 48,
228
229
  customOptions: []
@@ -236,6 +237,7 @@ export function getDefaultFFMpegOptionsForSpeech(fileExtension: string, customBi
236
237
  }
237
238
  } else if (fileExtension == 'ogg') {
238
239
  ffmpegOptions = {
240
+ format: 'ogg',
239
241
  codec: 'libvorbis',
240
242
  bitrate: 48,
241
243
  customOptions: []
@@ -7,14 +7,14 @@ const log = logToStderr
7
7
  export async function phonemizeSentence(sentence: string, espeakVoice: string, substitutionMap?: Map<string, string[]>, useIpa = true) {
8
8
  const ipaString = await EspeakTTS.textToPhonemes(sentence, espeakVoice, useIpa)
9
9
 
10
- const clauseStrings = ipaString.split(' | ')
10
+ const phraseStrings = ipaString.split(' | ')
11
11
 
12
- const clauses: string[][][] = []
12
+ const phrases: string[][][] = []
13
13
 
14
- for (let clauseIndex = 0; clauseIndex < clauseStrings.length; clauseIndex++) {
15
- const clauseString = clauseStrings[clauseIndex]
14
+ for (let phraseIndex = 0; phraseIndex < phraseStrings.length; phraseIndex++) {
15
+ const phraseString = phraseStrings[phraseIndex]
16
16
 
17
- const wordStrings = clauseString.trim().split(/ +/g)
17
+ const wordStrings = phraseString.trim().split(/ +/g)
18
18
  const words: string[][] = []
19
19
 
20
20
  for (let wordIndex = 0; wordIndex < wordStrings.length; wordIndex++) {
@@ -40,11 +40,11 @@ export async function phonemizeSentence(sentence: string, espeakVoice: string, s
40
40
  }
41
41
 
42
42
  if (words.length > 0) {
43
- clauses.push(words)
43
+ phrases.push(words)
44
44
  }
45
45
  }
46
46
 
47
- return clauses
47
+ return phrases
48
48
  }
49
49
 
50
50
  export async function phonemizeText(text: string, voice: string, substitutionMap?: Map<string, string[]>) {
@@ -58,46 +58,46 @@ export async function phonemizeText(text: string, voice: string, substitutionMap
58
58
  .replaceAll('»', ', ')
59
59
 
60
60
  const segmentedText = await Segmentation.parse(text, voice)
61
- const preparedClauses: string[] = []
62
- const clauseBreakers: string[] = []
61
+ const preparedPhrases: string[] = []
62
+ const phraseBreakers: string[] = []
63
63
 
64
64
  for (const sentence of segmentedText) {
65
- for (const clause of sentence.phrases) {
66
- const words = clause.words.filter(wordObject => Segmentation.isWordOrSymbolWord(wordObject.text))
67
- const preparedClauseText = words.map(word => word.text.replace(/\./g, ' ')).join(' ')
65
+ for (const phrase of sentence.phrases) {
66
+ const words = phrase.words.filter(wordObject => Segmentation.isWordOrSymbolWord(wordObject.text))
67
+ const preparedPhraseText = words.map(word => word.text.replace(/\./g, ' ')).join(' ')
68
68
 
69
- preparedClauses.push(preparedClauseText)
69
+ preparedPhrases.push(preparedPhraseText)
70
70
 
71
- const trimmedClauseText = clause.text.trim()
72
- const lastChar = trimmedClauseText[trimmedClauseText.length - 1]
71
+ const trimmedPhraseText = phrase.text.trim()
72
+ const lastChar = trimmedPhraseText[trimmedPhraseText.length - 1]
73
73
 
74
- if (clause.isSentenceFinalizer) {
75
- if (trimmedClauseText.endsWith('?') || trimmedClauseText.endsWith(`?"`)) {
76
- clauseBreakers.push('?')
77
- } else if (trimmedClauseText.endsWith('!') || trimmedClauseText.endsWith(`!"`)) {
78
- clauseBreakers.push('!')
74
+ if (phrase.isSentenceFinalizer) {
75
+ if (trimmedPhraseText.endsWith('?') || trimmedPhraseText.endsWith(`?"`)) {
76
+ phraseBreakers.push('?')
77
+ } else if (trimmedPhraseText.endsWith('!') || trimmedPhraseText.endsWith(`!"`)) {
78
+ phraseBreakers.push('!')
79
79
  } else {
80
- clauseBreakers.push('.')
80
+ phraseBreakers.push('.')
81
81
  }
82
82
  } else {
83
83
  if (lastChar == ':' || lastChar == ';') {
84
- clauseBreakers.push(lastChar)
84
+ phraseBreakers.push(lastChar)
85
85
  } else {
86
- clauseBreakers.push(',')
86
+ phraseBreakers.push(',')
87
87
  }
88
88
  }
89
89
  }
90
90
  }
91
91
 
92
- return phonemizeClauses(preparedClauses, voice, clauseBreakers, substitutionMap)
92
+ return phonemizePhrases(preparedPhrases, voice, phraseBreakers, substitutionMap)
93
93
  }
94
94
 
95
- export async function phonemizeClauses(clauses: string[], voice: string, clauseBreakers: string[], substitutionMap?: Map<string, string[]>) {
96
- if (clauses.length == 0) {
95
+ export async function phonemizePhrases(phrases: string[], voice: string, phraseBreakers: string[], substitutionMap?: Map<string, string[]>) {
96
+ if (phrases.length == 0) {
97
97
  return []
98
98
  }
99
99
 
100
- const preparedText = clauses.join('\n\n') // filter(clause => clause.trim().length > 0)
100
+ const preparedText = phrases.join('\n\n') // filter(phrase => phrase.trim().length > 0)
101
101
 
102
102
  const ipaString = await EspeakTTS.textToIPA(preparedText, voice)
103
103
 
@@ -130,31 +130,31 @@ export async function phonemizeClauses(clauses: string[], voice: string, clauseB
130
130
  })
131
131
  })
132
132
 
133
- if (ipaLines.length != clauseBreakers.length) {
134
- log(clauses)
133
+ if (ipaLines.length != phraseBreakers.length) {
134
+ log(phrases)
135
135
  log(ipaLines)
136
- log(clauseBreakers)
136
+ log(phraseBreakers)
137
137
 
138
- throw new Error(`Unexpected: IPA lines count (${ipaLines.length}) is not equal to clause breakers count (${clauseBreakers.length})`)
138
+ throw new Error(`Unexpected: IPA lines count (${ipaLines.length}) is not equal to phrase breakers count (${phraseBreakers.length})`)
139
139
  }
140
140
 
141
141
  for (let i = 0; i < phonemeLines.length; i++) {
142
142
  const line = phonemeLines[i]
143
143
  const lastWordInLine = line[line.length - 1]
144
144
 
145
- lastWordInLine.push(clauseBreakers[i])
145
+ lastWordInLine.push(phraseBreakers[i])
146
146
  }
147
147
 
148
148
  return phonemeLines
149
149
  }
150
150
 
151
- export function phonemizedClausesToSentences(phonemizedClauses: string[][][]) {
151
+ export function phonemizedPhrasesToSentences(phonemizedPhrases: string[][][]) {
152
152
  let phonemizedSentences: string[][][] = [[]]
153
153
 
154
- for (const phonemizedClause of phonemizedClauses) {
155
- phonemizedSentences[phonemizedSentences.length - 1].push(...phonemizedClause)
154
+ for (const phonemizedPhrase of phonemizedPhrases) {
155
+ phonemizedSentences[phonemizedSentences.length - 1].push(...phonemizedPhrase)
156
156
 
157
- const lastWord = phonemizedClause[phonemizedClause.length - 1]
157
+ const lastWord = phonemizedPhrase[phonemizedPhrase.length - 1]
158
158
  const lastPhoneme = lastWord[lastWord.length - 1]
159
159
 
160
160
  if (['.', '?', '!'].includes(lastPhoneme)) {