@andresaya/edge-tts 1.3.2 → 1.4.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/README.md +61 -0
- package/dist/cli/edge-tts.js +1 -1
- package/dist/commands/SynthesizeCommand.js +29 -6
- package/dist/config/constants.d.ts +1 -0
- package/dist/config/constants.js +4 -3
- package/dist/services/EdgeTTS.d.ts +14 -0
- package/dist/services/EdgeTTS.js +142 -14
- package/package.json +48 -48
- package/dist/config/formats.d.ts +0 -81
- package/dist/config/formats.js +0 -54
package/README.md
CHANGED
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
- **Audio Information**: Get detailed information about generated audio (size, duration, format).
|
|
12
12
|
- **Audio Export Options**: Export synthesized audio in different formats (raw, base64, or directly to a file).
|
|
13
13
|
- **Streaming Support**: Stream audio data in real-time for better performance.
|
|
14
|
+
- **Word Boundaries Metadata**: Get word boundary information with precise timestamps.
|
|
14
15
|
- **Command-Line Interface**: Use a simple CLI for easy access to functionality.
|
|
15
16
|
- **Easy Integration**: Modular structure allows for easy inclusion in existing projects.
|
|
17
|
+
- **SSML Custom**: 🥳🥳 Edge TTS accepts raw SSML with all characteristics of Azure AI Speech.
|
|
16
18
|
|
|
17
19
|
## Installation
|
|
18
20
|
|
|
@@ -40,6 +42,11 @@ To synthesize speech from text:
|
|
|
40
42
|
edge-tts synthesize -t "Hello, world!" -o hello_world_audio
|
|
41
43
|
```
|
|
42
44
|
|
|
45
|
+
From file in format SSML:
|
|
46
|
+
```bash
|
|
47
|
+
edge-tts synthesize -f ssml.txt --ssml -o salida
|
|
48
|
+
```
|
|
49
|
+
|
|
43
50
|
To list available voices:
|
|
44
51
|
```bash
|
|
45
52
|
edge-tts voice-list
|
|
@@ -84,6 +91,49 @@ const maleVoices = await tts.getVoicesByGender('Male');
|
|
|
84
91
|
|
|
85
92
|
### Text Synthesis
|
|
86
93
|
|
|
94
|
+
#### Custom SSML (Advanced)
|
|
95
|
+
Edge TTS accepts raw SSML so you can control prosody, styles, pauses, pronunciations, and more. You can pass SSML from code or the CLI. By default the library auto-detects if your input is SSML; you can also force the mode.
|
|
96
|
+
|
|
97
|
+
More information
|
|
98
|
+
[Azure AI Speech](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-structure)
|
|
99
|
+
|
|
100
|
+
### SSML-builder
|
|
101
|
+
[@andresaya/ssml-builder](https://www.npmjs.com/package/@andresaya/ssml-builder) A powerful, type-safe TypeScript library for building Speech Synthesis Markup Language (SSML) documents. Create expressive text-to-speech applications with Azure Speech Service and other SSML-compliant engines.
|
|
102
|
+
|
|
103
|
+
#### What the library does for you
|
|
104
|
+
|
|
105
|
+
- Auto-detect / force mode: options.inputType may be 'auto' | 'ssml' | 'text' (default: auto).
|
|
106
|
+
- Validation: Throws helpful errors if SSML is malformed (e.g., missing <speak>, <voice>, or the synthesis namespace).
|
|
107
|
+
- Voice injection: If your SSML lacks <voice>, it injects one with the voice you passed.
|
|
108
|
+
- Text wrapping: If you pass plain text (or inputType: 'text'), it wraps it in a valid SSML envelope using your rate, pitch, and volume.
|
|
109
|
+
|
|
110
|
+
```js
|
|
111
|
+
import { EdgeTTS } from '@andresaya/edge-tts';
|
|
112
|
+
|
|
113
|
+
const tts = new EdgeTTS();
|
|
114
|
+
|
|
115
|
+
const ssml = `
|
|
116
|
+
<speak version="1.0"
|
|
117
|
+
xmlns="http://www.w3.org/2001/10/synthesis"
|
|
118
|
+
xmlns:mstts="https://www.w3.org/2001/mstts"
|
|
119
|
+
xml:lang="es-CO">
|
|
120
|
+
<voice name="es-CO-GonzaloNeural">
|
|
121
|
+
<mstts:express-as style="narration-professional">
|
|
122
|
+
<prosody rate="+5%" pitch="+10Hz" volume="+0%">
|
|
123
|
+
Hola, este es un ejemplo de <emphasis>SSML</emphasis>.
|
|
124
|
+
<break time="400ms" />
|
|
125
|
+
El número es <say-as interpret-as="cardinal">2025</say-as>.
|
|
126
|
+
La palabra se pronuncia
|
|
127
|
+
<phoneme alphabet="ipa" ph="ˈxola">hola</phoneme>.
|
|
128
|
+
</prosody>
|
|
129
|
+
</mstts:express-as>
|
|
130
|
+
</voice>
|
|
131
|
+
</speak>`.trim();
|
|
132
|
+
|
|
133
|
+
// Auto-detects SSML, or force it with inputType: 'ssml'
|
|
134
|
+
await tts.synthesize(ssml, 'es-CO-GonzaloNeural', { inputType: 'ssml' });
|
|
135
|
+
```
|
|
136
|
+
|
|
87
137
|
#### Basic Synthesis
|
|
88
138
|
```js
|
|
89
139
|
// Simple synthesis with default voice
|
|
@@ -151,6 +201,17 @@ console.log(`Audio saved to: ${filePath}`);
|
|
|
151
201
|
// Creates: output_audio.mp3
|
|
152
202
|
```
|
|
153
203
|
|
|
204
|
+
### Word Boundaries Metadata
|
|
205
|
+
|
|
206
|
+
```php
|
|
207
|
+
// Get word boundaries with timestamps
|
|
208
|
+
$boundaries = $tts->getWordBoundaries();
|
|
209
|
+
|
|
210
|
+
// Save metadata to file
|
|
211
|
+
$tts->saveMetadata('metadata.json');
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
|
|
154
215
|
## Examples
|
|
155
216
|
|
|
156
217
|
### Complete Example with Voice Selection
|
package/dist/cli/edge-tts.js
CHANGED
|
@@ -6,7 +6,7 @@ const VoiceListCommand_1 = require("../commands/VoiceListCommand");
|
|
|
6
6
|
const commander_1 = require("commander");
|
|
7
7
|
const program = new commander_1.Command();
|
|
8
8
|
program
|
|
9
|
-
.version('1.
|
|
9
|
+
.version('1.4.0')
|
|
10
10
|
.addCommand(SynthesizeCommand_1.SynthesizeCommand)
|
|
11
11
|
.addCommand(VoiceListCommand_1.VoiceListCommand);
|
|
12
12
|
try {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.SynthesizeCommand = void 0;
|
|
4
4
|
const commander_1 = require("commander");
|
|
5
5
|
const EdgeTTS_1 = require("../services/EdgeTTS");
|
|
6
|
+
const promises_1 = require("fs/promises");
|
|
6
7
|
exports.SynthesizeCommand = new commander_1.Command('synthesize')
|
|
7
8
|
.description('Edge TTS: synthesize text to audio')
|
|
8
9
|
.option('-t, --text <text>', 'Text to convert to audio')
|
|
@@ -11,14 +12,36 @@ exports.SynthesizeCommand = new commander_1.Command('synthesize')
|
|
|
11
12
|
.option('-l, --volume [volume]', 'Volume of speech', '0%')
|
|
12
13
|
.option('-p, --pitch [pitch]', 'Pitch of speech', '0Hz')
|
|
13
14
|
.option('-o, --output [output]', 'Output file name', `output_${Date.now()}`)
|
|
15
|
+
.option('-f, --file [file]', 'Input file (text or SSML)')
|
|
16
|
+
.option('--ssml', 'Force interpret input as SSML (auto-detect by default)')
|
|
14
17
|
.action(async (options) => {
|
|
15
|
-
const { text, voice, pitch, rate, volume, output } = options;
|
|
16
|
-
if (!text) {
|
|
17
|
-
console.error('Text is required');
|
|
18
|
+
const { text, voice, pitch, rate, volume, output, file, ssml: forceSSML } = options;
|
|
19
|
+
if (!text && !file) {
|
|
20
|
+
console.error('Error: Text (-t) or file (-f) is required');
|
|
18
21
|
process.exit(1);
|
|
19
22
|
}
|
|
23
|
+
if (text && file) {
|
|
24
|
+
console.error('Error: Cannot use both text (-t) and file (-f) options');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
let content = text;
|
|
28
|
+
if (file) {
|
|
29
|
+
try {
|
|
30
|
+
content = await (0, promises_1.readFile)(file, 'utf-8');
|
|
31
|
+
console.log(`Reading from file: ${file}`);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
console.error(`Error reading file: ${error}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
20
38
|
const tts = new EdgeTTS_1.EdgeTTS();
|
|
21
|
-
await tts.synthesize(
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
await tts.synthesize(content, voice, {
|
|
40
|
+
pitch,
|
|
41
|
+
rate,
|
|
42
|
+
volume,
|
|
43
|
+
inputType: forceSSML ? 'ssml' : 'auto'
|
|
44
|
+
});
|
|
45
|
+
const savedPath = await tts.toFile(output);
|
|
46
|
+
console.log(`Audio file generated: ${savedPath}`);
|
|
24
47
|
});
|
package/dist/config/constants.js
CHANGED
|
@@ -3,7 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Constants = void 0;
|
|
4
4
|
exports.Constants = {
|
|
5
5
|
TRUSTED_CLIENT_TOKEN: '6A5AA1D4EAFF4E9FB37E23D68491D6F4',
|
|
6
|
-
WSS_URL: 'wss://
|
|
7
|
-
VOICES_URL: 'https://
|
|
8
|
-
USER_AGENT: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0'
|
|
6
|
+
WSS_URL: 'wss://api.msedgeservices.com/tts/cognitiveservices/websocket/v1',
|
|
7
|
+
VOICES_URL: 'https://api.msedgeservices.com/tts/cognitiveservices/voices/list',
|
|
8
|
+
USER_AGENT: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0',
|
|
9
|
+
VERSION_MS_GEC: '1-140.0.3485.14'
|
|
9
10
|
};
|
|
@@ -5,15 +5,24 @@ export interface Voice {
|
|
|
5
5
|
Gender: string;
|
|
6
6
|
Locale: string;
|
|
7
7
|
FriendlyName: string;
|
|
8
|
+
LocalName: string;
|
|
8
9
|
}
|
|
9
10
|
export interface SynthesisOptions {
|
|
10
11
|
pitch?: string | number;
|
|
11
12
|
rate?: string | number;
|
|
12
13
|
volume?: string | number;
|
|
14
|
+
inputType?: 'auto' | 'ssml' | 'text';
|
|
15
|
+
}
|
|
16
|
+
export interface WordBoundary {
|
|
17
|
+
type: "WordBoundary";
|
|
18
|
+
offset: number;
|
|
19
|
+
duration: number;
|
|
20
|
+
text: string;
|
|
13
21
|
}
|
|
14
22
|
export declare class EdgeTTS {
|
|
15
23
|
private audio_stream;
|
|
16
24
|
private audio_format;
|
|
25
|
+
private word_boundaries;
|
|
17
26
|
private ws;
|
|
18
27
|
getVoices(): Promise<Voice[]>;
|
|
19
28
|
getVoicesByLanguage(locale: string): Promise<Voice[]>;
|
|
@@ -23,10 +32,13 @@ export declare class EdgeTTS {
|
|
|
23
32
|
private validateRate;
|
|
24
33
|
private validateVolume;
|
|
25
34
|
synthesize(text: string, voice?: string, options?: SynthesisOptions): Promise<void>;
|
|
35
|
+
private detectSSML;
|
|
36
|
+
private escapeXML;
|
|
26
37
|
private getSSML;
|
|
27
38
|
private buildTTSConfigMessage;
|
|
28
39
|
synthesizeStream(text: string, voice?: string, options?: SynthesisOptions): AsyncGenerator<Uint8Array, void, unknown>;
|
|
29
40
|
private processAudioData;
|
|
41
|
+
private parseMetadata;
|
|
30
42
|
private generateSecMsGec;
|
|
31
43
|
getDuration(): number;
|
|
32
44
|
getAudioInfo(): {
|
|
@@ -38,4 +50,6 @@ export declare class EdgeTTS {
|
|
|
38
50
|
toRaw(): string;
|
|
39
51
|
toBase64(): string;
|
|
40
52
|
toBuffer(): Buffer;
|
|
53
|
+
saveMetadata(outputPath: string): Promise<void>;
|
|
54
|
+
getWordBoundaries(): WordBoundary[];
|
|
41
55
|
}
|
package/dist/services/EdgeTTS.js
CHANGED
|
@@ -26,17 +26,19 @@ function ensureBuffer(data) {
|
|
|
26
26
|
class EdgeTTS {
|
|
27
27
|
audio_stream = [];
|
|
28
28
|
audio_format = 'mp3';
|
|
29
|
+
word_boundaries = [];
|
|
29
30
|
ws;
|
|
30
31
|
async getVoices() {
|
|
31
|
-
const
|
|
32
|
+
const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
|
|
33
|
+
const response = await fetch(`${constants_1.Constants.VOICES_URL}?Ocp-Apim-Subscription-Key=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}`, {
|
|
32
34
|
headers: {
|
|
33
35
|
"User-Agent": constants_1.Constants.USER_AGENT
|
|
34
36
|
},
|
|
35
37
|
});
|
|
36
38
|
const data = await response.json();
|
|
37
39
|
return data.map((voice) => {
|
|
38
|
-
|
|
39
|
-
delete voice.
|
|
40
|
+
voice.FriendlyName = voice.FriendlyName || voice.LocalName;
|
|
41
|
+
delete voice.SampleRateHertz;
|
|
40
42
|
delete voice.Status;
|
|
41
43
|
return voice;
|
|
42
44
|
});
|
|
@@ -99,8 +101,8 @@ class EdgeTTS {
|
|
|
99
101
|
const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
|
|
100
102
|
return new Promise((resolve, reject) => {
|
|
101
103
|
this.audio_stream = [];
|
|
102
|
-
const
|
|
103
|
-
const url = `${constants_1.Constants.WSS_URL}?
|
|
104
|
+
const reqId = this.generateUUID();
|
|
105
|
+
const url = `${constants_1.Constants.WSS_URL}?Ocp-Apim-Subscription-Key=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
|
|
104
106
|
this.ws = new ws_1.default(url, {
|
|
105
107
|
headers: {
|
|
106
108
|
"User-Agent": constants_1.Constants.USER_AGENT
|
|
@@ -116,7 +118,7 @@ class EdgeTTS {
|
|
|
116
118
|
this.ws.on('open', () => {
|
|
117
119
|
const message = this.buildTTSConfigMessage();
|
|
118
120
|
this.ws.send(message);
|
|
119
|
-
const speechMessage = `X-RequestId:${
|
|
121
|
+
const speechMessage = `X-RequestId:${reqId}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${new Date().toISOString()}Z\r\nPath:ssml\r\n\r\n${SSML_text}`;
|
|
120
122
|
this.ws.send(speechMessage);
|
|
121
123
|
});
|
|
122
124
|
this.ws.on('message', (data) => {
|
|
@@ -135,14 +137,83 @@ class EdgeTTS {
|
|
|
135
137
|
});
|
|
136
138
|
});
|
|
137
139
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
detectSSML(content) {
|
|
141
|
+
const trimmedContent = content.trim();
|
|
142
|
+
const looksLikeSSML = /^<\?xml|^<speak/i.test(trimmedContent);
|
|
143
|
+
if (!looksLikeSSML) {
|
|
144
|
+
return { isValid: true, isSSML: false };
|
|
145
|
+
}
|
|
146
|
+
const errors = [];
|
|
147
|
+
const hasSpeakTag = /<speak\b[^>]*>[\s\S]*<\/speak>/i.test(trimmedContent);
|
|
148
|
+
const hasVoiceTag = /<voice\b[^>]*>[\s\S]*<\/voice>/i.test(trimmedContent);
|
|
149
|
+
if (!hasSpeakTag) {
|
|
150
|
+
throw new Error('Invalid SSML: Missing <speak> tag');
|
|
151
|
+
}
|
|
152
|
+
if (!hasVoiceTag) {
|
|
153
|
+
throw new Error('Invalid SSML: Missing <voice> tag');
|
|
154
|
+
}
|
|
155
|
+
const hasCorrectNamespace = /xmlns="http:\/\/www\.w3\.org\/2001\/10\/synthesis"/i.test(trimmedContent);
|
|
156
|
+
if (!hasCorrectNamespace && hasSpeakTag) {
|
|
157
|
+
throw new Error('Invalid SSML: Missing or incorrect namespace declaration');
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
isValid: errors.length === 0,
|
|
161
|
+
isSSML: hasSpeakTag || hasVoiceTag,
|
|
162
|
+
errors: errors.length > 0 ? errors : undefined
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
escapeXML(text) {
|
|
166
|
+
return text
|
|
167
|
+
.replace(/&/g, '&')
|
|
168
|
+
.replace(/</g, '<')
|
|
169
|
+
.replace(/>/g, '>')
|
|
170
|
+
.replace(/"/g, '"')
|
|
171
|
+
.replace(/'/g, ''');
|
|
172
|
+
}
|
|
173
|
+
getSSML(content, voice, options = {}) {
|
|
174
|
+
const inputType = options.inputType || 'auto';
|
|
175
|
+
let treatAsSSML = false;
|
|
176
|
+
if (inputType === 'ssml') {
|
|
177
|
+
treatAsSSML = true;
|
|
178
|
+
}
|
|
179
|
+
else if (inputType === 'text') {
|
|
180
|
+
treatAsSSML = false;
|
|
181
|
+
}
|
|
182
|
+
else { // 'auto'
|
|
183
|
+
const detection = this.detectSSML(content);
|
|
184
|
+
treatAsSSML = detection.isSSML;
|
|
185
|
+
if (detection.isSSML) {
|
|
186
|
+
console.log('→ Detected SSML input');
|
|
187
|
+
if (!detection.isValid) {
|
|
188
|
+
console.warn('⚠ SSML validation warnings:', detection.errors);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
console.log('→ Detected plain text input');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (treatAsSSML) {
|
|
196
|
+
let ssml = content.trim();
|
|
197
|
+
if (!ssml.includes('xmlns=')) {
|
|
198
|
+
ssml = ssml.replace(/<speak([^>]*)>/i, '<speak$1 xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts">');
|
|
199
|
+
}
|
|
200
|
+
if (!/<voice\b[^>]*>/i.test(ssml) && voice) {
|
|
201
|
+
ssml = ssml.replace(/(<speak[^>]*>)([\s\S]*?)(<\/speak>)/i, `$1<voice name="${voice}">$2</voice>$3`);
|
|
202
|
+
}
|
|
203
|
+
return ssml;
|
|
141
204
|
}
|
|
142
205
|
const pitch = this.validatePitch(options.pitch ?? 0);
|
|
143
206
|
const rate = this.validateRate(options.rate ?? 0);
|
|
144
207
|
const volume = this.validateVolume(options.volume ?? 0);
|
|
145
|
-
|
|
208
|
+
const escapedText = this.escapeXML(content);
|
|
209
|
+
return `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="en-US">
|
|
210
|
+
<voice name="${voice}">
|
|
211
|
+
<prosody pitch="${pitch}" rate="${rate}" volume="${volume}">
|
|
212
|
+
${escapedText}
|
|
213
|
+
</prosody>
|
|
214
|
+
</voice>
|
|
215
|
+
</speak>
|
|
216
|
+
`;
|
|
146
217
|
}
|
|
147
218
|
buildTTSConfigMessage() {
|
|
148
219
|
return `X-Timestamp:${new Date().toISOString()}Z\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n` +
|
|
@@ -150,10 +221,14 @@ class EdgeTTS {
|
|
|
150
221
|
}
|
|
151
222
|
async *synthesizeStream(text, voice = 'en-US-AnaNeural', options = {}) {
|
|
152
223
|
this.audio_stream = [];
|
|
153
|
-
const
|
|
224
|
+
const reqId = this.generateUUID();
|
|
154
225
|
const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
|
|
155
|
-
const url = `${constants_1.Constants.WSS_URL}?
|
|
156
|
-
this.ws = new ws_1.default(url
|
|
226
|
+
const url = `${constants_1.Constants.WSS_URL}?Ocp-Apim-Subscription-Key=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
|
|
227
|
+
this.ws = new ws_1.default(url, {
|
|
228
|
+
headers: {
|
|
229
|
+
"User-Agent": constants_1.Constants.USER_AGENT
|
|
230
|
+
}
|
|
231
|
+
});
|
|
157
232
|
const SSML_text = this.getSSML(text, voice, options);
|
|
158
233
|
const queue = [];
|
|
159
234
|
let done = false;
|
|
@@ -174,7 +249,7 @@ class EdgeTTS {
|
|
|
174
249
|
this.ws.on('open', () => {
|
|
175
250
|
const message = this.buildTTSConfigMessage();
|
|
176
251
|
this.ws.send(message);
|
|
177
|
-
const speechMessage = `X-RequestId:${
|
|
252
|
+
const speechMessage = `X-RequestId:${reqId}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${new Date().toISOString()}Z\r\nPath:ssml\r\n\r\n${SSML_text}`;
|
|
178
253
|
this.ws.send(speechMessage);
|
|
179
254
|
});
|
|
180
255
|
this.ws.on('message', (data) => {
|
|
@@ -187,6 +262,15 @@ class EdgeTTS {
|
|
|
187
262
|
this.audio_stream.push(chunk);
|
|
188
263
|
push(chunk);
|
|
189
264
|
}
|
|
265
|
+
if (buffer.toString().includes("Path:audio.metadata")) {
|
|
266
|
+
const metadataStart = buffer.indexOf("\r\n\r\n") + 4;
|
|
267
|
+
const metadataJson = buffer.toString().substring(metadataStart);
|
|
268
|
+
const meta = this.parseMetadata(metadataJson);
|
|
269
|
+
if (meta !== null) {
|
|
270
|
+
this.word_boundaries.push(meta);
|
|
271
|
+
}
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
190
274
|
if (buffer.toString().includes('Path:turn.end')) {
|
|
191
275
|
this.ws?.close();
|
|
192
276
|
}
|
|
@@ -229,10 +313,44 @@ class EdgeTTS {
|
|
|
229
313
|
const audioChunk = buffer.subarray(audioStartIndex + needle.length);
|
|
230
314
|
this.audio_stream.push(new Uint8Array(audioChunk));
|
|
231
315
|
}
|
|
316
|
+
if (buffer.toString().includes("Path:audio.metadata")) {
|
|
317
|
+
const metadataStart = buffer.indexOf("\r\n\r\n") + 4;
|
|
318
|
+
const metadataJson = buffer.toString().substring(metadataStart);
|
|
319
|
+
const meta = this.parseMetadata(metadataJson);
|
|
320
|
+
if (meta !== null) {
|
|
321
|
+
this.word_boundaries.push(meta);
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
232
325
|
if (buffer.toString().includes("Path:turn.end")) {
|
|
233
326
|
this.ws?.close();
|
|
234
327
|
}
|
|
235
328
|
}
|
|
329
|
+
parseMetadata(data, offsetCompensation = 0) {
|
|
330
|
+
let metadata;
|
|
331
|
+
try {
|
|
332
|
+
metadata = JSON.parse(data);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
if (!metadata.Metadata) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
for (const metaObj of metadata.Metadata) {
|
|
341
|
+
if (metaObj.Type === "WordBoundary") {
|
|
342
|
+
const currentOffset = metaObj.Data.Offset + offsetCompensation;
|
|
343
|
+
const currentDuration = metaObj.Data.Duration;
|
|
344
|
+
return {
|
|
345
|
+
type: "WordBoundary",
|
|
346
|
+
offset: currentOffset,
|
|
347
|
+
duration: currentDuration,
|
|
348
|
+
text: metaObj.Data.text?.Text,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
236
354
|
async generateSecMsGec(trustedClientToken) {
|
|
237
355
|
const ticks = Math.floor(Date.now() / 1000) + 11644473600;
|
|
238
356
|
const rounded = ticks - (ticks % 300);
|
|
@@ -282,5 +400,15 @@ class EdgeTTS {
|
|
|
282
400
|
}
|
|
283
401
|
return buffer_1.Buffer.concat(this.audio_stream);
|
|
284
402
|
}
|
|
403
|
+
async saveMetadata(outputPath) {
|
|
404
|
+
if (this.word_boundaries.length === 0) {
|
|
405
|
+
throw new Error("No metadata available to save.");
|
|
406
|
+
}
|
|
407
|
+
const json = JSON.stringify(this.word_boundaries, null, 4);
|
|
408
|
+
await (0, promises_1.writeFile)(outputPath, json);
|
|
409
|
+
}
|
|
410
|
+
getWordBoundaries() {
|
|
411
|
+
return this.word_boundaries;
|
|
412
|
+
}
|
|
285
413
|
}
|
|
286
414
|
exports.EdgeTTS = EdgeTTS;
|
package/package.json
CHANGED
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@andresaya/edge-tts",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Edge TTS is a package that allows access to the online text-to-speech service used by Microsoft Edge without the need for Microsoft Edge, Windows, or an API key.",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"keywords": [
|
|
7
|
-
"edge",
|
|
8
|
-
"tts",
|
|
9
|
-
"text-to-speech",
|
|
10
|
-
"microsoft",
|
|
11
|
-
"microsoft edge",
|
|
12
|
-
"speech-synthesis"
|
|
13
|
-
],
|
|
14
|
-
"author": "Andres Aya",
|
|
15
|
-
"license": "GPL-3.0-only",
|
|
16
|
-
"homepage": "https://github.com/andresayac/edge-tts",
|
|
17
|
-
"bugs": {
|
|
18
|
-
"url": "https://github.com/andresayac/edge-tts/issues"
|
|
19
|
-
},
|
|
20
|
-
"scripts": {
|
|
21
|
-
"build": "tsc",
|
|
22
|
-
"build:bin": "bun scripts/build.ts"
|
|
23
|
-
},
|
|
24
|
-
"devDependencies": {
|
|
25
|
-
"@types/bun": "latest",
|
|
26
|
-
"@types/ws": "^8.18.1",
|
|
27
|
-
"typescript": "^5.6.2"
|
|
28
|
-
},
|
|
29
|
-
"dependencies": {
|
|
30
|
-
"@types/uuid": "^10.0.0",
|
|
31
|
-
"commander": "^12.1.0",
|
|
32
|
-
"events": "^3.3.0",
|
|
33
|
-
"fs": "^0.0.1-security",
|
|
34
|
-
"ws": "^8.18.0"
|
|
35
|
-
},
|
|
36
|
-
"peerDependencies": {
|
|
37
|
-
"typescript": "^5.0.0"
|
|
38
|
-
},
|
|
39
|
-
"files": [
|
|
40
|
-
"dist"
|
|
41
|
-
],
|
|
42
|
-
"bin": {
|
|
43
|
-
"edge-tts": "./dist/cli/edge-tts.js"
|
|
44
|
-
},
|
|
45
|
-
"engines": {
|
|
46
|
-
"bun": ">=0.5.0"
|
|
47
|
-
}
|
|
48
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@andresaya/edge-tts",
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Edge TTS is a package that allows access to the online text-to-speech service used by Microsoft Edge without the need for Microsoft Edge, Windows, or an API key.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"edge",
|
|
8
|
+
"tts",
|
|
9
|
+
"text-to-speech",
|
|
10
|
+
"microsoft",
|
|
11
|
+
"microsoft edge",
|
|
12
|
+
"speech-synthesis"
|
|
13
|
+
],
|
|
14
|
+
"author": "Andres Aya",
|
|
15
|
+
"license": "GPL-3.0-only",
|
|
16
|
+
"homepage": "https://github.com/andresayac/edge-tts",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/andresayac/edge-tts/issues"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsc",
|
|
22
|
+
"build:bin": "bun scripts/build.ts"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/bun": "latest",
|
|
26
|
+
"@types/ws": "^8.18.1",
|
|
27
|
+
"typescript": "^5.6.2"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@types/uuid": "^10.0.0",
|
|
31
|
+
"commander": "^12.1.0",
|
|
32
|
+
"events": "^3.3.0",
|
|
33
|
+
"fs": "^0.0.1-security",
|
|
34
|
+
"ws": "^8.18.0"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"typescript": "^5.0.0"
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist"
|
|
41
|
+
],
|
|
42
|
+
"bin": {
|
|
43
|
+
"edge-tts": "./dist/cli/edge-tts.js"
|
|
44
|
+
},
|
|
45
|
+
"engines": {
|
|
46
|
+
"bun": ">=0.5.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/dist/config/formats.d.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
export declare const AUDIO_FORMATS_STREAM: {
|
|
2
|
-
readonly 'raw-8khz-8bit-mono-alaw': "audio/basic";
|
|
3
|
-
readonly 'raw-8khz-8bit-mono-mulaw': "audio/basic";
|
|
4
|
-
readonly 'raw-8khz-16bit-mono-pcm': "audio/basic";
|
|
5
|
-
readonly 'raw-16khz-16bit-mono-pcm': "audio/basic";
|
|
6
|
-
readonly 'raw-22050hz-16bit-mono-pcm': "audio/basic";
|
|
7
|
-
readonly 'raw-24khz-16bit-mono-pcm': "audio/basic";
|
|
8
|
-
readonly 'raw-44100hz-16bit-mono-pcm': "audio/basic";
|
|
9
|
-
readonly 'raw-48khz-16bit-mono-pcm': "audio/basic";
|
|
10
|
-
readonly 'raw-16khz-16bit-mono-truesilk': "audio/SILK";
|
|
11
|
-
readonly 'raw-24khz-16bit-mono-truesilk': "audio/SILK";
|
|
12
|
-
readonly 'audio-16khz-32kbitrate-mono-mp3': "audio/mpeg";
|
|
13
|
-
readonly 'audio-16khz-64kbitrate-mono-mp3': "audio/mpeg";
|
|
14
|
-
readonly 'audio-16khz-128kbitrate-mono-mp3': "audio/mpeg";
|
|
15
|
-
readonly 'audio-24khz-48kbitrate-mono-mp3': "audio/mpeg";
|
|
16
|
-
readonly 'audio-24khz-96kbitrate-mono-mp3': "audio/mpeg";
|
|
17
|
-
readonly 'audio-24khz-160kbitrate-mono-mp3': "audio/mpeg";
|
|
18
|
-
readonly 'audio-48khz-96kbitrate-mono-mp3': "audio/mpeg";
|
|
19
|
-
readonly 'audio-48khz-192kbitrate-mono-mp3': "audio/mpeg";
|
|
20
|
-
readonly 'audio-16khz-16bit-32kbps-mono-opus': "audio/opus";
|
|
21
|
-
readonly 'audio-24khz-16bit-24kbps-mono-opus': "audio/opus";
|
|
22
|
-
readonly 'audio-24khz-16bit-48kbps-mono-opus': "audio/opus";
|
|
23
|
-
readonly 'ogg-16khz-16bit-mono-opus': "audio/ogg; codecs=opus; rate=16000";
|
|
24
|
-
readonly 'ogg-24khz-16bit-mono-opus': "audio/ogg; codecs=opus; rate=24000";
|
|
25
|
-
readonly 'ogg-48khz-16bit-mono-opus': "audio/ogg; codecs=opus; rate=48000";
|
|
26
|
-
readonly 'webm-16khz-16bit-mono-opus': "audio/webm; codec=opus";
|
|
27
|
-
readonly 'webm-24khz-16bit-mono-opus': "audio/webm; codec=opus";
|
|
28
|
-
readonly 'webm-24khz-16bit-24kbps-mono-opus': "audio/webm; codec=opus";
|
|
29
|
-
readonly 'amr-wb-16000hz': "audio/amr-wb";
|
|
30
|
-
readonly 'g722-16khz-64kbps': "audio/g722";
|
|
31
|
-
};
|
|
32
|
-
export declare const AUDIO_FORMATS_NON_STREAM: {
|
|
33
|
-
readonly 'riff-8khz-8bit-mono-alaw': "audio/x-wav";
|
|
34
|
-
readonly 'riff-8khz-8bit-mono-mulaw': "audio/x-wav";
|
|
35
|
-
readonly 'riff-8khz-16bit-mono-pcm': "audio/x-wav";
|
|
36
|
-
readonly 'riff-22050hz-16bit-mono-pcm': "audio/x-wav";
|
|
37
|
-
readonly 'riff-24khz-16bit-mono-pcm': "audio/x-wav";
|
|
38
|
-
readonly 'riff-44100hz-16bit-mono-pcm': "audio/x-wav";
|
|
39
|
-
readonly 'riff-48khz-16bit-mono-pcm': "audio/x-wav";
|
|
40
|
-
};
|
|
41
|
-
type AudioOutputFormat = keyof typeof AUDIO_FORMATS_STREAM;
|
|
42
|
-
declare const ALL_AUDIO_FORMATS: {
|
|
43
|
-
'riff-8khz-8bit-mono-alaw': "audio/x-wav";
|
|
44
|
-
'riff-8khz-8bit-mono-mulaw': "audio/x-wav";
|
|
45
|
-
'riff-8khz-16bit-mono-pcm': "audio/x-wav";
|
|
46
|
-
'riff-22050hz-16bit-mono-pcm': "audio/x-wav";
|
|
47
|
-
'riff-24khz-16bit-mono-pcm': "audio/x-wav";
|
|
48
|
-
'riff-44100hz-16bit-mono-pcm': "audio/x-wav";
|
|
49
|
-
'riff-48khz-16bit-mono-pcm': "audio/x-wav";
|
|
50
|
-
'raw-8khz-8bit-mono-alaw': "audio/basic";
|
|
51
|
-
'raw-8khz-8bit-mono-mulaw': "audio/basic";
|
|
52
|
-
'raw-8khz-16bit-mono-pcm': "audio/basic";
|
|
53
|
-
'raw-16khz-16bit-mono-pcm': "audio/basic";
|
|
54
|
-
'raw-22050hz-16bit-mono-pcm': "audio/basic";
|
|
55
|
-
'raw-24khz-16bit-mono-pcm': "audio/basic";
|
|
56
|
-
'raw-44100hz-16bit-mono-pcm': "audio/basic";
|
|
57
|
-
'raw-48khz-16bit-mono-pcm': "audio/basic";
|
|
58
|
-
'raw-16khz-16bit-mono-truesilk': "audio/SILK";
|
|
59
|
-
'raw-24khz-16bit-mono-truesilk': "audio/SILK";
|
|
60
|
-
'audio-16khz-32kbitrate-mono-mp3': "audio/mpeg";
|
|
61
|
-
'audio-16khz-64kbitrate-mono-mp3': "audio/mpeg";
|
|
62
|
-
'audio-16khz-128kbitrate-mono-mp3': "audio/mpeg";
|
|
63
|
-
'audio-24khz-48kbitrate-mono-mp3': "audio/mpeg";
|
|
64
|
-
'audio-24khz-96kbitrate-mono-mp3': "audio/mpeg";
|
|
65
|
-
'audio-24khz-160kbitrate-mono-mp3': "audio/mpeg";
|
|
66
|
-
'audio-48khz-96kbitrate-mono-mp3': "audio/mpeg";
|
|
67
|
-
'audio-48khz-192kbitrate-mono-mp3': "audio/mpeg";
|
|
68
|
-
'audio-16khz-16bit-32kbps-mono-opus': "audio/opus";
|
|
69
|
-
'audio-24khz-16bit-24kbps-mono-opus': "audio/opus";
|
|
70
|
-
'audio-24khz-16bit-48kbps-mono-opus': "audio/opus";
|
|
71
|
-
'ogg-16khz-16bit-mono-opus': "audio/ogg; codecs=opus; rate=16000";
|
|
72
|
-
'ogg-24khz-16bit-mono-opus': "audio/ogg; codecs=opus; rate=24000";
|
|
73
|
-
'ogg-48khz-16bit-mono-opus': "audio/ogg; codecs=opus; rate=48000";
|
|
74
|
-
'webm-16khz-16bit-mono-opus': "audio/webm; codec=opus";
|
|
75
|
-
'webm-24khz-16bit-mono-opus': "audio/webm; codec=opus";
|
|
76
|
-
'webm-24khz-16bit-24kbps-mono-opus': "audio/webm; codec=opus";
|
|
77
|
-
'amr-wb-16000hz': "audio/amr-wb";
|
|
78
|
-
'g722-16khz-64kbps': "audio/g722";
|
|
79
|
-
};
|
|
80
|
-
declare const FORMAT_CONTENT_TYPE: Map<string, "audio/basic" | "audio/SILK" | "audio/mpeg" | "audio/opus" | "audio/ogg; codecs=opus; rate=16000" | "audio/ogg; codecs=opus; rate=24000" | "audio/ogg; codecs=opus; rate=48000" | "audio/webm; codec=opus" | "audio/amr-wb" | "audio/g722" | "audio/x-wav">;
|
|
81
|
-
export { ALL_AUDIO_FORMATS, FORMAT_CONTENT_TYPE, AudioOutputFormat };
|
package/dist/config/formats.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FORMAT_CONTENT_TYPE = exports.ALL_AUDIO_FORMATS = exports.AUDIO_FORMATS_NON_STREAM = exports.AUDIO_FORMATS_STREAM = void 0;
|
|
4
|
-
exports.AUDIO_FORMATS_STREAM = {
|
|
5
|
-
// RAW PCM
|
|
6
|
-
'raw-8khz-8bit-mono-alaw': 'audio/basic',
|
|
7
|
-
'raw-8khz-8bit-mono-mulaw': 'audio/basic',
|
|
8
|
-
'raw-8khz-16bit-mono-pcm': 'audio/basic',
|
|
9
|
-
'raw-16khz-16bit-mono-pcm': 'audio/basic',
|
|
10
|
-
'raw-22050hz-16bit-mono-pcm': 'audio/basic',
|
|
11
|
-
'raw-24khz-16bit-mono-pcm': 'audio/basic',
|
|
12
|
-
'raw-44100hz-16bit-mono-pcm': 'audio/basic',
|
|
13
|
-
'raw-48khz-16bit-mono-pcm': 'audio/basic',
|
|
14
|
-
// TRUESILK
|
|
15
|
-
'raw-16khz-16bit-mono-truesilk': 'audio/SILK',
|
|
16
|
-
'raw-24khz-16bit-mono-truesilk': 'audio/SILK',
|
|
17
|
-
// MP3
|
|
18
|
-
'audio-16khz-32kbitrate-mono-mp3': 'audio/mpeg',
|
|
19
|
-
'audio-16khz-64kbitrate-mono-mp3': 'audio/mpeg',
|
|
20
|
-
'audio-16khz-128kbitrate-mono-mp3': 'audio/mpeg',
|
|
21
|
-
'audio-24khz-48kbitrate-mono-mp3': 'audio/mpeg',
|
|
22
|
-
'audio-24khz-96kbitrate-mono-mp3': 'audio/mpeg',
|
|
23
|
-
'audio-24khz-160kbitrate-mono-mp3': 'audio/mpeg',
|
|
24
|
-
'audio-48khz-96kbitrate-mono-mp3': 'audio/mpeg',
|
|
25
|
-
'audio-48khz-192kbitrate-mono-mp3': 'audio/mpeg',
|
|
26
|
-
// OPUS in container
|
|
27
|
-
'audio-16khz-16bit-32kbps-mono-opus': 'audio/opus',
|
|
28
|
-
'audio-24khz-16bit-24kbps-mono-opus': 'audio/opus',
|
|
29
|
-
'audio-24khz-16bit-48kbps-mono-opus': 'audio/opus',
|
|
30
|
-
// OGG
|
|
31
|
-
'ogg-16khz-16bit-mono-opus': 'audio/ogg; codecs=opus; rate=16000',
|
|
32
|
-
'ogg-24khz-16bit-mono-opus': 'audio/ogg; codecs=opus; rate=24000',
|
|
33
|
-
'ogg-48khz-16bit-mono-opus': 'audio/ogg; codecs=opus; rate=48000',
|
|
34
|
-
// WEBM
|
|
35
|
-
'webm-16khz-16bit-mono-opus': 'audio/webm; codec=opus',
|
|
36
|
-
'webm-24khz-16bit-mono-opus': 'audio/webm; codec=opus',
|
|
37
|
-
'webm-24khz-16bit-24kbps-mono-opus': 'audio/webm; codec=opus',
|
|
38
|
-
'amr-wb-16000hz': 'audio/amr-wb',
|
|
39
|
-
'g722-16khz-64kbps': 'audio/g722'
|
|
40
|
-
};
|
|
41
|
-
exports.AUDIO_FORMATS_NON_STREAM = {
|
|
42
|
-
// RIFF (WAV)
|
|
43
|
-
'riff-8khz-8bit-mono-alaw': 'audio/x-wav',
|
|
44
|
-
'riff-8khz-8bit-mono-mulaw': 'audio/x-wav',
|
|
45
|
-
'riff-8khz-16bit-mono-pcm': 'audio/x-wav',
|
|
46
|
-
'riff-22050hz-16bit-mono-pcm': 'audio/x-wav',
|
|
47
|
-
'riff-24khz-16bit-mono-pcm': 'audio/x-wav',
|
|
48
|
-
'riff-44100hz-16bit-mono-pcm': 'audio/x-wav',
|
|
49
|
-
'riff-48khz-16bit-mono-pcm': 'audio/x-wav',
|
|
50
|
-
};
|
|
51
|
-
const ALL_AUDIO_FORMATS = { ...exports.AUDIO_FORMATS_STREAM, ...exports.AUDIO_FORMATS_NON_STREAM };
|
|
52
|
-
exports.ALL_AUDIO_FORMATS = ALL_AUDIO_FORMATS;
|
|
53
|
-
const FORMAT_CONTENT_TYPE = new Map(Object.entries(ALL_AUDIO_FORMATS));
|
|
54
|
-
exports.FORMAT_CONTENT_TYPE = FORMAT_CONTENT_TYPE;
|