@leiboss/dsh-tts 0.0.0-stage → 0.3.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/CHANGELOG.md +111 -0
- package/LICENSE +21 -0
- package/README.md +374 -2
- package/cordis.patch.yml +4 -0
- package/dist/audio/mp3-duration.d.ts +23 -0
- package/dist/audio/mp3-duration.d.ts.map +1 -0
- package/dist/audio/mp3-duration.js +140 -0
- package/dist/audio/mp3-duration.js.map +1 -0
- package/dist/audio-route.d.ts +122 -0
- package/dist/audio-route.d.ts.map +1 -0
- package/dist/audio-route.js +408 -0
- package/dist/audio-route.js.map +1 -0
- package/dist/cache.d.ts +50 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +105 -0
- package/dist/cache.js.map +1 -0
- package/dist/chunked-tts.d.ts +34 -0
- package/dist/chunked-tts.d.ts.map +1 -0
- package/dist/chunked-tts.js +121 -0
- package/dist/chunked-tts.js.map +1 -0
- package/dist/chunker.d.ts +19 -0
- package/dist/chunker.d.ts.map +1 -0
- package/dist/chunker.js +98 -0
- package/dist/chunker.js.map +1 -0
- package/dist/client.js +90 -0
- package/dist/concurrency.d.ts +33 -0
- package/dist/concurrency.d.ts.map +1 -0
- package/dist/concurrency.js +95 -0
- package/dist/concurrency.js.map +1 -0
- package/dist/config.d.ts +174 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +186 -0
- package/dist/config.js.map +1 -0
- package/dist/edge-tts.d.ts +74 -0
- package/dist/edge-tts.d.ts.map +1 -0
- package/dist/edge-tts.js +161 -0
- package/dist/edge-tts.js.map +1 -0
- package/dist/engine-health.d.ts +108 -0
- package/dist/engine-health.d.ts.map +1 -0
- package/dist/engine-health.js +170 -0
- package/dist/engine-health.js.map +1 -0
- package/dist/engines/edge-js.d.ts +51 -0
- package/dist/engines/edge-js.d.ts.map +1 -0
- package/dist/engines/edge-js.js +98 -0
- package/dist/engines/edge-js.js.map +1 -0
- package/dist/errors.d.ts +66 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +110 -0
- package/dist/errors.js.map +1 -0
- package/dist/fallback.d.ts +69 -0
- package/dist/fallback.d.ts.map +1 -0
- package/dist/fallback.js +89 -0
- package/dist/fallback.js.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +723 -0
- package/dist/index.js.map +1 -0
- package/dist/probe.d.ts +42 -0
- package/dist/probe.d.ts.map +1 -0
- package/dist/probe.js +63 -0
- package/dist/probe.js.map +1 -0
- package/dist/provider.d.ts +33 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +7 -0
- package/dist/provider.js.map +1 -0
- package/dist/register-service.d.ts +25 -0
- package/dist/register-service.d.ts.map +1 -0
- package/dist/register-service.js +33 -0
- package/dist/register-service.js.map +1 -0
- package/dist/sapi-tts.d.ts +29 -0
- package/dist/sapi-tts.d.ts.map +1 -0
- package/dist/sapi-tts.js +131 -0
- package/dist/sapi-tts.js.map +1 -0
- package/dist/self-check.d.ts +32 -0
- package/dist/self-check.d.ts.map +1 -0
- package/dist/self-check.js +75 -0
- package/dist/self-check.js.map +1 -0
- package/dist/service.d.ts +119 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +404 -0
- package/dist/service.js.map +1 -0
- package/dist/voices.d.ts +21 -0
- package/dist/voices.d.ts.map +1 -0
- package/dist/voices.js +74 -0
- package/dist/voices.js.map +1 -0
- package/icon.svg +6 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +93 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Throttling is retried, not papered over: a 429 is classified as
|
|
6
|
+
`UPSTREAM_THROTTLED` and the same engine is retried after 2s, 6s and 18s
|
|
7
|
+
before the fallback chain runs. Backing off keeps the good voice; falling back
|
|
8
|
+
to SAPI would hide the load problem and produce worse audio.
|
|
9
|
+
|
|
10
|
+
- `AudioRoute` accepts a `prefix` option (default unchanged). A consumer such as the
|
|
11
|
+
voiceover plugin reuses the route under its own path instead of contending for
|
|
12
|
+
`/dsh-tts/audio`.
|
|
13
|
+
|
|
14
|
+
- The pure-JS backend now returns word boundaries: `node-edge-tts` only parses
|
|
15
|
+
`WordBoundary` metadata when `saveSubtitles` is set, and `streamAudio()` writes
|
|
16
|
+
no files, so the adapter enables it unconditionally.
|
|
17
|
+
- Persist word timings in the cache sidecar and return them on a cache hit;
|
|
18
|
+
previously a cached chapter silently downgraded subtitles to `estimated`.
|
|
19
|
+
- `capabilities.wordBoundaries` is now conservative (false until the backend is
|
|
20
|
+
resolved) instead of assuming a non-CLI engine.
|
|
21
|
+
|
|
22
|
+
- Add the pure-JS backend (`src/engines/edge-js.ts`, optional `node-edge-tts`):
|
|
23
|
+
no Python, and it returns word boundaries, which upgrades subtitle timing
|
|
24
|
+
from `estimated` to `exact`. Word offsets are shifted per chunk so chunked
|
|
25
|
+
long text keeps absolute timing.
|
|
26
|
+
- Add `engine: auto | edge-js | edge-py | sapi` (default `auto`, resolved once
|
|
27
|
+
per service instance) and surface `words` on `TtsAudioResult`.
|
|
28
|
+
|
|
29
|
+
- Add `TtsService` (`@leiboss/dsh-tts/service`): one orchestrator over cache,
|
|
30
|
+
concurrency, chunking, and the fallback chain, returning structured audio
|
|
31
|
+
facts (`durationMs`, `timingQuality`, `engine`, `fromCache`).
|
|
32
|
+
- Publish the engine as `ctx.tts` on mount (`inject: ['tts']` in consumers).
|
|
33
|
+
Registration is best-effort: a host without a service seam still gets the
|
|
34
|
+
tools.
|
|
35
|
+
- Add pure-JS MP3 duration measurement (`src/audio/mp3-duration.ts`), so
|
|
36
|
+
duration and subtitle timing never require ffmpeg.
|
|
37
|
+
- Fix a cache-key gap: `pitch` and `volume` now participate in the synthesis
|
|
38
|
+
key, so two calls that differ only in prosody no longer share a file.
|
|
39
|
+
|
|
40
|
+
All notable changes to this project are documented in this file.
|
|
41
|
+
|
|
42
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
43
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
44
|
+
|
|
45
|
+
## [0.2.1] - 2026-11-11
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- Inline client player: `tts_generate` results render a native `<audio controls>` in DSH Web.
|
|
50
|
+
- `tts_probe_network` tool for TCP reachability checks against the Edge-TTS endpoint.
|
|
51
|
+
- `proxy` config forwarded to Edge-TTS as `--proxy`; `tts_self_check` reports `proxyConfigured`.
|
|
52
|
+
- `fallbackReason` reporting the engine chain when synthesis falls back to `speechSynthesis`.
|
|
53
|
+
|
|
54
|
+
### Fixed
|
|
55
|
+
|
|
56
|
+
- SAPI fallback wrote to `*.mp3.partial`, so ffmpeg could not infer the container and the chain
|
|
57
|
+
always fell through to `speechSynthesis`; the partial file now ends in `.mp3`.
|
|
58
|
+
- Corrected README claims about link shareability and playback rendering.
|
|
59
|
+
|
|
60
|
+
## [0.2.0] - 2026-11-11
|
|
61
|
+
|
|
62
|
+
### Added
|
|
63
|
+
|
|
64
|
+
- Edge-TTS synthesis that writes a playable MP3 to disk.
|
|
65
|
+
- Ten built-in voices, listed by `tts_self_check.availableVoices`.
|
|
66
|
+
- Long-text synthesis: split on sentence boundaries, synthesize chunks serially,
|
|
67
|
+
concatenate them into a single MP3.
|
|
68
|
+
- Fallback chain: Edge-TTS, then SAPI on Windows, then a `speechSynthesis` marker
|
|
69
|
+
for the caller to read aloud.
|
|
70
|
+
- Concurrency limit with a waiting queue and `TTS_BUSY` backpressure.
|
|
71
|
+
- Signed playback route: HTTP Range (206/416), byte-range serving, same-origin
|
|
72
|
+
checks, and a signing secret persisted across reloads.
|
|
73
|
+
- `totalTimeoutMs`, a budget for the whole `tts_generate` call.
|
|
74
|
+
- Cache sidecar recording which engine produced each cached file.
|
|
75
|
+
- Optional `trustedHosts` for reverse-proxy and remote access setups.
|
|
76
|
+
- `tts_self_check` diagnostics for the runtime, sandbox and output directory.
|
|
77
|
+
- Plugin manifest metadata: localized title/description and an icon.
|
|
78
|
+
|
|
79
|
+
### Changed
|
|
80
|
+
|
|
81
|
+
- Package name migrated from the bare `dsh-tts` to `@leiboss/dsh-tts`. The host
|
|
82
|
+
entry id stays `dsh-tts`, so existing profile configuration keeps working.
|
|
83
|
+
- Tool results render a clickable absolute playback link; the JSON
|
|
84
|
+
`browserUrl` field remains a same-origin relative path.
|
|
85
|
+
- `pitch` and `volume` are validated before any subprocess starts.
|
|
86
|
+
- Trust checks prefer `ctx.connection.requestRejection` when the host provides it.
|
|
87
|
+
|
|
88
|
+
### Fixed
|
|
89
|
+
|
|
90
|
+
- Injecting the nonexistent `httpServer` service left `browserUrl` permanently
|
|
91
|
+
undefined; the plugin now uses `webServer`, with an `httpServer` fallback.
|
|
92
|
+
- Hosts that do not export `DSH_HOME` had no output directory at all; the
|
|
93
|
+
default now falls back to `~/.dsh/tts-output`.
|
|
94
|
+
- Merged in-flight requests reused the first caller's abort signal, so one
|
|
95
|
+
cancellation failed every waiter.
|
|
96
|
+
- `engines.dsh: >=0.2.2` excluded the prerelease runtime `0.2.2-rc.1`, which
|
|
97
|
+
rejected every installation; the range is now `>=0.2.2-rc.1`.
|
|
98
|
+
|
|
99
|
+
### Removed
|
|
100
|
+
|
|
101
|
+
- `durationMs` from the tool output schema. It was declared but never produced.
|
|
102
|
+
|
|
103
|
+
### Security
|
|
104
|
+
|
|
105
|
+
- Playback tokens are HMAC-signed and expire.
|
|
106
|
+
- The signing secret persists in `<outputDir>/.audio-route-secret`; deleting it
|
|
107
|
+
rotates every previously issued link.
|
|
108
|
+
- Route requests prefer the host's Host/Origin and browser authentication checks.
|
|
109
|
+
|
|
110
|
+
[0.2.1]: https://github.com/leiboss/dsh-tts/releases/tag/v0.2.1
|
|
111
|
+
[0.2.0]: https://github.com/leiboss/dsh-tts/releases/tag/v0.2.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 leiboss
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,375 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-tts
|
|
2
|
+
|
|
3
|
+
npm 包名为 `@leiboss/dsh-tts`。
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@leiboss/dsh-tts)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://github.com/leiboss/dsh-tts/actions/workflows/ci.yml)
|
|
8
|
+
|
|
9
|
+
> **For plugin authors:** a reliable TTS backend with concurrency control, layered
|
|
10
|
+
> error codes, and provider fallback — designed to be depended on, not just clicked.
|
|
11
|
+
>
|
|
12
|
+
> **For end users:** returns playable audio files with browser-accessible URLs.
|
|
13
|
+
> Bring your own player, or use a reader plugin that calls this backend.
|
|
14
|
+
|
|
15
|
+
**Install:** `dsh plugin --profile web add @leiboss/dsh-tts`
|
|
16
|
+
|
|
17
|
+
## 简介
|
|
18
|
+
|
|
19
|
+
`@leiboss/dsh-tts` 是一个 DSH 工具插件:把文本合成为音频文件,返回本地路径与可选的同源播放地址。它不是播放器,不做语音转写,也不做音色克隆。
|
|
20
|
+
|
|
21
|
+
核心特性:
|
|
22
|
+
|
|
23
|
+
- 稳定的 Edge-TTS 合成与 `(text, voice, rate)` 缓存,重复请求不启动子进程。
|
|
24
|
+
- DSH Web 内可直接播放的 `browserUrl`,支持 HTTP Range 206 与签名 token。
|
|
25
|
+
- 长文本按句子边界分块、串行合成、拼接为单个 MP3。
|
|
26
|
+
- Edge-TTS 失败后按可配置链降级:SAPI(仅 Windows)或浏览器 `speechSynthesis`。
|
|
27
|
+
- 并发上限、等待队列与 `TTS_BUSY` 背压;缓存命中不占槽位。
|
|
28
|
+
- `tts_self_check` 一次性报告运行环境与静态音色清单。
|
|
29
|
+
|
|
30
|
+
## 工作原理
|
|
31
|
+
|
|
32
|
+
| DSH 能力 | 用途 |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `ctx.tools.register` | 注册 `tts_generate` 与 `tts_self_check`;注册随插件 fiber 注销,支持热更新 |
|
|
35
|
+
| `ctx.inject(['webServer'])` | 可选依赖宿主 Web 路由;旧宿主回退 `ctx.get('httpServer')`,都不存在时只返回 `filePath` |
|
|
36
|
+
| `webServer.register` | 注册 `prefix` 路由 `/dsh-tts/audio`,返回音频字节并处理 Range |
|
|
37
|
+
| `ctx.connection.requestRejection` | 宿主 Host/Origin 与浏览器会话校验;链接与当前 DSH 浏览器会话绑定,不带会话 cookie 的请求返回 401/403 |
|
|
38
|
+
| `ctx.get('connection')` | 读取 `allowsRemoteAuthorities`,远程访问时放宽 Host 校验 |
|
|
39
|
+
| DSH 配置层(`Config` + `apply` 的 config) | 读取 13 项配置;插件不自行读写 `cordis.patch.yml` |
|
|
40
|
+
| `child_process.spawn` | 调用 `edge-tts`;降级时调用 `powershell.exe` 与 `ffmpeg` |
|
|
41
|
+
| `node:child_process` / `node:fs` | 直接调用 CLI 并写文件,不经 `ctx.subprocess` / 文件服务;见「能力接缝与沙箱」 |
|
|
42
|
+
| 文件系统 | 写入 `$DSH_HOME/tts-output/<YYYY-MM-DD>/<hash>-<timestamp>.mp3` |
|
|
43
|
+
|
|
44
|
+
一次请求的路径:参数校验 → 缓存查找(命中直接返回)→ 同 key 在途请求合并 → 并发槽位 → 分块或一次性合成 → 失败时降级 → 落盘 → 生成签名播放 URL。
|
|
45
|
+
|
|
46
|
+
## Reliability semantics
|
|
47
|
+
|
|
48
|
+
This plugin is designed to be called by other plugins and agents, not just by
|
|
49
|
+
end users. The following behaviors are part of its contract:
|
|
50
|
+
|
|
51
|
+
| Behavior | Guarantee | Error code / field |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| Concurrency | At most `maxConcurrent` synthesis tasks run simultaneously; excess requests queue up to `queueSize`, then fail fast | `TTS_BUSY` (includes current running count and queue length) |
|
|
54
|
+
| Sandbox rejection | `spawn` blocked by the sandbox is distinguished from upstream failure | `SPAWN_EPERM` (points to sandbox policy, not filesystem) |
|
|
55
|
+
| Upstream failure | Edge-TTS non-zero exit or network timeout | `EDGE_TTS_FAILED` / `SYNTHESIS_TIMEOUT` |
|
|
56
|
+
| Fallback | On Edge-TTS failure, tries engines in `fallbackOrder`; the response marks the engine actually used | `engine` / `fallback` fields |
|
|
57
|
+
| Engine health | Unhealthy engines are skipped during cooldown; a half-open probe recovers them on success. Protocol drift (403 / handshake rejection) opens the circuit with a longer cooldown than transient failures. | `engineHealth` in `tts_self_check` and `tts_generate` |
|
|
58
|
+
| Cache semantics | Cache key is fixed to `(text, voice, rate)`; `pitch`/`volume` are excluded for predictable cache behavior | See “Known limitations” |
|
|
59
|
+
| Lifecycle | Tools, routes, and config listeners are effect-registered and cleaned up on dispose; HMR-safe | — |
|
|
60
|
+
|
|
61
|
+
These are not feature claims. They are behaviors callers can rely on when
|
|
62
|
+
deciding whether to depend on this plugin.
|
|
63
|
+
|
|
64
|
+
## 工具一览
|
|
65
|
+
|
|
66
|
+
| 工具 | 参数 | 返回 | 何时调用 |
|
|
67
|
+
| --- | --- | --- | --- |
|
|
68
|
+
| `tts_generate` | `text`(必填)、`voice`、`rate`、`pitch`、`volume` | `{ filePath, format, browserUrl?, engine?, fallback?, text?, voice?, rate?, fallbackReason? }` | 用户要求把文字读出来、生成配音或音频文件时 |
|
|
69
|
+
| `tts_self_check` | 无 | `{ edgeTtsAvailable, edgeTtsVersion?, spawnAllowed, outputDirWritable, dshHome, proxyConfigured, availableVoices }` | 安装后自检,或 `tts_generate` 报环境类错误时 |
|
|
70
|
+
| `tts_probe_network` | `timeoutMs`(可选,默认 1500,上限 5000) | `{ host, port, reachable, elapsedMs, proxyConfigured }` | 怀疑网络到 Edge-TTS 端点不通时显式调用;会发起一次 TCP 连接,不合成音频、不消耗配额 |
|
|
71
|
+
|
|
72
|
+
`tts_generate` 不播放音频、不转录音频、不克隆或修改音色、不做音频后处理,也不生成音乐。`filePath` 与 `format` 在正常合成时存在;当返回 `fallback: "speechSynthesis"` 时只返回 `text`、`voice`、`rate`,以及可选的 `fallbackReason`(各引擎失败原因摘要),由调用方朗读。当前不提供音频时长。
|
|
73
|
+
|
|
74
|
+
默认值:`voice = zh-CN-XiaoxiaoNeural`,`rate = +0%`,`pitch = +0Hz`,`volume = +0%`。
|
|
75
|
+
|
|
76
|
+
## 安装
|
|
77
|
+
|
|
78
|
+
环境要求:
|
|
79
|
+
|
|
80
|
+
- Node.js ≥ 22.18
|
|
81
|
+
- DSH ≥ 0.2.2
|
|
82
|
+
- Python 3
|
|
83
|
+
- `pip install edge-tts`
|
|
84
|
+
- Windows 上使用 SAPI 降级时还需要 `ffmpeg` 在 PATH 中(用于 WAV → MP3)
|
|
85
|
+
|
|
86
|
+
首选 npm(包发布后可用):
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
dsh plugin --profile web add @leiboss/dsh-tts # 发布后
|
|
90
|
+
dsh plugin --profile web add github:leiboss/dsh-tts # 发布前/备用
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
从旧版本迁移:旧依赖目录名是 `dsh-tts`,与新的 scoped 包名不匹配,必须先移除再安装,否则会残留 `Cannot find module 'dsh-tts'` 或工具重名:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
dsh plugin --profile web remove dsh-tts
|
|
97
|
+
dsh plugin --profile web add @leiboss/dsh-tts
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
仓库包含编译产物 `dist/`。如果从源码工作,先执行:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
npm install
|
|
104
|
+
npm run build
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
从本地目录安装(`dsh plugin add <目录>`)走的是 link,不会为该目录安装依赖;请先在仓库里执行 `npm ci`,否则激活时会报 `Cannot find package '@deepseek-ai/schemastery'`。从 npm 或 GitHub 安装不受影响,依赖由 pnpm 正常安装。
|
|
108
|
+
|
|
109
|
+
安装后在 DSH 对话中确认工具存在:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
你有 tts_generate 工具吗
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
或直接调用 `tts_self_check`,期望 `edgeTtsAvailable`、`spawnAllowed`、`outputDirWritable` 为 `true`,且 `availableVoices` 非空。
|
|
116
|
+
|
|
117
|
+
最小示例:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
把“你好”合成语音
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## For plugin authors
|
|
124
|
+
|
|
125
|
+
If you are building a reader plugin, a notification plugin, or an agent that needs
|
|
126
|
+
reliable audio synthesis, you can call `tts_generate` as a tool, or depend on the
|
|
127
|
+
`TTSProvider` interface directly.
|
|
128
|
+
|
|
129
|
+
**Option A: call the tool** (recommended for agents)
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
const result = await ctx.tools.call('tts_generate', {
|
|
133
|
+
text: 'Build completed',
|
|
134
|
+
voice: 'zh-CN-YunxiNeural',
|
|
135
|
+
})
|
|
136
|
+
// result: { filePath, format, browserUrl?, engine?, fallback? }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Option B: import the provider** (for plugins that want to own the playback UX)
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import type { TTSProvider, SynthesizeResult } from '@leiboss/dsh-tts/provider'
|
|
143
|
+
|
|
144
|
+
// Resolve a provider instance from the DSH service registry,
|
|
145
|
+
// or construct the Edge-TTS provider directly if you are embedding it.
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Error handling contract**
|
|
149
|
+
|
|
150
|
+
Treat these error codes differently — the distinction is the point:
|
|
151
|
+
|
|
152
|
+
| Code | Meaning | Recommended handling |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `TTS_BUSY` | Concurrency limit or queue full | Retry after a short delay |
|
|
155
|
+
| `SPAWN_EPERM` | Sandbox blocked subprocess creation | Surface to user as environment issue; do not retry |
|
|
156
|
+
| `EDGE_TTS_FAILED` | Upstream Edge-TTS failure | Trigger your own fallback, or rely on this plugin's fallback chain |
|
|
157
|
+
| `SYNTHESIS_TIMEOUT` | Synthesis exceeded timeout | Retry with shorter text or larger `timeoutMs` |
|
|
158
|
+
| `ALL_FALLBACKS_FAILED` | All engines in `fallbackOrder` failed | Surface to user; do not retry without environment change |
|
|
159
|
+
|
|
160
|
+
Do not treat all errors as equivalent. A caller that retries `SPAWN_EPERM` will
|
|
161
|
+
loop forever; a caller that does not retry `TTS_BUSY` will drop work unnecessarily.
|
|
162
|
+
|
|
163
|
+
## 配置
|
|
164
|
+
|
|
165
|
+
所有配置都通过 DSH 配置层覆盖,不在工具参数里传入。
|
|
166
|
+
|
|
167
|
+
| 配置项 | 默认值 | 作用 | 覆盖方式 |
|
|
168
|
+
| --- | --- | --- | --- |
|
|
169
|
+
| `outputDir` | `$DSH_HOME/tts-output` | 音频输出根目录;未设 `DSH_HOME` 时回退 `~/.dsh/tts-output` | 插件 config 的 `outputDir` |
|
|
170
|
+
| `defaultVoice` | `zh-CN-XiaoxiaoNeural` | 未传 `voice` 时使用的音色 | 插件 config 的 `defaultVoice` |
|
|
171
|
+
| `timeoutMs` | `30000` | 单个 Edge-TTS 子进程(或单个分块)的超时 | 插件 config 的 `timeoutMs` |
|
|
172
|
+
| `edgeTtsBinary` | `edge-tts` | Edge-TTS CLI 名称或绝对路径 | 插件 config 的 `edgeTtsBinary` |
|
|
173
|
+
| `chunkSize` | `1200` | 超过该字符数时按句子边界分块 | 插件 config 的 `chunkSize` |
|
|
174
|
+
| `chunkingEnabled` | `true` | 是否启用长文本分块 | 插件 config 的 `chunkingEnabled` |
|
|
175
|
+
| `fallbackEnabled` | `true` | Edge-TTS 失败后是否尝试降级链 | 插件 config 的 `fallbackEnabled` |
|
|
176
|
+
| `fallbackOrder` | `['edge-tts', 'sapi', 'speechSynthesis']` | 降级顺序;未知引擎名会被忽略 | 插件 config 的 `fallbackOrder` |
|
|
177
|
+
| `maxConcurrent` | `2` | 同时运行的完整合成任务数 | 插件 config 的 `maxConcurrent` |
|
|
178
|
+
| `queueSize` | `4` | 等待槽位的请求上限 | 插件 config 的 `queueSize` |
|
|
179
|
+
| `queueTimeoutMs` | `10000` | 排队等待上限,超时返回 `TTS_BUSY` | 插件 config 的 `queueTimeoutMs` |
|
|
180
|
+
| `totalTimeoutMs` | `180000` | 一次 `tts_generate` 的总预算,覆盖所有分块与块间延迟 | 插件 config 的 `totalTimeoutMs` |
|
|
181
|
+
| `trustedHosts` | `[]` | 反向代理场景的额外 Host 白名单;非空时跳过端口钉死 | 插件 config 的 `trustedHosts` |
|
|
182
|
+
| `proxy` | `undefined` | 为 Edge-TTS 子进程配置 HTTP 代理,透传为 `--proxy`;留空则不追加参数 | 插件 config 的 `proxy` |
|
|
183
|
+
| `engineHealth.enabled` | `true` | 是否启用引擎健康状态机;关闭后每个引擎总是被尝试 | 插件 config 的 `engineHealth.enabled` |
|
|
184
|
+
| `engineHealth.failureThreshold` | `3` | 连续普通失败达到该次数后打开熔断 | 插件 config 的 `engineHealth.failureThreshold` |
|
|
185
|
+
| `engineHealth.driftCooldownMs` | `300000` | 协议漂移(403 / 握手失败)触发的冷却期 | 插件 config 的 `engineHealth.driftCooldownMs` |
|
|
186
|
+
| `engineHealth.transientCooldownMs` | `60000` | 普通失败达到阈值后触发的冷却期 | 插件 config 的 `engineHealth.transientCooldownMs` |
|
|
187
|
+
| `engineHealth.maxCooldownMs` | `600000` | 半开探测失败后冷却时长翻倍的上限 | 插件 config 的 `engineHealth.maxCooldownMs` |
|
|
188
|
+
|
|
189
|
+
## 使用
|
|
190
|
+
|
|
191
|
+
默认音色:
|
|
192
|
+
|
|
193
|
+
```text
|
|
194
|
+
把“欢迎使用 DSH,这是一段语音合成示例”合成语音。
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
指定音色:
|
|
198
|
+
|
|
199
|
+
```text
|
|
200
|
+
用 zh-CN-YunyangNeural 把这段新闻稿合成音频文件。
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
长文本:
|
|
204
|
+
|
|
205
|
+
```text
|
|
206
|
+
把下面这篇文章合成语音(直接粘贴 2000 字以上的正文)。
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
调整音调与音量:
|
|
210
|
+
|
|
211
|
+
```text
|
|
212
|
+
用 pitch +10Hz、volume -20% 把这段话合成语音。
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
返回值示例(Web 运行模式):
|
|
216
|
+
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"filePath": "/home/user/.dsh/tts-output/2026-11-11/4f2c9a1b3d5e6f70-1770000000000.mp3",
|
|
220
|
+
"format": "mp3",
|
|
221
|
+
"browserUrl": "/dsh-tts/audio/eyJleHAiOjE3NzAwMDM2MDB9..."
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
`browserUrl` 是同源相对地址,链接与当前 DSH 浏览器会话绑定:在浏览器里打开可以播放,但**不适合对外分享**。它带有效期与签名;签名密钥持久化在输出目录的 `.audio-route-secret`,插件重载后旧链接仍可用;删除该文件会轮换全部链接。需要分享时请直接发送音频文件。
|
|
226
|
+
|
|
227
|
+
DSH Web 中,本插件还声明了客户端入口(`dsh.client`,`exports["./client"]`):`tts_generate` 的工具结果里只要有 `browserUrl`,就会在对话中渲染一个原生 `<audio controls preload="none">` 播放器,点击即可播放。播放器不自动播放,URL 缺失(headless、路由未注册、`speechSynthesis` 降级)时不渲染任何内容。宿主 `ToolResultNode` 不暴露结构化返回值,客户端因此从结果的渲染文本里取同源 `/dsh-tts/audio/...` 路径。
|
|
228
|
+
|
|
229
|
+
工具结果的 `render` 在拿到宿主 `webServer.getOrigin()` 时还会把 `browserUrl` 拼成绝对地址,输出 `[▶ 播放](...)`。**这是 Markdown 文本**,DSH 的工具结果按纯文本展示,不会自动渲染成链接;可点击的播放入口是上面的内联播放器,或在回复正文里重新引用该地址。JSON 返回值里的 `browserUrl` 仍是相对路径。拿不到 origin(例如 headless 或 `file://`)时不猜 `localhost`,退回相对 `Playback URL` 文本。
|
|
230
|
+
|
|
231
|
+
当所有宿主引擎都不可用时,返回(`fallbackReason` 为各引擎失败原因摘要,见「错误码」):
|
|
232
|
+
|
|
233
|
+
```json
|
|
234
|
+
{
|
|
235
|
+
"fallback": "speechSynthesis",
|
|
236
|
+
"text": "原文",
|
|
237
|
+
"voice": "zh-CN-XiaoxiaoNeural",
|
|
238
|
+
"rate": "+0%",
|
|
239
|
+
"fallbackReason": "edge-tts: SYNTHESIS_TIMEOUT; sapi: ffmpeg exited with code -22"
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## 可用音色
|
|
244
|
+
|
|
245
|
+
音色清单静态维护在 `src/voices.ts`,`tts_self_check.availableVoices` 返回同样内容,不会联网查询。该清单不是白名单:`tts_generate` 仍接受 Edge-TTS 支持的其他音色名。
|
|
246
|
+
|
|
247
|
+
| 音色名 | 性别 | 语言/方言 | 适用场景 |
|
|
248
|
+
| --- | --- | --- | --- |
|
|
249
|
+
| `zh-CN-XiaoxiaoNeural` | 女 | 中文(普通话) | 默认女声,自然通用 |
|
|
250
|
+
| `zh-CN-YunxiNeural` | 男 | 中文(普通话) | 年轻自然男声 |
|
|
251
|
+
| `zh-CN-XiaoyiNeural` | 女 | 中文(普通话) | 活泼女声 |
|
|
252
|
+
| `zh-CN-YunyangNeural` | 男 | 中文(普通话) | 播音腔 |
|
|
253
|
+
| `zh-CN-YunjianNeural` | 男 | 中文(普通话) | 沉稳男声 |
|
|
254
|
+
| `zh-CN-liaoning-XiaobeiNeural` | 女 | 中文(东北话) | 方言 |
|
|
255
|
+
| `zh-CN-shaanxi-XiaoniNeural` | 女 | 中文(陕西话) | 方言 |
|
|
256
|
+
| `en-US-AriaNeural` | 女 | 英文(美式) | 默认英文女声 |
|
|
257
|
+
| `en-US-GuyNeural` | 男 | 英文(美式) | 英文男声 |
|
|
258
|
+
| `en-US-JennyNeural` | 女 | 英文(美式) | 对话感英文女声 |
|
|
259
|
+
|
|
260
|
+
## 验证
|
|
261
|
+
|
|
262
|
+
### 已验证(Windows 11 + DSH 0.2.2-rc.1)
|
|
263
|
+
|
|
264
|
+
- 从 GitHub 安装:`dsh plugin --profile web add github:leiboss/dsh-tts`,插件正常激活。
|
|
265
|
+
- `tts_self_check`:`edgeTtsAvailable` / `spawnAllowed` / `outputDirWritable` 均为 `true`。
|
|
266
|
+
- `tts_generate`:返回文件路径与 `[▶ 播放](http://127.0.0.1:3080/dsh-tts/audio/...)` 文本;文件写入 `~/.dsh/tts-output/<date>/`。
|
|
267
|
+
- 播放路由:无浏览器会话的 `curl` 返回 401(`connection.requestRejection` 生效,属预期行为),带会话的浏览器可正常取到音频。
|
|
268
|
+
|
|
269
|
+
### 未验证
|
|
270
|
+
|
|
271
|
+
- 内联播放器在浏览器中的实际渲染与点击播放(客户端入口只做了静态检查与 `extractAudioUrls` 单测)。
|
|
272
|
+
- 在浏览器中点击 Markdown 形式的 `[▶ 播放](...)`、拖动进度条。
|
|
273
|
+
- SAPI 降级(本机无 `ffmpeg`)。
|
|
274
|
+
- 反向代理场景(`trustedHosts`)。
|
|
275
|
+
|
|
276
|
+
### 本地自测
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
npm run build
|
|
280
|
+
npm test # 离线单元测试,子进程已注入替身
|
|
281
|
+
npm run check:engines
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
长文本分块、缓存、并发、降级链与路由 Range 均有离线用例覆盖。
|
|
285
|
+
|
|
286
|
+
端到端(需要真实 `edge-tts` 与网络):
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
npm run test:integration
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
期望:生成一个非空 MP3 文件;终端输出包含文件路径;重复执行命中缓存且不再启动子进程。
|
|
293
|
+
|
|
294
|
+
## 能力接缝与沙箱
|
|
295
|
+
|
|
296
|
+
插件用 `node:child_process.spawn` 调用 `edge-tts`(降级时 `powershell.exe`、`ffmpeg`),用 `node:fs` 直接读写输出目录,不经过 `ctx.subprocess` 与宿主文件服务。TTS 是一次性 sidecar CLI,直连让行为在 Web / headless / Electron(`file://`) 下一致,也不把可执行文件解析、终端环境等宿主细节耦合进插件。
|
|
297
|
+
|
|
298
|
+
代价是沙箱策略按进程级生效,错误必须按来源区分:
|
|
299
|
+
|
|
300
|
+
| 错误码 | 含义 | 修法 |
|
|
301
|
+
| --- | --- | --- |
|
|
302
|
+
| `SPAWN_EPERM` | 创建子进程被策略拒绝,不是文件不可写 | 调整 DSH 沙箱的进程策略 |
|
|
303
|
+
| `OUTPUT_DIR_NOT_WRITABLE` | 子进程正常,但写文件被拒绝 | 检查输出目录权限与文件策略 |
|
|
304
|
+
|
|
305
|
+
两者混为一谈会让人去改错的地方,这是最常见的误判。文件权限跟随 Host 进程用户;输出目录默认 `$DSH_HOME/tts-output`,未设置时回退 `~/.dsh/tts-output`。
|
|
306
|
+
|
|
307
|
+
## 已知限制
|
|
308
|
+
|
|
309
|
+
- **需要联网访问 Edge-TTS**:离线时子进程超时或非零退出。缓解:配置 `fallbackEnabled` 与 `fallbackOrder`,Windows 可降级到 SAPI。
|
|
310
|
+
- **需要 Python 的 `edge-tts` 包**:未安装时报 `EDGE_TTS_NOT_FOUND`。缓解:`pip install edge-tts`,或用 `edgeTtsBinary` 指向可执行文件。
|
|
311
|
+
- **沙箱可能拒绝子进程**:报 `SPAWN_EPERM`,这是进程创建被策略拦截,不是文件不可写。缓解:调整 DSH 沙箱进程策略,或接受 `speechSynthesis` 标记由客户端朗读。
|
|
312
|
+
- **`pitch`/`volume` 不参与缓存键**:相同文本、音色、语速的第二次调用会返回首次生成的文件,即使本次指定了不同的 pitch/volume。缓解:更换 `voice` 或 `rate`,或手动删除对应缓存文件。
|
|
313
|
+
- **非 Windows 没有 SAPI 降级**:Linux/macOS 上会跳过 SAPI。缓解:保证 Edge-TTS 可用,或将 `speechSynthesis` 保留在降级链中。
|
|
314
|
+
- **内联播放器只在 DSH Web 生效**:客户端入口注册的是 keyed `tool.call.toolview`,只在工具结果里出现 `browserUrl` 时渲染原生 `<audio>`;headless、`file://`、未加载客户端插件的宿主不会显示播放器。若播放器不可用,回退到在回复正文里引用 `browserUrl` 或直接使用返回的 `filePath`。
|
|
315
|
+
- **来源校验交给宿主**:路由优先调用 `ctx.connection.requestRejection`(Host/Origin + 浏览器会话校验)。链接与当前 DSH 浏览器会话绑定:不带会话 cookie 的请求(包括 `curl`)会返回 401/403,因此不要把 `browserUrl` 当作可对外分享的公开链接,需要分享时请直接发送音频文件。旧宿主没有 `connection` 时回退为内置 Host 端口 + `Origin`/`Host` 同源校验,并保留签名 token 作为第二道防线。
|
|
316
|
+
- **反向代理需要 `trustedHosts`**:代理常改端口,配置 `trustedHosts` 后跳过端口钉死;留空则要求 Host 端口与监听端口一致。
|
|
317
|
+
- **签名密钥可轮换**:密钥存于 `<outputDir>/.audio-route-secret`,删除即让已发出的全部链接失效;文件读取/写入失败时回退内存密钥并只影响本次进程。
|
|
318
|
+
- **分块是字节拼接**:MP3 直接按字节拼接,不重新编码。缓解:长文本仍产出一个可播放文件;若播放器兼容性异常,可关闭分块或改用外部工具后处理。
|
|
319
|
+
- **不提供音频时长**:输出 schema 不再声明 `durationMs`,需要时长请用外部工具探测。
|
|
320
|
+
- **环境变量代理对 Edge-TTS 无效**:edge-tts 的 WSS 连接不读取 `HTTPS_PROXY` / `https_proxy`,设置环境变量不会生效;必须通过本插件的 `proxy` 配置项透传 `--proxy`。
|
|
321
|
+
- **端点单一,无自动迁移**:edge-tts 7.3.1 只有一个端点 `speech.platform.bing.com`。如果该端点在当前网络不可达,只能通过 `proxy` 或降级到 SAPI / `speechSynthesis`;可用 `tts_probe_network` 做一次 TCP 可达性判断。
|
|
322
|
+
- **引擎健康状态仅存在于内存**:插件重启后重置为 `healthy`。它反映的是最近一次尝试的结果,不是实时探测。如需确认当前网络可达性,请使用 `tts_probe_network`。
|
|
323
|
+
|
|
324
|
+
## 错误码
|
|
325
|
+
|
|
326
|
+
| 错误码 | 触发条件 | 处理建议 |
|
|
327
|
+
| --- | --- | --- |
|
|
328
|
+
| `EMPTY_TEXT` | `text` 为空白 | 传入非空文本 |
|
|
329
|
+
| `INVALID_VOICE` | `voice` 不符合 Edge-TTS 命名格式 | 使用如 `zh-CN-XiaoxiaoNeural` 的音色名 |
|
|
330
|
+
| `INVALID_RATE` | `rate` 不是 `+N%` / `-N%` | 使用 `+0%`、`-10%` 这类值 |
|
|
331
|
+
| `INVALID_PITCH` | `pitch` 不是 `+NHz` / `-NHz`(1–3 位数字) | 使用 `+0Hz`、`+10Hz` 这类值 |
|
|
332
|
+
| `INVALID_VOLUME` | `volume` 不是 `+N%` / `-N%`(1–3 位数字) | 使用 `+0%`、`-20%` 这类值 |
|
|
333
|
+
| `INVALID_CONFIG` | 配置值非法(如 `timeoutMs`、`totalTimeoutMs`、`chunkSize`、`maxConcurrent`、`queueSize`) | 按默认值范围修正配置 |
|
|
334
|
+
| `DSH_HOME_NOT_SET` | 未设置 `DSH_HOME` 且未配置 `outputDir` | 设置 `DSH_HOME` 或 `outputDir` |
|
|
335
|
+
| `OUTPUT_DIR_NOT_WRITABLE` | 输出目录无法写入(参见「能力接缝与沙箱」) | 检查沙箱文件策略与目录权限 |
|
|
336
|
+
| `EDGE_TTS_NOT_FOUND` | 找不到 `edge-tts` | `pip install edge-tts` 或设置 `edgeTtsBinary` |
|
|
337
|
+
| `SPAWN_EPERM` | 沙箱拒绝创建子进程(参见「能力接缝与沙箱」) | 调整 DSH 沙箱进程策略,不是文件权限问题 |
|
|
338
|
+
| `SPAWN_FAILED` | 其他子进程创建失败 | 检查 `edgeTtsBinary` 是否可执行 |
|
|
339
|
+
| `EDGE_TTS_FAILED` | Edge-TTS 非零退出 | 查看错误中的退出码与 stderr,检查网络与音色;持续失败时先运行 `tts_probe_network`,若不可达请配置 `proxy`(环境变量对 WSS 无效) |
|
|
340
|
+
| `SYNTHESIS_TIMEOUT` | 单个子进程超过 `timeoutMs`,或整次调用超过 `totalTimeoutMs` | 增大对应超时、缩短文本;若网络正常但仍超时,运行 `tts_probe_network` 并用 `proxy` 配置项透传 `--proxy` |
|
|
341
|
+
| `EMPTY_AUDIO` | Edge-TTS 退出成功但音频为空 | 重试;仍失败时运行 `tts_self_check` |
|
|
342
|
+
| `ABORTED` | 调用被取消 | 无需处理;调用方已取消 |
|
|
343
|
+
| `TTS_BUSY` | 队列满或排队超时 | 稍后重试,或增大 `maxConcurrent` / `queueSize` / `queueTimeoutMs` |
|
|
344
|
+
| `ALL_FALLBACKS_FAILED` | 降级链全部失败且链中无 `speechSynthesis` | 恢复 Edge-TTS,或把 `speechSynthesis` 加入 `fallbackOrder` |
|
|
345
|
+
| `SPEECH_SYNTHESIS_REQUIRED` | 宿主无法产出音频,返回 `fallback: "speechSynthesis"` | 由调用方朗读返回的 `text`;`fallbackReason` 给出各引擎失败原因,如需文件请安装/修复 edge-tts 后重试 |
|
|
346
|
+
|
|
347
|
+
## 开发与测试
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
npm run build
|
|
351
|
+
npm test
|
|
352
|
+
npm run test:integration
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
- `npm run build`:TypeScript 编译到 `dist/`。
|
|
356
|
+
- `npm test`:离线单元测试,使用注入的子进程替身,不依赖网络与真实 Edge-TTS。
|
|
357
|
+
- `npm run test:integration`:真实端到端链路,需要 `edge-tts` 与网络。
|
|
358
|
+
|
|
359
|
+
`engines.dsh` 必须能匹配当前运行的 DSH 版本;预发布版本(如 `0.2.2-rc.1`)低于同号正式版,写 `>=0.2.2` 会排除它。升级 DSH 后先确认该范围仍然成立。
|
|
360
|
+
## 引擎选择(0.3.0)
|
|
2
361
|
|
|
3
|
-
|
|
362
|
+
| `engine` | 依赖 | 词边界 | 用途 |
|
|
363
|
+
|---|---|---|---|
|
|
364
|
+
| `auto`(默认) | 有 `node-edge-tts` 就用它,否则回落到 CLI | 纯 JS 时精确 | 装完即用 |
|
|
365
|
+
| `edge-js` | `node-edge-tts`(可选依赖,纯 JS) | 精确(`words` 字段) | 无 Python 环境 |
|
|
366
|
+
| `edge-py` | Python + `edge-tts` | 无 | 旧环境 / 已装用户 |
|
|
367
|
+
| `sapi` | Windows 内置 | 无 | 完全离线 |
|
|
368
|
+
|
|
369
|
+
`ctx.tts.synthesize()` 返回 `{ filePath, format, durationMs, timingQuality, engine, fromCache, words? }`。
|
|
370
|
+
`words` 存在时字幕时间轴可用 `exact`;否则调用方必须按 `estimated` 处理。
|
|
371
|
+
|
|
372
|
+
```ts
|
|
373
|
+
const audio = await ctx.tts.synthesize({ text: '一段话', voice: 'zh-CN-XiaoxiaoNeural' })
|
|
374
|
+
if (audio.kind === 'audio') console.log(audio.durationMs, audio.words?.length ?? 0)
|
|
375
|
+
```
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure-JS MP3 timing, so duration and subtitle alignment never depend on
|
|
3
|
+
* ffmpeg or an external probe. Edge-TTS writes `audio-24khz-*` MPEG-2 Layer III
|
|
4
|
+
* (576 samples per frame), but the parser covers MPEG-1/2/2.5 Layer III.
|
|
5
|
+
*
|
|
6
|
+
* Cache metadata and the manifest both promise a `timingQuality`, so a parse
|
|
7
|
+
* failure must be reported as "unknown", never guessed here.
|
|
8
|
+
*/
|
|
9
|
+
/** Result of a successful frame-level parse. */
|
|
10
|
+
export interface Mp3Info {
|
|
11
|
+
/** Total playable duration. */
|
|
12
|
+
durationMs: number;
|
|
13
|
+
/** Frames counted (or declared by a Xing/Info header). */
|
|
14
|
+
frames: number;
|
|
15
|
+
sampleRate: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Measure an MP3 buffer by walking its frames. Returns undefined for anything
|
|
19
|
+
* that is not a Layer III stream this parser understands, so callers can fall
|
|
20
|
+
* back to an explicit "estimated" timing quality instead of silently lying.
|
|
21
|
+
*/
|
|
22
|
+
export declare function measureMp3(buffer: Buffer): Mp3Info | undefined;
|
|
23
|
+
//# sourceMappingURL=mp3-duration.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mp3-duration.d.ts","sourceRoot":"","sources":["../../src/audio/mp3-duration.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,gDAAgD;AAChD,MAAM,WAAW,OAAO;IACtB,+BAA+B;IAC/B,UAAU,EAAE,MAAM,CAAA;IAClB,0DAA0D;IAC1D,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,MAAM,CAAA;CACnB;AAgGD;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CA2C9D"}
|