🚀 Auto-deploy: Camila atualizado em 26/05/2026 13:59:50

This commit is contained in:
2026-05-26 13:59:50 +00:00
parent ace6c53448
commit 71e633ce71
316 changed files with 59354 additions and 86 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Migushthe2nd
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+136
View File
@@ -0,0 +1,136 @@
# MsEdgeTTS
[![npm version](https://badge.fury.io/js/msedge-tts.svg)](https://badge.fury.io/js/msedge-tts)
**[Update Dec 2025]**
The Read Aloud API now requires a user agent matching the Microsoft Edge browser, and thus will not work in browsers other than Microsoft Edge.
It will of course still work in any server-side runtime.
A simple Azure Speech Service module that uses the Microsoft Edge Read Aloud API.
~~Full support for SSML~~ Only supports `speak`, `voice`, and `prosody` element types. The following is the default SSML object:
```xml
<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts"
xml:lang="${this._voiceLang}">
<voice name="${voiceName}">
<prosody rate="${rate}" pitch="${pitch}" volume="${volume}">
${input}
</prosody>
</voice>
</speak>
```
Documentation on the SSML
format [can be found here](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/speech-synthesis-markup)
. All supported audio formats [can be found here](./src/Output.ts).
## Example usage
Make sure to **escape/sanitize** your user's input!
Use a library like [xml-escape](https://www.npmjs.com/package/xml-escape).
### Write to stream
```js
import {MsEdgeTTS, OUTPUT_FORMAT} from "msedge-tts";
const tts = new MsEdgeTTS();
await tts.setMetadata("en-IE-ConnorNeural", OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS);
const {audioStream} = tts.toStream("Hi, how are you?");
audioStream.on("data", (data) => {
console.log("DATA RECEIVED", data);
// raw audio file data
});
audioStream.on("close", () => {
console.log("STREAM CLOSED");
});
```
### Write to file
```js
import {MsEdgeTTS, OUTPUT_FORMAT} from "msedge-tts";
(async () => {
const tts = new MsEdgeTTS();
await tts.setMetadata("en-US-AriaNeural", OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS);
const {audioFilePath} = await tts.toFile("./tmpfolder", "Hi, how are you?");
})();
```
### Change voice rate, pitch and volume
```js
import {MsEdgeTTS, OUTPUT_FORMAT} from "msedge-tts";
(async () => {
const tts = new MsEdgeTTS();
await tts.setMetadata("en-US-AriaNeural", OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS);
const {audioStream} = await tts.toStream("Hi, how are you?", {rate: 0.5, pitch: "+200Hz"});
})();
```
### Use an alternative HTTP Agent
Use a custom http.Agent implementation like [https-proxy-agent](https://github.com/TooTallNate/proxy-agents) or [socks-proxy-agent](https://github.com/TooTallNate/proxy-agents/tree/main/packages/socks-proxy-agent).
```js
import {MsEdgeTTS, OUTPUT_FORMAT} from "msedge-tts";
import {SocksProxyAgent} from 'socks-proxy-agent';
(async () => {
const agent = new SocksProxyAgent("socks://your-name%40gmail.com:abcdef12345124@br41.nordvpn.com")
const tts = new MsEdgeTTS(agent);
await tts.setMetadata("en-US-AriaNeural", OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS);
const {audioStream} = await tts.toStream("Hi, how are you?");
})();
```
### Get sentence and word boundaries
```js
import {MsEdgeTTS, OUTPUT_FORMAT} from "msedge-tts";
(async () => {
const tts = new MsEdgeTTS();
await tts.setMetadata("en-US-AriaNeural", OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS, {
wordBoundaryEnabled: true,
sentenceBoundaryEnabled: true
});
// as stream
const {metadataStream} = await tts.toStream("Hi, how are you doing today hello hello hello?");
/* ->
{
"Metadata": [
{
"Type": "SentenceBoundary",
"Data": {
"Offset": 1000000,
"Duration": 35875000,
"text": {
"Text": "Hi, how are you doing today hello hello hello?",
"Length": 46,
"BoundaryType": "SentenceBoundary"
}
}
}
]
}
*/
// or as file
const {metadataFilePath} = await tts.toFile("Hi, how are you?");
/* ->
{
"Metadata": [
<all metadata combined>
]
}
*/
})();
```
## API
For the full documentation check out the [API Documentation](https://migushthe2nd.github.io/MsEdgeTTS).
This library only supports promises.
+141
View File
@@ -0,0 +1,141 @@
import { OUTPUT_FORMAT } from "./Output";
import { Readable } from "stream";
import { Agent } from "http";
import { ProsodyOptions } from "./Prosody";
export type Voice = {
Name: string;
ShortName: string;
Gender: string;
Locale: string;
SuggestedCodec: string;
FriendlyName: string;
Status: string;
};
export declare class MetadataOptions {
/**
* (optional) any voice locale that is supported by the voice. See the list of all voices for compatibility. If not provided, the locale will be inferred from the `voiceName`.
* Changing the voiceName will reset the voiceLocale.
*/
voiceLocale?: string;
/**
* (optional) whether to enable sentence boundary metadata. Default is `false`
*/
sentenceBoundaryEnabled?: boolean;
/**
* (optional) whether to enable word boundary metadata. Default is `false`
*/
wordBoundaryEnabled?: boolean;
}
/**
* MsEdgeTTS constructor options
*/
type Options = {
/**
* (optional, **NOT SUPPORTED IN BROWSER**) Use a custom http.Agent implementation like [https-proxy-agent](https://github.com/TooTallNate/proxy-agents) or [socks-proxy-agent](https://github.com/TooTallNate/proxy-agents/tree/main/packages/socks-proxy-agent).
* */
agent?: Agent;
/** whether to enable the built-in logger. This logs connections inits, disconnects, and incoming data to the console */
enableLogger?: boolean;
};
export declare class MsEdgeTTS {
private static TRUSTED_CLIENT_TOKEN;
private static VOICES_URL;
private static WSS_URL;
private static JSON_XML_DELIM;
private static AUDIO_DELIM;
private static VOICE_LANG_REGEX;
private readonly _enableLogger;
private readonly _isBrowser;
private _ws;
private _voice;
private _outputFormat;
private _metadataOptions;
private _streams;
private _startTime;
private readonly _agent;
private _log;
/**
* Create a new `MsEdgeTTS` instance.
*
* @param options (optional) {@link Options}
*/
constructor(options?: Options);
private static getSynthUrl;
private _initClient;
private static generateUUID;
private static generateSecMsGec;
private _send;
private _pushAudioData;
private _pushMetadata;
private _SSMLTemplate;
/**
* Fetch the list of voices available in Microsoft Edge.
* These, however, are not all. The complete list of voices supported by this module [can be found here](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/language-support) (neural, standard, and preview).
*/
getVoices(): Promise<Voice[]>;
/**
* Sets the required information for the speech to be synthesised and inits a new WebSocket connection.
* Must be called at least once before text can be synthesised.
* Saved in this instance. Can be called at any time times to update the metadata.
* Merges specified values into previously provided values.
*
* @param voiceName a string with any `ShortName`. A list of all available neural voices can be found [here](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/language-support#neural-voices). However, it is not limited to neural voices: standard voices can also be used. A list of standard voices can be found [here](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/language-support#standard-voices). Changing the voiceName will reset the voiceLocale.
* @param outputFormat any {@link OUTPUT_FORMAT}
* @param metadataOptions (optional) {@link MetadataOptions}
*/
setMetadata(voiceName: string, outputFormat: OUTPUT_FORMAT, metadataOptions?: MetadataOptions): Promise<void>;
private _metadataCheck;
/**
* Close the WebSocket connection.
*/
close(): void;
/**
* Writes raw audio synthesised from text to a file. Uses a basic {@link _SSMLTemplate SML template}.
*
* @param dirPath a valid output directory path
* @param input the input to synthesise
* @param options (optional) {@link ProsodyOptions}
@returns {Promise<{audioFilePath: string, metadataFilePath: string | null}>} - a `Promise` with the full filepaths
*/
toFile(dirPath: string, input: string, options?: ProsodyOptions): Promise<{
audioFilePath: string;
metadataFilePath: string | null;
}>;
/**
* Writes raw audio synthesised from text in real-time to a {@link Readable}. Uses a basic {@link _SSMLTemplate SML template}.
*
* @param input the text to synthesise. Can include SSML elements.
* @param options (optional) {@link ProsodyOptions}
@returns {Promise<{audioStream: Readable, metadataStream: Readable | null}>} - a `Promise` with the streams
*/
toStream(input: string, options?: ProsodyOptions): {
audioStream: Readable;
metadataStream: Readable | null;
};
/**
* Writes raw audio synthesised from text to a file. Has no SSML template. Basic SSML should be provided in the request.
*
* @param dirPath a valid output directory path.
* @param requestSSML the SSML to send. SSML elements required in order to work.
* @returns {Promise<{audioFilePath: string, metadataFilePath: string | null}>} - a `Promise` with the full filepaths
*/
rawToFile(dirPath: string, requestSSML: string): Promise<{
audioFilePath: string;
metadataFilePath: string | null;
}>;
/**
* Writes raw audio synthesised from a request in real-time to a {@link Readable}. Has no SSML template. Basic SSML should be provided in the request.
*
* @param requestSSML the SSML to send. SSML elements required in order to work.
@returns {Promise<{audioStream: Readable, metadataStream: Readable | null}>} - a `Promise` with the streams
*/
rawToStream(requestSSML: string): {
audioStream: Readable;
metadataStream: Readable | null;
};
private _hasMetadataBoundaries;
private _rawSSMLRequestToFile;
private static randomHex;
private _rawSSMLRequest;
}
export {};
+420
View File
@@ -0,0 +1,420 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MsEdgeTTS = exports.MetadataOptions = void 0;
const axios_1 = __importDefault(require("axios"));
const isomorphic_ws_1 = __importDefault(require("isomorphic-ws"));
const index_1 = require("buffer/index"); // /index is important for browser compatibility
const Output_1 = require("./Output");
const stream_1 = require("stream");
const fs = __importStar(require("fs"));
const Prosody_1 = require("./Prosody");
const utils_1 = require("./utils");
class MetadataOptions {
/**
* (optional) any voice locale that is supported by the voice. See the list of all voices for compatibility. If not provided, the locale will be inferred from the `voiceName`.
* Changing the voiceName will reset the voiceLocale.
*/
voiceLocale;
/**
* (optional) whether to enable sentence boundary metadata. Default is `false`
*/
sentenceBoundaryEnabled = false;
/**
* (optional) whether to enable word boundary metadata. Default is `false`
*/
wordBoundaryEnabled = false;
}
exports.MetadataOptions = MetadataOptions;
var messageTypes;
(function (messageTypes) {
messageTypes["TURN_START"] = "turn.start";
messageTypes["TURN_END"] = "turn.end";
messageTypes["RESPONSE"] = "response";
messageTypes["SPEECH_CONFIG"] = "speech.config";
messageTypes["AUDIO_METADATA"] = "audio.metadata";
messageTypes["AUDIO"] = "audio";
messageTypes["SSML"] = "ssml";
})(messageTypes || (messageTypes = {}));
class MsEdgeTTS {
static TRUSTED_CLIENT_TOKEN = "6A5AA1D4EAFF4E9FB37E23D68491D6F4";
static VOICES_URL = `https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list?trustedclienttoken=${MsEdgeTTS.TRUSTED_CLIENT_TOKEN}`;
static WSS_URL = "wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1";
static JSON_XML_DELIM = "\r\n\r\n";
static AUDIO_DELIM = "Path:audio\r\n";
static VOICE_LANG_REGEX = /\w{2}-\w{2}/;
_enableLogger;
_isBrowser;
_ws;
_voice;
_outputFormat;
_metadataOptions = new MetadataOptions();
_streams = {};
_startTime = 0;
_agent;
_log(...o) {
if (this._enableLogger) {
console.log(...o);
}
}
/**
* Create a new `MsEdgeTTS` instance.
*
* @param options (optional) {@link Options}
*/
constructor(options) {
this._agent = options?.agent || undefined;
this._enableLogger = options?.enableLogger || false;
this._isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined";
}
static async getSynthUrl() {
const req_id = MsEdgeTTS.generateUUID(); // Generate the request ID
const secMsGEC = await MsEdgeTTS.generateSecMsGec(this.TRUSTED_CLIENT_TOKEN);
return `${this.WSS_URL}?TrustedClientToken=${this.TRUSTED_CLIENT_TOKEN}&Sec-MS-GEC=${secMsGEC}&Sec-MS-GEC-Version=1-143.0.3650.96&ConnectionId=${req_id}`;
}
async _initClient() {
const synthUrl = await MsEdgeTTS.getSynthUrl();
const options = {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0",
"Origin": "chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold"
}
};
this._ws = this._isBrowser
? new isomorphic_ws_1.default(synthUrl, options)
: new isomorphic_ws_1.default(synthUrl, { ...options, agent: this._agent });
this._ws.binaryType = "arraybuffer";
return new Promise((resolve, reject) => {
this._ws.onopen = () => {
this._log("Connected in", (Date.now() - this._startTime) / 1000, "seconds");
this._send(`Content-Type:application/json; charset=utf-8\r\nPath:${messageTypes.SPEECH_CONFIG}${MsEdgeTTS.JSON_XML_DELIM}
{
"context": {
"synthesis": {
"audio": {
"metadataoptions": {
"sentenceBoundaryEnabled": "${this._metadataOptions.sentenceBoundaryEnabled}",
"wordBoundaryEnabled": "${this._metadataOptions.wordBoundaryEnabled}"
},
"outputFormat": "${this._outputFormat}"
}
}
}
}
`).then(resolve);
};
this._ws.onmessage = (m) => {
const buffer = index_1.Buffer.from(m.data);
const message = buffer.toString();
const requestId = /X-RequestId:(.*?)\r\n/gm.exec(message)[1];
if (message.includes(`Path:${messageTypes.TURN_START}`)) {
// start of turn, ignore
this._log("->", message);
}
else if (message.includes(`Path:${messageTypes.TURN_END}`)) {
// end of turn, close stream
this._log("->", message);
this._streams[requestId].audio.push(null);
}
else if (message.includes(`Path:${messageTypes.RESPONSE}`)) {
// context response, ignore
this._log("->", message);
}
else if (message.includes(`Path:${messageTypes.AUDIO_METADATA}`)) {
// audio metadata, wordboundary/sentenceboundary
const dataStartIndex = buffer.indexOf(MsEdgeTTS.JSON_XML_DELIM) + MsEdgeTTS.JSON_XML_DELIM.length;
const data = buffer.subarray(dataStartIndex);
this._log("->", message);
this._pushMetadata(data, requestId);
}
else if (message.includes(`Path:${messageTypes.AUDIO}`) && m.data instanceof ArrayBuffer) {
const dataStartIndex = buffer.indexOf(MsEdgeTTS.AUDIO_DELIM) + MsEdgeTTS.AUDIO_DELIM.length;
const headers = buffer.subarray(0, dataStartIndex).toString();
const data = buffer.subarray(dataStartIndex);
this._log("->", headers);
this._pushAudioData(data, requestId);
}
else {
this._log("->", "UNKNOWN MESSAGE", message);
}
};
this._ws.onclose = () => {
this._log("disconnected after:", (Date.now() - this._startTime) / 1000, "seconds");
for (const requestId in this._streams) {
this._streams[requestId].audio.push(null);
}
};
this._ws.onerror = (event) => {
// Node's `ws` library fires either a native Error or an ErrorEvent wrapping one.
const underlying = event?.error ?? event;
const message = underlying?.message ?? String(underlying);
const code = underlying?.code ?? event?.code;
const wrapped = new Error(`Edge TTS WebSocket error: ${message}${code ? ` (code=${code})` : ""}`);
wrapped.cause = underlying;
reject(wrapped);
};
});
}
static generateUUID() {
return 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
static async generateSecMsGec(trustedClientToken) {
const ticks = Math.floor(Date.now() / 1000) + 11644473600;
const rounded = ticks - (ticks % 300);
const windowsTicks = rounded * 10000000;
const encoder = new TextEncoder();
const data = encoder.encode(`${windowsTicks}${trustedClientToken}`);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
return Array.from(new Uint8Array(hashBuffer))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
.toUpperCase();
}
async _send(message) {
for (let i = 1; i <= 3 && this._ws.readyState !== this._ws.OPEN; i++) {
if (i == 1) {
this._startTime = Date.now();
}
this._log("connecting: ", i);
await this._initClient();
}
this._ws.send(message, () => {
this._log("<-", message);
});
}
_pushAudioData(data, requestId) {
this._streams[requestId].audio.push(data);
}
_pushMetadata(data, requestId) {
this._streams[requestId].metadata.push(data);
}
_SSMLTemplate(input, options = {}) {
// in case future updates to the edge API block these elements, we'll be concatenating strings.
options = { ...new Prosody_1.ProsodyOptions(), ...options };
return `<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xmlns:mstts="https://www.w3.org/2001/mstts" xml:lang="${this._metadataOptions.voiceLocale}">
<voice name="${this._voice}">
<prosody pitch="${options.pitch}" rate="${options.rate}" volume="${options.volume}">
${input}
</prosody>
</voice>
</speak>`;
}
/**
* Fetch the list of voices available in Microsoft Edge.
* These, however, are not all. The complete list of voices supported by this module [can be found here](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/language-support) (neural, standard, and preview).
*/
getVoices() {
return new Promise((resolve, reject) => {
axios_1.default.get(MsEdgeTTS.VOICES_URL)
.then((res) => resolve(res.data))
.catch(reject);
});
}
/**
* Sets the required information for the speech to be synthesised and inits a new WebSocket connection.
* Must be called at least once before text can be synthesised.
* Saved in this instance. Can be called at any time times to update the metadata.
* Merges specified values into previously provided values.
*
* @param voiceName a string with any `ShortName`. A list of all available neural voices can be found [here](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/language-support#neural-voices). However, it is not limited to neural voices: standard voices can also be used. A list of standard voices can be found [here](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/language-support#standard-voices). Changing the voiceName will reset the voiceLocale.
* @param outputFormat any {@link OUTPUT_FORMAT}
* @param metadataOptions (optional) {@link MetadataOptions}
*/
async setMetadata(voiceName, outputFormat, metadataOptions) {
const oldVoice = this._voice;
const oldOutputFormat = this._outputFormat;
const oldOptions = JSON.stringify(this._metadataOptions);
this._voice = voiceName;
if (!this._metadataOptions.voiceLocale || (!metadataOptions.voiceLocale && oldVoice !== this._voice)) {
const voiceLangMatch = MsEdgeTTS.VOICE_LANG_REGEX.exec(this._voice);
if (!voiceLangMatch)
throw new Error("Could not infer voiceLocale from voiceName, and no voiceLocale was specified!");
this._metadataOptions.voiceLocale = voiceLangMatch[0];
}
this._outputFormat = outputFormat;
Object.assign(this._metadataOptions, metadataOptions);
const changed = oldVoice !== this._voice
|| oldOutputFormat !== this._outputFormat
|| oldOptions !== JSON.stringify(this._metadataOptions);
if (!changed && this._ws.readyState === this._ws.OPEN) {
return;
}
// create new client
this._startTime = Date.now();
await this._initClient();
}
_metadataCheck() {
if (!this._ws)
throw new Error("Speech synthesis not configured yet. Run setMetadata before calling toStream or toFile.");
}
/**
* Close the WebSocket connection.
*/
close() {
this._ws?.close();
}
/**
* Writes raw audio synthesised from text to a file. Uses a basic {@link _SSMLTemplate SML template}.
*
* @param dirPath a valid output directory path
* @param input the input to synthesise
* @param options (optional) {@link ProsodyOptions}
@returns {Promise<{audioFilePath: string, metadataFilePath: string | null}>} - a `Promise` with the full filepaths
*/
toFile(dirPath, input, options) {
return this._rawSSMLRequestToFile(dirPath, this._SSMLTemplate(input, options));
}
/**
* Writes raw audio synthesised from text in real-time to a {@link Readable}. Uses a basic {@link _SSMLTemplate SML template}.
*
* @param input the text to synthesise. Can include SSML elements.
* @param options (optional) {@link ProsodyOptions}
@returns {Promise<{audioStream: Readable, metadataStream: Readable | null}>} - a `Promise` with the streams
*/
toStream(input, options) {
return this._rawSSMLRequest(this._SSMLTemplate(input, options));
}
/**
* Writes raw audio synthesised from text to a file. Has no SSML template. Basic SSML should be provided in the request.
*
* @param dirPath a valid output directory path.
* @param requestSSML the SSML to send. SSML elements required in order to work.
* @returns {Promise<{audioFilePath: string, metadataFilePath: string | null}>} - a `Promise` with the full filepaths
*/
rawToFile(dirPath, requestSSML) {
return this._rawSSMLRequestToFile(dirPath, requestSSML);
}
/**
* Writes raw audio synthesised from a request in real-time to a {@link Readable}. Has no SSML template. Basic SSML should be provided in the request.
*
* @param requestSSML the SSML to send. SSML elements required in order to work.
@returns {Promise<{audioStream: Readable, metadataStream: Readable | null}>} - a `Promise` with the streams
*/
rawToStream(requestSSML) {
return this._rawSSMLRequest(requestSSML);
}
_hasMetadataBoundaries() {
return this._metadataOptions.sentenceBoundaryEnabled
|| this._metadataOptions.wordBoundaryEnabled;
}
async _rawSSMLRequestToFile(dirPath, requestSSML) {
const { audioStream, metadataStream, requestId } = this._rawSSMLRequest(requestSSML);
const audioFilePath = (0, utils_1.joinPath)(dirPath, "audio." + Output_1.OUTPUT_EXTENSIONS[this._outputFormat]);
const metadataFilePath = !!metadataStream ? (0, utils_1.joinPath)(dirPath, "metadata.json") : null;
try {
await Promise.all([
new Promise((resolve, reject) => {
const writableAudioFile = audioStream.pipe(fs.createWriteStream(audioFilePath));
writableAudioFile.once("close", async () => {
if (writableAudioFile.bytesWritten > 0) {
resolve(audioFilePath);
}
else {
reject("No audio data received");
fs.unlinkSync(audioFilePath);
}
});
writableAudioFile.once("error", reject);
}),
new Promise((resolve, reject) => {
if (!metadataFilePath) {
return resolve(null);
}
// get metadata from buffer and combine all MetaData root elements
const metadataItems = { Metadata: [] };
metadataStream.on("data", (chunk) => {
const chunkObj = JSON.parse(chunk.toString());
// .Metadata is an array of objects, just combine them
metadataItems.Metadata.push(...chunkObj["Metadata"]);
});
metadataStream.once("close", () => {
if (metadataItems.Metadata.length > 0) {
// create file if not exists
fs.writeFileSync(metadataFilePath, JSON.stringify(metadataItems, null, 2));
resolve(metadataFilePath);
}
else {
reject("No metadata received");
fs.unlinkSync(metadataFilePath);
}
});
metadataStream.once("error", reject);
}),
]);
return { audioFilePath, metadataFilePath, requestId };
}
catch (e) {
audioStream.destroy();
metadataStream?.destroy();
throw e;
}
}
static randomHex(bytes) {
const arr = new Uint8Array(bytes);
crypto.getRandomValues(arr);
return Array.from(arr, b => b.toString(16).padStart(2, "0")).join("");
}
_rawSSMLRequest(requestSSML) {
this._metadataCheck();
const requestId = MsEdgeTTS.randomHex(16);
const request = `X-RequestId:${requestId}\r\nContent-Type:application/ssml+xml\r\nPath:${messageTypes.SSML}${MsEdgeTTS.JSON_XML_DELIM}` + requestSSML.trim();
// https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/speech-synthesis-markup
const self = this;
const audioStream = new stream_1.Readable({
read() {
},
destroy(error, callback) {
delete self._streams[requestId];
callback(error);
},
});
const metadataStream = this._hasMetadataBoundaries() ? new stream_1.Readable({
read() {
},
}) : null;
audioStream.on("error", (e) => {
audioStream.destroy();
metadataStream?.destroy();
});
audioStream.once("close", () => {
audioStream.destroy();
metadataStream?.destroy();
});
this._streams[requestId] = {
audio: audioStream,
metadata: metadataStream,
};
this._send(request).then();
return { audioStream, metadataStream, requestId };
}
}
exports.MsEdgeTTS = MsEdgeTTS;
+13
View File
@@ -0,0 +1,13 @@
/**
* Only a few of the [possible formats](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/rest-text-to-speech#audio-outputs) are accepted.
*/
export declare enum OUTPUT_FORMAT {
AUDIO_24KHZ_48KBITRATE_MONO_MP3 = "audio-24khz-48kbitrate-mono-mp3",
AUDIO_24KHZ_96KBITRATE_MONO_MP3 = "audio-24khz-96kbitrate-mono-mp3",
WEBM_24KHZ_16BIT_MONO_OPUS = "webm-24khz-16bit-mono-opus"
}
export declare const OUTPUT_EXTENSIONS: {
"audio-24khz-48kbitrate-mono-mp3": string;
"audio-24khz-96kbitrate-mono-mp3": string;
"webm-24khz-16bit-mono-opus": string;
};
+51
View File
@@ -0,0 +1,51 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.OUTPUT_EXTENSIONS = exports.OUTPUT_FORMAT = void 0;
/**
* Only a few of the [possible formats](https://docs.microsoft.com/en-us/azure/cognitive-services/speech-service/rest-text-to-speech#audio-outputs) are accepted.
*/
var OUTPUT_FORMAT;
(function (OUTPUT_FORMAT) {
// Streaming =============================
// AMR_WB_16000HZ = "amr-wb-16000hz",
// AUDIO_16KHZ_16BIT_32KBPS_MONO_OPUS = "audio-16khz-16bit-32kbps-mono-opus",
// AUDIO_16KHZ_32KBITRATE_MONO_MP3 = "audio-16khz-32kbitrate-mono-mp3",
// AUDIO_16KHZ_64KBITRATE_MONO_MP3 = "audio-16khz-64kbitrate-mono-mp3",
// AUDIO_16KHZ_128KBITRATE_MONO_MP3 = "audio-16khz-128kbitrate-mono-mp3",
// AUDIO_24KHZ_16BIT_24KBPS_MONO_OPUS = "audio-24khz-16bit-24kbps-mono-opus",
// AUDIO_24KHZ_16BIT_48KBPS_MONO_OPUS = "audio-24khz-16bit-48kbps-mono-opus",
OUTPUT_FORMAT["AUDIO_24KHZ_48KBITRATE_MONO_MP3"] = "audio-24khz-48kbitrate-mono-mp3";
OUTPUT_FORMAT["AUDIO_24KHZ_96KBITRATE_MONO_MP3"] = "audio-24khz-96kbitrate-mono-mp3";
// AUDIO_24KHZ_160KBITRATE_MONO_MP3 = "audio-24khz-160kbitrate-mono-mp3",
// AUDIO_48KHZ_96KBITRATE_MONO_MP3 = "audio-48khz-96kbitrate-mono-mp3",
// AUDIO_48KHZ_192KBITRATE_MONO_MP3 = "audio-48khz-192kbitrate-mono-mp3",
// OGG_16KHZ_16BIT_MONO_OPUS = "ogg-16khz-16bit-mono-opus",
// OGG_24KHZ_16BIT_MONO_OPUS = "ogg-24khz-16bit-mono-opus",
// OGG_48KHZ_16BIT_MONO_OPUS = "ogg-48khz-16bit-mono-opus",
// RAW_8KHZ_8BIT_MONO_ALAW = "raw-8khz-8bit-mono-alaw",
// RAW_8KHZ_8BIT_MONO_MULAW = "raw-8khz-8bit-mono-mulaw",
// RAW_8KHZ_16BIT_MONO_PCM = "raw-8khz-16bit-mono-pcm",
// RAW_16KHZ_16BIT_MONO_PCM = "raw-16khz-16bit-mono-pcm",
// RAW_16KHZ_16BIT_MONO_TRUESILK = "raw-16khz-16bit-mono-truesilk",
// RAW_22050HZ_16BIT_MONO_PCM = "raw-22050hz-16bit-mono-pcm",
// RAW_24KHZ_16BIT_MONO_PCM = "raw-24khz-16bit-mono-pcm",
// RAW_24KHZ_16BIT_MONO_TRUESILK = "raw-24khz-16bit-mono-truesilk",
// RAW_44100HZ_16BIT_MONO_PCM = "raw-44100hz-16bit-mono-pcm",
// RAW_48KHZ_16BIT_MONO_PCM = "raw-48khz-16bit-mono-pcm",
// WEBM_16KHZ_16BIT_MONO_OPUS = "webm-16khz-16bit-mono-opus",
// WEBM_24KHZ_16BIT_24KBPS_MONO_OPUS = "webm-24khz-16bit-24kbps-mono-opus",
OUTPUT_FORMAT["WEBM_24KHZ_16BIT_MONO_OPUS"] = "webm-24khz-16bit-mono-opus";
// Non-streaming =============================
// RIFF_8KHZ_8BIT_MONO_ALAW = "riff-8khz-8bit-mono-alaw",
// RIFF_8KHZ_8BIT_MONO_MULAW = "riff-8khz-8bit-mono-mulaw",
// RIFF_8KHZ_16BIT_MONO_PCM = "riff-8khz-16bit-mono-pcm",
// RIFF_22050HZ_16BIT_MONO_PCM = "riff-22050hz-16bit-mono-pcm",
// RIFF_24KHZ_16BIT_MONO_PCM = "riff-24khz-16bit-mono-pcm",
// RIFF_44100HZ_16BIT_MONO_PCM = "riff-44100hz-16bit-mono-pcm",
// RIFF_48KHZ_16BIT_MONO_PCM = "riff-48khz-16bit-mono-pcm",
})(OUTPUT_FORMAT || (exports.OUTPUT_FORMAT = OUTPUT_FORMAT = {}));
exports.OUTPUT_EXTENSIONS = {
[OUTPUT_FORMAT.AUDIO_24KHZ_48KBITRATE_MONO_MP3]: "mp3",
[OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3]: "mp3",
[OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS]: "webm",
};
+54
View File
@@ -0,0 +1,54 @@
export declare class ProsodyOptions {
/**
* The pitch to use.
* Can be any {@link PITCH}, or a relative frequency in Hz (+50Hz), a relative semitone (+2st), or a relative percentage (+50%).
* [SSML documentation](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,pitch,-Indicates%20the%20baseline)
*/
pitch?: PITCH | string;
/**
* The rate to use.
* Can be any {@link RATE}, or a relative number (0.5), or string with a relative percentage (+50%).
* [SSML documentation](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,rate,-Indicates%20the%20speaking)
*/
rate?: RATE | string | number;
/**
* The volume to use.
* Can be any {@link VOLUME}, or an absolute number (0, 100), a string with a relative number (+50), or a relative percentage (+50%).
* [SSML documentation](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,volume,-Indicates%20the%20volume)
*/
volume?: VOLUME | string | number;
}
/**
* https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,rate,-Indicates%20the%20speaking
*/
export declare enum RATE {
X_SLOW = "x-slow",
SLOW = "slow",
MEDIUM = "medium",
FAST = "fast",
X_FAST = "x-fast",
DEFAULT = "default"
}
/**
* https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,pitch,-Indicates%20the%20baseline
*/
export declare enum PITCH {
X_LOW = "x-low",
LOW = "low",
MEDIUM = "medium",
HIGH = "high",
X_HIGH = "x-high",
DEFAULT = "default"
}
/**
* https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,volume,-Indicates%20the%20volume
*/
export declare enum VOLUME {
SILENT = "silent",
X_SOFT = "x-soft",
SOFT = "soft",
MEDIUM = "medium",
LOUD = "loud",
X_LOUD = "x-LOUD",
DEFAULT = "default"
}
+61
View File
@@ -0,0 +1,61 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.VOLUME = exports.PITCH = exports.RATE = exports.ProsodyOptions = void 0;
class ProsodyOptions {
/**
* The pitch to use.
* Can be any {@link PITCH}, or a relative frequency in Hz (+50Hz), a relative semitone (+2st), or a relative percentage (+50%).
* [SSML documentation](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,pitch,-Indicates%20the%20baseline)
*/
pitch = "+0Hz";
/**
* The rate to use.
* Can be any {@link RATE}, or a relative number (0.5), or string with a relative percentage (+50%).
* [SSML documentation](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,rate,-Indicates%20the%20speaking)
*/
rate = 1.0;
/**
* The volume to use.
* Can be any {@link VOLUME}, or an absolute number (0, 100), a string with a relative number (+50), or a relative percentage (+50%).
* [SSML documentation](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,volume,-Indicates%20the%20volume)
*/
volume = 100.0;
}
exports.ProsodyOptions = ProsodyOptions;
/**
* https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,rate,-Indicates%20the%20speaking
*/
var RATE;
(function (RATE) {
RATE["X_SLOW"] = "x-slow";
RATE["SLOW"] = "slow";
RATE["MEDIUM"] = "medium";
RATE["FAST"] = "fast";
RATE["X_FAST"] = "x-fast";
RATE["DEFAULT"] = "default";
})(RATE || (exports.RATE = RATE = {}));
/**
* https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,pitch,-Indicates%20the%20baseline
*/
var PITCH;
(function (PITCH) {
PITCH["X_LOW"] = "x-low";
PITCH["LOW"] = "low";
PITCH["MEDIUM"] = "medium";
PITCH["HIGH"] = "high";
PITCH["X_HIGH"] = "x-high";
PITCH["DEFAULT"] = "default";
})(PITCH || (exports.PITCH = PITCH = {}));
/**
* https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-voice#:~:text=Optional-,volume,-Indicates%20the%20volume
*/
var VOLUME;
(function (VOLUME) {
VOLUME["SILENT"] = "silent";
VOLUME["X_SOFT"] = "x-soft";
VOLUME["SOFT"] = "soft";
VOLUME["MEDIUM"] = "medium";
VOLUME["LOUD"] = "loud";
VOLUME["X_LOUD"] = "x-LOUD";
VOLUME["DEFAULT"] = "default";
})(VOLUME || (exports.VOLUME = VOLUME = {}));
+3
View File
@@ -0,0 +1,3 @@
export * from "./MsEdgeTTS";
export * from "./Output";
export * from "./Prosody";
+19
View File
@@ -0,0 +1,19 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __exportStar = (this && this.__exportStar) || function(m, exports) {
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
};
Object.defineProperty(exports, "__esModule", { value: true });
__exportStar(require("./MsEdgeTTS"), exports);
__exportStar(require("./Output"), exports);
__exportStar(require("./Prosody"), exports);
+1
View File
@@ -0,0 +1 @@
export declare const joinPath: (...parts: any[]) => string;
+10
View File
@@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.joinPath = void 0;
const joinPath = (...parts) => {
return parts
.filter(Boolean)
.join("/")
.replace(/\/{2,}/g, "/");
};
exports.joinPath = joinPath;
+63
View File
@@ -0,0 +1,63 @@
{
"name": "msedge-tts",
"version": "2.0.5",
"description": "An Azure Speech Service module that uses the Microsoft Edge Read Aloud API.",
"author": "Migushthe2nd <Migushthe2nd@users.noreply.github.com>",
"license": "MIT",
"repository": "https://github.com/Migushthe2nd/MsEdgeTTS.git",
"private": false,
"module": "./dist/index",
"main": "./dist/index",
"engines": {
"node": ">=16.0.0"
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^20.11.17",
"@types/ws": "^8.5.10",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.1",
"typedoc": "^0.25.8",
"typescript": "^5.3.3"
},
"dependencies": {
"axios": "^1.11.0",
"buffer": "^6.0.3",
"isomorphic-ws": "^5.0.0",
"stream-browserify": "^3.0.0",
"ws": "^8.14.1"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node",
"setupFilesAfterEnv": [],
"testTimeout": 15000
},
"files": [
"dist/"
],
"scripts": {
"preinstall": "npx only-allow pnpm",
"dev": "pnpm run build && ts-node src/test/test.ts",
"build": "tsc",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config src/test/jest-e2e.json"
}
}