echogarden 2.4.0 → 2.5.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.
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.4.0",
3
+ "version": "2.5.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.772.0",
56
- "@aws-sdk/client-transcribe-streaming": "^3.772.0",
55
+ "@aws-sdk/client-polly": "^3.787.0",
56
+ "@aws-sdk/client-transcribe-streaming": "^3.787.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",
@@ -79,13 +79,13 @@
79
79
  "html-to-text": "^9.0.5",
80
80
  "import-meta-resolve": "^4.1.0",
81
81
  "jieba-wasm": "^2.2.0",
82
- "jsdom": "^26.0.0",
82
+ "jsdom": "^26.1.0",
83
83
  "json5": "^2.2.3",
84
84
  "kuromoji": "^0.1.2",
85
- "microsoft-cognitiveservices-speech-sdk": "^1.43.0",
85
+ "microsoft-cognitiveservices-speech-sdk": "^1.43.1",
86
86
  "msgpack-lite": "^0.1.26",
87
87
  "onnxruntime-node": "^1.21.0",
88
- "openai": "^4.89.0",
88
+ "openai": "^4.95.1",
89
89
  "sam-js": "^0.3.1",
90
90
  "strip-ansi": "^7.1.0",
91
91
  "tar": "^7.4.3",
@@ -112,11 +112,11 @@
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.13",
115
+ "@types/node": "^22.14.1",
116
116
  "@types/tar": "^6.1.13",
117
- "@types/ws": "^8.18.0",
118
- "ts-json-schema-generator": "^2.3.0",
119
- "typescript": "^5.8.2"
117
+ "@types/ws": "^8.18.1",
118
+ "ts-json-schema-generator": "^2.4.0",
119
+ "typescript": "^5.8.3"
120
120
  },
