@andresaya/edge-tts 1.7.1 → 1.8.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/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.EdgeTTS = void 0;
3
+ exports.Constants = exports.EdgeTTS = void 0;
4
4
  var EdgeTTS_1 = require("./services/EdgeTTS");
5
5
  Object.defineProperty(exports, "EdgeTTS", { enumerable: true, get: function () { return EdgeTTS_1.EdgeTTS; } });
6
+ var constants_1 = require("./config/constants");
7
+ Object.defineProperty(exports, "Constants", { enumerable: true, get: function () { return constants_1.Constants; } });
@@ -11,7 +11,6 @@ export interface SynthesisOptions {
11
11
  pitch?: string | number;
12
12
  rate?: string | number;
13
13
  volume?: string | number;
14
- inputType?: 'auto' | 'ssml' | 'text';
15
14
  outputFormat?: string;
16
15
  }
17
16
  export interface WordBoundary {
@@ -26,6 +25,7 @@ export declare class EdgeTTS {
26
25
  private output_format;
27
26
  private word_boundaries;
28
27
  private ws;
28
+ normalizeVoices(data: any[]): Promise<Voice[]>;
29
29
  getVoices(): Promise<Voice[]>;
30
30
  getVoicesByLanguage(locale: string): Promise<Voice[]>;
31
31
  getVoicesByGender(gender: 'Male' | 'Female'): Promise<Voice[]>;
@@ -34,7 +34,6 @@ export declare class EdgeTTS {
34
34
  private validateRate;
35
35
  private validateVolume;
36
36
  synthesize(text: string, voice?: string, options?: SynthesisOptions): Promise<void>;
37
- private detectSSML;
38
37
  private escapeXML;
39
38
  private getSSML;
40
39
  private nowRFC1123;
@@ -30,30 +30,88 @@ class EdgeTTS {
30
30
  output_format = 'audio-24khz-48kbitrate-mono-mp3';
31
31
  word_boundaries = [];
32
32
  ws;
33
+ async normalizeVoices(data) {
34
+ const out = [];
35
+ for (const v of data || []) {
36
+ const short = v?.ShortName || "";
37
+ const locale = v?.Locale || "";
38
+ // base: remove locale prefix and Neural/NeuralHD suffix
39
+ let base = short.replace(/^[a-z]{2}-[A-Z]{2}-/, "");
40
+ base = base.replace(/NeuralHD$/, "").replace(/Neural$/, "").trim();
41
+ // VoiceType: if NeuralHD/Neural in Name or ShortName
42
+ const mix = `${v?.Name || ""} ${short}`;
43
+ const voiceType = v?.VoiceType || (/NeuralHD/i.test(mix) ? "NeuralHD" : "Neural");
44
+ // LocaleName: prefer LocaleName -> LanguageName -> locale
45
+ const localeName = v?.LocaleName || (locale || null);
46
+ // DisplayName: prefer DisplayName -> FriendlyName -> base -> short
47
+ let display = v?.DisplayName || v?.FriendlyName || base || short;
48
+ display = display.replace(/^Microsoft\s+/i, "");
49
+ display = display.split(" - ")[0].trim();
50
+ display = display.replace(/\s*Online\s*\(Natural\)\s*/i, " ");
51
+ display = display.replace(/\s*Online\s*/i, " ");
52
+ display = display.replace(/\s+/g, " ").trim();
53
+ // VoiceTag parsing
54
+ const tag = (v?.VoiceTag && typeof v.VoiceTag === "object") ? v.VoiceTag : {};
55
+ const tailored = Array.isArray(tag.TailoredScenarios)
56
+ ? tag.TailoredScenarios
57
+ : (Array.isArray(tag.ContentCategories) ? tag.ContentCategories : []);
58
+ const personalities = Array.isArray(tag.VoicePersonalities)
59
+ ? tag.VoicePersonalities
60
+ : [];
61
+ out.push({
62
+ Name: short || (v?.Name || ""),
63
+ DisplayName: display,
64
+ LocalName: display,
65
+ ShortName: short || (v?.Name || ""),
66
+ Gender: v?.Gender ?? null,
67
+ Locale: locale || null,
68
+ LocaleName: localeName,
69
+ SecondaryLocaleList: Array.isArray(v?.SecondaryLocaleList) ? v.SecondaryLocaleList : [],
70
+ VoiceType: voiceType,
71
+ VoiceTag: {
72
+ TailoredScenarios: tailored,
73
+ VoicePersonalities: personalities,
74
+ },
75
+ FriendlyName: `${display} (${voiceType}) - ${localeName}`,
76
+ });
77
+ }
78
+ return out;
79
+ }
33
80
  async getVoices() {
34
81
  const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
35
- const httpsAgent = new https_1.default.Agent({
36
- rejectUnauthorized: false,
37
- });
38
- const response = await new Promise((resolve, reject) => {
39
- const req = https_1.default.request(`${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}`, {
40
- headers: constants_1.Constants.getBaseHeaders(),
41
- agent: httpsAgent
42
- }, (res) => {
43
- let data = '';
44
- res.on('data', (chunk) => (data += chunk));
45
- res.on('end', () => resolve(new Response(data, { status: res.statusCode || 200 })));
82
+ const httpsAgent = new https_1.default.Agent({ rejectUnauthorized: false });
83
+ const url = `${constants_1.Constants.VOICES_URL}` +
84
+ `?TrustedClientToken=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}` +
85
+ `&Sec-MS-GEC=${secMsGEC}` +
86
+ `&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}`;
87
+ const headers = {
88
+ ...constants_1.Constants.getBaseHeaders(),
89
+ "Accept-Encoding": "identity", // evita gzip/br/zstd
90
+ };
91
+ const voicesRaw = await new Promise((resolve, reject) => {
92
+ const req = https_1.default.request(url, { method: "GET", headers, agent: httpsAgent }, (res) => {
93
+ const chunks = [];
94
+ res.on("data", (chunk) => chunks.push(buffer_1.Buffer.isBuffer(chunk) ? chunk : buffer_1.Buffer.from(chunk)));
95
+ res.on("error", reject);
96
+ res.on("end", () => {
97
+ const body = buffer_1.Buffer.concat(chunks).toString("utf8");
98
+ try {
99
+ const parsed = JSON.parse(body);
100
+ // soporta array directo o { voices: [...] }
101
+ const voices = Array.isArray(parsed)
102
+ ? parsed
103
+ : (parsed.voices || parsed.Voices || []);
104
+ resolve(Array.isArray(voices) ? voices : []);
105
+ }
106
+ catch (e) {
107
+ reject(new Error("JSON inválido: " + (e?.message || String(e))));
108
+ }
109
+ });
46
110
  });
47
- req.on('error', reject);
111
+ req.on("error", reject);
48
112
  req.end();
49
113
  });
50
- const data = await response.json();
51
- return data.map((voice) => {
52
- voice.FriendlyName = voice.FriendlyName || voice.LocalName;
53
- delete voice.SampleRateHertz;
54
- delete voice.Status;
55
- return voice;
56
- });
114
+ return this.normalizeVoices(voicesRaw);
57
115
  }
58
116
  async getVoicesByLanguage(locale) {
59
117
  const voices = await this.getVoices();
@@ -114,7 +172,7 @@ class EdgeTTS {
114
172
  return new Promise((resolve, reject) => {
115
173
  this.audio_stream = [];
116
174
  const reqId = this.generateUUID();
117
- 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}`;
175
+ const url = `${constants_1.Constants.WSS_URL}?TrustedClientToken=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
118
176
  this.ws = new ws_1.default(url, {
119
177
  headers: constants_1.Constants.getBaseHeaders(),
120
178
  rejectUnauthorized: false
@@ -161,31 +219,6 @@ class EdgeTTS {
161
219
  });
162
220
  });
163
221
  }
164
- detectSSML(content) {
165
- const trimmedContent = content.trim();
166
- const looksLikeSSML = /^<\?xml|^<speak/i.test(trimmedContent);
167
- if (!looksLikeSSML) {
168
- return { isValid: true, isSSML: false };
169
- }
170
- const errors = [];
171
- const hasSpeakTag = /<speak\b[^>]*>[\s\S]*<\/speak>/i.test(trimmedContent);
172
- const hasVoiceTag = /<voice\b[^>]*>[\s\S]*<\/voice>/i.test(trimmedContent);
173
- if (!hasSpeakTag) {
174
- throw new Error('Invalid SSML: Missing <speak> tag');
175
- }
176
- if (!hasVoiceTag) {
177
- throw new Error('Invalid SSML: Missing <voice> tag');
178
- }
179
- const hasCorrectNamespace = /xmlns="http:\/\/www\.w3\.org\/2001\/10\/synthesis"/i.test(trimmedContent);
180
- if (!hasCorrectNamespace && hasSpeakTag) {
181
- throw new Error('Invalid SSML: Missing or incorrect namespace declaration');
182
- }
183
- return {
184
- isValid: errors.length === 0,
185
- isSSML: hasSpeakTag || hasVoiceTag,
186
- errors: errors.length > 0 ? errors : undefined
187
- };
188
- }
189
222
  escapeXML(text) {
190
223
  return text
191
224
  .replace(/&/g, '&amp;')
@@ -195,37 +228,6 @@ class EdgeTTS {
195
228
  .replace(/'/g, '&apos;');
196
229
  }
197
230
  getSSML(content, voice, options = {}) {
198
- const inputType = options.inputType || 'auto';
199
- let treatAsSSML = false;
200
- if (inputType === 'ssml') {
201
- treatAsSSML = true;
202
- }
203
- else if (inputType === 'text') {
204
- treatAsSSML = false;
205
- }
206
- else { // 'auto'
207
- const detection = this.detectSSML(content);
208
- treatAsSSML = detection.isSSML;
209
- if (detection.isSSML) {
210
- console.log('→ Detected SSML input');
211
- if (!detection.isValid) {
212
- console.warn('⚠ SSML validation warnings:', detection.errors);
213
- }
214
- }
215
- else {
216
- console.log('→ Detected plain text input');
217
- }
218
- }
219
- if (treatAsSSML) {
220
- let ssml = content.trim();
221
- if (!ssml.includes('xmlns=')) {
222
- ssml = ssml.replace(/<speak([^>]*)>/i, '<speak$1 xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts">');
223
- }
224
- if (!/<voice\b[^>]*>/i.test(ssml) && voice) {
225
- ssml = ssml.replace(/(<speak[^>]*>)([\s\S]*?)(<\/speak>)/i, `$1<voice name="${voice}">$2</voice>$3`);
226
- }
227
- return ssml;
228
- }
229
231
  const pitch = this.validatePitch(options.pitch ?? 0);
230
232
  const rate = this.validateRate(options.rate ?? 0);
231
233
  const volume = this.validateVolume(options.volume ?? 0);
@@ -266,7 +268,7 @@ class EdgeTTS {
266
268
  this.audio_stream = [];
267
269
  const reqId = this.generateUUID();
268
270
  const secMsGEC = await this.generateSecMsGec(constants_1.Constants.TRUSTED_CLIENT_TOKEN);
269
- 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}`;
271
+ const url = `${constants_1.Constants.WSS_URL}?TrustedClientToken=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=${constants_1.Constants.VERSION_MS_GEC}&ConnectionId=${reqId}`;
270
272
  this.ws = new ws_1.default(url, {
271
273
  headers: constants_1.Constants.getBaseHeaders(),
272
274
  rejectUnauthorized: false
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andresaya/edge-tts",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
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
  "browser": "dist/browser/edge-tts.esm.js",
@@ -46,7 +46,7 @@
46
46
  "dist"
47
47
  ],
48
48
  "bin": {
49
- "edge-tts": "./dist/cli/edge-tts.js"
49
+ "edge-tts": "dist/cli/edge-tts.js"
50
50
  },
51
51
  "engines": {
52
52
  "bun": ">=0.5.0"