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