121
121
  "overrides": {
122
122
  "whatwg-url": "^14.0.0"
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 = {}
@@ -5,7 +5,7 @@ export function ipaPhoneToKirshenbaum(ipaPhone: string) {
5
5
  const convertedChar = ipaToKirshenbaum[char]
6
6
 
7
7
  if (convertedChar == undefined) {
8
- throw new Error(`Couldn't convert IPA character '${char}' (part of phone '${ipaPhone}' the Kirshenbaum notation`)
8
+ throw new Error(`Couldn't convert IPA character '${char}' (part of phone '${ipaPhone}') to the Kirshenbaum notation`)
9
9
  }
10
10
 
11
11
  result += convertedChar || '_'
@@ -288,8 +288,9 @@ export const ipaToKirshenbaum: Record<string, string> = {
288
288
  // Open-mid:
289
289
  'ɛ': 'E',
290
290
  'œ': 'W',
291
- //'ɜ': '3', // eSpeak
292
- 'ɜ': 'V"',
291
+ 'ɜ': '3', // eSpeak
292
+ 'ɜː': '3:',
293
+ //'ɜ': 'V"',
293
294
  'ɞ': 'O"',
294
295
  'ʌ': 'V',
295
296
  'ɔ': 'O',
@@ -1,11 +1,16 @@
1
1
  import { getShortLanguageCode } from '../utilities/Locale.js'
2
2
  import { substituteCharactersUsingLookup } from '../utilities/StringUtilities.js'
3
3
 
4
- export function getNormalizedFragmentsForSpeech(words: string[], language: string) {
4
+ export function getNormalizedFragmentsForSpeech(
5
+ words: string[],
6
+ nonWhitespaceWords: string[],
7
+ nonWhitespaceWordOriginalIndex: number[],
8
+ language: string) {
9
+
5
10
  language = getShortLanguageCode(language)
6
11
 
7
12
  if (language != 'en') {
8
- return { normalizedFragments: [...words], referenceFragments: [...words] }
13
+ return { normalizedFragments: [...nonWhitespaceWords], referenceFragments: [...nonWhitespaceWords] }
9
14
  }
10
15
 
11
16
  const numberPattern = /^[0-9][0-9\,\.]*$/
@@ -41,73 +46,79 @@ export function getNormalizedFragmentsForSpeech(words: string[], language: strin
41
46
  const normalizedFragments: string[] = []
42
47
  const referenceFragments: string[] = []
43
48
 
44
- for (let wordIndex = 0; wordIndex < words.length; wordIndex++) {
45
- const word = words[wordIndex]
49
+ for (let wordIndex = 0; wordIndex < nonWhitespaceWords.length; wordIndex++) {
50
+ const word = nonWhitespaceWords[wordIndex]
46
51
  const lowerCaseWord = word.toLowerCase()
47
52
 
48
- const nextWords = words.slice(wordIndex + 1)
49
- const nextWord = nextWords[0]
53
+ const nextNonWhitespaceWords = nonWhitespaceWords.slice(wordIndex + 1)
54
+ const nextNonWhitespaceWord = nextNonWhitespaceWords[0]
55
+
56
+ const originalWordIndex = nonWhitespaceWordOriginalIndex[wordIndex]
57
+ const isFollowedByWhitespace = words[originalWordIndex + 1]?.trim().length === 0
50
58
 
51
59
  if ( // Normalize a four digit year pattern, e.g. 'in 1995'.
52
60
  wordsPrecedingAYear.includes(lowerCaseWord) &&
53
- fourDigitYearPattern.test(nextWord)) {
61
+ isFollowedByWhitespace &&
62
+ fourDigitYearPattern.test(nextNonWhitespaceWord)) {
54
63
 
55
- const normalizedString = normalizeFourDigitYearString(nextWord)
64
+ const normalizedString = normalizeFourDigitYearString(nextNonWhitespaceWord)
56
65
 
57
66
  normalizedFragments.push(word)
58
67
  referenceFragments.push(word)
59
68
 
60
69
  normalizedFragments.push(normalizedString)
61
- referenceFragments.push(nextWord)
70
+ referenceFragments.push(nextNonWhitespaceWord)
62
71
 
63
72
  wordIndex += 1
64
73
  } else if ( // Normalize a four digit decade pattern, e.g. 'the 1980s'.
65
74
  wordsPrecedingADecade.includes(lowerCaseWord) &&
66
- fourDigitDecadePattern.test(nextWord)) {
75
+ isFollowedByWhitespace &&
76
+ fourDigitDecadePattern.test(nextNonWhitespaceWord)) {
67
77
 
68
- const normalizedString = normalizeFourDigitDecadeString(nextWord)
78
+ const normalizedString = normalizeFourDigitDecadeString(nextNonWhitespaceWord)
69
79
 
70
80
  normalizedFragments.push(word)
71
81
  referenceFragments.push(word)
72
82
 
73
83
  normalizedFragments.push(normalizedString)
74
- referenceFragments.push(nextWord)
84
+ referenceFragments.push(nextNonWhitespaceWord)
75
85
 
76
86
  wordIndex += 1
77
- } else if ( // Normalize a year range pattern, e.g. '1835-1896'
78
- fourDigitYearRangePattern.test(words.slice(wordIndex, wordIndex + 3).join(''))) {
87
+ } else if ( // Normalize a year range pattern, e.g. '1835-1896', ensure there are no spaces between words
88
+ fourDigitYearRangePattern.test(words.slice(originalWordIndex, originalWordIndex + 3).join(''))) {
79
89
 
80
- normalizedFragments.push(normalizeFourDigitYearString(words[wordIndex]))
81
- referenceFragments.push(words[wordIndex])
90
+ normalizedFragments.push(normalizeFourDigitYearString(nonWhitespaceWords[wordIndex]))
91
+ referenceFragments.push(nonWhitespaceWords[wordIndex])
82
92
 
83
93
  normalizedFragments.push('to')
84
- referenceFragments.push(words[wordIndex + 1])
94
+ referenceFragments.push(nonWhitespaceWords[wordIndex + 1])
85
95
 
86
- normalizedFragments.push(normalizeFourDigitYearString(words[wordIndex + 2]))
87
- referenceFragments.push(words[wordIndex + 2])
96
+ normalizedFragments.push(normalizeFourDigitYearString(nonWhitespaceWords[wordIndex + 2]))
97
+ referenceFragments.push(nonWhitespaceWords[wordIndex + 2])
88
98
 
89
99
  wordIndex += 2
90
100
  } else if ( // Normalize a currency pattern, e.g. '$53.1 million', '€3.53'
91
101
  symbolsPrecedingACurrency.includes(lowerCaseWord) &&
92
- numberPattern.test(nextWord)) {
102
+ !isFollowedByWhitespace &&
103
+ numberPattern.test(nextNonWhitespaceWord)) {
93
104
 
94
105
  let currencyWord = symbolsPrecedingACurrencyAsWords[symbolsPrecedingACurrency.indexOf(lowerCaseWord)]
95
106
 
96
- if (wordsSucceedingACurrency.includes(nextWords[1].toLowerCase())) {
97
- const normalizedString = `${nextWord} ${nextWords[1]} ${currencyWord}`
107
+ if (wordsSucceedingACurrency.includes(nextNonWhitespaceWords[1]?.toLowerCase())) {
108
+ const normalizedString = `${nextNonWhitespaceWord} ${nextNonWhitespaceWords[1]} ${currencyWord}`
98
109
 
99
110
  normalizedFragments.push(normalizedString)
100
111
 
101
- const referenceString = `${word}${nextWord} ${nextWords[1]}`
112
+ const referenceString = `${word}${nextNonWhitespaceWord} ${nextNonWhitespaceWords[1]}`
102
113
  referenceFragments.push(referenceString)
103
114
 
104
115
  wordIndex += 2
105
116
  } else {
106
- const normalizedString = `${nextWord} ${currencyWord}`
117
+ const normalizedString = `${nextNonWhitespaceWord} ${currencyWord}`
107
118
 
108
119
  normalizedFragments.push(normalizedString)
109
120
 
110
- const referenceString = `${word}${nextWord}`
121
+ const referenceString = `${word}${nextNonWhitespaceWord}`
111
122
  referenceFragments.push(referenceString)
112
123
 
113
124
  wordIndex += 1
@@ -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
@@ -40,30 +40,45 @@ export async function preprocessAndSynthesize(text: string, language: string, es
40
40
  let words = await splitToWords(text, language)
41
41
 
42
42
  // Merge repeating non-words to a single word to work around eSpeak bug
43
- const wordsWithMerges: string[] = []
43
+ {
44
+ const wordsWithMerges: string[] = []
44
45
 
45
- for (let i = 0; i < words.length; i++) {
46
- const currentWord = words[i]
47
- const previousWord = words[i - 1]
46
+ for (let i = 0; i < words.length; i++) {
47
+ const currentWord = words[i]
48
+ const previousWord = words[i - 1]
48
49
 
49
- if (
50
- i > 0 &&
51
- currentWord === previousWord &&
52
- !['[', ']'].includes(currentWord) && // Work around eSpeak-NG marker bug with repeating squared brackets
53
- !wordCharacterPattern.test(currentWord)) {
50
+ if (
51
+ i > 0 &&
52
+ currentWord === previousWord &&
53
+ !['[', ']'].includes(currentWord) && // Work around eSpeak-NG marker bug with repeating squared brackets
54
+ !wordCharacterPattern.test(currentWord)) {
54
55
 
55
- wordsWithMerges[wordsWithMerges.length - 1] += currentWord
56
- } else {
57
- wordsWithMerges.push(currentWord)
56
+ wordsWithMerges[wordsWithMerges.length - 1] += currentWord
57
+ } else {
58
+ wordsWithMerges.push(currentWord)
59
+ }
58
60
  }
61
+
62
+ words = wordsWithMerges
59
63
  }
60
64
 
61
- words = wordsWithMerges
65
+ // Remove words containing only whitespace
66
+ const nonWhitespaceWords: string[] = []
67
+ const nonWhitespaceWordsOriginalIndex: number[] = []
62
68
 
63
- // Trim words and remove words containing only whitespace
64
- words = words.map(word => word.trim()).filter(word => word !== '')
69
+ {
70
+ for (let i = 0; i < words.length; i++) {
71
+ const word = words[i]
72
+ const wordIsWhitespace = word.trim().length === 0
73
+
74
+ if (!wordIsWhitespace) {
75
+ nonWhitespaceWords.push(word)
76
+ nonWhitespaceWordsOriginalIndex.push(i)
77
+ }
78
+ }
79
+ }
65
80
 
66
- const { normalizedFragments, referenceFragments } = getNormalizedFragmentsForSpeech(words, language)
81
+ const { normalizedFragments, referenceFragments } = getNormalizedFragmentsForSpeech(words, nonWhitespaceWords, nonWhitespaceWordsOriginalIndex, language)
67
82
 
68
83
  const simplifiedFragments = normalizedFragments.map(word => simplifyPunctuationCharacters(word).toLocaleLowerCase())
69
84
 
@@ -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
  ]