echogarden 2.3.5 → 2.5.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/docs/Options.md CHANGED
@@ -101,7 +101,8 @@ Applies to CLI operations: `speak`, `speak-file`, `speak-url`, `speak-wikipedia`
101
101
  * `openAICloud.apiKey`: API key (required)
102
102
  * `openAICloud.organization`: organization identifier. Optional
103
103
  * `openAICloud.baseURL`: override the default base URL for the API. Optional
104
- * `openAICloud.model`: model to use. Can be either `tts-1` or `tts-1-hd`. Defaults to `tts-1`
104
+ * `openAICloud.model`: model to use. Can be either `tts-1`, `tts-1-hd` or `gpt-4o-mini-tts`. Defaults to `tts-1`
105
+ * `openAICloud.instructions`: instructions for model. Only works with `gpt-4o-mini-tts`. Optional
105
106
  * `openAICloud.timeout`: request timeout. Optional
106
107
  * `openAICloud.maxRetries`: maximum retries on failure. Defaults to 10
107
108
 
@@ -213,7 +214,7 @@ Applies to CLI operation: `transcribe`, API method: `recognize`
213
214
 
214
215
  **OpenAI Cloud**:
215
216
  * `openAICloud.apiKey`: API key (required)
216
- * `openAICloud.model`: model to use. When using the default provider (OpenAI), can only be `whisper-1`. For a custom provider, like Groq, see its documentation
217
+ * `openAICloud.model`: model to use. When using the default provider (OpenAI), can be `whisper-1`, `gpt-4o-mini-transcribe` or `gpt-4o-transcribe`. For a custom provider, like Groq, see its documentation
217
218
  * `openAICloud.organization`: organization identifier. Optional
218
219
  * `openAICloud.baseURL`: override the default endpoint used by the API. For example, set `https://api.groq.com/openai/v1` to use Groq's OpenAI-compatible API instead of the default one. Optional
219
220
  * `openAICloud.temperature`: temperature. Choosing `0` uses a dynamic temperature approach. Defaults to `0`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "echogarden",
3
- "version": "2.3.5",
3
+ "version": "2.5.0",
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.758.0",
56
- "@aws-sdk/client-transcribe-streaming": "^3.758.0",
55
+ "@aws-sdk/client-polly": "^3.777.0",
56
+ "@aws-sdk/client-transcribe-streaming": "^3.777.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",
@@ -67,7 +67,7 @@
67
67
  "@echogarden/speex-resampler-wasm": "^0.3.0",
68
68
  "@echogarden/svoxpico-wasm": "^0.2.0",
69
69
  "@echogarden/transformers-nodejs-lite": "^2.17.1-lite.3",
70
- "@mozilla/readability": "^0.5.0",
70
+ "@mozilla/readability": "^0.6.0",
71
71
  "alawmulaw": "^6.0.0",
72
72
  "chalk": "^5.4.1",
73
73
  "cldr-segmentation": "^2.2.1",
@@ -82,10 +82,10 @@
82
82
  "jsdom": "^26.0.0",
83
83
  "json5": "^2.2.3",
84
84
  "kuromoji": "^0.1.2",
85
- "microsoft-cognitiveservices-speech-sdk": "^1.42.0",
85
+ "microsoft-cognitiveservices-speech-sdk": "^1.43.0",
86
86
  "msgpack-lite": "^0.1.26",
87
- "onnxruntime-node": "^1.20.1",
88
- "openai": "^4.86.1",
87
+ "onnxruntime-node": "^1.21.0",
88
+ "openai": "^4.90.0",
89
89
  "sam-js": "^0.3.1",
90
90
  "strip-ansi": "^7.1.0",
91
91
  "tar": "^7.4.3",
@@ -112,9 +112,9 @@
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.9",
115
+ "@types/node": "^22.13.14",
116
116
  "@types/tar": "^6.1.13",
117
- "@types/ws": "^8.5.14",
117
+ "@types/ws": "^8.18.0",
118
118
  "ts-json-schema-generator": "^2.3.0",
119
119
  "typescript": "^5.8.2"
120
120
  },
