@andresaya/edge-tts 1.8.3 → 1.9.1
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 +184 -46
- package/dist/browser/edge-tts.esm.js +495 -287
- 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 +495 -287
- 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 +2 -1
- package/dist/commands/SynthesizeCommand.js +19 -3
- package/dist/commands/VoiceListCommand.js +6 -2
- package/dist/config/constants.js +2 -1
- package/dist/core/base.d.ts +33 -0
- package/dist/core/base.js +189 -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 +33 -0
- package/dist/core/session.js +103 -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 +79 -376
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -3,17 +3,30 @@
|
|
|
3
3
|
|
|
4
4
|
**Edge TTS** is a powerful Text-to-Speech (TTS) package that leverages Microsoft's Edge capabilities. This package allows you to synthesize speech from text and manage voice options easily through a command-line interface (CLI).
|
|
5
5
|
|
|
6
|
+
## Available in three languages
|
|
7
|
+
|
|
8
|
+
The same library for the Edge text-to-speech service, in Node.js, PHP and Go:
|
|
9
|
+
|
|
10
|
+
| Language | Install | Repository |
|
|
11
|
+
| --- | --- | --- |
|
|
12
|
+
| **Node.js and browsers** (this one) | `npm install @andresaya/edge-tts` | [andresayac/edge-tts](https://github.com/andresayac/edge-tts) |
|
|
13
|
+
| PHP | `composer require afaya/edge-tts` | [andresayac/edge-tts-php](https://github.com/andresayac/edge-tts-php) |
|
|
14
|
+
| Go | `go get github.com/andresayac/edge-tts-go` | [andresayac/edge-tts-go](https://github.com/andresayac/edge-tts-go) |
|
|
15
|
+
|
|
6
16
|
## Features
|
|
7
17
|
|
|
8
18
|
- **Text-to-Speech**: Convert text into natural-sounding speech using Microsoft Edge's TTS capabilities.
|
|
9
19
|
- **TypeScript Support**: Full TypeScript support with comprehensive type definitions included.
|
|
10
|
-
- **
|
|
20
|
+
- **Audio Formats**: MP3 at 48 or 96 kbit/s, and WebM/Opus.
|
|
11
21
|
- **Multiple Voices**: Access a variety of voices to suit your project's needs.
|
|
12
22
|
- **Voice Filtering**: Filter voices by language and gender for better selection.
|
|
13
23
|
- **Audio Information**: Get detailed information about generated audio (size, duration, format).
|
|
14
24
|
- **Audio Export Options**: Export synthesized audio in different formats (raw, base64, or directly to a file).
|
|
15
25
|
- **Streaming Support**: Stream audio data in real-time for better performance.
|
|
16
|
-
- **Word Boundaries
|
|
26
|
+
- **Word and Sentence Boundaries**: Get word and sentence timings with precise timestamps.
|
|
27
|
+
- **Subtitles**: Generate SRT or WebVTT subtitles, one cue per sentence.
|
|
28
|
+
- **Concurrent Use**: `generate()` returns its own result, so one instance can run several syntheses at once.
|
|
29
|
+
- **Configurable**: Override the endpoint, headers, timeout and voice cache without waiting for a release.
|
|
17
30
|
- **Command-Line Interface**: Use a simple CLI for easy access to functionality.
|
|
18
31
|
- **Easy Integration**: Modular structure allows for easy inclusion in existing projects.
|
|
19
32
|
|
|
@@ -28,6 +41,8 @@ bun add @andresaya/edge-tts
|
|
|
28
41
|
npm install @andresaya/edge-tts
|
|
29
42
|
```
|
|
30
43
|
|
|
44
|
+
Tested on Node.js 18, 20 and 24, and on Bun.
|
|
45
|
+
|
|
31
46
|
## TypeScript Support
|
|
32
47
|
|
|
33
48
|
Edge TTS is written in **TypeScript** and includes full type definitions. No additional `@types` packages are needed.
|
|
@@ -37,20 +52,29 @@ Edge TTS is written in **TypeScript** and includes full type definitions. No add
|
|
|
37
52
|
```typescript
|
|
38
53
|
import {
|
|
39
54
|
EdgeTTS,
|
|
55
|
+
EdgeTTSError,
|
|
40
56
|
Constants,
|
|
41
57
|
Voice,
|
|
42
58
|
SynthesisOptions,
|
|
43
|
-
|
|
59
|
+
SynthesisResult,
|
|
60
|
+
EdgeTTSOptions,
|
|
61
|
+
WordBoundary,
|
|
62
|
+
SentenceBoundary
|
|
44
63
|
} from '@andresaya/edge-tts';
|
|
45
64
|
|
|
46
65
|
// Voice interface
|
|
47
66
|
interface Voice {
|
|
48
67
|
Name: string;
|
|
49
68
|
ShortName: string;
|
|
50
|
-
Gender: 'Male' | 'Female'
|
|
69
|
+
Gender: string; // 'Male' | 'Female'
|
|
51
70
|
Locale: string;
|
|
52
71
|
FriendlyName: string;
|
|
53
72
|
LocalName: string;
|
|
73
|
+
DisplayName?: string;
|
|
74
|
+
LocaleName?: string;
|
|
75
|
+
VoiceType?: string;
|
|
76
|
+
SecondaryLocaleList?: string[];
|
|
77
|
+
VoiceTag?: { TailoredScenarios: string[]; VoicePersonalities: string[] };
|
|
54
78
|
}
|
|
55
79
|
|
|
56
80
|
// Synthesis options
|
|
@@ -59,15 +83,34 @@ interface SynthesisOptions {
|
|
|
59
83
|
rate?: string | number; // e.g., '50%' or 50
|
|
60
84
|
volume?: string | number; // e.g., '90%' or 90
|
|
61
85
|
outputFormat?: string; // e.g., Constants.OUTPUT_FORMAT.AUDIO_24KHZ_96KBITRATE_MONO_MP3
|
|
86
|
+
signal?: AbortSignal; // cancels the synthesis
|
|
87
|
+
timeout?: number; // inactivity timeout in ms for this call
|
|
62
88
|
}
|
|
63
89
|
|
|
64
|
-
// Word boundary metadata
|
|
90
|
+
// Word and sentence boundary metadata (offset and duration in 100 ns ticks)
|
|
65
91
|
interface WordBoundary {
|
|
66
92
|
type: "WordBoundary";
|
|
67
93
|
offset: number;
|
|
68
94
|
duration: number;
|
|
69
95
|
text: string;
|
|
70
96
|
}
|
|
97
|
+
|
|
98
|
+
interface SentenceBoundary {
|
|
99
|
+
type: "SentenceBoundary";
|
|
100
|
+
offset: number;
|
|
101
|
+
duration: number;
|
|
102
|
+
text: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Returned by generate()
|
|
106
|
+
interface SynthesisResult {
|
|
107
|
+
audio: Uint8Array; // a Buffer in Node.js
|
|
108
|
+
format: string; // file extension: 'mp3' or 'webm'
|
|
109
|
+
outputFormat: string;
|
|
110
|
+
wordBoundaries: WordBoundary[];
|
|
111
|
+
sentenceBoundaries: SentenceBoundary[];
|
|
112
|
+
duration: number; // estimated, in seconds
|
|
113
|
+
}
|
|
71
114
|
```
|
|
72
115
|
|
|
73
116
|
### Type-Safe Usage Example
|
|
@@ -105,9 +148,19 @@ To synthesize speech from text:
|
|
|
105
148
|
edge-tts synthesize -t "Hello, world!" -o hello_world_audio
|
|
106
149
|
```
|
|
107
150
|
|
|
108
|
-
|
|
151
|
+
Other options:
|
|
152
|
+
```bash
|
|
153
|
+
# Choose the voice, read the text from a file, write subtitles too
|
|
154
|
+
edge-tts synthesize -f story.txt -v es-CO-SalomeNeural -o story.mp3 --subtitles story.srt
|
|
155
|
+
|
|
156
|
+
# Read from stdin, WebM/Opus output, WebVTT subtitles
|
|
157
|
+
cat story.txt | edge-tts synthesize -f - --format webm-24khz-16bit-mono-opus -o story --subtitles story.vtt
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
To list available voices, optionally filtered:
|
|
109
161
|
```bash
|
|
110
162
|
edge-tts voice-list
|
|
163
|
+
edge-tts voice-list --locale es-CO --gender female
|
|
111
164
|
```
|
|
112
165
|
|
|
113
166
|
### Integration into Your Project
|
|
@@ -119,6 +172,23 @@ import { EdgeTTS } from '@andresaya/edge-tts';
|
|
|
119
172
|
const tts = new EdgeTTS();
|
|
120
173
|
```
|
|
121
174
|
|
|
175
|
+
### Configuration
|
|
176
|
+
|
|
177
|
+
Every option is optional. The defaults come from `Constants`, so when Microsoft changes the service you can point the library at the new values yourself instead of waiting for a release.
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
const tts = new EdgeTTS({
|
|
181
|
+
wssUrl: 'wss://api.msedgeservices.com/tts/cognitiveservices/websocket/v1',
|
|
182
|
+
voicesUrl: 'https://speech.platform.bing.com/consumer/speech/synthesize/readaloud/voices/list',
|
|
183
|
+
secMsGecVersion: '1-155.0.4283.45',
|
|
184
|
+
headers: { 'User-Agent': '...' }, // Node.js only, added to the default headers
|
|
185
|
+
timeout: 30000, // ms without data before giving up
|
|
186
|
+
voicesCacheTtl: 600000, // ms to reuse the voice list, 0 to always fetch it
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
If the computer's clock is off, the service refuses the connection. In Node.js the library then reads the time from the server and retries once.
|
|
191
|
+
|
|
122
192
|
## API Reference
|
|
123
193
|
|
|
124
194
|
### Voice Management
|
|
@@ -186,9 +256,26 @@ await tts.synthesize("Hello, world!", 'en-US-AriaNeural', {
|
|
|
186
256
|
});
|
|
187
257
|
```
|
|
188
258
|
|
|
259
|
+
#### Independent Results with generate()
|
|
260
|
+
|
|
261
|
+
`synthesize()` keeps the audio on the instance, so two calls at the same time on one instance overwrite each other. `generate()` returns everything in its own result instead and leaves the instance alone:
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
const [hello, bye] = await Promise.all([
|
|
265
|
+
tts.generate("Hello!", 'en-US-AriaNeural'),
|
|
266
|
+
tts.generate("Goodbye!", 'en-US-AriaNeural'),
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
hello.audio; // Buffer in Node.js, Uint8Array in the browser
|
|
270
|
+
hello.format; // 'mp3'
|
|
271
|
+
hello.duration; // estimated seconds
|
|
272
|
+
hello.wordBoundaries; // WordBoundary[]
|
|
273
|
+
hello.sentenceBoundaries; // SentenceBoundary[]
|
|
274
|
+
```
|
|
275
|
+
|
|
189
276
|
#### Audio Output Formats
|
|
190
277
|
|
|
191
|
-
|
|
278
|
+
You can specify the format using the `outputFormat` option:
|
|
192
279
|
|
|
193
280
|
```js
|
|
194
281
|
import { EdgeTTS, Constants } from '@andresaya/edge-tts';
|
|
@@ -206,28 +293,17 @@ await tts.synthesize("Hello!", 'en-US-AriaNeural', {
|
|
|
206
293
|
outputFormat: Constants.OUTPUT_FORMAT.WEBM_24KHZ_16BIT_MONO_OPUS
|
|
207
294
|
});
|
|
208
295
|
await tts.toFile('./output/audio'); // Automatically saved as .webm
|
|
209
|
-
|
|
210
|
-
// Lossless WAV
|
|
211
|
-
await tts.synthesize("Hello!", 'en-US-AriaNeural', {
|
|
212
|
-
outputFormat: Constants.OUTPUT_FORMAT.RIFF_24KHZ_16BIT_MONO_PCM
|
|
213
|
-
});
|
|
214
|
-
await tts.toFile('./output/audio'); // Automatically saved as .wav
|
|
215
296
|
```
|
|
216
297
|
|
|
217
|
-
**Available formats
|
|
298
|
+
**Available formats:**
|
|
218
299
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
300
|
+
| Constant | Value |
|
|
301
|
+
|----------|-------|
|
|
302
|
+
| `AUDIO_24KHZ_48KBITRATE_MONO_MP3` (default) | `audio-24khz-48kbitrate-mono-mp3` |
|
|
303
|
+
| `AUDIO_24KHZ_96KBITRATE_MONO_MP3` | `audio-24khz-96kbitrate-mono-mp3` |
|
|
304
|
+
| `WEBM_24KHZ_16BIT_MONO_OPUS` | `webm-24khz-16bit-mono-opus` |
|
|
223
305
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
**Format recommendations:**
|
|
227
|
-
- 🌐 **Web streaming**: `WEBM_24KHZ_16BIT_MONO_OPUS` or `AUDIO_24KHZ_96KBITRATE_MONO_MP3`
|
|
228
|
-
- 📱 **Mobile apps**: `AUDIO_24KHZ_48KBITRATE_MONO_MP3`
|
|
229
|
-
- 💾 **High quality**: `AUDIO_48KHZ_192KBITRATE_MONO_MP3` or `RIFF_48KHZ_16BIT_MONO_PCM`
|
|
230
|
-
- ⚡ **Low bandwidth**: `AUDIO_16KHZ_32KBITRATE_MONO_MP3`
|
|
306
|
+
The service returns no audio for other formats, such as WAV, PCM or other MP3 bitrates.
|
|
231
307
|
|
|
232
308
|
|
|
233
309
|
#### Streaming Synthesis
|
|
@@ -239,6 +315,22 @@ for await (const chunk of tts.synthesizeStream("Long text to stream...", 'en-US-
|
|
|
239
315
|
}
|
|
240
316
|
```
|
|
241
317
|
|
|
318
|
+
Leaving the loop early with `break` closes the connection.
|
|
319
|
+
|
|
320
|
+
#### Cancelling and Timeouts
|
|
321
|
+
```js
|
|
322
|
+
const controller = new AbortController();
|
|
323
|
+
setTimeout(() => controller.abort(), 5000);
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
await tts.synthesize("Hello!", 'en-US-AriaNeural', { signal: controller.signal, timeout: 10000 });
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (error.name === 'AbortError') console.log('Cancelled');
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
`signal` works with `synthesize()`, `synthesizeStream()` and `generate()`. `timeout` is how long to wait without receiving data, 30 seconds by default.
|
|
333
|
+
|
|
242
334
|
### Audio Information
|
|
243
335
|
|
|
244
336
|
#### Get Audio Details
|
|
@@ -277,16 +369,39 @@ const buffer = tts.toBuffer(); // Get as Buffer object
|
|
|
277
369
|
const filePath = await tts.toFile("output_audio");
|
|
278
370
|
console.log(`Audio saved to: ${filePath}`);
|
|
279
371
|
// Creates: output_audio.mp3
|
|
372
|
+
|
|
373
|
+
await tts.toFile("speech.mp3"); // Creates: speech.mp3 (the extension is not repeated)
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
### Word and Sentence Boundaries
|
|
377
|
+
|
|
378
|
+
Offsets and durations are in 100-nanosecond ticks (10,000,000 per second).
|
|
379
|
+
|
|
380
|
+
```js
|
|
381
|
+
await tts.synthesize("Hello, world. How are you?", 'en-US-AriaNeural');
|
|
382
|
+
|
|
383
|
+
const words = tts.getWordBoundaries(); // [{ type: 'WordBoundary', offset, duration, text: 'Hello' }, ...]
|
|
384
|
+
const sentences = tts.getSentenceBoundaries(); // [{ type: 'SentenceBoundary', offset, duration, text: 'Hello, world.' }, ...]
|
|
385
|
+
|
|
386
|
+
// Save word boundaries to a JSON file
|
|
387
|
+
await tts.saveMetadata('metadata.json');
|
|
280
388
|
```
|
|
281
389
|
|
|
282
|
-
###
|
|
390
|
+
### Subtitles
|
|
391
|
+
|
|
392
|
+
One cue per sentence, with the punctuation of the original text.
|
|
283
393
|
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
|
|
394
|
+
```js
|
|
395
|
+
import { writeFile } from 'fs/promises';
|
|
396
|
+
import { toSRT, toVTT } from '@andresaya/edge-tts';
|
|
287
397
|
|
|
288
|
-
|
|
289
|
-
|
|
398
|
+
await tts.synthesize("¿Hola? Esto es una prueba.", 'es-CO-SalomeNeural');
|
|
399
|
+
await writeFile('speech.srt', tts.toSRT());
|
|
400
|
+
await writeFile('speech.vtt', tts.toVTT());
|
|
401
|
+
|
|
402
|
+
// From a generate() result
|
|
403
|
+
const result = await tts.generate("Hello, world.", 'en-US-AriaNeural');
|
|
404
|
+
await writeFile('hello.srt', toSRT(result.sentenceBoundaries));
|
|
290
405
|
```
|
|
291
406
|
|
|
292
407
|
|
|
@@ -396,7 +511,7 @@ exploreVoices().catch(console.error);
|
|
|
396
511
|
|
|
397
512
|
This library can be used directly in web browsers via CDN or ES modules.
|
|
398
513
|
|
|
399
|
-
**⚠️ Important:**
|
|
514
|
+
**⚠️ Important:** Connecting directly, synthesis only works in **Microsoft Edge**. The speech service refuses connections whose user agent is not Edge, and a web page cannot change the user agent its browser sends. Chrome, Firefox and Safari get a 403 when synthesizing. For those browsers, use a relay (see [Other Browsers](#other-browsers-relay)). The Node.js version is not affected.
|
|
400
515
|
|
|
401
516
|
### CDN Usage (UMD)
|
|
402
517
|
|
|
@@ -470,6 +585,23 @@ This library can be used directly in web browsers via CDN or ES modules.
|
|
|
470
585
|
</script>
|
|
471
586
|
```
|
|
472
587
|
|
|
588
|
+
### Other Browsers (Relay)
|
|
589
|
+
|
|
590
|
+
A small relay running in Node.js connects to the service with Edge's headers and passes the messages through. Start the example relay:
|
|
591
|
+
|
|
592
|
+
```bash
|
|
593
|
+
node examples/relay-server.mjs # ws://127.0.0.1:8080
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
And point the browser at it:
|
|
597
|
+
|
|
598
|
+
```js
|
|
599
|
+
const tts = new EdgeTTS({ wssUrl: 'ws://127.0.0.1:8080' });
|
|
600
|
+
await tts.synthesize("Hello from Chrome!", 'en-US-AriaNeural');
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
The example only listens on `127.0.0.1`. If you deploy it, put it behind your own authentication or origin check so it is not open to everyone.
|
|
604
|
+
|
|
473
605
|
### Complete Browser Example
|
|
474
606
|
|
|
475
607
|
For a full working example with voice selection and synthesis, see [`examples/browser-standalone.html`](examples/browser-standalone.html).
|
|
@@ -500,37 +632,43 @@ For a full working example with voice selection and synthesis, see [`examples/br
|
|
|
500
632
|
|
|
501
633
|
## Error Handling
|
|
502
634
|
|
|
635
|
+
Errors raised by the library are `EdgeTTSError` instances with a `code`. Network errors from Node.js (such as `ECONNREFUSED`) are passed through unchanged.
|
|
636
|
+
|
|
637
|
+
| `code` | When |
|
|
638
|
+
|--------|------|
|
|
639
|
+
| `HANDSHAKE` | The service refused the connection. `status` holds the HTTP status in Node.js (usually 403). |
|
|
640
|
+
| `NETWORK` | The connection failed after it was open (browser). |
|
|
641
|
+
| `TIMEOUT` | No data arrived for `timeout` ms. |
|
|
642
|
+
| `NO_AUDIO` | No synthesis yet, or the service returned no audio (voice and text in different scripts, or a text of only punctuation). |
|
|
643
|
+
| `REFUSED` | The service refused the request and said why, such as `Unsupported voice xx-XX-NobodyNeural.` or an unsupported output format. `status` holds the WebSocket close code (1007). |
|
|
644
|
+
| `INVALID_OPTION` | The text is empty, or `pitch`, `rate` or `volume` is not valid. |
|
|
645
|
+
| `VOICES` | The voice list request failed. `status` holds the HTTP status. |
|
|
646
|
+
|
|
647
|
+
Cancelling with an `AbortSignal` rejects with the signal's reason, an `AbortError`.
|
|
648
|
+
|
|
503
649
|
```js
|
|
504
|
-
import { EdgeTTS } from '@andresaya/edge-tts';
|
|
650
|
+
import { EdgeTTS, EdgeTTSError } from '@andresaya/edge-tts';
|
|
505
651
|
|
|
506
652
|
async function handleErrors() {
|
|
507
653
|
const tts = new EdgeTTS();
|
|
508
654
|
|
|
509
655
|
try {
|
|
510
|
-
await tts.synthesize("Test
|
|
656
|
+
await tts.synthesize("Test", 'en-US-AriaNeural', { volume: -150 });
|
|
511
657
|
} catch (error) {
|
|
512
|
-
|
|
658
|
+
if (error instanceof EdgeTTSError && error.code === 'INVALID_OPTION') {
|
|
659
|
+
console.error('Invalid parameter:', error.message);
|
|
660
|
+
}
|
|
513
661
|
}
|
|
514
662
|
|
|
515
663
|
try {
|
|
516
664
|
// This will throw an error - no audio data
|
|
517
665
|
const duration = tts.getDuration();
|
|
518
666
|
} catch (error) {
|
|
519
|
-
console.error(
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
try {
|
|
523
|
-
// Invalid volume range
|
|
524
|
-
await tts.synthesize("Test", 'en-US-AriaNeural', { volume: -150 });
|
|
525
|
-
} catch (error) {
|
|
526
|
-
console.error('Invalid parameter:', error.message);
|
|
667
|
+
console.error(error.code, error.message); // NO_AUDIO ...
|
|
527
668
|
}
|
|
528
669
|
}
|
|
529
670
|
```
|
|
530
671
|
|
|
531
|
-
## PHP Version
|
|
532
|
-
If you want to use Edge TTS with PHP, you can check out the PHP version of this package: [Edge TTS PHP](https://github.com/andresayac/edge-tts-php)
|
|
533
|
-
|
|
534
672
|
## License
|
|
535
673
|
This project is licensed under the GNU General Public License v3 (GPLv3).
|
|
536
674
|
|