dsh-voice 0.4.3 → 0.5.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/CHANGELOG.md +13 -0
- package/LICENSES/edge-tts-LGPLv3.txt +195 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/docs/USAGE.en.md +4 -2
- package/docs/USAGE.md +4 -2
- package/docs/VALIDATION.md +4 -0
- package/docs/validation-0.5.0.md +11 -0
- package/lib/edge-drm-v2.d.ts +8 -0
- package/lib/edge-drm-v2.js +122 -0
- package/lib/edge-tts.d.ts +1 -1
- package/lib/edge-tts.js +6 -5
- package/lib/paths.d.ts +2 -2
- package/lib/paths.js +52 -9
- package/lib/stt.js +10 -0
- package/lib/tools.js +6 -6
- package/lib/update-client.js +1 -1
- package/package.json +7 -4
- package/src/edge-drm-v2.ts +105 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
[历史英文记录](docs/CHANGELOG.en.md)
|
|
6
6
|
|
|
7
|
+
## 0.5.1 (2026-10-29)
|
|
8
|
+
|
|
9
|
+
- 同步仓库与 npm 的简明介绍、中英文 README 和反馈入口;本次为文档与包元数据更新。
|
|
10
|
+
- Sync npm descriptions and both README guides with the repository, including issue and pull-request links.
|
|
11
|
+
|
|
12
|
+
## 0.5.0 (2026-10-28)
|
|
13
|
+
|
|
14
|
+
- 跟进 edge-tts 7.3.1 的 v2 令牌与 Cookie 协议,恢复真实 Edge 在线语音;独立模块保留上游 LGPLv3 许可与源码,主体仍为 MIT。
|
|
15
|
+
|
|
16
|
+
- ASR 不再跟随重定向上传音频;请将 asrBaseUrl 直接设为可信的最终接口。直连和插件代理均适用。
|
|
17
|
+
- 修复同名音频并发生成时的覆盖问题,转写和试听也返回实际保存的独立文件名。
|
|
18
|
+
- 显式覆盖只允许没有硬链接的普通文件,保留工作区和链接边界检查。
|
|
19
|
+
|
|
7
20
|
## 0.4.3 (2026-10-27)
|
|
8
21
|
|
|
9
22
|
- 修复分批更新时的新旧更新控件混用:保留原页面、草稿与安装保护,空闲后接管旧控制器,兼容旧入口。
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
The MIT license is used for 'src/edge_tts/srt_composer.py' only. All
|
|
2
|
+
remaining files are licensed under the LGPLv3.
|
|
3
|
+
|
|
4
|
+
-----------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
The MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2014-2023 Christopher Down
|
|
9
|
+
Copyright (c) 2025- rany <rany@riseup.net>
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in
|
|
19
|
+
all copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
27
|
+
THE SOFTWARE.
|
|
28
|
+
|
|
29
|
+
-----------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
GNU LESSER GENERAL PUBLIC LICENSE
|
|
32
|
+
Version 3, 29 June 2007
|
|
33
|
+
|
|
34
|
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
|
35
|
+
Everyone is permitted to copy and distribute verbatim copies
|
|
36
|
+
of this license document, but changing it is not allowed.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
This version of the GNU Lesser General Public License incorporates
|
|
40
|
+
the terms and conditions of version 3 of the GNU General Public
|
|
41
|
+
License, supplemented by the additional permissions listed below.
|
|
42
|
+
|
|
43
|
+
0. Additional Definitions.
|
|
44
|
+
|
|
45
|
+
As used herein, "this License" refers to version 3 of the GNU Lesser
|
|
46
|
+
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
|
47
|
+
General Public License.
|
|
48
|
+
|
|
49
|
+
"The Library" refers to a covered work governed by this License,
|
|
50
|
+
other than an Application or a Combined Work as defined below.
|
|
51
|
+
|
|
52
|
+
An "Application" is any work that makes use of an interface provided
|
|
53
|
+
by the Library, but which is not otherwise based on the Library.
|
|
54
|
+
Defining a subclass of a class defined by the Library is deemed a mode
|
|
55
|
+
of using an interface provided by the Library.
|
|
56
|
+
|
|
57
|
+
A "Combined Work" is a work produced by combining or linking an
|
|
58
|
+
Application with the Library. The particular version of the Library
|
|
59
|
+
with which the Combined Work was made is also called the "Linked
|
|
60
|
+
Version".
|
|
61
|
+
|
|
62
|
+
The "Minimal Corresponding Source" for a Combined Work means the
|
|
63
|
+
Corresponding Source for the Combined Work, excluding any source code
|
|
64
|
+
for portions of the Combined Work that, considered in isolation, are
|
|
65
|
+
based on the Application, and not on the Linked Version.
|
|
66
|
+
|
|
67
|
+
The "Corresponding Application Code" for a Combined Work means the
|
|
68
|
+
object code and/or source code for the Application, including any data
|
|
69
|
+
and utility programs needed for reproducing the Combined Work from the
|
|
70
|
+
Application, but excluding the System Libraries of the Combined Work.
|
|
71
|
+
|
|
72
|
+
1. Exception to Section 3 of the GNU GPL.
|
|
73
|
+
|
|
74
|
+
You may convey a covered work under sections 3 and 4 of this License
|
|
75
|
+
without being bound by section 3 of the GNU GPL.
|
|
76
|
+
|
|
77
|
+
2. Conveying Modified Versions.
|
|
78
|
+
|
|
79
|
+
If you modify a copy of the Library, and, in your modifications, a
|
|
80
|
+
facility refers to a function or data to be supplied by an Application
|
|
81
|
+
that uses the facility (other than as an argument passed when the
|
|
82
|
+
facility is invoked), then you may convey a copy of the modified
|
|
83
|
+
version:
|
|
84
|
+
|
|
85
|
+
a) under this License, provided that you make a good faith effort to
|
|
86
|
+
ensure that, in the event an Application does not supply the
|
|
87
|
+
function or data, the facility still operates, and performs
|
|
88
|
+
whatever part of its purpose remains meaningful, or
|
|
89
|
+
|
|
90
|
+
b) under the GNU GPL, with none of the additional permissions of
|
|
91
|
+
this License applicable to that copy.
|
|
92
|
+
|
|
93
|
+
3. Object Code Incorporating Material from Library Header Files.
|
|
94
|
+
|
|
95
|
+
The object code form of an Application may incorporate material from
|
|
96
|
+
a header file that is part of the Library. You may convey such object
|
|
97
|
+
code under terms of your choice, provided that, if the incorporated
|
|
98
|
+
material is not limited to numerical parameters, data structure
|
|
99
|
+
layouts and accessors, or small macros, inline functions and templates
|
|
100
|
+
(ten or fewer lines in length), you do both of the following:
|
|
101
|
+
|
|
102
|
+
a) Give prominent notice with each copy of the object code that the
|
|
103
|
+
Library is used in it and that the Library and its use are
|
|
104
|
+
covered by this License.
|
|
105
|
+
|
|
106
|
+
b) Accompany the object code with a copy of the GNU GPL and this license
|
|
107
|
+
document.
|
|
108
|
+
|
|
109
|
+
4. Combined Works.
|
|
110
|
+
|
|
111
|
+
You may convey a Combined Work under terms of your choice that,
|
|
112
|
+
taken together, effectively do not restrict modification of the
|
|
113
|
+
portions of the Library contained in the Combined Work and reverse
|
|
114
|
+
engineering for debugging such modifications, if you also do each of
|
|
115
|
+
the following:
|
|
116
|
+
|
|
117
|
+
a) Give prominent notice with each copy of the Combined Work that
|
|
118
|
+
the Library is used in it and that the Library and its use are
|
|
119
|
+
covered by this License.
|
|
120
|
+
|
|
121
|
+
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
|
122
|
+
document.
|
|
123
|
+
|
|
124
|
+
c) For a Combined Work that displays copyright notices during
|
|
125
|
+
execution, include the copyright notice for the Library among
|
|
126
|
+
these notices, as well as a reference directing the user to the
|
|
127
|
+
copies of the GNU GPL and this license document.
|
|
128
|
+
|
|
129
|
+
d) Do one of the following:
|
|
130
|
+
|
|
131
|
+
0) Convey the Minimal Corresponding Source under the terms of this
|
|
132
|
+
License, and the Corresponding Application Code in a form
|
|
133
|
+
suitable for, and under terms that permit, the user to
|
|
134
|
+
recombine or relink the Application with a modified version of
|
|
135
|
+
the Linked Version to produce a modified Combined Work, in the
|
|
136
|
+
manner specified by section 6 of the GNU GPL for conveying
|
|
137
|
+
Corresponding Source.
|
|
138
|
+
|
|
139
|
+
1) Use a suitable shared library mechanism for linking with the
|
|
140
|
+
Library. A suitable mechanism is one that (a) uses at run time
|
|
141
|
+
a copy of the Library already present on the user's computer
|
|
142
|
+
system, and (b) will operate properly with a modified version
|
|
143
|
+
of the Library that is interface-compatible with the Linked
|
|
144
|
+
Version.
|
|
145
|
+
|
|
146
|
+
e) Provide Installation Information, but only if you would otherwise
|
|
147
|
+
be required to provide such information under section 6 of the
|
|
148
|
+
GNU GPL, and only to the extent that such information is
|
|
149
|
+
necessary to install and execute a modified version of the
|
|
150
|
+
Combined Work produced by recombining or relinking the
|
|
151
|
+
Application with a modified version of the Linked Version. (If
|
|
152
|
+
you use option 4d0, the Installation Information must accompany
|
|
153
|
+
the Minimal Corresponding Source and Corresponding Application
|
|
154
|
+
Code. If you use option 4d1, you must provide the Installation
|
|
155
|
+
Information in the manner specified by section 6 of the GNU GPL
|
|
156
|
+
for conveying Corresponding Source.)
|
|
157
|
+
|
|
158
|
+
5. Combined Libraries.
|
|
159
|
+
|
|
160
|
+
You may place library facilities that are a work based on the
|
|
161
|
+
Library side by side in a single library together with other library
|
|
162
|
+
facilities that are not Applications and are not covered by this
|
|
163
|
+
License, and convey such a combined library under terms of your
|
|
164
|
+
choice, if you do both of the following:
|
|
165
|
+
|
|
166
|
+
a) Accompany the combined library with a copy of the same work based
|
|
167
|
+
on the Library, uncombined with any other library facilities,
|
|
168
|
+
conveyed under the terms of this License.
|
|
169
|
+
|
|
170
|
+
b) Give prominent notice with the combined library that part of it
|
|
171
|
+
is a work based on the Library, and explaining where to find the
|
|
172
|
+
accompanying uncombined form of the same work.
|
|
173
|
+
|
|
174
|
+
6. Revised Versions of the GNU Lesser General Public License.
|
|
175
|
+
|
|
176
|
+
The Free Software Foundation may publish revised and/or new versions
|
|
177
|
+
of the GNU Lesser General Public License from time to time. Such new
|
|
178
|
+
versions will be similar in spirit to the present version, but may
|
|
179
|
+
differ in detail to address new problems or concerns.
|
|
180
|
+
|
|
181
|
+
Each version is given a distinguishing version number. If the
|
|
182
|
+
Library as you received it specifies that a certain numbered version
|
|
183
|
+
of the GNU Lesser General Public License "or any later version"
|
|
184
|
+
applies to it, you have the option of following the terms and
|
|
185
|
+
conditions either of that published version or of any later version
|
|
186
|
+
published by the Free Software Foundation. If the Library as you
|
|
187
|
+
received it does not specify a version number of the GNU Lesser
|
|
188
|
+
General Public License, you may choose any version of the GNU Lesser
|
|
189
|
+
General Public License ever published by the Free Software Foundation.
|
|
190
|
+
|
|
191
|
+
If the Library as you received it specifies that a proxy can decide
|
|
192
|
+
whether future versions of the GNU Lesser General Public License shall
|
|
193
|
+
apply, that proxy's public statement of acceptance of any version is
|
|
194
|
+
permanent authorization for you to choose that version for the
|
|
195
|
+
Library.
|
package/README.en.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
Generate speech from
|
|
7
|
+
Generate speech or transcribe workspace recordings from DSH. Supports Edge TTS and OpenAI, Groq or custom compatible ASR; transcription uses your configured endpoint and output files avoid overwriting by default.
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/dsh-voice) [](https://www.npmjs.com/package/dsh-voice)
|
|
10
10
|
|
|
@@ -51,4 +51,4 @@ Older releases need one manual update in native plugin management to acquire the
|
|
|
51
51
|
|
|
52
52
|
## License
|
|
53
53
|
|
|
54
|
-
[MIT](LICENSE)
|
|
54
|
+
Original code: [MIT](LICENSE). The separate Edge v2 token module is LGPLv3; its source and license are included. See [third-party notices](THIRD_PARTY_NOTICES.md).
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|

|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
在 DSH 中把文字合成为语音,或将工作区录音转为文字。支持 Edge TTS 和 OpenAI / Groq / 自建兼容 ASR;转写使用你配置的地址,输出文件默认避免覆盖。
|
|
8
8
|
|
|
9
9
|
[](https://www.npmjs.com/package/dsh-voice) [](https://www.npmjs.com/package/dsh-voice)
|
|
10
10
|
|
|
@@ -51,4 +51,4 @@ dsh plugin --profile desktop add dsh-voice
|
|
|
51
51
|
|
|
52
52
|
## License
|
|
53
53
|
|
|
54
|
-
[MIT](LICENSE)
|
|
54
|
+
主体采用 [MIT](LICENSE);独立的 Edge v2 令牌模块采用上游 LGPLv3,源码与许可随包提供,见[第三方声明](THIRD_PARTY_NOTICES.md)。
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Third-party notices
|
|
2
|
+
|
|
3
|
+
`src/edge-drm-v2.ts` and its compiled module are adapted from
|
|
4
|
+
[rany2/edge-tts 7.3.1](https://github.com/rany2/edge-tts/tree/7.3.1),
|
|
5
|
+
`src/edge_tts/drm.py` (blob `707dc7724b63242624b9b62175556fa1fdcb7fad`),
|
|
6
|
+
copyright rany and contributors, licensed under LGPLv3.
|
|
7
|
+
The adaptation replaces the Python digest implementation with a separate
|
|
8
|
+
TypeScript module, retaining the protocol constants and calculations.
|
|
9
|
+
Its source and the upstream license are included in the npm package, under
|
|
10
|
+
`src/edge-drm-v2.ts` and `LICENSES/edge-tts-LGPLv3.txt`.
|
|
11
|
+
You may modify and replace this module under LGPLv3; no replacement lock is used.
|
|
12
|
+
The remaining original dsh-voice code remains under the repository's MIT license.
|
package/docs/USAGE.en.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
## Current improvements
|
|
6
6
|
|
|
7
|
+
ASR stops on redirects instead of forwarding audio to another endpoint, including through the plugin proxy. Set `asrBaseUrl` to the trusted final API address. With `overwrite=false`, concurrent outputs are saved separately and tools return the actual saved path. Explicit overwrite requires a regular file with no hard links.
|
|
8
|
+
|
|
7
9
|
Check the ASR endpoint, key and response format after a failure. Blank results are not saved as successful transcripts. Verify TTS and ASR service availability separately.
|
|
8
10
|
|
|
9
11
|
## Installation
|
|
@@ -58,7 +60,7 @@ voice_health {} # self-check TTS /
|
|
|
58
60
|
|
|
59
61
|
## Implementation and file limits
|
|
60
62
|
|
|
61
|
-
- **Direct edge-tts protocol**:
|
|
63
|
+
- **Direct edge-tts protocol**: follows open-source edge-tts 7.3.1 Sec-MS-GEC v2, generating tokens locally in five-minute windows. Uses `ws` and optional HTTP CONNECT. This is not a Microsoft SDK, and the service protocol may change.
|
|
62
64
|
- **API configuration**: TTS needs no separate API key. STT uses your configured ASR service, whose pricing and limits apply.
|
|
63
65
|
- Up-front validation: text ≤ 5000 chars, audio ≤ 25MB (re-checked against the bytes actually read); same-name outputs auto-suffixed
|
|
64
66
|
- **Reads and writes stay inside the session workspace**: `audio`, `output` and `outputDir` must resolve (symlinks included) under `session.header.cwd`; anything outside is rejected. Copy the file in first.
|
|
@@ -77,4 +79,4 @@ pnpm test:integration # opt-in real edge-tts request; network and assertion err
|
|
|
77
79
|
|
|
78
80
|
## License
|
|
79
81
|
|
|
80
|
-
MIT
|
|
82
|
+
Original code MIT; separate Edge v2 token module LGPLv3. See [third-party notices](../THIRD_PARTY_NOTICES.md).
|
package/docs/USAGE.md
CHANGED
|
@@ -58,13 +58,15 @@ voice_health {} # 自检 TTS / ASR /
|
|
|
58
58
|
|
|
59
59
|
## 实现与文件限制
|
|
60
60
|
|
|
61
|
-
- **edge-tts
|
|
61
|
+
- **edge-tts 协议直连**:对齐开源 edge-tts 7.3.1 的 Sec-MS-GEC v2 协议,令牌在本地按 5 分钟窗口生成;WS 使用 `ws` 库与可选 HTTP CONNECT 代理。不是微软官方 SDK,服务协议仍可能变更。
|
|
62
62
|
- **接口配置**:TTS 无需另填 API Key;STT 使用你配置的 ASR 服务,其计费与额度由该服务决定。
|
|
63
63
|
- 文本 ≤5000 字符、音频 ≤25MB 前置校验(读入后再按实际字节数复查一次);输出同名自动加序号
|
|
64
64
|
- **读写都在会话工作区内**:`audio` 与 `output` / `outputDir` 解析后(含符号链接)必须落在 `session.header.cwd` 之下,越界直接报错。想转写工作区外的录音,先复制进来;输出同理
|
|
65
65
|
- **不跟随符号链接写文件**:目标是符号链接(含悬空链接)时拒绝写入,新建文件用 `O_EXCL`,避免检查完再被换成链接
|
|
66
66
|
- **转写文本 0600**:`voice_stt` 写出的 `.txt` 只给当前用户读写(Windows 不检查权限位)
|
|
67
67
|
- **密钥只发给配置的地址**:ASR 请求地址一律取配置 `asrBaseUrl`(groq/openai 的预设只是缺省值),模型传的 `engine` 与配置不一致会直接报错,避免把密钥发到另一家的主机
|
|
68
|
+
- **重定向需要用户确认地址**:接口返回 3xx 时停止,不向跳转地址发送音频。请检查服务文档,把 `asrBaseUrl` 设为可信的最终接口;这也适用于配置了插件代理的请求。
|
|
69
|
+
- **并发生成不覆盖**:`overwrite=false` 时即使两个任务同时写同名文件,或合成期间出现用户同名文件,也会另存并返回实际文件名。显式覆盖只接受没有硬链接的普通文件。
|
|
68
70
|
- Edge TTS 协议适配保留在插件实现中,不使用旧令牌端点。
|
|
69
71
|
|
|
70
72
|
## 开发
|
|
@@ -77,4 +79,4 @@ pnpm test:integration # 显式联网,调用真实 edge-tts;网络或断言
|
|
|
77
79
|
|
|
78
80
|
## License
|
|
79
81
|
|
|
80
|
-
MIT
|
|
82
|
+
主体 MIT;独立的 Edge v2 令牌模块 LGPLv3,见[第三方声明](../THIRD_PARTY_NOTICES.md)。
|
package/docs/VALIDATION.md
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 0.5.0 验证记录(2026-10-28)
|
|
2
|
+
|
|
3
|
+
Windows 回归 74 项:72 通过、2 跳过(文件符号链接权限、POSIX 0600 位)。硬链接、目录 junction、并发合成/转写/试听均另有真实 Windows 文件系统用例。
|
|
4
|
+
|
|
5
|
+
官方 DSH 0.2.1-rc.4 隔离 profile 实际注册工具并生成两段在线 Edge MP3,同名输出各自保留;旧 v1 协议连接失败,更新后真实合成通过。v2 计算同时匹配固定的 upstream Python 7.3.1 参考向量 44 组。不是仅用模拟音频验收。
|
|
6
|
+
|
|
7
|
+
ASR 使用本机 HTTP 服务与真实 multipart 请求验证:引擎错配在联网前拒绝、工作区外文件拒绝、307 不转发音频;301/302/303/307/308 同时覆盖直连与真实 HTTP 代理。模拟 ASR 返回不代表真实商业转写服务通过。
|
|
8
|
+
|
|
9
|
+
官方 Web 浏览器实际选择工作区、打开会话轨迹并查看转写结果与插件列表;窄屏无水平溢出。未操作用户桌面窗口,未验证 Electron 原生窗口或真实 ASR 账号。
|
|
10
|
+
|
|
11
|
+
Edge v2 模块单独保留上游 LGPLv3 许可与可替换源码,其他原代码仍为 MIT,见 THIRD_PARTY_NOTICES.md。在线服务后续仍可能变更。
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: LGPL-3.0-only
|
|
3
|
+
* Adapted from rany2/edge-tts 7.3.1 src/edge_tts/drm.py (rany and contributors).
|
|
4
|
+
* https://github.com/rany2/edge-tts/blob/7.3.1/src/edge_tts/drm.py
|
|
5
|
+
* TypeScript adaptation, 2026 STARDUSTLC666. See LICENSES/edge-tts-LGPLv3.txt.
|
|
6
|
+
* Kept as a separate, replaceable module; the rest of dsh-voice remains MIT.
|
|
7
|
+
*/
|
|
8
|
+
export declare function edgeTokenV2(data: string): string;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: LGPL-3.0-only
|
|
3
|
+
* Adapted from rany2/edge-tts 7.3.1 src/edge_tts/drm.py (rany and contributors).
|
|
4
|
+
* https://github.com/rany2/edge-tts/blob/7.3.1/src/edge_tts/drm.py
|
|
5
|
+
* TypeScript adaptation, 2026 STARDUSTLC666. See LICENSES/edge-tts-LGPLv3.txt.
|
|
6
|
+
* Kept as a separate, replaceable module; the rest of dsh-voice remains MIT.
|
|
7
|
+
*/
|
|
8
|
+
const INITIAL = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19];
|
|
9
|
+
const STATE = [0xFD9FBEA2, 0x91243CE7, 0xBE499094, 0xCD2E1E0A, 0x6C006F49, 0x04BD35AB, 0xFEB0A7E4, 0x03EFFBD2];
|
|
10
|
+
const SCHEDULE = [0xEB24BF9E, 0x1558691F, 0xA0C09EE2, 0x0CFA32BA, 0xA622EE8C, 0xD1DD9491, 0xC0DD25A0, 0xF9E70AB6, 0xF0C31573, 0x7058CE4E, 0x7188E606, 0x00E5AB33, 0xD258B060, 0x72766564];
|
|
11
|
+
const K = [
|
|
12
|
+
0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
|
|
13
|
+
0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
|
|
14
|
+
0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
|
|
15
|
+
0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
|
|
16
|
+
0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
|
|
17
|
+
0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
|
|
18
|
+
0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
|
|
19
|
+
0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2,
|
|
20
|
+
];
|
|
21
|
+
const COEFFICIENTS = [
|
|
22
|
+
[0xE82E91B1, 0xB32700D0], [0x44DE24C6, 0x88DFF51D], [0x3BEB65BC, 0x29514038], [0xC89C5933, 0xB328CBDF],
|
|
23
|
+
[0x8D1EC211, 0xD405B4D5], [0x314EF5D5, 0x26E55F3C], [0xE92491B1, 0x607801C6], [0xF0A5B9D5, 0x82B2C1FB],
|
|
24
|
+
[0x06064CF3, 0xFA51A3DA], [0xDA79FA60, 0x207CC464], [0xBED20560, 0x3C3C74F3], [0x89F9C9F8, 0xD2D14B61],
|
|
25
|
+
[0x35FA38C6, 0xBB04A2C1], [0xCB49E3C6, 0xC5626167],
|
|
26
|
+
];
|
|
27
|
+
const rol16 = (x) => ((x << 16) | (x >>> 16)) >>> 0;
|
|
28
|
+
const rotr = (x, n) => (x >>> n) | (x << (32 - n));
|
|
29
|
+
const sigma0 = (x) => (rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3)) >>> 0;
|
|
30
|
+
const sigma1 = (x) => (rotr(x, 17) ^ rotr(x, 19) ^ (x >>> 10)) >>> 0;
|
|
31
|
+
const mul = (x, y) => Math.imul(x, y) >>> 0;
|
|
32
|
+
function expand(words, first = 16) {
|
|
33
|
+
for (let i = first; i < 64; i++)
|
|
34
|
+
words.push((sigma1(words[i - 2]) + words[i - 7] + sigma0(words[i - 15]) + words[i - 16]) >>> 0);
|
|
35
|
+
}
|
|
36
|
+
function rounds(state, words, first = 0) {
|
|
37
|
+
let [a, b, c, d, e, f, g, h] = state;
|
|
38
|
+
for (let i = first; i < 64; i++) {
|
|
39
|
+
const constant = first === 14 && i === 14 ? 0xC9DC06A7 : K[i];
|
|
40
|
+
const t1 = (h + ((rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) >>> 0) + ((g ^ (e & (f ^ g))) >>> 0) + constant + words[i]) >>> 0;
|
|
41
|
+
const t2 = (((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) >>> 0) + (((a & b) | (c & (a | b))) >>> 0)) >>> 0;
|
|
42
|
+
[a, b, c, d, e, f, g, h] = [(t1 + t2) >>> 0, a, b, c, (d + t1) >>> 0, e, f, g];
|
|
43
|
+
}
|
|
44
|
+
return [a, b, c, d, e, f, g, h];
|
|
45
|
+
}
|
|
46
|
+
function tokenDigest(data) {
|
|
47
|
+
const words = new Array(14).fill(0);
|
|
48
|
+
words.push(data.readUIntBE(0, 3), data.readUInt32BE(3));
|
|
49
|
+
for (let i = 16; i < 30; i++) {
|
|
50
|
+
let value = SCHEDULE[i - 16];
|
|
51
|
+
if (i - 2 >= 14)
|
|
52
|
+
value += (sigma1(words[i - 2]) ^ (i === 16 ? 0x000B92C0 : 0)) >>> 0;
|
|
53
|
+
if (i - 7 >= 14)
|
|
54
|
+
value += words[i - 7];
|
|
55
|
+
if (i - 15 >= 14)
|
|
56
|
+
value += (sigma0(words[i - 15]) ^ (i === 29 ? 0x059C0B80 : 0)) >>> 0;
|
|
57
|
+
words.push(value >>> 0);
|
|
58
|
+
}
|
|
59
|
+
words.push((sigma1(words[28]) + words[23] + sigma0(words[15]) + words[14] + 0xC9DC06A7 - K[14]) >>> 0);
|
|
60
|
+
expand(words, 31);
|
|
61
|
+
let state = rounds(STATE, words, 14).map((x, i) => (x + INITIAL[i]) >>> 0);
|
|
62
|
+
const length = 57 + data.length, padding = ((55 - length) % 64 + 64) % 64;
|
|
63
|
+
const tail = Buffer.concat([data.subarray(7), Buffer.from([0x80]), Buffer.alloc(padding + 8)]);
|
|
64
|
+
tail.writeBigUInt64BE(BigInt(length) * 8n, tail.length - 8);
|
|
65
|
+
for (let offset = 0; offset < tail.length; offset += 64) {
|
|
66
|
+
const block = Array.from({ length: 16 }, (_, i) => tail.readUInt32BE(offset + 4 * i));
|
|
67
|
+
expand(block);
|
|
68
|
+
state = rounds(state, block).map((x, i) => (x + state[i]) >>> 0);
|
|
69
|
+
}
|
|
70
|
+
const out = Buffer.alloc(32);
|
|
71
|
+
state.forEach((x, i) => out.writeUInt32BE(x, 4 * i));
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function cs64(data, digest) {
|
|
75
|
+
const keyA = (digest.readUInt32LE(0) | 1) >>> 0, keyB = (digest.readUInt32LE(4) | 1) >>> 0;
|
|
76
|
+
const factors = [mul(keyA, 0x82893933), mul(keyB, 0x82893933)];
|
|
77
|
+
const terms = COEFFICIENTS.map(([coefficient], i) => mul(coefficient, factors[i % 2])), offsets = COEFFICIENTS.map(([, offset]) => offset);
|
|
78
|
+
const padded = Buffer.alloc(Math.ceil((data.length + 1) / 8) * 8);
|
|
79
|
+
data.copy(padded, 1);
|
|
80
|
+
for (let i = 0; i < padded.length / 4; i++) {
|
|
81
|
+
const word = padded.readUInt32LE(4 * i);
|
|
82
|
+
let term = mul(word, i % 2 === 0 ? keyA : keyB), offset = mul(rol16(word), i % 2 === 0 ? 0xEF0569FB : 0xC31713DB);
|
|
83
|
+
if (i === 0) {
|
|
84
|
+
term = (term + mul(0xF2F90B1A, factors[0])) >>> 0;
|
|
85
|
+
offset = (offset + 0x0B1A0000) >>> 0;
|
|
86
|
+
}
|
|
87
|
+
terms.push(term);
|
|
88
|
+
offsets.push(offset);
|
|
89
|
+
}
|
|
90
|
+
let swapAcc = 0, swapLast = 0, state = 0, acc = 0;
|
|
91
|
+
for (let i = 0; i < terms.length; i += 2) {
|
|
92
|
+
const x = terms[i], y = terms[i + 1];
|
|
93
|
+
let t = rol16((offsets[i] + x) >>> 0), q = mul(t, 0x76F50F34);
|
|
94
|
+
swapAcc += (mul(rol16(q), 0xC3EFEA97) + q) >>> 0;
|
|
95
|
+
t = rol16((offsets[i + 1] + y) >>> 0);
|
|
96
|
+
q = mul(t, 0x1881CE3C);
|
|
97
|
+
swapLast = (mul(rol16(q), 0x35BD1EC9) + q) >>> 0;
|
|
98
|
+
swapAcc = (swapAcc + swapLast) >>> 0;
|
|
99
|
+
state = (mul(state, keyA) + x) >>> 0;
|
|
100
|
+
for (const factor of [0xCF98B111, 0x87085B9F, 0x12CEB96D, 0x257E1D83])
|
|
101
|
+
state = mul(rol16(state), factor);
|
|
102
|
+
acc += state;
|
|
103
|
+
t = (mul(state, keyB) + y) >>> 0;
|
|
104
|
+
for (const factor of [0xA27416F5, 0xD38396FF])
|
|
105
|
+
t = mul(rol16(t), factor);
|
|
106
|
+
state = mul(rol16(mul(rol16(t), 0x7C932B89)), 0xBFA49F69);
|
|
107
|
+
acc = (acc + state) >>> 0;
|
|
108
|
+
}
|
|
109
|
+
const out = Buffer.alloc(8);
|
|
110
|
+
out.writeUInt32LE((swapLast ^ state) >>> 0);
|
|
111
|
+
out.writeUInt32LE((swapAcc ^ acc) >>> 0, 4);
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
let lastData = '', lastToken = '';
|
|
115
|
+
export function edgeTokenV2(data) {
|
|
116
|
+
if (data === lastData)
|
|
117
|
+
return lastToken;
|
|
118
|
+
const bytes = Buffer.from(data, 'ascii'), digest = tokenDigest(bytes);
|
|
119
|
+
lastToken = cs64(bytes, digest).toString('hex').toUpperCase() + '-' + digest.toString('hex').toUpperCase();
|
|
120
|
+
lastData = data;
|
|
121
|
+
return lastToken;
|
|
122
|
+
}
|
package/lib/edge-tts.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export type WebSocketFactory = (url: string, options: {
|
|
|
26
26
|
headers: Record<string, string>;
|
|
27
27
|
}) => WebSocketLike;
|
|
28
28
|
/**
|
|
29
|
-
* 本地生成 Sec-MS-GEC 令牌(对齐 edge-tts
|
|
29
|
+
* 本地生成 Sec-MS-GEC v2 令牌(对齐 edge-tts 7.3.1)。
|
|
30
30
|
*/
|
|
31
31
|
export declare function generateSecMsGec(nowSeconds?: number): string;
|
|
32
32
|
/** 生成 SSML。 */
|
package/lib/edge-tts.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module dsh-voice/edge-tts
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
9
|
+
import { edgeTokenV2 } from './edge-drm-v2.js';
|
|
9
10
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
10
11
|
import WebSocket from 'ws';
|
|
11
12
|
const TRUSTED_CLIENT_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4';
|
|
12
13
|
const WS_BASE = 'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1';
|
|
13
14
|
const OUTPUT_FORMAT = 'audio-24khz-48kbitrate-mono-mp3';
|
|
14
|
-
const SEC_MS_GEC_VERSION = '
|
|
15
|
+
const SEC_MS_GEC_VERSION = '2-143.0.3650.75';
|
|
15
16
|
const WIN_EPOCH_SECONDS = 11644473600;
|
|
16
17
|
const WSS_HEADERS = {
|
|
17
18
|
Pragma: 'no-cache',
|
|
@@ -32,14 +33,14 @@ function proxyWebSocket(proxyUrl) {
|
|
|
32
33
|
return (url, options) => new WebSocket(url, { headers: options.headers, agent });
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
|
-
* 本地生成 Sec-MS-GEC 令牌(对齐 edge-tts
|
|
36
|
+
* 本地生成 Sec-MS-GEC v2 令牌(对齐 edge-tts 7.3.1)。
|
|
36
37
|
*/
|
|
37
38
|
export function generateSecMsGec(nowSeconds = Date.now() / 1000) {
|
|
38
39
|
let ticks = Math.floor(nowSeconds) + WIN_EPOCH_SECONDS;
|
|
39
40
|
ticks -= ticks % 300;
|
|
40
41
|
const windowsTicks = ticks * 10000000;
|
|
41
42
|
const raw = String(windowsTicks) + TRUSTED_CLIENT_TOKEN;
|
|
42
|
-
return
|
|
43
|
+
return edgeTokenV2(raw);
|
|
43
44
|
}
|
|
44
45
|
/** XML 转义。 */
|
|
45
46
|
function escapeXml(text) {
|
|
@@ -75,7 +76,7 @@ export async function synthesizeSpeech(options, deps = {}, timeoutMs = 30000, si
|
|
|
75
76
|
// 对齐 edge-tts 当前协议:Sec-MS-GEC 走 URL 查询参数,MUID 走 header
|
|
76
77
|
const url = WS_BASE + '?TrustedClientToken=' + TRUSTED_CLIENT_TOKEN + '&ConnectionId=' + randomUUID().replace(/-/g, '') + '&Sec-MS-GEC=' + token + '&Sec-MS-GEC-Version=' + SEC_MS_GEC_VERSION;
|
|
77
78
|
const socket = factory(url, {
|
|
78
|
-
headers: { ...WSS_HEADERS,
|
|
79
|
+
headers: { ...WSS_HEADERS, Cookie: 'muid=' + randomBytes(16).toString('hex').toUpperCase() + ';' },
|
|
79
80
|
});
|
|
80
81
|
const chunks = [];
|
|
81
82
|
let done = false;
|
package/lib/paths.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ export declare function resolveOutputPath(explicit: string | undefined, defaultN
|
|
|
16
16
|
/** 确保输出目录存在且落在工作区内:不存在则创建,越界或符号链接报错。 */
|
|
17
17
|
export declare function ensureWorkspaceDirectory(input: string, cwd?: string): string;
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
19
|
+
* 写工作区内的文件,返回实际写入的路径。同名竞争通过 wx 排他创建解决。
|
|
20
20
|
*
|
|
21
21
|
* 新建时用 O_EXCL,避免检查完成到写入之间被人塞进一个符号链接。
|
|
22
22
|
*/
|
|
23
|
-
export declare function writeWorkspaceFile(target: string, data: string | Buffer, cwd?: string, mode?: number):
|
|
23
|
+
export declare function writeWorkspaceFile(target: string, data: string | Buffer, cwd?: string, mode?: number, overwrite?: boolean): string;
|
package/lib/paths.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module dsh-voice/paths
|
|
5
5
|
*/
|
|
6
|
-
import { lstatSync, mkdirSync, openSync, closeSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import { lstatSync, mkdirSync, openSync, closeSync, realpathSync, statSync, writeFileSync, fstatSync, ftruncateSync, fchmodSync, constants } from 'node:fs';
|
|
7
7
|
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
8
8
|
/** 会话工作区的真实路径(解析符号链接);取不到就退回解析后的绝对路径。 */
|
|
9
9
|
function workspaceRoot(cwd) {
|
|
@@ -24,10 +24,17 @@ function pathState(target) {
|
|
|
24
24
|
try {
|
|
25
25
|
return lstatSync(target).isSymbolicLink() ? 'symlink' : 'present';
|
|
26
26
|
}
|
|
27
|
-
catch {
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code !== 'ENOENT')
|
|
29
|
+
throw error;
|
|
28
30
|
return 'missing';
|
|
29
31
|
}
|
|
30
32
|
}
|
|
33
|
+
function assertOverwriteFile(target, stat = lstatSync(target)) {
|
|
34
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
|
|
35
|
+
throw new Error('覆盖目标必须是没有硬链接的普通文件:' + target + '。请改用新的输出文件名。');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
31
38
|
/**
|
|
32
39
|
* 沿着最近的已存在祖先解析真实路径,再接回尚不存在的后缀。
|
|
33
40
|
*
|
|
@@ -96,6 +103,8 @@ export function resolveOutputPath(explicit, defaultName, overwrite, cwd = proces
|
|
|
96
103
|
const requested = explicit !== undefined && explicit.trim() !== '' ? explicit.trim() : defaultName;
|
|
97
104
|
const target = resolve(root, requested);
|
|
98
105
|
assertWritableTarget(target, root, '输出路径');
|
|
106
|
+
if (overwrite && pathState(target) === 'present')
|
|
107
|
+
assertOverwriteFile(target);
|
|
99
108
|
if (overwrite || pathState(target) === 'missing')
|
|
100
109
|
return target;
|
|
101
110
|
const directory = dirname(target);
|
|
@@ -129,24 +138,58 @@ export function ensureWorkspaceDirectory(input, cwd = process.cwd()) {
|
|
|
129
138
|
return target;
|
|
130
139
|
}
|
|
131
140
|
/**
|
|
132
|
-
*
|
|
141
|
+
* 写工作区内的文件,返回实际写入的路径。同名竞争通过 wx 排他创建解决。
|
|
133
142
|
*
|
|
134
143
|
* 新建时用 O_EXCL,避免检查完成到写入之间被人塞进一个符号链接。
|
|
135
144
|
*/
|
|
136
|
-
export function writeWorkspaceFile(target, data, cwd = process.cwd(), mode) {
|
|
145
|
+
export function writeWorkspaceFile(target, data, cwd = process.cwd(), mode, overwrite = false) {
|
|
137
146
|
const root = workspaceRoot(cwd);
|
|
138
147
|
assertWritableTarget(target, root, '输出路径');
|
|
139
148
|
mkdirSync(dirname(target), { recursive: true });
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
149
|
+
const extension = extname(target), base = basename(target, extension);
|
|
150
|
+
for (let index = 0; index < 1000; index++) {
|
|
151
|
+
const candidate = index === 0 ? target : join(dirname(target), base + '_' + index + extension);
|
|
152
|
+
if (index > 0 && pathState(candidate) !== 'missing')
|
|
153
|
+
continue;
|
|
154
|
+
assertWritableTarget(candidate, root, '输出路径');
|
|
155
|
+
let fd;
|
|
156
|
+
try {
|
|
157
|
+
fd = openSync(candidate, 'wx', mode);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (error.code !== 'EEXIST')
|
|
161
|
+
throw error;
|
|
162
|
+
if (!overwrite)
|
|
163
|
+
continue;
|
|
164
|
+
// 不在校验前截断文件。打开后还需检查真实句柄,防止静态硬链接和替换。
|
|
165
|
+
const before = lstatSync(candidate);
|
|
166
|
+
assertOverwriteFile(candidate, before);
|
|
167
|
+
fd = openSync(candidate, constants.O_WRONLY | (constants.O_NOFOLLOW ?? 0));
|
|
168
|
+
try {
|
|
169
|
+
const opened = fstatSync(fd), current = lstatSync(candidate);
|
|
170
|
+
assertOverwriteFile(candidate, opened);
|
|
171
|
+
assertOverwriteFile(candidate, current);
|
|
172
|
+
assertWritableTarget(candidate, root, '输出路径');
|
|
173
|
+
if (opened.dev !== before.dev || opened.ino !== before.ino || opened.dev !== current.dev || opened.ino !== current.ino) {
|
|
174
|
+
throw new Error('输出文件在写入前发生变化,请重试:' + candidate);
|
|
175
|
+
}
|
|
176
|
+
if (mode !== undefined && process.platform !== 'win32')
|
|
177
|
+
fchmodSync(fd, mode);
|
|
178
|
+
ftruncateSync(fd, 0);
|
|
179
|
+
writeFileSync(fd, data);
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
closeSync(fd);
|
|
183
|
+
}
|
|
184
|
+
return candidate;
|
|
185
|
+
}
|
|
143
186
|
try {
|
|
144
187
|
writeFileSync(fd, data);
|
|
145
188
|
}
|
|
146
189
|
finally {
|
|
147
190
|
closeSync(fd);
|
|
148
191
|
}
|
|
149
|
-
return;
|
|
192
|
+
return candidate;
|
|
150
193
|
}
|
|
151
|
-
|
|
194
|
+
throw new Error('找不到可用的输出文件名(同名文件超过 999 个),请显式指定 output。');
|
|
152
195
|
}
|
package/lib/stt.js
CHANGED
|
@@ -56,6 +56,8 @@ export async function transcribe(baseUrl, apiKey, options, fetchImpl = globalThi
|
|
|
56
56
|
try {
|
|
57
57
|
response = await raceAbort(fetchImpl(baseUrl + '/audio/transcriptions', {
|
|
58
58
|
method: 'POST',
|
|
59
|
+
// 音频只交给配置的端点;307/308 也不能把 multipart 转发到另一站。
|
|
60
|
+
redirect: 'manual',
|
|
59
61
|
headers: { authorization: 'Bearer ' + apiKey },
|
|
60
62
|
body: form,
|
|
61
63
|
signal: requestSignal,
|
|
@@ -67,6 +69,14 @@ export async function transcribe(baseUrl, apiKey, options, fetchImpl = globalThi
|
|
|
67
69
|
throw new Error('ASR 请求超时,请重试或缩短音频。');
|
|
68
70
|
throw new Error('ASR 请求失败:' + (error instanceof Error ? error.message : String(error)) + '。若接口需要特殊代理(梯子),请在 cordis.patch.yml 配置 proxyUrl 后重启。');
|
|
69
71
|
}
|
|
72
|
+
if (response.status >= 300 && response.status < 400) {
|
|
73
|
+
try {
|
|
74
|
+
await raceAbort(response.body?.cancel(), requestSignal);
|
|
75
|
+
}
|
|
76
|
+
catch { /* 尽力释放响应,仍受请求超时限制。 */ }
|
|
77
|
+
throwIfAborted(abortSignal);
|
|
78
|
+
throw new Error('ASR 接口返回重定向(HTTP ' + response.status + '),未向跳转地址发送音频。请确认服务地址,并在 asrBaseUrl 中直接填写可信的最终接口地址。');
|
|
79
|
+
}
|
|
70
80
|
if (!response.ok) {
|
|
71
81
|
let body;
|
|
72
82
|
try {
|
package/lib/tools.js
CHANGED
|
@@ -197,10 +197,11 @@ export function buildVoiceTools(config, deps = {}) {
|
|
|
197
197
|
if (!/^[+-]?\d+(\.\d+)?(%|Hz|st)$/.test(value))
|
|
198
198
|
throw new Error(label + ' 不合法:' + value + '。合法格式如 +10%、-2Hz、+1st。');
|
|
199
199
|
}
|
|
200
|
-
const
|
|
200
|
+
const requested = optionalString(args, 'output'), cwd = sessionCwd(exec);
|
|
201
|
+
resolveOutputPath(requested, 'voice_output.mp3', cfg.overwrite, cwd);
|
|
201
202
|
const audio = await raceAbort((deps.tts ?? synthesizeSpeech)({ text, voice, rate, pitch }, { proxyUrl: cfg.proxyUrl }, timeout, signal), signal);
|
|
202
203
|
throwIfAborted(signal);
|
|
203
|
-
writeWorkspaceFile(
|
|
204
|
+
const output = writeWorkspaceFile(resolveOutputPath(requested, 'voice_output.mp3', cfg.overwrite, cwd), audio, cwd, undefined, cfg.overwrite);
|
|
204
205
|
return { output, bytes: audio.length, voice, rate, pitch, textLength: text.length };
|
|
205
206
|
},
|
|
206
207
|
timeoutMs: timeout + 10000,
|
|
@@ -258,8 +259,7 @@ export function buildVoiceTools(config, deps = {}) {
|
|
|
258
259
|
if (output !== undefined) {
|
|
259
260
|
const target = resolveOutputPath(output, '', cfg.overwrite, cwd);
|
|
260
261
|
// 转写文本可能含隐私内容,只用 0600 写入。
|
|
261
|
-
writeWorkspaceFile(target, text, cwd, 0o600);
|
|
262
|
-
transcriptFile = target;
|
|
262
|
+
transcriptFile = writeWorkspaceFile(target, text, cwd, 0o600, cfg.overwrite);
|
|
263
263
|
}
|
|
264
264
|
// The output schema declares these as strings — never return null, or
|
|
265
265
|
// dsh rejects the whole tool result (issue #1).
|
|
@@ -319,8 +319,8 @@ export function buildVoiceTools(config, deps = {}) {
|
|
|
319
319
|
const name = 'voice-preview-' + voice.replace(/[^a-zA-Z0-9-]/g, '_') + '.mp3';
|
|
320
320
|
// 复用 resolveOutputPath:overwrite=false 时改名加序号,不再直接覆盖同名样例。
|
|
321
321
|
const file = resolveOutputPath(join(outDir, name), '', cfg.overwrite, cwd);
|
|
322
|
-
writeWorkspaceFile(file, audio, cwd);
|
|
323
|
-
return { voice, output
|
|
322
|
+
const output = writeWorkspaceFile(file, audio, cwd, undefined, cfg.overwrite);
|
|
323
|
+
return { voice, output, bytes: audio.length };
|
|
324
324
|
}
|
|
325
325
|
catch (error) {
|
|
326
326
|
throwIfAborted(signal);
|
package/lib/update-client.js
CHANGED
|
@@ -437,7 +437,7 @@ function __stardustUpdatesClient(ctx, React, require, shared) {
|
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
/* STARDUSTLC_UPDATE_CLIENT_BODY_START */
|
|
440
|
-
window.__ModuleLoader__.load(__stardustUpdatesWrap({id:"dsh-voice",factory:require=>({apply(){}})},{"name":"dsh-voice","version":"0.1.0","hasWorkbench":false,"packageVersion":"0.
|
|
440
|
+
window.__ModuleLoader__.load(__stardustUpdatesWrap({id:"dsh-voice",factory:require=>({apply(){}})},{"name":"dsh-voice","version":"0.1.0","hasWorkbench":false,"packageVersion":"0.5.1","label":"语音","labelEn":"Voice","suiteComponent":true}));
|
|
441
441
|
|
|
442
442
|
/* STARDUSTLC_UPDATE_CLIENT_BODY_END */
|
|
443
443
|
})();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-voice",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "DSH 语音工具:Edge TTS 朗读、兼容 ASR 转写、工作区文件保护与并发输出。欢迎提交 issues 和 PR。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"README.md",
|
|
21
21
|
"README.en.md",
|
|
22
22
|
"docs",
|
|
23
|
-
"CHANGELOG.md"
|
|
23
|
+
"CHANGELOG.md",
|
|
24
|
+
"src/edge-drm-v2.ts",
|
|
25
|
+
"LICENSES",
|
|
26
|
+
"THIRD_PARTY_NOTICES.md"
|
|
24
27
|
],
|
|
25
28
|
"scripts": {
|
|
26
29
|
"build": "tsc -p tsconfig.json && node scripts/build-updates.mjs",
|
|
@@ -52,7 +55,7 @@
|
|
|
52
55
|
"voice",
|
|
53
56
|
"dsh-plugin"
|
|
54
57
|
],
|
|
55
|
-
"license": "MIT",
|
|
58
|
+
"license": "MIT AND LGPL-3.0-only",
|
|
56
59
|
"engines": {
|
|
57
60
|
"node": "^22.19.0 || >=24.0.0",
|
|
58
61
|
"dsh": ">=0.2.1-rc.3"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-License-Identifier: LGPL-3.0-only
|
|
3
|
+
* Adapted from rany2/edge-tts 7.3.1 src/edge_tts/drm.py (rany and contributors).
|
|
4
|
+
* https://github.com/rany2/edge-tts/blob/7.3.1/src/edge_tts/drm.py
|
|
5
|
+
* TypeScript adaptation, 2026 STARDUSTLC666. See LICENSES/edge-tts-LGPLv3.txt.
|
|
6
|
+
* Kept as a separate, replaceable module; the rest of dsh-voice remains MIT.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const INITIAL = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19]
|
|
10
|
+
const STATE = [0xFD9FBEA2, 0x91243CE7, 0xBE499094, 0xCD2E1E0A, 0x6C006F49, 0x04BD35AB, 0xFEB0A7E4, 0x03EFFBD2]
|
|
11
|
+
const SCHEDULE = [0xEB24BF9E, 0x1558691F, 0xA0C09EE2, 0x0CFA32BA, 0xA622EE8C, 0xD1DD9491, 0xC0DD25A0, 0xF9E70AB6, 0xF0C31573, 0x7058CE4E, 0x7188E606, 0x00E5AB33, 0xD258B060, 0x72766564]
|
|
12
|
+
const K = [
|
|
13
|
+
0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
|
|
14
|
+
0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
|
|
15
|
+
0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
|
|
16
|
+
0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
|
|
17
|
+
0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
|
|
18
|
+
0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
|
|
19
|
+
0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
|
|
20
|
+
0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2,
|
|
21
|
+
]
|
|
22
|
+
const COEFFICIENTS = [
|
|
23
|
+
[0xE82E91B1, 0xB32700D0], [0x44DE24C6, 0x88DFF51D], [0x3BEB65BC, 0x29514038], [0xC89C5933, 0xB328CBDF],
|
|
24
|
+
[0x8D1EC211, 0xD405B4D5], [0x314EF5D5, 0x26E55F3C], [0xE92491B1, 0x607801C6], [0xF0A5B9D5, 0x82B2C1FB],
|
|
25
|
+
[0x06064CF3, 0xFA51A3DA], [0xDA79FA60, 0x207CC464], [0xBED20560, 0x3C3C74F3], [0x89F9C9F8, 0xD2D14B61],
|
|
26
|
+
[0x35FA38C6, 0xBB04A2C1], [0xCB49E3C6, 0xC5626167],
|
|
27
|
+
]
|
|
28
|
+
const rol16 = (x: number) => ((x << 16) | (x >>> 16)) >>> 0
|
|
29
|
+
const rotr = (x: number, n: number) => (x >>> n) | (x << (32 - n))
|
|
30
|
+
const sigma0 = (x: number) => (rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3)) >>> 0
|
|
31
|
+
const sigma1 = (x: number) => (rotr(x, 17) ^ rotr(x, 19) ^ (x >>> 10)) >>> 0
|
|
32
|
+
const mul = (x: number, y: number) => Math.imul(x, y) >>> 0
|
|
33
|
+
|
|
34
|
+
function expand(words: number[], first = 16): void {
|
|
35
|
+
for (let i = first; i < 64; i++) words.push((sigma1(words[i - 2]) + words[i - 7] + sigma0(words[i - 15]) + words[i - 16]) >>> 0)
|
|
36
|
+
}
|
|
37
|
+
function rounds(state: number[], words: number[], first = 0): number[] {
|
|
38
|
+
let [a, b, c, d, e, f, g, h] = state
|
|
39
|
+
for (let i = first; i < 64; i++) {
|
|
40
|
+
const constant = first === 14 && i === 14 ? 0xC9DC06A7 : K[i]
|
|
41
|
+
const t1 = (h + ((rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) >>> 0) + ((g ^ (e & (f ^ g))) >>> 0) + constant + words[i]) >>> 0
|
|
42
|
+
const t2 = (((rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) >>> 0) + (((a & b) | (c & (a | b))) >>> 0)) >>> 0
|
|
43
|
+
;[a, b, c, d, e, f, g, h] = [(t1 + t2) >>> 0, a, b, c, (d + t1) >>> 0, e, f, g]
|
|
44
|
+
}
|
|
45
|
+
return [a, b, c, d, e, f, g, h]
|
|
46
|
+
}
|
|
47
|
+
function tokenDigest(data: Buffer): Buffer {
|
|
48
|
+
const words = new Array<number>(14).fill(0)
|
|
49
|
+
words.push(data.readUIntBE(0, 3), data.readUInt32BE(3))
|
|
50
|
+
for (let i = 16; i < 30; i++) {
|
|
51
|
+
let value = SCHEDULE[i - 16]
|
|
52
|
+
if (i - 2 >= 14) value += (sigma1(words[i - 2]) ^ (i === 16 ? 0x000B92C0 : 0)) >>> 0
|
|
53
|
+
if (i - 7 >= 14) value += words[i - 7]
|
|
54
|
+
if (i - 15 >= 14) value += (sigma0(words[i - 15]) ^ (i === 29 ? 0x059C0B80 : 0)) >>> 0
|
|
55
|
+
words.push(value >>> 0)
|
|
56
|
+
}
|
|
57
|
+
words.push((sigma1(words[28]) + words[23] + sigma0(words[15]) + words[14] + 0xC9DC06A7 - K[14]) >>> 0)
|
|
58
|
+
expand(words, 31)
|
|
59
|
+
let state = rounds(STATE, words, 14).map((x, i) => (x + INITIAL[i]) >>> 0)
|
|
60
|
+
const length = 57 + data.length, padding = ((55 - length) % 64 + 64) % 64
|
|
61
|
+
const tail = Buffer.concat([data.subarray(7), Buffer.from([0x80]), Buffer.alloc(padding + 8)])
|
|
62
|
+
tail.writeBigUInt64BE(BigInt(length) * 8n, tail.length - 8)
|
|
63
|
+
for (let offset = 0; offset < tail.length; offset += 64) {
|
|
64
|
+
const block = Array.from({ length: 16 }, (_, i) => tail.readUInt32BE(offset + 4 * i))
|
|
65
|
+
expand(block)
|
|
66
|
+
state = rounds(state, block).map((x, i) => (x + state[i]) >>> 0)
|
|
67
|
+
}
|
|
68
|
+
const out = Buffer.alloc(32); state.forEach((x, i) => out.writeUInt32BE(x, 4 * i)); return out
|
|
69
|
+
}
|
|
70
|
+
function cs64(data: Buffer, digest: Buffer): Buffer {
|
|
71
|
+
const keyA = (digest.readUInt32LE(0) | 1) >>> 0, keyB = (digest.readUInt32LE(4) | 1) >>> 0
|
|
72
|
+
const factors = [mul(keyA, 0x82893933), mul(keyB, 0x82893933)]
|
|
73
|
+
const terms = COEFFICIENTS.map(([coefficient], i) => mul(coefficient, factors[i % 2])), offsets = COEFFICIENTS.map(([, offset]) => offset)
|
|
74
|
+
const padded = Buffer.alloc(Math.ceil((data.length + 1) / 8) * 8); data.copy(padded, 1)
|
|
75
|
+
for (let i = 0; i < padded.length / 4; i++) {
|
|
76
|
+
const word = padded.readUInt32LE(4 * i)
|
|
77
|
+
let term = mul(word, i % 2 === 0 ? keyA : keyB), offset = mul(rol16(word), i % 2 === 0 ? 0xEF0569FB : 0xC31713DB)
|
|
78
|
+
if (i === 0) { term = (term + mul(0xF2F90B1A, factors[0])) >>> 0; offset = (offset + 0x0B1A0000) >>> 0 }
|
|
79
|
+
terms.push(term); offsets.push(offset)
|
|
80
|
+
}
|
|
81
|
+
let swapAcc = 0, swapLast = 0, state = 0, acc = 0
|
|
82
|
+
for (let i = 0; i < terms.length; i += 2) {
|
|
83
|
+
const x = terms[i], y = terms[i + 1]
|
|
84
|
+
let t = rol16((offsets[i] + x) >>> 0), q = mul(t, 0x76F50F34)
|
|
85
|
+
swapAcc += (mul(rol16(q), 0xC3EFEA97) + q) >>> 0
|
|
86
|
+
t = rol16((offsets[i + 1] + y) >>> 0); q = mul(t, 0x1881CE3C)
|
|
87
|
+
swapLast = (mul(rol16(q), 0x35BD1EC9) + q) >>> 0; swapAcc = (swapAcc + swapLast) >>> 0
|
|
88
|
+
state = (mul(state, keyA) + x) >>> 0
|
|
89
|
+
for (const factor of [0xCF98B111, 0x87085B9F, 0x12CEB96D, 0x257E1D83]) state = mul(rol16(state), factor)
|
|
90
|
+
acc += state
|
|
91
|
+
t = (mul(state, keyB) + y) >>> 0
|
|
92
|
+
for (const factor of [0xA27416F5, 0xD38396FF]) t = mul(rol16(t), factor)
|
|
93
|
+
state = mul(rol16(mul(rol16(t), 0x7C932B89)), 0xBFA49F69); acc = (acc + state) >>> 0
|
|
94
|
+
}
|
|
95
|
+
const out = Buffer.alloc(8); out.writeUInt32LE((swapLast ^ state) >>> 0); out.writeUInt32LE((swapAcc ^ acc) >>> 0, 4); return out
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
let lastData = '', lastToken = ''
|
|
99
|
+
export function edgeTokenV2(data: string): string {
|
|
100
|
+
if (data === lastData) return lastToken
|
|
101
|
+
const bytes = Buffer.from(data, 'ascii'), digest = tokenDigest(bytes)
|
|
102
|
+
lastToken = cs64(bytes, digest).toString('hex').toUpperCase() + '-' + digest.toString('hex').toUpperCase()
|
|
103
|
+
lastData = data
|
|
104
|
+
return lastToken
|
|
105
|
+
}
|