@@ -701,8 +701,7 @@ async function synthesizeSegment(text: string, options: SynthesisOptions) {
701
701
 
702
702
  logger.end()
703
703
 
704
- const { audioData, timepoints } = await GoogleCloudTTS.synthesize(text, apiKey, language, voice, speed, pitchDeltaSemitones, 0, inputIsSSML)
705
- const rawAudio = await FFMpegTranscoder.decodeToChannels(audioData)
704
+ const { rawAudio, timepoints } = await GoogleCloudTTS.synthesize(text, apiKey, language, voice, speed, pitchDeltaSemitones, 0, inputIsSSML)
706
705
 
707
706
  synthesizedAudio = rawAudio
708
707
 
package/src/cli/CLI.ts CHANGED
@@ -1698,21 +1698,27 @@ async function writeSourceSeparationOutputIfNeeded(outputFilename: string, isola
1698
1698
  {
1699
1699
  const isolatedOutputFilePath = prefixIsolated ? `${pathWithoutExtension}.isolated.${fileExtension}` : outputFilename
1700
1700
 
1701
- const fileSaver = getFileSaver(isolatedOutputFilePath, allowOverwrite)
1702
-
1703
- await fileSaver(isolatedRawAudio, [], '')
1701
+ await saveAudioWithBitrate(isolatedRawAudio, isolatedOutputFilePath, fileExtension, 64)
1704
1702
  }
1705
1703
 
1706
1704
  {
1707
1705
  const backgroundOutputFilePath = `${pathWithoutExtension}.background.${fileExtension}`
1708
1706
 
1709
- const fileSaver = getFileSaver(backgroundOutputFilePath, allowOverwrite)
1710
-
1711
- await fileSaver(backgroundRawAudio, [], '')
1707
+ await saveAudioWithBitrate(backgroundRawAudio, backgroundOutputFilePath, fileExtension, 128)
1712
1708
  }
1713
1709
  }
1714
1710
  }
1715
1711
 
