@andresaya/edge-tts 1.8.1 → 1.9.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 +173 -43
- package/dist/browser/edge-tts.esm.js +496 -304
- package/dist/browser/edge-tts.esm.js.map +4 -4
- package/dist/browser/edge-tts.esm.min.js +20 -16
- package/dist/browser/edge-tts.esm.min.js.map +4 -4
- package/dist/browser/edge-tts.umd.js +496 -304
- package/dist/browser/edge-tts.umd.js.map +4 -4
- package/dist/browser/edge-tts.umd.min.js +20 -16
- package/dist/browser/edge-tts.umd.min.js.map +4 -4
- package/dist/cli/edge-tts.js +4 -6
- package/dist/commands/SynthesizeCommand.js +19 -3
- package/dist/commands/VoiceListCommand.js +7 -3
- package/dist/config/constants.d.ts +1 -2
- package/dist/config/constants.js +11 -10
- package/dist/core/base.d.ts +33 -0
- package/dist/core/base.js +187 -0
- package/dist/core/clock.d.ts +3 -0
- package/dist/core/clock.js +20 -0
- package/dist/core/crypto.d.ts +8 -0
- package/dist/core/crypto.js +14 -0
- package/dist/core/errors.d.ts +9 -0
- package/dist/core/errors.js +15 -0
- package/dist/core/protocol.d.ts +23 -0
- package/dist/core/protocol.js +206 -0
- package/dist/core/session.d.ts +24 -0
- package/dist/core/session.js +87 -0
- package/dist/core/subtitles.d.ts +7 -0
- package/dist/core/subtitles.js +24 -0
- package/dist/core/types.d.ts +53 -0
- package/dist/core/types.js +2 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +6 -1
- package/dist/services/EdgeTTS.d.ts +18 -41
- package/dist/services/EdgeTTS.js +83 -376
- package/package.json +10 -15
|
@@ -17,13 +17,12 @@ export declare const Constants: {
|
|
|
17
17
|
Pragma: string;
|
|
18
18
|
'Cache-Control': string;
|
|
19
19
|
Origin: string;
|
|
20
|
-
'Sec-WebSocket-Protocol': string;
|
|
21
|
-
'Sec-WebSocket-Version': string;
|
|
22
20
|
'User-Agent': string;
|
|
23
21
|
};
|
|
24
22
|
VOICE_HEADERS: {
|
|
25
23
|
'Sec-CH-UA': string;
|
|
26
24
|
'Sec-CH-UA-Mobile': string;
|
|
25
|
+
'Sec-CH-UA-Platform': string;
|
|
27
26
|
Accept: string;
|
|
28
27
|
'Sec-Fetch-Site': string;
|
|
29
28
|
'Sec-Fetch-Mode': string;
|
package/dist/config/constants.js
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.Constants = void 0;
|
|
4
|
+
const crypto_1 = require("../core/crypto");
|
|
4
5
|
exports.Constants = {
|
|
5
6
|
TRUSTED_CLIENT_TOKEN: '6A5AA1D4EAFF4E9FB37E23D68491D6F4',
|
|
6
7
|
BASE_URL: 'https://speech.platform.bing.com/consumer/speech/synthesize/readaloud',
|
|
7
|
-
|
|
8
|
+
// NOTE: token goes as Ocp-Apim-Subscription-Key here; Edge user agents only.
|
|
9
|
+
WSS_URL: 'wss://api.msedgeservices.com/tts/cognitiveservices/websocket/v1',
|
|
8
10
|
VOICES_URL: 'https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list',
|
|
9
|
-
CHROMIUM_FULL_VERSION: '
|
|
10
|
-
CHROMIUM_MAJOR_VERSION: '
|
|
11
|
-
VERSION_MS_GEC: '1-
|
|
11
|
+
CHROMIUM_FULL_VERSION: '155.0.0.0',
|
|
12
|
+
CHROMIUM_MAJOR_VERSION: '155',
|
|
13
|
+
VERSION_MS_GEC: '1-155.0.4283.45',
|
|
12
14
|
token32() {
|
|
13
15
|
const bytes = new Uint8Array(16);
|
|
14
|
-
|
|
16
|
+
(0, crypto_1.webCrypto)().getRandomValues(bytes);
|
|
15
17
|
return Array.from(bytes)
|
|
16
18
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
17
19
|
.join('')
|
|
@@ -19,7 +21,7 @@ exports.Constants = {
|
|
|
19
21
|
},
|
|
20
22
|
getBaseHeaders() {
|
|
21
23
|
return {
|
|
22
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
24
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36 Edg/155.0.0.0',
|
|
23
25
|
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
|
24
26
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
25
27
|
'Cookie': 'MUID=' + this.token32()
|
|
@@ -29,13 +31,12 @@ exports.Constants = {
|
|
|
29
31
|
'Pragma': 'no-cache',
|
|
30
32
|
'Cache-Control': 'no-cache',
|
|
31
33
|
'Origin': 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold',
|
|
32
|
-
'
|
|
33
|
-
'Sec-WebSocket-Version': '13',
|
|
34
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.3650.75 Safari/537.36 Edg/143.0.3650.75'
|
|
34
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36 Edg/155.0.0.0'
|
|
35
35
|
},
|
|
36
36
|
VOICE_HEADERS: {
|
|
37
|
-
'Sec-CH-UA': '"
|
|
37
|
+
'Sec-CH-UA': '"Microsoft Edge";v="155", "Chromium";v="155", "Not(A:Brand";v="24"',
|
|
38
38
|
'Sec-CH-UA-Mobile': '?0',
|
|
39
|
+
'Sec-CH-UA-Platform': '"Windows"',
|
|
39
40
|
'Accept': '*/*',
|
|
40
41
|
'Sec-Fetch-Site': 'none',
|
|
41
42
|
'Sec-Fetch-Mode': 'cors',
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type SocketLike } from './session';
|
|
2
|
+
import type { EdgeTTSOptions, SentenceBoundary, SynthesisOptions, SynthesisResult, Voice, WordBoundary } from './types';
|
|
3
|
+
export declare const NO_AUDIO_MESSAGE: string;
|
|
4
|
+
export declare function noAudioMessage(outputFormat: string): string;
|
|
5
|
+
export declare abstract class EdgeTTSCore {
|
|
6
|
+
protected audio_stream: Uint8Array[];
|
|
7
|
+
protected output_format: string;
|
|
8
|
+
protected word_boundaries: WordBoundary[];
|
|
9
|
+
protected sentence_boundaries: SentenceBoundary[];
|
|
10
|
+
protected readonly options: EdgeTTSOptions;
|
|
11
|
+
private voicesCache?;
|
|
12
|
+
protected abstract readonly defaultVoice: string;
|
|
13
|
+
constructor(options?: EdgeTTSOptions);
|
|
14
|
+
protected abstract openSocket(url: string): SocketLike;
|
|
15
|
+
protected abstract requestVoices(url: string): Promise<any[]>;
|
|
16
|
+
protected abstract buildSSML(text: string, voice: string, options: SynthesisOptions): string;
|
|
17
|
+
protected abstract timestamp(): string;
|
|
18
|
+
protected abstract socketError(event: unknown, opened: boolean): Error;
|
|
19
|
+
protected recoverHandshake(): Promise<boolean>;
|
|
20
|
+
protected signature(): Promise<string>;
|
|
21
|
+
normalizeVoices(data: any[]): Promise<Voice[]>;
|
|
22
|
+
getVoices(): Promise<Voice[]>;
|
|
23
|
+
getVoicesByLanguage(locale: string): Promise<Voice[]>;
|
|
24
|
+
getVoicesByGender(gender: 'Male' | 'Female'): Promise<Voice[]>;
|
|
25
|
+
synthesize(text: string, voice?: string, options?: SynthesisOptions): Promise<void>;
|
|
26
|
+
synthesizeStream(text: string, voice?: string, options?: SynthesisOptions): AsyncGenerator<Uint8Array, void, unknown>;
|
|
27
|
+
generate(text: string, voice?: string, options?: SynthesisOptions): Promise<SynthesisResult>;
|
|
28
|
+
getWordBoundaries(): WordBoundary[];
|
|
29
|
+
getSentenceBoundaries(): SentenceBoundary[];
|
|
30
|
+
toSRT(): string;
|
|
31
|
+
toVTT(): string;
|
|
32
|
+
private run;
|
|
33
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EdgeTTSCore = exports.NO_AUDIO_MESSAGE = void 0;
|
|
4
|
+
exports.noAudioMessage = noAudioMessage;
|
|
5
|
+
const constants_1 = require("../config/constants");
|
|
6
|
+
const clock_1 = require("./clock");
|
|
7
|
+
const errors_1 = require("./errors");
|
|
8
|
+
const protocol_1 = require("./protocol");
|
|
9
|
+
const session_1 = require("./session");
|
|
10
|
+
const subtitles_1 = require("./subtitles");
|
|
11
|
+
const DEFAULT_TIMEOUT = 30_000;
|
|
12
|
+
const DEFAULT_VOICES_CACHE_TTL = 10 * 60_000;
|
|
13
|
+
exports.NO_AUDIO_MESSAGE = "The service returned no audio. Voices are locale specific, so a voice cannot " +
|
|
14
|
+
"read text written in another script. Check that the voice matches the language " +
|
|
15
|
+
"of the text.";
|
|
16
|
+
function noAudioMessage(outputFormat) {
|
|
17
|
+
const supported = Object.values(constants_1.Constants.OUTPUT_FORMAT);
|
|
18
|
+
if (supported.includes(outputFormat))
|
|
19
|
+
return exports.NO_AUDIO_MESSAGE;
|
|
20
|
+
return `${exports.NO_AUDIO_MESSAGE} The output format "${outputFormat}" may also be the cause: ` +
|
|
21
|
+
`the service is known to support ${supported.join(', ')}.`;
|
|
22
|
+
}
|
|
23
|
+
function collect(text, words, sentences) {
|
|
24
|
+
const restore = (0, protocol_1.sentenceTextRestorer)(text);
|
|
25
|
+
return (boundary) => {
|
|
26
|
+
if (boundary.type === 'WordBoundary')
|
|
27
|
+
words.push(boundary);
|
|
28
|
+
else
|
|
29
|
+
sentences.push({ ...boundary, text: restore(boundary.text) });
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
class EdgeTTSCore {
|
|
33
|
+
audio_stream = [];
|
|
34
|
+
output_format = protocol_1.DEFAULT_OUTPUT_FORMAT;
|
|
35
|
+
word_boundaries = [];
|
|
36
|
+
sentence_boundaries = [];
|
|
37
|
+
options;
|
|
38
|
+
voicesCache;
|
|
39
|
+
constructor(options = {}) {
|
|
40
|
+
this.options = { ...options };
|
|
41
|
+
}
|
|
42
|
+
async recoverHandshake() {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
signature() {
|
|
46
|
+
return (0, protocol_1.generateSecMsGec)(constants_1.Constants.TRUSTED_CLIENT_TOKEN, (0, clock_1.now)());
|
|
47
|
+
}
|
|
48
|
+
async normalizeVoices(data) {
|
|
49
|
+
return (0, protocol_1.normalizeVoices)(data);
|
|
50
|
+
}
|
|
51
|
+
async getVoices() {
|
|
52
|
+
if (this.voicesCache && this.voicesCache.expires > Date.now()) {
|
|
53
|
+
return JSON.parse(JSON.stringify(this.voicesCache.voices));
|
|
54
|
+
}
|
|
55
|
+
const secMsGEC = await this.signature();
|
|
56
|
+
const url = `${this.options.voicesUrl ?? constants_1.Constants.VOICES_URL}` +
|
|
57
|
+
`?TrustedClientToken=${constants_1.Constants.TRUSTED_CLIENT_TOKEN}` +
|
|
58
|
+
`&Sec-MS-GEC=${secMsGEC}` +
|
|
59
|
+
`&Sec-MS-GEC-Version=${this.options.secMsGecVersion ?? constants_1.Constants.VERSION_MS_GEC}`;
|
|
60
|
+
const voices = await this.normalizeVoices(await this.requestVoices(url));
|
|
61
|
+
const ttl = this.options.voicesCacheTtl ?? DEFAULT_VOICES_CACHE_TTL;
|
|
62
|
+
if (ttl > 0) {
|
|
63
|
+
this.voicesCache = { expires: Date.now() + ttl, voices: JSON.parse(JSON.stringify(voices)) };
|
|
64
|
+
}
|
|
65
|
+
return voices;
|
|
66
|
+
}
|
|
67
|
+
async getVoicesByLanguage(locale) {
|
|
68
|
+
const voices = await this.getVoices();
|
|
69
|
+
return voices.filter(voice => voice.Locale?.startsWith(locale));
|
|
70
|
+
}
|
|
71
|
+
async getVoicesByGender(gender) {
|
|
72
|
+
const voices = await this.getVoices();
|
|
73
|
+
return voices.filter(voice => voice.Gender === gender);
|
|
74
|
+
}
|
|
75
|
+
async synthesize(text, voice = this.defaultVoice, options = {}) {
|
|
76
|
+
const audio = [];
|
|
77
|
+
const words = [];
|
|
78
|
+
const sentences = [];
|
|
79
|
+
this.audio_stream = audio;
|
|
80
|
+
this.word_boundaries = words;
|
|
81
|
+
this.sentence_boundaries = sentences;
|
|
82
|
+
this.output_format = options.outputFormat || protocol_1.DEFAULT_OUTPUT_FORMAT;
|
|
83
|
+
await this.run(text, voice, options, chunk => audio.push(chunk), collect(text, words, sentences));
|
|
84
|
+
}
|
|
85
|
+
async *synthesizeStream(text, voice = this.defaultVoice, options = {}) {
|
|
86
|
+
const audio = [];
|
|
87
|
+
const words = [];
|
|
88
|
+
const sentences = [];
|
|
89
|
+
this.audio_stream = audio;
|
|
90
|
+
this.word_boundaries = words;
|
|
91
|
+
this.sentence_boundaries = sentences;
|
|
92
|
+
this.output_format = options.outputFormat || protocol_1.DEFAULT_OUTPUT_FORMAT;
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
const forwardAbort = () => controller.abort((0, session_1.abortReason)(options.signal));
|
|
95
|
+
if (options.signal?.aborted)
|
|
96
|
+
forwardAbort();
|
|
97
|
+
else
|
|
98
|
+
options.signal?.addEventListener('abort', forwardAbort, { once: true });
|
|
99
|
+
const queue = [];
|
|
100
|
+
let wake = null;
|
|
101
|
+
let done = false;
|
|
102
|
+
let failure;
|
|
103
|
+
const notify = () => {
|
|
104
|
+
wake?.();
|
|
105
|
+
wake = null;
|
|
106
|
+
};
|
|
107
|
+
this.run(text, voice, { ...options, signal: controller.signal }, chunk => {
|
|
108
|
+
audio.push(chunk);
|
|
109
|
+
queue.push(chunk);
|
|
110
|
+
notify();
|
|
111
|
+
}, collect(text, words, sentences)).then(() => { done = true; notify(); }, error => { failure = error; done = true; notify(); });
|
|
112
|
+
try {
|
|
113
|
+
while (!done || queue.length > 0) {
|
|
114
|
+
if (queue.length === 0) {
|
|
115
|
+
await new Promise(resolve => (wake = resolve));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
yield queue.shift();
|
|
119
|
+
}
|
|
120
|
+
if (failure !== undefined)
|
|
121
|
+
throw failure;
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
options.signal?.removeEventListener('abort', forwardAbort);
|
|
125
|
+
if (!done)
|
|
126
|
+
controller.abort();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async generate(text, voice = this.defaultVoice, options = {}) {
|
|
130
|
+
const chunks = [];
|
|
131
|
+
const words = [];
|
|
132
|
+
const sentences = [];
|
|
133
|
+
await this.run(text, voice, options, chunk => chunks.push(chunk), collect(text, words, sentences));
|
|
134
|
+
const audio = (0, protocol_1.concatChunks)(chunks);
|
|
135
|
+
const outputFormat = options.outputFormat || protocol_1.DEFAULT_OUTPUT_FORMAT;
|
|
136
|
+
if (audio.length === 0)
|
|
137
|
+
throw new errors_1.EdgeTTSError(noAudioMessage(outputFormat), 'NO_AUDIO');
|
|
138
|
+
return {
|
|
139
|
+
audio,
|
|
140
|
+
format: (0, protocol_1.fileExtension)(outputFormat),
|
|
141
|
+
outputFormat,
|
|
142
|
+
wordBoundaries: words,
|
|
143
|
+
sentenceBoundaries: sentences,
|
|
144
|
+
duration: (0, protocol_1.estimateDuration)(audio.length, outputFormat),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
getWordBoundaries() {
|
|
148
|
+
return this.word_boundaries;
|
|
149
|
+
}
|
|
150
|
+
getSentenceBoundaries() {
|
|
151
|
+
return this.sentence_boundaries;
|
|
152
|
+
}
|
|
153
|
+
toSRT() {
|
|
154
|
+
return (0, subtitles_1.toSRT)(this.sentence_boundaries.length > 0 ? this.sentence_boundaries : this.word_boundaries);
|
|
155
|
+
}
|
|
156
|
+
toVTT() {
|
|
157
|
+
return (0, subtitles_1.toVTT)(this.sentence_boundaries.length > 0 ? this.sentence_boundaries : this.word_boundaries);
|
|
158
|
+
}
|
|
159
|
+
async run(text, voice, options, onAudio, onBoundary) {
|
|
160
|
+
const ssml = this.buildSSML(text, voice, options);
|
|
161
|
+
const outputFormat = options.outputFormat || protocol_1.DEFAULT_OUTPUT_FORMAT;
|
|
162
|
+
const attempt = async () => {
|
|
163
|
+
const requestId = (0, protocol_1.generateUUID)();
|
|
164
|
+
const url = (0, protocol_1.buildSynthesisUrl)(this.options.wssUrl ?? constants_1.Constants.WSS_URL, constants_1.Constants.TRUSTED_CLIENT_TOKEN, await this.signature(), this.options.secMsGecVersion ?? constants_1.Constants.VERSION_MS_GEC, requestId);
|
|
165
|
+
await (0, session_1.runSession)(() => this.openSocket(url), {
|
|
166
|
+
configMessage: (0, protocol_1.buildConfigMessage)(this.timestamp(), outputFormat),
|
|
167
|
+
ssmlMessage: (0, protocol_1.buildSSMLMessage)(requestId, this.timestamp(), ssml),
|
|
168
|
+
timeout: options.timeout ?? this.options.timeout ?? DEFAULT_TIMEOUT,
|
|
169
|
+
signal: options.signal,
|
|
170
|
+
onAudio,
|
|
171
|
+
onBoundary,
|
|
172
|
+
toError: (event, opened) => this.socketError(event, opened),
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
try {
|
|
176
|
+
await attempt();
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (!(error instanceof errors_1.EdgeTTSError && error.code === 'HANDSHAKE'))
|
|
180
|
+
throw error;
|
|
181
|
+
if (!(await this.recoverHandshake()))
|
|
182
|
+
throw error;
|
|
183
|
+
await attempt();
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
exports.EdgeTTSCore = EdgeTTSCore;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.now = now;
|
|
4
|
+
exports.adjustClock = adjustClock;
|
|
5
|
+
exports.resetClock = resetClock;
|
|
6
|
+
let skewMs = 0;
|
|
7
|
+
function now() {
|
|
8
|
+
return Date.now() + skewMs;
|
|
9
|
+
}
|
|
10
|
+
// NOTE: the Date header has one-second resolution, so smaller differences are noise.
|
|
11
|
+
function adjustClock(serverTimeMs) {
|
|
12
|
+
const delta = serverTimeMs - now();
|
|
13
|
+
if (!Number.isFinite(delta) || Math.abs(delta) < 2000)
|
|
14
|
+
return false;
|
|
15
|
+
skewMs += delta;
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
function resetClock() {
|
|
19
|
+
skewMs = 0;
|
|
20
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface WebCrypto {
|
|
2
|
+
getRandomValues<T extends Uint8Array>(array: T): T;
|
|
3
|
+
subtle: {
|
|
4
|
+
digest(algorithm: string, data: Uint8Array): Promise<ArrayBuffer>;
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export declare function setWebCrypto(crypto: WebCrypto): void;
|
|
8
|
+
export declare function webCrypto(): WebCrypto;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setWebCrypto = setWebCrypto;
|
|
4
|
+
exports.webCrypto = webCrypto;
|
|
5
|
+
let impl;
|
|
6
|
+
function setWebCrypto(crypto) {
|
|
7
|
+
impl = crypto;
|
|
8
|
+
}
|
|
9
|
+
function webCrypto() {
|
|
10
|
+
const crypto = impl ?? globalThis.crypto;
|
|
11
|
+
if (!crypto)
|
|
12
|
+
throw new Error('Web Crypto is not available in this environment');
|
|
13
|
+
return crypto;
|
|
14
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type EdgeTTSErrorCode = 'NO_AUDIO' | 'TIMEOUT' | 'HANDSHAKE' | 'NETWORK' | 'INVALID_OPTION' | 'VOICES';
|
|
2
|
+
export declare class EdgeTTSError extends Error {
|
|
3
|
+
readonly code: EdgeTTSErrorCode;
|
|
4
|
+
readonly status?: number;
|
|
5
|
+
constructor(message: string, code: EdgeTTSErrorCode, options?: {
|
|
6
|
+
status?: number;
|
|
7
|
+
cause?: unknown;
|
|
8
|
+
});
|
|
9
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EdgeTTSError = void 0;
|
|
4
|
+
class EdgeTTSError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
status;
|
|
7
|
+
constructor(message, code, options = {}) {
|
|
8
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
9
|
+
this.name = 'EdgeTTSError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
if (options.status !== undefined)
|
|
12
|
+
this.status = options.status;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
exports.EdgeTTSError = EdgeTTSError;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SentenceBoundary, Voice, WordBoundary } from './types';
|
|
2
|
+
export declare const DEFAULT_OUTPUT_FORMAT = "audio-24khz-48kbitrate-mono-mp3";
|
|
3
|
+
export declare function buildSynthesisUrl(base: string, token: string, secMsGec: string, version: string, connectionId: string): string;
|
|
4
|
+
export declare function buildConfigMessage(timestamp: string, outputFormat: string): string;
|
|
5
|
+
export declare function buildSSMLMessage(requestId: string, timestamp: string, ssml: string): string;
|
|
6
|
+
export declare function escapeXML(text: string): string;
|
|
7
|
+
export declare function buildSSML(text: string, voice: string, pitch: string, rate: string, volume: string): string;
|
|
8
|
+
export declare function parseTextFrame(text: string): {
|
|
9
|
+
path: string;
|
|
10
|
+
body: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function parseBinaryFrame(data: Uint8Array): {
|
|
13
|
+
path: string;
|
|
14
|
+
body: Uint8Array;
|
|
15
|
+
};
|
|
16
|
+
export declare function parseMetadata(body: string): (WordBoundary | SentenceBoundary)[];
|
|
17
|
+
export declare function sentenceTextRestorer(source: string): (text: string) => string;
|
|
18
|
+
export declare function generateSecMsGec(trustedClientToken: string, nowMs: number): Promise<string>;
|
|
19
|
+
export declare function generateUUID(): string;
|
|
20
|
+
export declare function fileExtension(format: string): string;
|
|
21
|
+
export declare function estimateDuration(bytes: number, outputFormat: string): number;
|
|
22
|
+
export declare function concatChunks(chunks: Uint8Array[]): Uint8Array;
|
|
23
|
+
export declare function normalizeVoices(data: any[]): Voice[];
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_OUTPUT_FORMAT = void 0;
|
|
4
|
+
exports.buildSynthesisUrl = buildSynthesisUrl;
|
|
5
|
+
exports.buildConfigMessage = buildConfigMessage;
|
|
6
|
+
exports.buildSSMLMessage = buildSSMLMessage;
|
|
7
|
+
exports.escapeXML = escapeXML;
|
|
8
|
+
exports.buildSSML = buildSSML;
|
|
9
|
+
exports.parseTextFrame = parseTextFrame;
|
|
10
|
+
exports.parseBinaryFrame = parseBinaryFrame;
|
|
11
|
+
exports.parseMetadata = parseMetadata;
|
|
12
|
+
exports.sentenceTextRestorer = sentenceTextRestorer;
|
|
13
|
+
exports.generateSecMsGec = generateSecMsGec;
|
|
14
|
+
exports.generateUUID = generateUUID;
|
|
15
|
+
exports.fileExtension = fileExtension;
|
|
16
|
+
exports.estimateDuration = estimateDuration;
|
|
17
|
+
exports.concatChunks = concatChunks;
|
|
18
|
+
exports.normalizeVoices = normalizeVoices;
|
|
19
|
+
const crypto_1 = require("./crypto");
|
|
20
|
+
exports.DEFAULT_OUTPUT_FORMAT = 'audio-24khz-48kbitrate-mono-mp3';
|
|
21
|
+
const decoder = new TextDecoder();
|
|
22
|
+
function buildSynthesisUrl(base, token, secMsGec, version, connectionId) {
|
|
23
|
+
return `${base}?Ocp-Apim-Subscription-Key=${token}&Sec-MS-GEC=${secMsGec}&Sec-MS-GEC-Version=${version}&ConnectionId=${connectionId}`;
|
|
24
|
+
}
|
|
25
|
+
function buildConfigMessage(timestamp, outputFormat) {
|
|
26
|
+
return `X-Timestamp:${timestamp}\r\nContent-Type:application/json; charset=utf-8\r\nPath:speech.config\r\n\r\n` +
|
|
27
|
+
`{"context":{"synthesis":{"audio":{"metadataoptions":{"sentenceBoundaryEnabled":true,"wordBoundaryEnabled":true},"outputFormat":"${outputFormat}"}}}}`;
|
|
28
|
+
}
|
|
29
|
+
function buildSSMLMessage(requestId, timestamp, ssml) {
|
|
30
|
+
return `X-RequestId:${requestId}\r\nContent-Type:application/ssml+xml\r\nX-Timestamp:${timestamp}\r\nPath:ssml\r\n\r\n${ssml}`;
|
|
31
|
+
}
|
|
32
|
+
function escapeXML(text) {
|
|
33
|
+
return text
|
|
34
|
+
.replace(/&/g, '&')
|
|
35
|
+
.replace(/</g, '<')
|
|
36
|
+
.replace(/>/g, '>')
|
|
37
|
+
.replace(/"/g, '"')
|
|
38
|
+
.replace(/'/g, ''');
|
|
39
|
+
}
|
|
40
|
+
function buildSSML(text, voice, pitch, rate, volume) {
|
|
41
|
+
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">
|
|
42
|
+
<voice name="${voice}">
|
|
43
|
+
<prosody pitch="${pitch}" rate="${rate}" volume="${volume}">
|
|
44
|
+
${escapeXML(text)}
|
|
45
|
+
</prosody>
|
|
46
|
+
</voice>
|
|
47
|
+
</speak>`;
|
|
48
|
+
}
|
|
49
|
+
function headerPath(head) {
|
|
50
|
+
return /Path:([^\r\n]*)/i.exec(head)?.[1]?.trim() ?? '';
|
|
51
|
+
}
|
|
52
|
+
function parseTextFrame(text) {
|
|
53
|
+
const split = text.indexOf('\r\n\r\n');
|
|
54
|
+
if (split === -1)
|
|
55
|
+
return { path: headerPath(text), body: '' };
|
|
56
|
+
return { path: headerPath(text.slice(0, split)), body: text.slice(split + 4) };
|
|
57
|
+
}
|
|
58
|
+
function parseBinaryFrame(data) {
|
|
59
|
+
if (data.length < 2)
|
|
60
|
+
return { path: '', body: new Uint8Array(0) };
|
|
61
|
+
const headerLength = (data[0] << 8) | data[1];
|
|
62
|
+
return {
|
|
63
|
+
path: headerPath(decoder.decode(data.subarray(2, 2 + headerLength))),
|
|
64
|
+
body: data.slice(2 + headerLength),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function parseMetadata(body) {
|
|
68
|
+
let metadata;
|
|
69
|
+
try {
|
|
70
|
+
metadata = JSON.parse(body);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(metadata?.Metadata))
|
|
76
|
+
return [];
|
|
77
|
+
const boundaries = [];
|
|
78
|
+
for (const item of metadata.Metadata) {
|
|
79
|
+
if (item?.Type !== 'WordBoundary' && item?.Type !== 'SentenceBoundary')
|
|
80
|
+
continue;
|
|
81
|
+
boundaries.push({
|
|
82
|
+
type: item.Type,
|
|
83
|
+
offset: item.Data.Offset,
|
|
84
|
+
duration: item.Data.Duration,
|
|
85
|
+
text: item.Data.text?.Text,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return boundaries;
|
|
89
|
+
}
|
|
90
|
+
const OPENING_PUNCTUATION = '¿¡«"\'“‘([';
|
|
91
|
+
// NOTE: the service drops opening punctuation such as ¿ and ¡ from sentence text.
|
|
92
|
+
function sentenceTextRestorer(source) {
|
|
93
|
+
let cursor = 0;
|
|
94
|
+
return (text) => {
|
|
95
|
+
const index = text ? source.indexOf(text, cursor) : -1;
|
|
96
|
+
if (index === -1)
|
|
97
|
+
return text;
|
|
98
|
+
let start = index;
|
|
99
|
+
while (start > cursor && OPENING_PUNCTUATION.includes(source[start - 1]))
|
|
100
|
+
start--;
|
|
101
|
+
cursor = index + text.length;
|
|
102
|
+
return source.slice(start, cursor);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
async function generateSecMsGec(trustedClientToken, nowMs) {
|
|
106
|
+
const ticks = Math.floor(nowMs / 1000) + 11644473600;
|
|
107
|
+
const rounded = ticks - (ticks % 300);
|
|
108
|
+
const windowsTicks = rounded * 10_000_000;
|
|
109
|
+
const data = new TextEncoder().encode(`${windowsTicks}${trustedClientToken}`);
|
|
110
|
+
const hashBuffer = await (0, crypto_1.webCrypto)().subtle.digest('SHA-256', data);
|
|
111
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
112
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
113
|
+
.join('')
|
|
114
|
+
.toUpperCase();
|
|
115
|
+
}
|
|
116
|
+
function generateUUID() {
|
|
117
|
+
return 'xxxxxxxx-xxxx-xxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
118
|
+
const r = Math.random() * 16 | 0;
|
|
119
|
+
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
120
|
+
return v.toString(16);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
function fileExtension(format) {
|
|
124
|
+
if (format.includes('mp3'))
|
|
125
|
+
return 'mp3';
|
|
126
|
+
if (format.includes('opus') && format.includes('webm'))
|
|
127
|
+
return 'webm';
|
|
128
|
+
if (format.includes('opus') && format.includes('ogg'))
|
|
129
|
+
return 'ogg';
|
|
130
|
+
if (format.includes('wav') || format.includes('riff'))
|
|
131
|
+
return 'wav';
|
|
132
|
+
if (format.includes('pcm') && format.includes('raw'))
|
|
133
|
+
return 'pcm';
|
|
134
|
+
if (format.includes('alaw'))
|
|
135
|
+
return 'alaw';
|
|
136
|
+
if (format.includes('mulaw'))
|
|
137
|
+
return 'mulaw';
|
|
138
|
+
if (format.includes('truesilk'))
|
|
139
|
+
return 'silk';
|
|
140
|
+
if (format.includes('g722'))
|
|
141
|
+
return 'g722';
|
|
142
|
+
if (format.includes('amr'))
|
|
143
|
+
return 'amr';
|
|
144
|
+
return 'audio';
|
|
145
|
+
}
|
|
146
|
+
// NOTE: MP3 is CBR; formats without a bitrate (opus) assume 48 kbit/s.
|
|
147
|
+
function estimateDuration(bytes, outputFormat) {
|
|
148
|
+
const kbps = Number(/(\d+)kbitrate/.exec(outputFormat)?.[1] ?? 48);
|
|
149
|
+
return (bytes * 8) / (kbps * 1000);
|
|
150
|
+
}
|
|
151
|
+
function concatChunks(chunks) {
|
|
152
|
+
const result = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
|
|
153
|
+
let offset = 0;
|
|
154
|
+
for (const chunk of chunks) {
|
|
155
|
+
result.set(chunk, offset);
|
|
156
|
+
offset += chunk.length;
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
function normalizeVoices(data) {
|
|
161
|
+
const out = [];
|
|
162
|
+
for (const v of data || []) {
|
|
163
|
+
const short = v?.ShortName || "";
|
|
164
|
+
const locale = v?.Locale || "";
|
|
165
|
+
// base: remove locale prefix and Neural/NeuralHD suffix
|
|
166
|
+
let base = short.replace(/^[a-z]{2}-[A-Z]{2}-/, "");
|
|
167
|
+
base = base.replace(/NeuralHD$/, "").replace(/Neural$/, "").trim();
|
|
168
|
+
// VoiceType: if NeuralHD/Neural in Name or ShortName
|
|
169
|
+
const mix = `${v?.Name || ""} ${short}`;
|
|
170
|
+
const voiceType = v?.VoiceType || (/NeuralHD/i.test(mix) ? "NeuralHD" : "Neural");
|
|
171
|
+
// LocaleName: prefer LocaleName -> LanguageName -> locale
|
|
172
|
+
const localeName = v?.LocaleName || (locale || null);
|
|
173
|
+
// DisplayName: prefer DisplayName -> FriendlyName -> base -> short
|
|
174
|
+
let display = v?.DisplayName || v?.FriendlyName || base || short;
|
|
175
|
+
display = display.replace(/^Microsoft\s+/i, "");
|
|
176
|
+
display = display.split(" - ")[0].trim();
|
|
177
|
+
display = display.replace(/\s*Online\s*\(Natural\)\s*/i, " ");
|
|
178
|
+
display = display.replace(/\s*Online\s*/i, " ");
|
|
179
|
+
display = display.replace(/\s+/g, " ").trim();
|
|
180
|
+
// VoiceTag parsing
|
|
181
|
+
const tag = (v?.VoiceTag && typeof v.VoiceTag === "object") ? v.VoiceTag : {};
|
|
182
|
+
const tailored = Array.isArray(tag.TailoredScenarios)
|
|
183
|
+
? tag.TailoredScenarios
|
|
184
|
+
: (Array.isArray(tag.ContentCategories) ? tag.ContentCategories : []);
|
|
185
|
+
const personalities = Array.isArray(tag.VoicePersonalities)
|
|
186
|
+
? tag.VoicePersonalities
|
|
187
|
+
: [];
|
|
188
|
+
out.push({
|
|
189
|
+
Name: short || (v?.Name || ""),
|
|
190
|
+
DisplayName: display,
|
|
191
|
+
LocalName: display,
|
|
192
|
+
ShortName: short || (v?.Name || ""),
|
|
193
|
+
Gender: v?.Gender ?? null,
|
|
194
|
+
Locale: locale || null,
|
|
195
|
+
LocaleName: localeName,
|
|
196
|
+
SecondaryLocaleList: Array.isArray(v?.SecondaryLocaleList) ? v.SecondaryLocaleList : [],
|
|
197
|
+
VoiceType: voiceType,
|
|
198
|
+
VoiceTag: {
|
|
199
|
+
TailoredScenarios: tailored,
|
|
200
|
+
VoicePersonalities: personalities,
|
|
201
|
+
},
|
|
202
|
+
FriendlyName: `${display} (${voiceType}) - ${localeName}`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { SentenceBoundary, WordBoundary } from './types';
|
|
2
|
+
export interface SocketLike {
|
|
3
|
+
binaryType: string;
|
|
4
|
+
readonly readyState: number;
|
|
5
|
+
onopen: ((event: any) => void) | null;
|
|
6
|
+
onmessage: ((event: {
|
|
7
|
+
data: unknown;
|
|
8
|
+
}) => void) | null;
|
|
9
|
+
onerror: ((event: any) => void) | null;
|
|
10
|
+
onclose: ((event: any) => void) | null;
|
|
11
|
+
send(data: string): void;
|
|
12
|
+
close(): void;
|
|
13
|
+
}
|
|
14
|
+
export interface SessionOptions {
|
|
15
|
+
configMessage: string;
|
|
16
|
+
ssmlMessage: string;
|
|
17
|
+
timeout: number;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
onAudio(chunk: Uint8Array): void;
|
|
20
|
+
onBoundary(boundary: WordBoundary | SentenceBoundary): void;
|
|
21
|
+
toError(event: unknown, opened: boolean): Error;
|
|
22
|
+
}
|
|
23
|
+
export declare function abortReason(signal: AbortSignal): unknown;
|
|
24
|
+
export declare function runSession(open: () => SocketLike, options: SessionOptions): Promise<void>;
|