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/data/lexicons/heteronyms.en.json +4 -4
- package/data/lexicons/words.en.json +260 -0
- package/data/schemas/options.json +5 -1
- package/dist/cli/CLI.js +9 -4
- package/dist/cli/CLI.js.map +1 -1
- package/dist/nlp/PhoneConversion.js +4 -3
- package/dist/nlp/PhoneConversion.js.map +1 -1
- package/dist/nlp/TextNormalizer.d.ts +1 -1
- package/dist/nlp/TextNormalizer.js +30 -25
- package/dist/nlp/TextNormalizer.js.map +1 -1
- package/dist/recognition/OpenAICloudSTT.d.ts +2 -2
- package/dist/recognition/OpenAICloudSTT.js +10 -3
- package/dist/recognition/OpenAICloudSTT.js.map +1 -1
- package/dist/synthesis/EspeakTTS.js +28 -15
- package/dist/synthesis/EspeakTTS.js.map +1 -1
- package/dist/synthesis/OpenAICloudTTS.d.ts +2 -1
- package/dist/synthesis/OpenAICloudTTS.js +58 -86
- package/dist/synthesis/OpenAICloudTTS.js.map +1 -1
- package/dist/utilities/WasmMemoryManager.d.ts +1 -1
- package/docs/Options.md +3 -2
- package/package.json +10 -10
- package/src/cli/CLI.ts +12 -6
- package/src/nlp/PhoneConversion.ts +4 -3
- package/src/nlp/TextNormalizer.ts +36 -25
- package/src/recognition/OpenAICloudSTT.ts +13 -5
- package/src/synthesis/EspeakTTS.ts +31 -16
- package/src/synthesis/OpenAICloudTTS.ts +62 -87
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 `
|
|
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
|
|
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
|
+
"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.
|
|
56
|
-
"@aws-sdk/client-transcribe-streaming": "^3.
|
|
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.
|
|
82
|
+
"jsdom": "^26.1.0",
|
|
83
83
|
"json5": "^2.2.3",
|
|
84
84
|
"kuromoji": "^0.1.2",
|
|
85
|
-
"microsoft-cognitiveservices-speech-sdk": "^1.43.
|
|
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.
|
|
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.
|
|
115
|
+
"@types/node": "^22.14.1",
|
|
116
116
|
"@types/tar": "^6.1.13",
|
|
117
|
-
"@types/ws": "^8.18.
|
|
118
|
-
"ts-json-schema-generator": "^2.
|
|
119
|
-
"typescript": "^5.8.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
'
|
|
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(
|
|
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: [...
|
|
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 <
|
|
45
|
-
const word =
|
|
49
|
+
for (let wordIndex = 0; wordIndex < nonWhitespaceWords.length; wordIndex++) {
|
|
50
|
+
const word = nonWhitespaceWords[wordIndex]
|
|
46
51
|
const lowerCaseWord = word.toLowerCase()
|
|
47
52
|
|
|
48
|
-
const
|
|
49
|
-
const
|
|
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
|
-
|
|
61
|
+
isFollowedByWhitespace &&
|
|
62
|
+
fourDigitYearPattern.test(nextNonWhitespaceWord)) {
|
|
54
63
|
|
|
55
|
-
const normalizedString = normalizeFourDigitYearString(
|
|
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(
|
|
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
|
-
|
|
75
|
+
isFollowedByWhitespace &&
|
|
76
|
+
fourDigitDecadePattern.test(nextNonWhitespaceWord)) {
|
|
67
77
|
|
|
68
|
-
const normalizedString = normalizeFourDigitDecadeString(
|
|
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(
|
|
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(
|
|
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(
|
|
81
|
-
referenceFragments.push(
|
|
90
|
+
normalizedFragments.push(normalizeFourDigitYearString(nonWhitespaceWords[wordIndex]))
|
|
91
|
+
referenceFragments.push(nonWhitespaceWords[wordIndex])
|
|
82
92
|
|
|
83
93
|
normalizedFragments.push('to')
|
|
84
|
-
referenceFragments.push(
|
|
94
|
+
referenceFragments.push(nonWhitespaceWords[wordIndex + 1])
|
|
85
95
|
|
|
86
|
-
normalizedFragments.push(normalizeFourDigitYearString(
|
|
87
|
-
referenceFragments.push(
|
|
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
|
-
|
|
102
|
+
!isFollowedByWhitespace &&
|
|
103
|
+
numberPattern.test(nextNonWhitespaceWord)) {
|
|
93
104
|
|
|
94
105
|
let currencyWord = symbolsPrecedingACurrencyAsWords[symbolsPrecedingACurrency.indexOf(lowerCaseWord)]
|
|
95
106
|
|
|
96
|
-
if (wordsSucceedingACurrency.includes(
|
|
97
|
-
const normalizedString = `${
|
|
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}${
|
|
112
|
+
const referenceString = `${word}${nextNonWhitespaceWord} ${nextNonWhitespaceWords[1]}`
|
|
102
113
|
referenceFragments.push(referenceString)
|
|
103
114
|
|
|
104
115
|
wordIndex += 2
|
|
105
116
|
} else {
|
|
106
|
-
const normalizedString = `${
|
|
117
|
+
const normalizedString = `${nextNonWhitespaceWord} ${currencyWord}`
|
|
107
118
|
|
|
108
119
|
normalizedFragments.push(normalizedString)
|
|
109
120
|
|
|
110
|
-
const referenceString = `${word}${
|
|
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:
|
|
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:
|
|
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
|
-
|
|
43
|
+
{
|
|
44
|
+
const wordsWithMerges: string[] = []
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
for (let i = 0; i < words.length; i++) {
|
|
47
|
+
const currentWord = words[i]
|
|
48
|
+
const previousWord = words[i - 1]
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
65
|
+
// Remove words containing only whitespace
|
|
66
|
+
const nonWhitespaceWords: string[] = []
|
|
67
|
+
const nonWhitespaceWordsOriginalIndex: number[] = []
|
|
62
68
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
'
|
|
66
|
-
'
|
|
67
|
-
'
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
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
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'it',
|
|
80
|
-
'id',
|
|
81
|
-
'hi',
|
|
84
|
+
'en',
|
|
85
|
+
'et',
|
|
82
86
|
'fi',
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
'
|
|
87
|
+
'fr',
|
|
88
|
+
'gl',
|
|
89
|
+
'de',
|
|
86
90
|
'el',
|
|
87
|
-
'
|
|
88
|
-
'
|
|
89
|
-
'ro',
|
|
90
|
-
'da',
|
|
91
|
+
'he',
|
|
92
|
+
'hi',
|
|
91
93
|
'hu',
|
|
92
|
-
'
|
|
93
|
-
'
|
|
94
|
-
'
|
|
95
|
-
'
|
|
96
|
-
'
|
|
97
|
-
'
|
|
94
|
+
'is',
|
|
95
|
+
'id',
|
|
96
|
+
'it',
|
|
97
|
+
'ja',
|
|
98
|
+
'kn',
|
|
99
|
+
'kk',
|
|
100
|
+
'ko',
|
|
101
|
+
'lv',
|
|
98
102
|
'lt',
|
|
99
|
-
'
|
|
103
|
+
'mk',
|
|
104
|
+
'ms',
|
|
105
|
+
'mr',
|
|
100
106
|
'mi',
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'sk',
|
|
104
|
-
'te',
|
|
107
|
+
'ne',
|
|
108
|
+
'no',
|
|
105
109
|
'fa',
|
|
106
|
-
'
|
|
107
|
-
'
|
|
110
|
+
'pl',
|
|
111
|
+
'pt',
|
|
112
|
+
'ro',
|
|
113
|
+
'ru',
|
|
108
114
|
'sr',
|
|
109
|
-
'
|
|
115
|
+
'sk',
|
|
110
116
|
'sl',
|
|
111
|
-
'
|
|
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
|
-
'
|
|
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
|
-
'
|
|
155
|
-
'
|
|
156
|
-
'
|
|
157
|
-
'
|
|
158
|
-
'
|
|
159
|
-
'
|
|
160
|
-
'
|
|
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
|
]
|