1712
+ async function saveAudioWithBitrate(rawAudio: RawAudio, filepath: string, fileExtension: string, bitrate: number) {
1713
+ const ffmpegOptions = getDefaultFFMpegOptionsForSpeech(fileExtension, bitrate)
1714
+ ffmpegOptions.filename = filepath
1715
+
1716
+ const fileDir = parsePath(filepath).dir || './'
1717
+
1718
+ await ensureDir(fileDir)
1719
+ await encodeFromChannels(rawAudio, ffmpegOptions)
1720
+ }
1721
+
1716
1722
  async function optionsLookupToTypedObject<K extends keyof APIOptions>(cliOptionsMap: Map<string, string>, optionsRoot: K, additionalOptionsSchema?: Map<string, SchemaTypeDefinition>): Promise<APIOptions[K]> {
1717
1723
  const optionsSchema = await getOptionsSchema()
1718
1724
  const resultingObj: any = {}
@@ -35,6 +35,14 @@ export async function recognize(rawAudio: RawAudio, languageCode: string, option
35
35
 
36
36
  logger.start(options.baseURL ? `Send request to ${options.baseURL}` : 'Send request to OpenAI Cloud API')
37
37
 
38
+ let responseFormat: 'verbose_json' | 'json'
39
+
40
+ if (options.model === 'gpt-4o-mini-transcribe' || options.model === 'gpt-4o-transcribe') {
41
+ responseFormat = 'json'
42
+ } else {
43
+ responseFormat = 'verbose_json'
44
+ }
45
+
38
46
  let response: VerboseResponse
39
47
 
40
48
  if (task == 'transcribe') {
@@ -46,7 +54,7 @@ export async function recognize(rawAudio: RawAudio, languageCode: string, option
46
54
  model: options.model,
47
55
  language: languageCode,
48
56
  prompt: options.prompt,
49
- response_format: 'verbose_json',
57
+ response_format: responseFormat,
50
58
  temperature: options.temperature,
51
59
  timestamp_granularities,
52
60
  }) as any as VerboseResponse
@@ -55,7 +63,7 @@ export async function recognize(rawAudio: RawAudio, languageCode: string, option
55
63
  file: virtualFileStream,
56
64
  model: options.model,
57
65
  prompt: options.prompt,
58
- response_format: 'verbose_json',
66
+ response_format: responseFormat,
59
67
  temperature: options.temperature,
60
68
  }) as any as VerboseResponse
61
69
  } else {
@@ -64,7 +72,7 @@ export async function recognize(rawAudio: RawAudio, languageCode: string, option
64
72
 
65
73
  const transcript = response.text.trim()
66
74
 
67
- let timeline: Timeline
75
+ let timeline: Timeline | undefined
68
76
 
69
77
  if (response.words) {
70
78
  timeline = response.words.map<TimelineEntry>(entry => ({
@@ -73,7 +81,7 @@ export async function recognize(rawAudio: RawAudio, languageCode: string, option
73
81
  startTime: entry.start,
74
82
  endTime: entry.end
75
83
  }))
76
- } else {
84
+ } else if (response.segments) {
77
85
  const segmentTimeline = response.segments.map<TimelineEntry>(entry => ({
78
86
  type: 'segment',
79
87
  text: entry.text,
@@ -129,7 +137,7 @@ interface VerboseResponse {
129
137
  type Task = 'transcribe' | 'translate'
130
138
 
131
139
  export interface OpenAICloudSTTOptions {
132
- model?: 'whisper-1' | string
140
+ model?: 'whisper-1' | 'gpt-4o-mini-transcribe' | 'gpt-4o-transcribe' | string
133
141
 
134
142
  apiKey?: string
135
143
  organization?: string
@@ -2,6 +2,7 @@ import { request } from 'gaxios'
2
2
  import { Logger } from '../utilities/Logger.js'
3
3
  import { logToStderr } from '../utilities/Utilities.js'
4
4
  import { decodeBase64 } from '../encodings/Base64.js'
5
+ import * as FFMpegTranscoder from '../codecs/FFMpegTranscoder.js'
5
6
 
6
7
  const log = logToStderr
7
8
 
@@ -14,7 +15,7 @@ export async function synthesize(
14
15
  pitchDeltaSemitones = 0.0,
15
16
  volumeGainDecibels = 0.0,
16
17
  ssml = false,
17
- audioEncoding: AudioEncoding = 'MP3_64_KBPS',
18
+ audioEncoding: AudioEncoding = 'MP3',
18
19
  sampleRate = 24000) {
19
20
 
20
21
  const logger = new Logger()
@@ -70,9 +71,14 @@ export async function synthesize(
70
71
 
71
72
  const result = parseResponseBody(response.data)
72
73
 
74
+ logger.start('Decode to raw audio')
75
+
76
+ const rawAudio = await FFMpegTranscoder.decodeToChannels(result.audioData)
77
+ const timepoints = result.timepoints
78
+
73
79
  logger.end()
74
80
 
75
- return result
81
+ return { rawAudio, timepoints }
76
82
  }
77
83
 
78
84
  function parseResponseBody(responseBody: any) {
@@ -116,7 +122,13 @@ export type GoogleCloudVoice = {
116
122
  naturalSampleRateHertz: number
117
123
  }
118
124
 
119
- export type AudioEncoding = 'LINEAR16' | 'MP3' | 'MP3_64_KBPS' | 'OGG_OPUS' | 'MULAW' | 'ALAW'
125
+ export type AudioEncoding =
126
+ 'AUDIO_ENCODING_UNSPECIFIED' |
127
+ 'LINEAR16' |
128
+ 'MP3' |
129
+ 'OGG_OPUS' |
130
+ 'MULAW' |
131
+ 'ALAW'
120
132
 
121
133
  export type timePoint = {
122
134
  markName: string,
@@ -22,6 +22,7 @@ export async function synthesize(text: string, voice: string, speed: number, opt
22
22
  input: text,
23
23
  model: options.model!,
24
24
  voice: voice as any,
25
+ instructions: options.instructions,
25
26
  response_format: 'opus',
26
27
  speed,
27
28
  }, {
@@ -43,7 +44,9 @@ export interface OpenAICloudTTSOptions {
43
44
  organization?: string
44
45
  baseURL?: string
45
46
 
46
- model?: 'tts-1' | 'tts-1-hd'
47
+ model?: 'tts-1' | 'tts-1-hd' | 'gpt-4o-mini-tts'
48
+
49
+ instructions?: string
47
50
 
48
51
  timeout?: number
49
52
  maxRetries?: number
@@ -56,110 +59,72 @@ export const defaultOpenAICloudTTSOptions: OpenAICloudTTSOptions = {
56
59
 
57
60
  model: 'tts-1',
58
61
 
62
+ instructions: undefined,
63
+
59
64
  timeout: undefined,
60
65
  maxRetries: 10,
61
66
  }
62
67
 
68
+ // Reference: https://platform.openai.com/docs/guides/text-to-speech#supported-languages
63
69
  export const supportedLanguages = [
64
70
  'en',
65
- 'zh',
66
- 'de',
67
- 'es',
68
- 'ru',
69
- 'ko',
70
- 'fr',
71
- 'ja',
72
- 'pt',
73
- 'tr',
74
- 'pl',
71
+ 'af',
72
+ 'ar',
73
+ 'hy',
74
+ 'az',
75
+ 'be',
76
+ 'bs',
77
+ 'bg',
75
78
  'ca',
79
+ 'zh',
80
+ 'hr',
81
+ 'cs',
82
+ 'da',
76
83
  'nl',
77
- 'ar',
78
- 'sv',
79
- 'it',
80
- 'id',
81
- 'hi',
84
+ 'en',
85
+ 'et',
82
86
  'fi',
83
- 'vi',
84
- 'iw',
85
- 'uk',
87
+ 'fr',
88
+ 'gl',
89
+ 'de',
86
90
  'el',
87
- 'ms',
88
- 'cs',
89
- 'ro',
90
- 'da',
91
+ 'he',
92
+ 'hi',
91
93
  'hu',
92
- 'ta',
93
- 'no',
94
- 'th',
95
- 'ur',
96
- 'hr',
97
- 'bg',
94
+ 'is',
95
+ 'id',
96
+ 'it',
97
+ 'ja',
98
+ 'kn',
99
+ 'kk',
100
+ 'ko',
101
+ 'lv',
98
102
  'lt',
99
- 'la',
103
+ 'mk',
104
+ 'ms',
105
+ 'mr',
100
106
  'mi',
101
- 'ml',
102
- 'cy',
103
- 'sk',
104
- 'te',
107
+ 'ne',
108
+ 'no',
105
109
  'fa',
106
- 'lv',
107
- 'bn',
110
+ 'pl',
111
+ 'pt',
112
+ 'ro',
113
+ 'ru',
108
114
  'sr',
109
- 'az',
115
+ 'sk',
110
116
  'sl',
111
- 'kn',
112
- 'et',
113
- 'mk',
114
- 'br',
115
- 'eu',
116
- 'is',
117
- 'hy',
118
- 'ne',
119
- 'mn',
120
- 'bs',
121
- 'kk',
122
- 'sq',
117
+ 'es',
123
118
  'sw',
124
- 'gl',
125
- 'mr',
126
- 'pa',
127
- 'si',
128
- 'km',
129
- 'sn',
130
- 'yo',
131
- 'so',
132
- 'af',
133
- 'oc',
134
- 'ka',
135
- 'be',
136
- 'tg',
137
- 'sd',
138
- 'gu',
139
- 'am',
140
- 'yi',
141
- 'lo',
142
- 'uz',
143
- 'fo',
144
- 'ht',
145
- 'ps',
146
- 'tk',
147
- 'nn',
148
- 'mt',
149
- 'sa',
150
- 'lb',
151
- 'my',
152
- 'bo',
119
+ 'sv',
153
120
  'tl',
154
- 'mg',
155
- 'as',
156
- 'tt',
157
- 'haw',
158
- 'ln',
159
- 'ha',
160
- 'ba',
161
- 'jw',
162
- 'su',
121
+ 'ta',
122
+ 'th',
123
+ 'tr',
124
+ 'uk',
125
+ 'ur',
126
+ 'vi',
127
+ 'cy',
163
128
  ]
164
129
 
165
130
  export const voiceList: SynthesisVoice[] = [
@@ -173,6 +138,11 @@ export const voiceList: SynthesisVoice[] = [
173
138
  languages: ['en-US', ...supportedLanguages],
174
139
  gender: 'male',
175
140
  },
141
+ {
142
+ name: 'ballad',
143
+ languages: ['en-GB', ...supportedLanguages],
144
+ gender: 'male',
145
+ },
176
146
  {
177
147
  name: 'coral',
178
148
  languages: ['en-US', ...supportedLanguages],
@@ -208,4 +178,9 @@ export const voiceList: SynthesisVoice[] = [
208
178
  languages: ['en-US', ...supportedLanguages],
209
179
  gender: 'female',
210
180
  },
181
+ {
182
+ name: 'verse',
183
+ languages: ['en-US', ...supportedLanguages],
184
+ gender: 'male',
185
+ },
211
186
  ]
@@ -55,6 +55,9 @@ declare module '@echogarden/speex-resampler-wasm'
55
55
  declare module '@echogarden/speex-resampler-wasm/simd'
56
56
  declare module '@echogarden/vosk'
57
57
 
58
+ /*
59
+ // Not needed anymore in `onnxruntime-node` 1.21.0:
60
+
58
61
  declare module 'onnxruntime-node' {
59
62
  // From index.d.ts
60
63
  export * from 'onnxruntime-common'
@@ -109,3 +112,4 @@ declare module 'onnxruntime-node' {
109
112
  // From version.d.ts
110
113
  export const version = '1.20.0'
111
114
  }
115
+ */