@andresaya/edge-tts 1.2.5 → 1.2.6
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.
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { SynthesizeCommand } from '../commands/SynthesizeCommand';
|
|
3
|
+
import { VoiceListCommand } from '../commands/VoiceListCommand';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
const program = new Command();
|
|
6
|
+
program
|
|
7
|
+
.version('1.2.6')
|
|
8
|
+
.addCommand(SynthesizeCommand)
|
|
9
|
+
.addCommand(VoiceListCommand);
|
|
10
|
+
try {
|
|
11
|
+
program.parse();
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
console.error(`Error: ${error.message}`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { EdgeTTS } from '../services/EdgeTTS';
|
|
3
|
+
export const SynthesizeCommand = new Command('synthesize')
|
|
4
|
+
.description('Edge TTS: synthesize text to audio')
|
|
5
|
+
.option('-t, --text <text>', 'Text to convert to audio')
|
|
6
|
+
.option('-v, --voice [voice]', 'Voice to use for the audio synthesis', 'en-US-AriaNeural')
|
|
7
|
+
.option('-r, --rate [rate]', 'Rate of speech', '0%')
|
|
8
|
+
.option('-l, --volume [volume]', 'Volume of speech', '0%')
|
|
9
|
+
.option('-p, --pitch [pitch]', 'Pitch of speech', '0Hz')
|
|
10
|
+
.option('-o, --output [output]', 'Output file name', `output_${Date.now()}`)
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
const { text, voice, pitch, rate, volume, output } = options;
|
|
13
|
+
if (!text) {
|
|
14
|
+
console.error('Text is required');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const tts = new EdgeTTS();
|
|
18
|
+
await tts.synthesize(text, voice, { pitch, rate, volume });
|
|
19
|
+
await tts.toFile(`${output}`);
|
|
20
|
+
console.log(`Audio file generated: ${output}.mp3`);
|
|
21
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { EdgeTTS } from '../services/EdgeTTS';
|
|
3
|
+
export const VoiceListCommand = new Command('voice-list')
|
|
4
|
+
.description('Get the list of available voices')
|
|
5
|
+
.action(async () => {
|
|
6
|
+
const tts = new EdgeTTS();
|
|
7
|
+
const voices = await tts.getVoices();
|
|
8
|
+
console.log('Lista de voces disponibles:');
|
|
9
|
+
voices.forEach((voice) => {
|
|
10
|
+
console.log(` - ${voice.ShortName}`);
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const Constants = {
|
|
2
|
+
TRUSTED_CLIENT_TOKEN: '6A5AA1D4EAFF4E9FB37E23D68491D6F4',
|
|
3
|
+
WSS_URL: 'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1',
|
|
4
|
+
VOICES_URL: 'https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list',
|
|
5
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { EdgeTTS } from './services/EdgeTTS';
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import WebSocket, {} from 'ws';
|
|
2
|
+
import { Constants } from '../config/constants';
|
|
3
|
+
import { writeFile } from 'fs/promises';
|
|
4
|
+
import { Buffer } from 'buffer';
|
|
5
|
+
function ensureBuffer(data) {
|
|
6
|
+
if (Buffer.isBuffer(data)) {
|
|
7
|
+
return data;
|
|
8
|
+
}
|
|
9
|
+
if (data instanceof ArrayBuffer) {
|
|
10
|
+
return Buffer.from(data);
|
|
11
|
+
}
|
|
12
|
+
if (Array.isArray(data)) {
|
|
13
|
+
return Buffer.concat(data);
|
|
14
|
+
}
|
|
15
|
+
if (typeof data === 'string') {
|
|
16
|
+
return Buffer.from(data, 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Unsupported RawData type: ${typeof data}`);
|
|
19
|
+
}
|
|
20
|
+
export class EdgeTTS {
|
|
21
|
+
audio_stream = [];
|
|
22
|
+
audio_format = 'mp3';
|
|
23
|
+
ws;
|
|
24
|
+
async getVoices() {
|
|
25
|
+
const response = await fetch(`${Constants.VOICES_URL}?trustedclienttoken=${Constants.TRUSTED_CLIENT_TOKEN}`);
|
|
26
|
+
const data = await response.json();
|
|
27
|
+
return data.map((voice) => {
|
|
28
|
+
delete voice.VoiceTag;
|
|
29
|
+
delete voice.SuggestedCodec;
|
|
30
|
+
delete voice.Status;
|
|
31
|
+
return voice;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
generateUUID() {
|
|
35
|
+
return 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
36
|
+
const r = Math.random() * 16 | 0;
|
|
37
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
38
|
+
return v.toString(16);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
validatePitch(pitch) {
|
|
42
|
+
if (typeof pitch === 'number') {
|
|
43
|
+
return (pitch >= 0 ? `+${pitch}Hz` : `${pitch}Hz`);
|
|
44
|
+
}
|
|
45
|
+
if (!/^[+-]?\d{1,3}(?:\.\d+)?Hz$/.test(pitch)) {
|
|
46
|
+
throw new Error("Invalid pitch format. Expected format: '-100Hz to +100Hz' or a number.");
|
|
47
|
+
}
|
|
48
|
+
return pitch;
|
|
49
|
+
}
|
|
50
|
+
validateRate(rate) {
|
|
51
|
+
let rateValue;
|
|
52
|
+
if (typeof rate === 'string') {
|
|
53
|
+
rateValue = parseFloat(rate.replace('%', ''));
|
|
54
|
+
if (isNaN(rateValue))
|
|
55
|
+
throw new Error("Invalid rate format.");
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
rateValue = rate;
|
|
59
|
+
}
|
|
60
|
+
if (rateValue >= 0) {
|
|
61
|
+
return `+${rateValue}%`;
|
|
62
|
+
}
|
|
63
|
+
return `${rateValue}%`;
|
|
64
|
+
}
|
|
65
|
+
validateVolume(volume) {
|
|
66
|
+
let volumeValue;
|
|
67
|
+
if (typeof volume === 'string') {
|
|
68
|
+
volumeValue = parseInt(volume.replace('%', ''), 10);
|
|
69
|
+
if (isNaN(volumeValue))
|
|
70
|
+
throw new Error("Invalid volume format.");
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
volumeValue = volume;
|
|
74
|
+
}
|
|
75
|
+
if (volumeValue < -100 || volumeValue > 100) {
|
|
76
|
+
throw new Error("Volume cannot be negative. Expected a value from -100% to 100% (or more).");
|
|
77
|
+
}
|
|
78
|
+
return `${volumeValue}%`;
|
|
79
|
+
}
|
|
80
|
+
async synthesize(text, voice = 'en-US-AnaNeural', options = {}) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
this.audio_stream = [];
|
|
83
|
+
const req_id = this.generateUUID();
|
|
84
|
+
this.ws = new WebSocket(`${Constants.WSS_URL}?trustedclienttoken=${Constants.TRUSTED_CLIENT_TOKEN}&ConnectionId=${req_id}`);
|
|
85
|
+
const SSML_text = this.getSSML(text, voice, options);
|
|
86
|
+
const timeout = setTimeout(() => {
|
|
87
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
88
|
+
this.ws.close();
|
|
89
|
+
}
|
|
90
|
+
reject(new Error("Synthesis timeout"));
|
|
91
|
+
}, 30000);
|
|
92
|
+
this.ws.on('open', () => {
|
|
93
|
+
const message = this.buildTTSConfigMessage();
|
|
94
|
+
this.ws.send(message);
|
|
95
|
+
const speechMessage = `X-RequestId:${req_id}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${new Date().toISOString()}Z\r\nPath:ssml\r\n\r\n${SSML_text}`;
|
|
96
|
+
this.ws.send(speechMessage);
|
|
97
|
+
});
|
|
98
|
+
this.ws.on('message', (data) => {
|
|
99
|
+
this.processAudioData(data);
|
|
100
|
+
});
|
|
101
|
+
this.ws.on('error', (err) => {
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
104
|
+
this.ws.close();
|
|
105
|
+
}
|
|
106
|
+
reject(err);
|
|
107
|
+
});
|
|
108
|
+
this.ws.on('close', () => {
|
|
109
|
+
clearTimeout(timeout);
|
|
110
|
+
resolve();
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
getSSML(text, voice, options = {}) {
|
|
115
|
+
if (typeof options.pitch === 'string') {
|
|
116
|
+
options.pitch = options.pitch.replace('hz', 'Hz');
|
|
117
|
+
}
|
|
118
|
+
const pitch = this.validatePitch(options.pitch ?? 0);
|
|
119
|
+
const rate = this.validateRate(options.rate ?? 0);
|
|
120
|
+
const volume = this.validateVolume(options.volume ?? 0);
|
|
121
|
+
return `<speak version='1.0' xml:lang='en-US'><voice name='${voice}'><prosody pitch='${pitch}' rate='${rate}' volume='${volume}'>${text}</prosody></voice></speak>`;
|
|
122
|
+
}
|
|
123
|
+
buildTTSConfigMessage() {
|
|
124
|
+
return `X-Timestamp:${new Date().toISOString()}Z\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n` +
|
|
125
|
+
`{"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":false,"wordBoundaryEnabled":true},"outputFormat":"audio-24khz-48kbitrate-mono-mp3"}}}}`;
|
|
126
|
+
}
|
|
127
|
+
processAudioData(data) {
|
|
128
|
+
const buffer = ensureBuffer(data);
|
|
129
|
+
const needle = Buffer.from("Path:audio\r\n");
|
|
130
|
+
const audioStartIndex = buffer.indexOf(new Uint8Array(needle));
|
|
131
|
+
if (audioStartIndex !== -1) {
|
|
132
|
+
const audioChunk = buffer.subarray(audioStartIndex + needle.length);
|
|
133
|
+
this.audio_stream.push(new Uint8Array(audioChunk));
|
|
134
|
+
}
|
|
135
|
+
if (buffer.toString().includes("Path:turn.end")) {
|
|
136
|
+
this.ws?.close();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async toFile(outputPath) {
|
|
140
|
+
const audioBuffer = this.toBuffer();
|
|
141
|
+
const finalPath = `${outputPath}.${this.audio_format}`;
|
|
142
|
+
await writeFile(finalPath, new Uint8Array(audioBuffer));
|
|
143
|
+
return finalPath;
|
|
144
|
+
}
|
|
145
|
+
toRaw() {
|
|
146
|
+
return this.toBase64();
|
|
147
|
+
}
|
|
148
|
+
toBase64() {
|
|
149
|
+
return this.toBuffer().toString('base64');
|
|
150
|
+
}
|
|
151
|
+
toBuffer() {
|
|
152
|
+
if (this.audio_stream.length === 0) {
|
|
153
|
+
throw new Error("No audio data available. Did you run synthesize() first?");
|
|
154
|
+
}
|
|
155
|
+
return Buffer.concat(this.audio_stream);
|
|
156
|
+
}
|
|
157
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@andresaya/edge-tts",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
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
5
|
"main": "dist/index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/bun": "latest",
|
|
26
|
+
"@types/ws": "^8.18.1",
|
|
26
27
|
"typescript": "^5.6.2"
|
|
27
28
|
},
|
|
28
29
|
"dependencies": {
|
|
@@ -44,4 +45,4 @@
|
|
|
44
45
|
"engines": {
|
|
45
46
|
"bun": ">=0.5.0"
|
|
46
47
|
}
|
|
47
|
-
}
|
|
48
|
+
}
|