@andresaya/edge-tts 1.3.1 → 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 +62 -0
- package/dist/cli/edge-tts.js +1 -1
- package/dist/commands/SynthesizeCommand.js +29 -6
- package/dist/config/constants.d.ts +2 -0
- package/dist/config/constants.js +4 -2
- package/dist/services/EdgeTTS.d.ts +15 -0
- package/dist/services/EdgeTTS.js +165 -13
- package/package.json +48 -48
package/README.md
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Edge TTS
|
|
2
|
+
[](https://deepwiki.com/andresayac/edge-tts)
|
|
2
3
|
|
|
3
4
|
**Edge TTS** is a powerful Text-to-Speech (TTS) package that leverages Microsoft's Edge capabilities. This package allows you to synthesize speech from text and manage voice options easily through a command-line interface (CLI).
|
|
4
5
|
|
|
@@ -10,8 +11,10 @@
|
|
|
10
11
|
- **Audio Information**: Get detailed information about generated audio (size, duration, format).
|
|
11
12
|
- **Audio Export Options**: Export synthesized audio in different formats (raw, base64, or directly to a file).
|
|
12
13
|
- **Streaming Support**: Stream audio data in real-time for better performance.
|
|
14
|
+
- **Word Boundaries Metadata**: Get word boundary information with precise timestamps.
|
|
13
15
|
- **Command-Line Interface**: Use a simple CLI for easy access to functionality.
|
|
14
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.
|
|
15
18
|
|
|
16
19
|
## Installation
|
|
17
20
|
|
|
@@ -39,6 +42,11 @@ To synthesize speech from text:
|
|
|
39
42
|
edge-tts synthesize -t "Hello, world!" -o hello_world_audio
|
|
40
43
|
```
|
|
41
44
|
|
|
45
|
+
From file in format SSML:
|
|
46
|
+
```bash
|
|
47
|
+
edge-tts synthesize -f ssml.txt --ssml -o salida
|
|
48
|
+
```
|
|
49
|
+
|
|
42
50
|
To list available voices:
|
|
43
51
|
```bash
|
|
44
52
|
edge-tts voice-list
|
|
@@ -83,6 +91,49 @@ const maleVoices = await tts.getVoicesByGender('Male');
|
|
|
83
91
|
|
|
84
92
|
### Text Synthesis
|
|
85
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
|
+
|
|
86
137
|
#### Basic Synthesis
|
|
87
138
|
```js
|
|
88
139
|
// Simple synthesis with default voice
|
|
@@ -150,6 +201,17 @@ console.log(`Audio saved to: ${filePath}`);
|
|
|
150
201
|
// Creates: output_audio.mp3
|
|
151
202
|
```
|
|
152
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
|
+
|
|
153
215
|
## Examples
|
|
154
216
|
|
|
155
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,6 +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://
|
|
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'
|
|
8
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,14 @@ 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;
|
|
42
|
+
private generateSecMsGec;
|
|
30
43
|
getDuration(): number;
|
|
31
44
|
getAudioInfo(): {
|
|
32
45
|
size: number;
|
|
@@ -37,4 +50,6 @@ export declare class EdgeTTS {
|
|
|
37
50
|
toRaw(): string;
|
|
38
51
|
toBase64(): string;
|
|
39
52
|
toBuffer(): Buffer;
|
|
53
|
+
saveMetadata(outputPath: string): Promise<void>;
|
|
54
|
+
getWordBoundaries(): WordBoundary[];
|
|
40
55
|
}
|
package/dist/services/EdgeTTS.js
CHANGED
|
@@ -26,13 +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}`, {
|
|
34
|
+
headers: {
|
|
35
|
+
"User-Agent": constants_1.Constants.USER_AGENT
|
|
36
|
+
},
|
|
37
|
+
});
|
|
32
38
|
const data = await response.json();
|
|
33
39
|
return data.map((voice) => {
|
|
34
|
-
|
|
35
|
-
delete voice.
|
|
40
|
+
voice.FriendlyName = voice.FriendlyName || voice.LocalName;
|
|
41
|
+
delete voice.SampleRateHertz;
|
|
36
42
|
delete voice.Status;
|
|
37
43
|
return voice;
|
|
38
44
|
});
|
|
@@ -92,10 +98,16 @@ class EdgeTTS {
|
|
|
92
98
|
return `${volumeValue}%`;
|
|
93
99
|
}
|
|
94
100
|
async synthesize(text, voice = 'en-US-AnaNeural', options = {}) {
|
|
101
|
+
const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
|
|
95
102
|
return new Promise((resolve, reject) => {
|
|
96
103
|
this.audio_stream = [];
|
|
97
|
-
const
|
|
98
|
-
|
|
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}`;
|
|
106
|
+
this.ws = new ws_1.default(url, {
|
|
107
|
+
headers: {
|
|
108
|
+
"User-Agent": constants_1.Constants.USER_AGENT
|
|
109
|
+
}
|
|
110
|
+
});
|
|
99
111
|
const SSML_text = this.getSSML(text, voice, options);
|
|
100
112
|
const timeout = setTimeout(() => {
|
|
101
113
|
if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
|
|
@@ -106,7 +118,7 @@ class EdgeTTS {
|
|
|
106
118
|
this.ws.on('open', () => {
|
|
107
119
|
const message = this.buildTTSConfigMessage();
|
|
108
120
|
this.ws.send(message);
|
|
109
|
-
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}`;
|
|
110
122
|
this.ws.send(speechMessage);
|
|
111
123
|
});
|
|
112
124
|
this.ws.on('message', (data) => {
|
|
@@ -125,14 +137,83 @@ class EdgeTTS {
|
|
|
125
137
|
});
|
|
126
138
|
});
|
|
127
139
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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;
|
|
131
204
|
}
|
|
132
205
|
const pitch = this.validatePitch(options.pitch ?? 0);
|
|
133
206
|
const rate = this.validateRate(options.rate ?? 0);
|
|
134
207
|
const volume = this.validateVolume(options.volume ?? 0);
|
|
135
|
-
|
|
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
|
+
`;
|
|
136
217
|
}
|
|
137
218
|
buildTTSConfigMessage() {
|
|
138
219
|
return `X-Timestamp:${new Date().toISOString()}Z\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n` +
|
|
@@ -140,8 +221,14 @@ class EdgeTTS {
|
|
|
140
221
|
}
|
|
141
222
|
async *synthesizeStream(text, voice = 'en-US-AnaNeural', options = {}) {
|
|
142
223
|
this.audio_stream = [];
|
|
143
|
-
const
|
|
144
|
-
|
|
224
|
+
const reqId = this.generateUUID();
|
|
225
|
+
const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
|
|
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
|
+
});
|
|
145
232
|
const SSML_text = this.getSSML(text, voice, options);
|
|
146
233
|
const queue = [];
|
|
147
234
|
let done = false;
|
|
@@ -162,7 +249,7 @@ class EdgeTTS {
|
|
|
162
249
|
this.ws.on('open', () => {
|
|
163
250
|
const message = this.buildTTSConfigMessage();
|
|
164
251
|
this.ws.send(message);
|
|
165
|
-
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}`;
|
|
166
253
|
this.ws.send(speechMessage);
|
|
167
254
|
});
|
|
168
255
|
this.ws.on('message', (data) => {
|
|
@@ -175,6 +262,15 @@ class EdgeTTS {
|
|
|
175
262
|
this.audio_stream.push(chunk);
|
|
176
263
|
push(chunk);
|
|
177
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
|
+
}
|
|
178
274
|
if (buffer.toString().includes('Path:turn.end')) {
|
|
179
275
|
this.ws?.close();
|
|
180
276
|
}
|
|
@@ -217,10 +313,56 @@ class EdgeTTS {
|
|
|
217
313
|
const audioChunk = buffer.subarray(audioStartIndex + needle.length);
|
|
218
314
|
this.audio_stream.push(new Uint8Array(audioChunk));
|
|
219
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
|
+
}
|
|
220
325
|
if (buffer.toString().includes("Path:turn.end")) {
|
|
221
326
|
this.ws?.close();
|
|
222
327
|
}
|
|
223
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
|
+
}
|
|
354
|
+
async generateSecMsGec(trustedClientToken) {
|
|
355
|
+
const ticks = Math.floor(Date.now() / 1000) + 11644473600;
|
|
356
|
+
const rounded = ticks - (ticks % 300);
|
|
357
|
+
const windowsTicks = rounded * 10000000;
|
|
358
|
+
const encoder = new TextEncoder();
|
|
359
|
+
const data = encoder.encode(`${windowsTicks}${trustedClientToken}`);
|
|
360
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
|
361
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
362
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
363
|
+
.join('')
|
|
364
|
+
.toUpperCase();
|
|
365
|
+
}
|
|
224
366
|
getDuration() {
|
|
225
367
|
if (this.audio_stream.length === 0) {
|
|
226
368
|
throw new Error("No audio data available");
|
|
@@ -258,5 +400,15 @@ class EdgeTTS {
|
|
|
258
400
|
}
|
|
259
401
|
return buffer_1.Buffer.concat(this.audio_stream);
|
|
260
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
|
+
}
|
|
261
413
|
}
|
|
262
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
|
+
}
|