edge-tts-rs-node 0.0.0-stage → 0.1.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/LICENSE +22 -0
- package/README.md +202 -2
- package/edge-tts-rs-node.darwin-arm64.node +0 -0
- package/edge-tts-rs-node.darwin-x64.node +0 -0
- package/edge-tts-rs-node.linux-x64-gnu.node +0 -0
- package/edge-tts-rs-node.win32-x64-msvc.node +0 -0
- package/index.d.ts +88 -0
- package/index.js +293 -0
- package/native.d.ts +55 -0
- package/native.js +322 -0
- package/package.json +42 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 edge-tts-rs-node contributors
|
|
4
|
+
Copyright (c) 2026 edge-tts-rust contributors
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,203 @@
|
|
|
1
|
-
#
|
|
1
|
+
# edge-tts-rs-node
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
把 Rust 的 [`edge-tts-rust`](https://crates.io/crates/edge-tts-rust) 封装成 Node.js / Electron 主进程可直接调用的 N-API 原生扩展。
|
|
4
|
+
|
|
5
|
+
对外 API 与 [`node-edge-tts`](https://www.npmjs.com/package/node-edge-tts) 保持一致,可以按同样的方式 `new EdgeTTS(options)`,调用 `ttsPromise`、`toBuffer`、`streamAudio`、`getVoices`。
|
|
6
|
+
|
|
7
|
+
- 原生实现来自 `edge-tts-rust`(Rust + tokio + rustls),Node 侧只做一层薄包装。
|
|
8
|
+
- N-API(Node-API)ABI 稳定,**Electron 下不需要 `electron-rebuild`**。
|
|
9
|
+
- 复用 Rust 侧的连接池:同一个 `EdgeTTS` 实例的多次请求会复用已预热的 WebSocket。
|
|
10
|
+
- 支持流式合成、逐词字幕时间戳、语音列表。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 目录结构
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
src/lib.rs # N-API 绑定(Rust)
|
|
18
|
+
index.js # EdgeTTS 类,node-edge-tts 兼容层
|
|
19
|
+
index.d.ts # 对外类型定义
|
|
20
|
+
native.js # napi build 生成的加载器(提交到仓库)
|
|
21
|
+
test/index.js # 与 node-edge-tts 测试文件等价的联网测试
|
|
22
|
+
test/smoke.js # 不联网的冒烟测试
|
|
23
|
+
.cargo/config.toml# 国内 crates 镜像(不需要时可删除)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 构建
|
|
27
|
+
|
|
28
|
+
前置条件:
|
|
29
|
+
|
|
30
|
+
- Node.js >= 18
|
|
31
|
+
- Rust 工具链(`rustup`,Windows 使用 `stable-x86_64-pc-windows-msvc`)
|
|
32
|
+
- Windows:Visual Studio Build Tools(含 “使用 C++ 的桌面开发”);macOS:Xcode Command Line Tools;Linux:`gcc`/`clang` 等
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install
|
|
36
|
+
npm run build # 等价于 napi build --platform --release --js native.js --dts native.d.ts
|
|
37
|
+
npm run test:smoke # 不联网,验证扩展可加载
|
|
38
|
+
npm test # 联网合成 test/test.mp3 和 test/test.mp3.json
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Windows 上会生成 `edge-tts-rs-node.win32-x64-msvc.node`。
|
|
42
|
+
|
|
43
|
+
> 国内网络下 `crates.io` 直连可能超时。仓库内的 `.cargo/config.toml` 已把 Cargo 源指向 `rsproxy.cn`,不需要时删除该文件即可。
|
|
44
|
+
|
|
45
|
+
## 快速开始
|
|
46
|
+
|
|
47
|
+
和 `node-edge-tts` 完全一致的用法:
|
|
48
|
+
|
|
49
|
+
```js
|
|
50
|
+
const path = require('node:path')
|
|
51
|
+
const { EdgeTTS } = require('edge-tts-rs-node')
|
|
52
|
+
|
|
53
|
+
const tts = new EdgeTTS({
|
|
54
|
+
outputFormat: 'audio-24khz-96kbitrate-mono-mp3',
|
|
55
|
+
saveSubtitles: true,
|
|
56
|
+
timeout: 10000,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
await tts.ttsPromise('你好,世界', path.join(__dirname, 'output.mp3'))
|
|
60
|
+
await tts.close()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`saveSubtitles: true` 时会额外写出 `output.mp3.json`:
|
|
64
|
+
|
|
65
|
+
```json
|
|
66
|
+
[
|
|
67
|
+
{ "part": "你好", "start": 100, "end": 500 },
|
|
68
|
+
{ "part": ",世界", "start": 500, "end": 1200 }
|
|
69
|
+
]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`start` / `end` 单位是毫秒。
|
|
73
|
+
|
|
74
|
+
## API
|
|
75
|
+
|
|
76
|
+
### `new EdgeTTS(options?)`
|
|
77
|
+
|
|
78
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
79
|
+
| --- | --- | --- | --- |
|
|
80
|
+
| `voice` | `string` | `zh-CN-XiaoyiNeural` | 语音短名或完整 Microsoft voice 标识 |
|
|
81
|
+
| `lang` | `string` | `zh-CN` | 仅为兼容 `node-edge-tts` 保留;实际语言由 `voice` 决定 |
|
|
82
|
+
| `outputFormat` | `string` | `audio-24khz-48kbitrate-mono-mp3` | 输出格式,如 `audio-24khz-96kbitrate-mono-mp3`、`webm-24khz-16bit-mono-opus` |
|
|
83
|
+
| `saveSubtitles` | `boolean` | `false` | `ttsPromise` 是否写 `.json` 字幕;`streamAudio` 是否产出字幕对象 |
|
|
84
|
+
| `rate` | `string` | `default` | 语速,如 `+10%`、`-10%` |
|
|
85
|
+
| `pitch` | `string` | `default` | 音调,如 `+10Hz`、`-10Hz` |
|
|
86
|
+
| `volume` | `string` | `default` | 音量,如 `+10%`、`-50%` |
|
|
87
|
+
| `timeout` | `number` | `10000` | WebSocket 连接超时(毫秒) |
|
|
88
|
+
| `receiveTimeout` | `number` | `60000` | 单帧接收超时(毫秒) |
|
|
89
|
+
| `wsPoolSize` | `number` | `1` | 保持预热的空闲 WebSocket 数量;设为 `0` 关闭连接池 |
|
|
90
|
+
| `proxy` | `string` | — | **暂不支持**,传入会直接报错(见“与 node-edge-tts 的差异”) |
|
|
91
|
+
|
|
92
|
+
### `ttsPromise(text, audioPath): Promise<void>`
|
|
93
|
+
|
|
94
|
+
合成文本并写入 `audioPath`。开启 `saveSubtitles` 时同时写入 `${audioPath}.json`。
|
|
95
|
+
|
|
96
|
+
### `toBuffer(text): Promise<Buffer>`
|
|
97
|
+
|
|
98
|
+
合成并返回音频 Buffer,不落盘。
|
|
99
|
+
|
|
100
|
+
### `streamAudio(text): AsyncGenerator<Buffer | SubtitleCue>`
|
|
101
|
+
|
|
102
|
+
边接收边产出:
|
|
103
|
+
|
|
104
|
+
- `Buffer`:音频分片;
|
|
105
|
+
- `{ part, start, end }`:逐词时间戳(仅当 `saveSubtitles: true`)。
|
|
106
|
+
|
|
107
|
+
提前 `break` 会取消请求并关闭 WebSocket:
|
|
108
|
+
|
|
109
|
+
```js
|
|
110
|
+
const tts = new EdgeTTS({ voice: 'en-US-AriaNeural', saveSubtitles: true })
|
|
111
|
+
|
|
112
|
+
for await (const chunk of tts.streamAudio('Hello world')) {
|
|
113
|
+
if (Buffer.isBuffer(chunk)) {
|
|
114
|
+
process.stdout.write(chunk)
|
|
115
|
+
} else {
|
|
116
|
+
console.error(chunk) // { part: 'Hello', start: 100, end: 487 }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
await tts.close()
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### `getVoices(): Promise<Voice[]>` / `listVoices()`
|
|
124
|
+
|
|
125
|
+
返回 Edge Read Aloud 的语音列表(Microsoft 原始字段:`Name`、`ShortName`、`Gender`、`Locale` 等,另有 `VoiceTag`)。
|
|
126
|
+
|
|
127
|
+
### `close(): Promise<void>`
|
|
128
|
+
|
|
129
|
+
释放原生客户端与连接池。建议在应用退出或不再使用时调用;实例被 GC 时也会兜底关闭。
|
|
130
|
+
|
|
131
|
+
## Electron 主进程
|
|
132
|
+
|
|
133
|
+
N-API 是 ABI 稳定的,同一个 `.node` 文件可以在 Node 与 Electron 中直接加载,无需为 Electron 重新编译。
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
// electron 主进程
|
|
137
|
+
const { app, ipcMain } = require('electron')
|
|
138
|
+
const path = require('node:path')
|
|
139
|
+
const { EdgeTTS } = require('edge-tts-rs-node')
|
|
140
|
+
|
|
141
|
+
const tts = new EdgeTTS({ voice: 'zh-CN-XiaoyiNeural' })
|
|
142
|
+
|
|
143
|
+
ipcMain.handle('tts:save', async (_event, text) => {
|
|
144
|
+
const file = path.join(app.getPath('userData'), `tts-${Date.now()}.mp3`)
|
|
145
|
+
await tts.ttsPromise(text, file)
|
|
146
|
+
return file
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
app.on('before-quit', () => {
|
|
150
|
+
tts.close()
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 打包注意事项
|
|
155
|
+
|
|
156
|
+
原生模块不能从 `asar` 包内直接 `dlopen`,需要解包:
|
|
157
|
+
|
|
158
|
+
```jsonc
|
|
159
|
+
// electron-builder
|
|
160
|
+
"build": {
|
|
161
|
+
"asarUnpack": ["**/*.node"]
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
`electron-forge` 同理,将 `**/*.node` 加入 `packagerConfig.asar.unpack`。
|
|
166
|
+
|
|
167
|
+
Node 侧版本要求:Electron 12+ / Node 14+ 均支持 N-API 8;本包 `engines` 标为 Node >= 18。
|
|
168
|
+
|
|
169
|
+
## 多平台分发
|
|
170
|
+
|
|
171
|
+
当前仓库的 `npm run build` 只构建当前平台。发布到 npm 并支持多平台时,可以使用 napi-rs 的平台子包方案:
|
|
172
|
+
|
|
173
|
+
1. 在各平台(或 CI)分别执行 `napi build --platform --release`;
|
|
174
|
+
2. 用 `napi create-npm-dirs` / 官方 GitHub Actions 模板生成 `edge-tts-rs-node-win32-x64-msvc` 等平台包;
|
|
175
|
+
3. `native.js` 会自动按 `process.platform` / `process.arch` 选择本地文件或平台包。
|
|
176
|
+
|
|
177
|
+
## 与 `node-edge-tts` 的差异
|
|
178
|
+
|
|
179
|
+
| 项目 | 说明 |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `proxy` | `edge-tts-rust 0.1.7` 没有代理支持,传入 `proxy` 会抛出明确的错误。如需代理,需要给 Rust crate 增加代理连接(HTTP CONNECT + TLS 隧道)后重新封装。 |
|
|
182
|
+
| `lang` | 仅保存,不参与 SSML 生成;语言由 `voice` 的 locale 决定,因此请保证两者匹配。 |
|
|
183
|
+
| `timeout` | 语义为连接超时;另外提供 `receiveTimeout` 控制单帧接收。`node-edge-tts` 的 `timeout` 是整体超时。 |
|
|
184
|
+
| `ttsPromise` | 内部一次性合成后写盘,音频会在内存中聚合。超长文本建议改用 `streamAudio` 边收边写。 |
|
|
185
|
+
| 字幕 | 使用 Rust 侧返回的逐词边界并补齐原文空白/标点,输出格式与 `node-edge-tts` 的 `.json` 一致。 |
|
|
186
|
+
| 错误 | 统一为 `Error`,消息带 `EdgeTTS:` 前缀。 |
|
|
187
|
+
| 语音列表 | `getVoices()` 返回数组,字段为 Microsoft 原始命名(`ShortName` 等)。 |
|
|
188
|
+
|
|
189
|
+
## 实现说明
|
|
190
|
+
|
|
191
|
+
Rust 侧(`src/lib.rs`)提供:
|
|
192
|
+
|
|
193
|
+
- `createClient` / `closeClient`:客户端生命周期与连接池;
|
|
194
|
+
- `synthesize`:一次性合成,返回 `{ audio, boundaries }`;
|
|
195
|
+
- `listVoices`:语音列表;
|
|
196
|
+
- `startStream` / `cancelStream`:通过 `ThreadsafeFunction` 把流式事件推回 JS,支持提前取消;
|
|
197
|
+
- `normalizeVoice` / `defaultExtensionForFormat`:与 crate 行为一致的工具函数。
|
|
198
|
+
|
|
199
|
+
异步任务运行在 napi-rs 内置的 tokio runtime 上(`tokio_rt` feature)。流式事件通过 TSFN 按序回调,JS 包装层再转成 `AsyncGenerator`。
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT。底层 [`edge-tts-rust`](https://github.com/initsnow/edge-tts-rust) 同样为 MIT。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
|
|
3
|
+
export interface EdgeTTSOptions {
|
|
4
|
+
/** Voice short name or full Microsoft voice identifier. Default: `zh-CN-XiaoyiNeural`. */
|
|
5
|
+
voice?: string
|
|
6
|
+
/** Kept for `node-edge-tts` compatibility. The effective language comes from `voice`. */
|
|
7
|
+
lang?: string
|
|
8
|
+
/** Audio output format. Default: `audio-24khz-48kbitrate-mono-mp3`. */
|
|
9
|
+
outputFormat?: string
|
|
10
|
+
/** Write `${audioPath}.json` word-boundary subtitles from `ttsPromise`. Default: `false`. */
|
|
11
|
+
saveSubtitles?: boolean
|
|
12
|
+
/** Not supported by edge-tts-rust 0.1.7; passing one rejects with an error. */
|
|
13
|
+
proxy?: string
|
|
14
|
+
/** Rate, for example `+10%` / `-10%` / `default`. */
|
|
15
|
+
rate?: string
|
|
16
|
+
/** Pitch, for example `+10Hz` / `-10Hz` / `default`. */
|
|
17
|
+
pitch?: string
|
|
18
|
+
/** Volume, for example `+10%` / `-50%` / `default`. */
|
|
19
|
+
volume?: string
|
|
20
|
+
/** WebSocket connect timeout in milliseconds. Default: `10000`. */
|
|
21
|
+
timeout?: number
|
|
22
|
+
/** Per-frame receive timeout in milliseconds. Default: `60000`. */
|
|
23
|
+
receiveTimeout?: number
|
|
24
|
+
/** Idle WebSocket connections kept warm between requests. Default: `1`. */
|
|
25
|
+
wsPoolSize?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SubtitleCue {
|
|
29
|
+
part: string
|
|
30
|
+
/** Milliseconds from the start of the audio. */
|
|
31
|
+
start: number
|
|
32
|
+
/** Milliseconds from the start of the audio. */
|
|
33
|
+
end: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface Voice {
|
|
37
|
+
Name: string
|
|
38
|
+
ShortName: string
|
|
39
|
+
Gender: string
|
|
40
|
+
Locale: string
|
|
41
|
+
SuggestedCodec?: string
|
|
42
|
+
FriendlyName?: string
|
|
43
|
+
Status?: string
|
|
44
|
+
VoiceTag?: {
|
|
45
|
+
ContentCategories?: string[]
|
|
46
|
+
VoicePersonalities?: string[]
|
|
47
|
+
}
|
|
48
|
+
[key: string]: unknown
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export declare class EdgeTTS {
|
|
52
|
+
constructor(options?: EdgeTTSOptions)
|
|
53
|
+
|
|
54
|
+
voice: string
|
|
55
|
+
lang: string
|
|
56
|
+
outputFormat: string
|
|
57
|
+
saveSubtitles: boolean
|
|
58
|
+
rate: string
|
|
59
|
+
pitch: string
|
|
60
|
+
volume: string
|
|
61
|
+
proxy?: string
|
|
62
|
+
timeout: number
|
|
63
|
+
receiveTimeout: number
|
|
64
|
+
wsPoolSize: number
|
|
65
|
+
|
|
66
|
+
/** Synthesize `text` and return the audio bytes. */
|
|
67
|
+
toBuffer(text: string): Promise<Buffer>
|
|
68
|
+
|
|
69
|
+
/** Synthesize `text` to `audioPath`, optionally writing `${audioPath}.json` subtitles. */
|
|
70
|
+
ttsPromise(text: string, audioPath: string): Promise<void>
|
|
71
|
+
|
|
72
|
+
/** List the voices served by the Edge Read Aloud endpoint. */
|
|
73
|
+
getVoices(): Promise<Voice[]>
|
|
74
|
+
|
|
75
|
+
/** Alias of `getVoices`. */
|
|
76
|
+
listVoices(): Promise<Voice[]>
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Stream audio chunks and, when `saveSubtitles` is enabled, word boundary
|
|
80
|
+
* cues. Break out of the loop to stop the request.
|
|
81
|
+
*/
|
|
82
|
+
streamAudio(text: string): AsyncGenerator<Buffer | SubtitleCue, void, undefined>
|
|
83
|
+
|
|
84
|
+
/** Release the native client and its pooled WebSocket connections. */
|
|
85
|
+
close(): Promise<void>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default EdgeTTS
|
package/index.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs/promises')
|
|
4
|
+
const native = require('./native.js')
|
|
5
|
+
|
|
6
|
+
const DEFAULT_OPTIONS = {
|
|
7
|
+
voice: 'zh-CN-XiaoyiNeural',
|
|
8
|
+
lang: 'zh-CN',
|
|
9
|
+
outputFormat: 'audio-24khz-48kbitrate-mono-mp3',
|
|
10
|
+
saveSubtitles: false,
|
|
11
|
+
rate: 'default',
|
|
12
|
+
pitch: 'default',
|
|
13
|
+
volume: 'default',
|
|
14
|
+
timeout: 10000,
|
|
15
|
+
receiveTimeout: 60000,
|
|
16
|
+
wsPoolSize: 1,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const clientFinalizer =
|
|
20
|
+
typeof FinalizationRegistry === 'function'
|
|
21
|
+
? new FinalizationRegistry((clientId) => {
|
|
22
|
+
try {
|
|
23
|
+
native.closeClient(clientId)
|
|
24
|
+
} catch {
|
|
25
|
+
// The environment is already tearing down.
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
: null
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Drop-in style replacement for `node-edge-tts`, backed by the Rust
|
|
32
|
+
* `edge-tts-rust` crate.
|
|
33
|
+
*/
|
|
34
|
+
class EdgeTTS {
|
|
35
|
+
constructor(options = {}) {
|
|
36
|
+
const opts = options || {}
|
|
37
|
+
|
|
38
|
+
this.voice = opts.voice ?? DEFAULT_OPTIONS.voice
|
|
39
|
+
this.lang = opts.lang ?? DEFAULT_OPTIONS.lang
|
|
40
|
+
this.outputFormat = opts.outputFormat ?? DEFAULT_OPTIONS.outputFormat
|
|
41
|
+
this.saveSubtitles = Boolean(opts.saveSubtitles)
|
|
42
|
+
this.rate = opts.rate ?? DEFAULT_OPTIONS.rate
|
|
43
|
+
this.pitch = opts.pitch ?? DEFAULT_OPTIONS.pitch
|
|
44
|
+
this.volume = opts.volume ?? DEFAULT_OPTIONS.volume
|
|
45
|
+
this.proxy = opts.proxy
|
|
46
|
+
this.timeout = opts.timeout ?? DEFAULT_OPTIONS.timeout
|
|
47
|
+
this.receiveTimeout = opts.receiveTimeout ?? DEFAULT_OPTIONS.receiveTimeout
|
|
48
|
+
this.wsPoolSize = opts.wsPoolSize ?? DEFAULT_OPTIONS.wsPoolSize
|
|
49
|
+
|
|
50
|
+
this._clientId = null
|
|
51
|
+
this._clientPromise = null
|
|
52
|
+
this._closed = false
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_nativeCreateOptions() {
|
|
56
|
+
return {
|
|
57
|
+
proxy: this.proxy,
|
|
58
|
+
connectTimeoutMs: Math.max(1, Math.floor(Number(this.timeout) || 0)),
|
|
59
|
+
receiveTimeoutMs: Math.max(1, Math.floor(Number(this.receiveTimeout) || 0)),
|
|
60
|
+
wsPoolSize: Math.max(0, Math.floor(Number(this.wsPoolSize) || 0)),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_speakOptions() {
|
|
65
|
+
return {
|
|
66
|
+
voice: this.voice,
|
|
67
|
+
rate: this.rate,
|
|
68
|
+
volume: this.volume,
|
|
69
|
+
pitch: this.pitch,
|
|
70
|
+
outputFormat: this.outputFormat,
|
|
71
|
+
boundary: 'word',
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async _ensureClient() {
|
|
76
|
+
if (this._closed) {
|
|
77
|
+
throw new Error('EdgeTTS: instance has already been closed')
|
|
78
|
+
}
|
|
79
|
+
if (this._clientId !== null) {
|
|
80
|
+
return this._clientId
|
|
81
|
+
}
|
|
82
|
+
if (!this._clientPromise) {
|
|
83
|
+
this._clientPromise = native
|
|
84
|
+
.createClient(this._nativeCreateOptions())
|
|
85
|
+
.then((clientId) => {
|
|
86
|
+
if (this._closed) {
|
|
87
|
+
native.closeClient(clientId)
|
|
88
|
+
throw new Error('EdgeTTS: instance has already been closed')
|
|
89
|
+
}
|
|
90
|
+
this._clientId = clientId
|
|
91
|
+
if (clientFinalizer) {
|
|
92
|
+
clientFinalizer.register(this, clientId, this)
|
|
93
|
+
}
|
|
94
|
+
return clientId
|
|
95
|
+
})
|
|
96
|
+
.finally(() => {
|
|
97
|
+
this._clientPromise = null
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
return this._clientPromise
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async _synthesize(text) {
|
|
104
|
+
if (typeof text !== 'string') {
|
|
105
|
+
throw new TypeError('EdgeTTS: text must be a string')
|
|
106
|
+
}
|
|
107
|
+
const clientId = await this._ensureClient()
|
|
108
|
+
const result = await native.synthesize(clientId, text, this._speakOptions())
|
|
109
|
+
return {
|
|
110
|
+
audio: result.audio,
|
|
111
|
+
boundaries: result.boundaries || [],
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Synthesize `text` and return the audio as a Buffer.
|
|
117
|
+
* @param {string} text
|
|
118
|
+
* @returns {Promise<Buffer>}
|
|
119
|
+
*/
|
|
120
|
+
async toBuffer(text) {
|
|
121
|
+
const { audio } = await this._synthesize(text)
|
|
122
|
+
return audio
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Synthesize `text` into `audioPath`. When `saveSubtitles` is enabled a
|
|
127
|
+
* `${audioPath}.json` file is written next to the audio, matching
|
|
128
|
+
* `node-edge-tts`.
|
|
129
|
+
* @param {string} text
|
|
130
|
+
* @param {string} audioPath
|
|
131
|
+
* @returns {Promise<void>}
|
|
132
|
+
*/
|
|
133
|
+
async ttsPromise(text, audioPath) {
|
|
134
|
+
const { audio, boundaries } = await this._synthesize(text)
|
|
135
|
+
await fs.writeFile(audioPath, audio)
|
|
136
|
+
|
|
137
|
+
if (this.saveSubtitles) {
|
|
138
|
+
const subtitles = correctSubtitleParts(
|
|
139
|
+
boundaries.map(({ part, start, end }) => ({ part, start, end })),
|
|
140
|
+
text,
|
|
141
|
+
)
|
|
142
|
+
await fs.writeFile(
|
|
143
|
+
`${audioPath}.json`,
|
|
144
|
+
JSON.stringify(subtitles, null, ' '),
|
|
145
|
+
'utf-8',
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* List the voices offered by the Edge service.
|
|
152
|
+
* @returns {Promise<Array<object>>}
|
|
153
|
+
*/
|
|
154
|
+
async getVoices() {
|
|
155
|
+
const clientId = await this._ensureClient()
|
|
156
|
+
return native.listVoices(clientId)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Alias of `getVoices`. */
|
|
160
|
+
async listVoices() {
|
|
161
|
+
return this.getVoices()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Stream audio chunks and (when `saveSubtitles` is enabled) word boundary
|
|
166
|
+
* cues as they arrive. Yields `Buffer` objects and
|
|
167
|
+
* `{ part, start, end }` objects.
|
|
168
|
+
* @param {string} text
|
|
169
|
+
* @returns {AsyncGenerator<Buffer | { part: string, start: number, end: number }>}
|
|
170
|
+
*/
|
|
171
|
+
async *streamAudio(text) {
|
|
172
|
+
if (typeof text !== 'string') {
|
|
173
|
+
throw new TypeError('EdgeTTS: text must be a string')
|
|
174
|
+
}
|
|
175
|
+
const clientId = await this._ensureClient()
|
|
176
|
+
|
|
177
|
+
const queue = []
|
|
178
|
+
let notify = null
|
|
179
|
+
let finished = false
|
|
180
|
+
let failure = null
|
|
181
|
+
|
|
182
|
+
const wake = () => {
|
|
183
|
+
const resolve = notify
|
|
184
|
+
notify = null
|
|
185
|
+
if (resolve) resolve()
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const callback = (error, event) => {
|
|
189
|
+
if (error) {
|
|
190
|
+
failure = error instanceof Error ? error : new Error(String(error))
|
|
191
|
+
} else if (event) {
|
|
192
|
+
if (event.error) {
|
|
193
|
+
failure = new Error(event.error)
|
|
194
|
+
} else if (event.audio) {
|
|
195
|
+
queue.push(Buffer.from(event.audio))
|
|
196
|
+
} else if (event.boundary && this.saveSubtitles) {
|
|
197
|
+
queue.push({
|
|
198
|
+
part: event.boundary.part,
|
|
199
|
+
start: event.boundary.start,
|
|
200
|
+
end: event.boundary.end,
|
|
201
|
+
})
|
|
202
|
+
} else if (event.done) {
|
|
203
|
+
finished = true
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
wake()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const streamId = await native.startStream(
|
|
210
|
+
clientId,
|
|
211
|
+
text,
|
|
212
|
+
this._speakOptions(),
|
|
213
|
+
callback,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
while (true) {
|
|
218
|
+
if (failure) throw failure
|
|
219
|
+
if (queue.length > 0) {
|
|
220
|
+
yield queue.shift()
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
if (finished) return
|
|
224
|
+
await new Promise((resolve) => {
|
|
225
|
+
notify = resolve
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
} finally {
|
|
229
|
+
if (!finished) {
|
|
230
|
+
try {
|
|
231
|
+
native.cancelStream(streamId)
|
|
232
|
+
} catch {
|
|
233
|
+
// Already finished.
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Release the native client and its pooled WebSocket connections.
|
|
241
|
+
* @returns {Promise<void>}
|
|
242
|
+
*/
|
|
243
|
+
async close() {
|
|
244
|
+
this._closed = true
|
|
245
|
+
if (this._clientPromise) {
|
|
246
|
+
try {
|
|
247
|
+
await this._clientPromise
|
|
248
|
+
} catch {
|
|
249
|
+
// Creation failed; nothing to release.
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (this._clientId !== null) {
|
|
253
|
+
if (clientFinalizer) clientFinalizer.unregister(this)
|
|
254
|
+
native.closeClient(this._clientId)
|
|
255
|
+
this._clientId = null
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* `node-edge-tts` reconstructs subtitle cues from the original text so that
|
|
262
|
+
* skipped whitespace and punctuation end up in the cue. Kept for parity.
|
|
263
|
+
*/
|
|
264
|
+
function correctSubtitleParts(subtitles, text) {
|
|
265
|
+
if (subtitles.length === 0) return subtitles
|
|
266
|
+
|
|
267
|
+
const chars = text.split('')
|
|
268
|
+
let charIndex = 0
|
|
269
|
+
|
|
270
|
+
subtitles.forEach((cue, index) => {
|
|
271
|
+
let fullPart = ''
|
|
272
|
+
let stepIndex = 0
|
|
273
|
+
const nextPart = subtitles[index + 1]?.part
|
|
274
|
+
|
|
275
|
+
for (let i = charIndex; i < chars.length; i++) {
|
|
276
|
+
if (chars[i] === cue.part[stepIndex]) {
|
|
277
|
+
fullPart += chars[i]
|
|
278
|
+
stepIndex += 1
|
|
279
|
+
} else if (nextPart && chars[i] === nextPart[0]) {
|
|
280
|
+
charIndex = i
|
|
281
|
+
break
|
|
282
|
+
} else {
|
|
283
|
+
fullPart += chars[i]
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
cue.part = fullPart
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
return subtitles
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = { EdgeTTS, default: EdgeTTS }
|
package/native.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/* auto-generated by NAPI-RS */
|
|
5
|
+
|
|
6
|
+
export interface NativeOptions {
|
|
7
|
+
voice?: string
|
|
8
|
+
rate?: string
|
|
9
|
+
volume?: string
|
|
10
|
+
pitch?: string
|
|
11
|
+
outputFormat?: string
|
|
12
|
+
/** `"word"` (default) or `"sentence"`. */
|
|
13
|
+
boundary?: string
|
|
14
|
+
connectTimeoutMs?: number
|
|
15
|
+
receiveTimeoutMs?: number
|
|
16
|
+
wsPoolSize?: number
|
|
17
|
+
proxy?: string
|
|
18
|
+
}
|
|
19
|
+
export interface BoundaryJs {
|
|
20
|
+
part: string
|
|
21
|
+
/** Milliseconds from the beginning of the audio. */
|
|
22
|
+
start: number
|
|
23
|
+
/** Milliseconds from the beginning of the audio. */
|
|
24
|
+
end: number
|
|
25
|
+
/** `"word"` or `"sentence"`. */
|
|
26
|
+
kind: string
|
|
27
|
+
}
|
|
28
|
+
export interface SynthesisResultJs {
|
|
29
|
+
audio: Buffer
|
|
30
|
+
boundaries: Array<BoundaryJs>
|
|
31
|
+
}
|
|
32
|
+
export interface StreamEventJs {
|
|
33
|
+
audio?: Buffer
|
|
34
|
+
boundary?: BoundaryJs
|
|
35
|
+
/** Set when the stream failed. The `done` event still follows. */
|
|
36
|
+
error?: string
|
|
37
|
+
done?: boolean
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create a client. Kept alive until `closeClient` is called (or the process
|
|
41
|
+
* exits). Reusing one client across requests keeps the warmed WebSocket pool.
|
|
42
|
+
*/
|
|
43
|
+
export declare function createClient(options?: NativeOptions | undefined | null): Promise<number>
|
|
44
|
+
export declare function closeClient(clientId: number): boolean
|
|
45
|
+
export declare function synthesize(clientId: number, text: string, options?: NativeOptions | undefined | null): Promise<SynthesisResultJs>
|
|
46
|
+
export declare function listVoices(clientId: number): Promise<any>
|
|
47
|
+
/**
|
|
48
|
+
* Start a streaming synthesis. `callback` is invoked as `(err, event)` on the
|
|
49
|
+
* JavaScript thread. The returned id can be passed to `cancelStream`.
|
|
50
|
+
*/
|
|
51
|
+
export declare function startStream(clientId: number, text: string, options: NativeOptions | undefined | null, callback: (err: Error | null, arg: StreamEventJs) => any): Promise<number>
|
|
52
|
+
/** Cancel an in-flight stream. Returns `false` if it already finished. */
|
|
53
|
+
export declare function cancelStream(streamId: number): boolean
|
|
54
|
+
export declare function normalizeVoice(voice: string): string
|
|
55
|
+
export declare function defaultExtensionForFormat(outputFormat: string): string
|
package/native.js
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
/* prettier-ignore */
|
|
4
|
+
|
|
5
|
+
/* auto-generated by NAPI-RS */
|
|
6
|
+
|
|
7
|
+
const { existsSync, readFileSync } = require('fs')
|
|
8
|
+
const { join } = require('path')
|
|
9
|
+
|
|
10
|
+
const { platform, arch } = process
|
|
11
|
+
|
|
12
|
+
let nativeBinding = null
|
|
13
|
+
let localFileExisted = false
|
|
14
|
+
let loadError = null
|
|
15
|
+
|
|
16
|
+
function isMusl() {
|
|
17
|
+
// For Node 10
|
|
18
|
+
if (!process.report || typeof process.report.getReport !== 'function') {
|
|
19
|
+
try {
|
|
20
|
+
const lddPath = require('child_process').execSync('which ldd').toString().trim()
|
|
21
|
+
return readFileSync(lddPath, 'utf8').includes('musl')
|
|
22
|
+
} catch (e) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
const { glibcVersionRuntime } = process.report.getReport().header
|
|
27
|
+
return !glibcVersionRuntime
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
switch (platform) {
|
|
32
|
+
case 'android':
|
|
33
|
+
switch (arch) {
|
|
34
|
+
case 'arm64':
|
|
35
|
+
localFileExisted = existsSync(join(__dirname, 'edge-tts-rs-node.android-arm64.node'))
|
|
36
|
+
try {
|
|
37
|
+
if (localFileExisted) {
|
|
38
|
+
nativeBinding = require('./edge-tts-rs-node.android-arm64.node')
|
|
39
|
+
} else {
|
|
40
|
+
nativeBinding = require('edge-tts-rs-node-android-arm64')
|
|
41
|
+
}
|
|
42
|
+
} catch (e) {
|
|
43
|
+
loadError = e
|
|
44
|
+
}
|
|
45
|
+
break
|
|
46
|
+
case 'arm':
|
|
47
|
+
localFileExisted = existsSync(join(__dirname, 'edge-tts-rs-node.android-arm-eabi.node'))
|
|
48
|
+
try {
|
|
49
|
+
if (localFileExisted) {
|
|
50
|
+
nativeBinding = require('./edge-tts-rs-node.android-arm-eabi.node')
|
|
51
|
+
} else {
|
|
52
|
+
nativeBinding = require('edge-tts-rs-node-android-arm-eabi')
|
|
53
|
+
}
|
|
54
|
+
} catch (e) {
|
|
55
|
+
loadError = e
|
|
56
|
+
}
|
|
57
|
+
break
|
|
58
|
+
default:
|
|
59
|
+
throw new Error(`Unsupported architecture on Android ${arch}`)
|
|
60
|
+
}
|
|
61
|
+
break
|
|
62
|
+
case 'win32':
|
|
63
|
+
switch (arch) {
|
|
64
|
+
case 'x64':
|
|
65
|
+
localFileExisted = existsSync(
|
|
66
|
+
join(__dirname, 'edge-tts-rs-node.win32-x64-msvc.node')
|
|
67
|
+
)
|
|
68
|
+
try {
|
|
69
|
+
if (localFileExisted) {
|
|
70
|
+
nativeBinding = require('./edge-tts-rs-node.win32-x64-msvc.node')
|
|
71
|
+
} else {
|
|
72
|
+
nativeBinding = require('edge-tts-rs-node-win32-x64-msvc')
|
|
73
|
+
}
|
|
74
|
+
} catch (e) {
|
|
75
|
+
loadError = e
|
|
76
|
+
}
|
|
77
|
+
break
|
|
78
|
+
case 'ia32':
|
|
79
|
+
localFileExisted = existsSync(
|
|
80
|
+
join(__dirname, 'edge-tts-rs-node.win32-ia32-msvc.node')
|
|
81
|
+
)
|
|
82
|
+
try {
|
|
83
|
+
if (localFileExisted) {
|
|
84
|
+
nativeBinding = require('./edge-tts-rs-node.win32-ia32-msvc.node')
|
|
85
|
+
} else {
|
|
86
|
+
nativeBinding = require('edge-tts-rs-node-win32-ia32-msvc')
|
|
87
|
+
}
|
|
88
|
+
} catch (e) {
|
|
89
|
+
loadError = e
|
|
90
|
+
}
|
|
91
|
+
break
|
|
92
|
+
case 'arm64':
|
|
93
|
+
localFileExisted = existsSync(
|
|
94
|
+
join(__dirname, 'edge-tts-rs-node.win32-arm64-msvc.node')
|
|
95
|
+
)
|
|
96
|
+
try {
|
|
97
|
+
if (localFileExisted) {
|
|
98
|
+
nativeBinding = require('./edge-tts-rs-node.win32-arm64-msvc.node')
|
|
99
|
+
} else {
|
|
100
|
+
nativeBinding = require('edge-tts-rs-node-win32-arm64-msvc')
|
|
101
|
+
}
|
|
102
|
+
} catch (e) {
|
|
103
|
+
loadError = e
|
|
104
|
+
}
|
|
105
|
+
break
|
|
106
|
+
default:
|
|
107
|
+
throw new Error(`Unsupported architecture on Windows: ${arch}`)
|
|
108
|
+
}
|
|
109
|
+
break
|
|
110
|
+
case 'darwin':
|
|
111
|
+
localFileExisted = existsSync(join(__dirname, 'edge-tts-rs-node.darwin-universal.node'))
|
|
112
|
+
try {
|
|
113
|
+
if (localFileExisted) {
|
|
114
|
+
nativeBinding = require('./edge-tts-rs-node.darwin-universal.node')
|
|
115
|
+
} else {
|
|
116
|
+
nativeBinding = require('edge-tts-rs-node-darwin-universal')
|
|
117
|
+
}
|
|
118
|
+
break
|
|
119
|
+
} catch {}
|
|
120
|
+
switch (arch) {
|
|
121
|
+
case 'x64':
|
|
122
|
+
localFileExisted = existsSync(join(__dirname, 'edge-tts-rs-node.darwin-x64.node'))
|
|
123
|
+
try {
|
|
124
|
+
if (localFileExisted) {
|
|
125
|
+
nativeBinding = require('./edge-tts-rs-node.darwin-x64.node')
|
|
126
|
+
} else {
|
|
127
|
+
nativeBinding = require('edge-tts-rs-node-darwin-x64')
|
|
128
|
+
}
|
|
129
|
+
} catch (e) {
|
|
130
|
+
loadError = e
|
|
131
|
+
}
|
|
132
|
+
break
|
|
133
|
+
case 'arm64':
|
|
134
|
+
localFileExisted = existsSync(
|
|
135
|
+
join(__dirname, 'edge-tts-rs-node.darwin-arm64.node')
|
|
136
|
+
)
|
|
137
|
+
try {
|
|
138
|
+
if (localFileExisted) {
|
|
139
|
+
nativeBinding = require('./edge-tts-rs-node.darwin-arm64.node')
|
|
140
|
+
} else {
|
|
141
|
+
nativeBinding = require('edge-tts-rs-node-darwin-arm64')
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
loadError = e
|
|
145
|
+
}
|
|
146
|
+
break
|
|
147
|
+
default:
|
|
148
|
+
throw new Error(`Unsupported architecture on macOS: ${arch}`)
|
|
149
|
+
}
|
|
150
|
+
break
|
|
151
|
+
case 'freebsd':
|
|
152
|
+
if (arch !== 'x64') {
|
|
153
|
+
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
|
|
154
|
+
}
|
|
155
|
+
localFileExisted = existsSync(join(__dirname, 'edge-tts-rs-node.freebsd-x64.node'))
|
|
156
|
+
try {
|
|
157
|
+
if (localFileExisted) {
|
|
158
|
+
nativeBinding = require('./edge-tts-rs-node.freebsd-x64.node')
|
|
159
|
+
} else {
|
|
160
|
+
nativeBinding = require('edge-tts-rs-node-freebsd-x64')
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
loadError = e
|
|
164
|
+
}
|
|
165
|
+
break
|
|
166
|
+
case 'linux':
|
|
167
|
+
switch (arch) {
|
|
168
|
+
case 'x64':
|
|
169
|
+
if (isMusl()) {
|
|
170
|
+
localFileExisted = existsSync(
|
|
171
|
+
join(__dirname, 'edge-tts-rs-node.linux-x64-musl.node')
|
|
172
|
+
)
|
|
173
|
+
try {
|
|
174
|
+
if (localFileExisted) {
|
|
175
|
+
nativeBinding = require('./edge-tts-rs-node.linux-x64-musl.node')
|
|
176
|
+
} else {
|
|
177
|
+
nativeBinding = require('edge-tts-rs-node-linux-x64-musl')
|
|
178
|
+
}
|
|
179
|
+
} catch (e) {
|
|
180
|
+
loadError = e
|
|
181
|
+
}
|
|
182
|
+
} else {
|
|
183
|
+
localFileExisted = existsSync(
|
|
184
|
+
join(__dirname, 'edge-tts-rs-node.linux-x64-gnu.node')
|
|
185
|
+
)
|
|
186
|
+
try {
|
|
187
|
+
if (localFileExisted) {
|
|
188
|
+
nativeBinding = require('./edge-tts-rs-node.linux-x64-gnu.node')
|
|
189
|
+
} else {
|
|
190
|
+
nativeBinding = require('edge-tts-rs-node-linux-x64-gnu')
|
|
191
|
+
}
|
|
192
|
+
} catch (e) {
|
|
193
|
+
loadError = e
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
break
|
|
197
|
+
case 'arm64':
|
|
198
|
+
if (isMusl()) {
|
|
199
|
+
localFileExisted = existsSync(
|
|
200
|
+
join(__dirname, 'edge-tts-rs-node.linux-arm64-musl.node')
|
|
201
|
+
)
|
|
202
|
+
try {
|
|
203
|
+
if (localFileExisted) {
|
|
204
|
+
nativeBinding = require('./edge-tts-rs-node.linux-arm64-musl.node')
|
|
205
|
+
} else {
|
|
206
|
+
nativeBinding = require('edge-tts-rs-node-linux-arm64-musl')
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
loadError = e
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
localFileExisted = existsSync(
|
|
213
|
+
join(__dirname, 'edge-tts-rs-node.linux-arm64-gnu.node')
|
|
214
|
+
)
|
|
215
|
+
try {
|
|
216
|
+
if (localFileExisted) {
|
|
217
|
+
nativeBinding = require('./edge-tts-rs-node.linux-arm64-gnu.node')
|
|
218
|
+
} else {
|
|
219
|
+
nativeBinding = require('edge-tts-rs-node-linux-arm64-gnu')
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
loadError = e
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
break
|
|
226
|
+
case 'arm':
|
|
227
|
+
if (isMusl()) {
|
|
228
|
+
localFileExisted = existsSync(
|
|
229
|
+
join(__dirname, 'edge-tts-rs-node.linux-arm-musleabihf.node')
|
|
230
|
+
)
|
|
231
|
+
try {
|
|
232
|
+
if (localFileExisted) {
|
|
233
|
+
nativeBinding = require('./edge-tts-rs-node.linux-arm-musleabihf.node')
|
|
234
|
+
} else {
|
|
235
|
+
nativeBinding = require('edge-tts-rs-node-linux-arm-musleabihf')
|
|
236
|
+
}
|
|
237
|
+
} catch (e) {
|
|
238
|
+
loadError = e
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
localFileExisted = existsSync(
|
|
242
|
+
join(__dirname, 'edge-tts-rs-node.linux-arm-gnueabihf.node')
|
|
243
|
+
)
|
|
244
|
+
try {
|
|
245
|
+
if (localFileExisted) {
|
|
246
|
+
nativeBinding = require('./edge-tts-rs-node.linux-arm-gnueabihf.node')
|
|
247
|
+
} else {
|
|
248
|
+
nativeBinding = require('edge-tts-rs-node-linux-arm-gnueabihf')
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
loadError = e
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
break
|
|
255
|
+
case 'riscv64':
|
|
256
|
+
if (isMusl()) {
|
|
257
|
+
localFileExisted = existsSync(
|
|
258
|
+
join(__dirname, 'edge-tts-rs-node.linux-riscv64-musl.node')
|
|
259
|
+
)
|
|
260
|
+
try {
|
|
261
|
+
if (localFileExisted) {
|
|
262
|
+
nativeBinding = require('./edge-tts-rs-node.linux-riscv64-musl.node')
|
|
263
|
+
} else {
|
|
264
|
+
nativeBinding = require('edge-tts-rs-node-linux-riscv64-musl')
|
|
265
|
+
}
|
|
266
|
+
} catch (e) {
|
|
267
|
+
loadError = e
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
localFileExisted = existsSync(
|
|
271
|
+
join(__dirname, 'edge-tts-rs-node.linux-riscv64-gnu.node')
|
|
272
|
+
)
|
|
273
|
+
try {
|
|
274
|
+
if (localFileExisted) {
|
|
275
|
+
nativeBinding = require('./edge-tts-rs-node.linux-riscv64-gnu.node')
|
|
276
|
+
} else {
|
|
277
|
+
nativeBinding = require('edge-tts-rs-node-linux-riscv64-gnu')
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {
|
|
280
|
+
loadError = e
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
break
|
|
284
|
+
case 's390x':
|
|
285
|
+
localFileExisted = existsSync(
|
|
286
|
+
join(__dirname, 'edge-tts-rs-node.linux-s390x-gnu.node')
|
|
287
|
+
)
|
|
288
|
+
try {
|
|
289
|
+
if (localFileExisted) {
|
|
290
|
+
nativeBinding = require('./edge-tts-rs-node.linux-s390x-gnu.node')
|
|
291
|
+
} else {
|
|
292
|
+
nativeBinding = require('edge-tts-rs-node-linux-s390x-gnu')
|
|
293
|
+
}
|
|
294
|
+
} catch (e) {
|
|
295
|
+
loadError = e
|
|
296
|
+
}
|
|
297
|
+
break
|
|
298
|
+
default:
|
|
299
|
+
throw new Error(`Unsupported architecture on Linux: ${arch}`)
|
|
300
|
+
}
|
|
301
|
+
break
|
|
302
|
+
default:
|
|
303
|
+
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!nativeBinding) {
|
|
307
|
+
if (loadError) {
|
|
308
|
+
throw loadError
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`Failed to load native binding`)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const { createClient, closeClient, synthesize, listVoices, startStream, cancelStream, normalizeVoice, defaultExtensionForFormat } = nativeBinding
|
|
314
|
+
|
|
315
|
+
module.exports.createClient = createClient
|
|
316
|
+
module.exports.closeClient = closeClient
|
|
317
|
+
module.exports.synthesize = synthesize
|
|
318
|
+
module.exports.listVoices = listVoices
|
|
319
|
+
module.exports.startStream = startStream
|
|
320
|
+
module.exports.cancelStream = cancelStream
|
|
321
|
+
module.exports.normalizeVoice = normalizeVoice
|
|
322
|
+
module.exports.defaultExtensionForFormat = defaultExtensionForFormat
|
package/package.json
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "edge-tts-rs-node",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Node.js / Electron bindings for the edge-tts-rust crate, with a node-edge-tts compatible API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"types": "index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"index.d.ts",
|
|
11
|
+
"native.js",
|
|
12
|
+
"native.d.ts",
|
|
13
|
+
"*.node",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=18"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "napi build --platform --release --js native.js --dts native.d.ts",
|
|
22
|
+
"build:debug": "napi build --platform --js native.js --dts native.d.ts",
|
|
23
|
+
"test": "node test/index.js",
|
|
24
|
+
"test:smoke": "node test/smoke.js"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@napi-rs/cli": "^2.18.4"
|
|
28
|
+
},
|
|
29
|
+
"napi": {
|
|
30
|
+
"name": "edge-tts-rs-node",
|
|
31
|
+
"triples": {
|
|
32
|
+
"defaults": true,
|
|
33
|
+
"additional": []
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"edge-tts",
|
|
38
|
+
"tts",
|
|
39
|
+
"text-to-speech",
|
|
40
|
+
"rust",
|
|
41
|
+
"napi",
|
|
42
|
+
"electron"
|
|
43
|
+
]
|
|
44
|
+
}
|