dsh-voice 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/README.en.md ADDED
@@ -0,0 +1,65 @@
1
+ [中文](README.md)
2
+
3
+ # dsh-voice
4
+
5
+ DSH (DeepSeek Harness) voice plugin pair: let the agent **speak and listen**.
6
+
7
+ - **voice_tts**: text-to-speech over the **edge-tts protocol** (Microsoft Edge read-aloud service — free, unlimited, 22+ curated voices)
8
+ - **voice_stt**: speech-to-text over any **OpenAI-compatible ASR endpoint** (Groq / OpenAI / custom)
9
+ - **voice_list**: voice catalog
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ dsh plugin --profile web add dsh-voice
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ `voice_tts` works with zero config; `voice_stt` needs an ASR key:
20
+
21
+ ```yaml
22
+ - id: voice
23
+ name: 'dsh-voice'
24
+ config:
25
+ asrEngine: groq # groq | openai | custom
26
+ asrModel: whisper-large-v3-turbo # Groq whisper model
27
+ # asrApiKey: gsk_... # prefer env var DSH_VOICE_ASR_KEY
28
+ ttsVoice: zh-CN-XiaoxiaoNeural # default voice
29
+ # proxyUrl: http://127.0.0.1:7890 # enable when the ASR endpoint needs a special proxy
30
+ ```
31
+
32
+ ## Tools
33
+
34
+ | Tool | Purpose | Key parameters |
35
+ | :-- | :-- | :-- |
36
+ | `voice_tts` | Synthesize MP3 from text (free) | `text` required; `voice`/`rate`/`pitch`/`output` optional |
37
+ | `voice_stt` | Transcribe audio to text | `audio` required; `engine`/`model`/`language`/`prompt`/`output` optional |
38
+ | `voice_list` | Curated voice catalog | none |
39
+
40
+ ### Examples
41
+
42
+ ```text
43
+ voice_tts { text: hello world } # outputs voice_output.mp3
44
+ voice_tts { text: hello, voice: en-US-AriaNeural } # English female voice
45
+ voice_stt { audio: E:\audio\meeting.mp3, language: zh } # transcribe a recording
46
+ voice_list {}
47
+ ```
48
+
49
+ ## Under the hood
50
+
51
+ - **Direct edge-tts protocol**: the Sec-MS-GEC token is **generated locally** with the official DRM algorithm (SHA256 of Windows file time + trusted client token, 5-minute windows); transport uses the `ws` library with permessage-deflate and an optional HTTP CONNECT proxy tunnel
52
+ - **Zero API cost**: TTS is completely free; STT costs only whatever your ASR provider charges
53
+ - Up-front validation: text ≤ 5000 chars, audio ≤ 25MB; same-name outputs auto-suffixed
54
+ - Protocol aligned with current open-source edge-tts (7.x) — no reliance on the outdated token endpoint
55
+
56
+ ## Development
57
+
58
+ ```bash
59
+ pnpm install
60
+ pnpm test # build + 31 tests, including a real-synthesis integration test
61
+ ```
62
+
63
+ ## License
64
+
65
+ MIT
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ [English](README.en.md)
2
+
3
+ # dsh-voice
4
+
5
+ DSH(DeepSeek Harness)语音双件套插件:让 agent **会说话、能听懂**。
6
+
7
+ - **voice_tts**:文字转语音,走 **edge-tts 协议**(微软 Edge 朗读服务,免费无限量,22+ 常用音色)
8
+ - **voice_stt**:语音转文字,走 **OpenAI 兼容 ASR 接口**(Groq / OpenAI / 自定义端点)
9
+ - **voice_list**:音色清单
10
+
11
+ ## 安装
12
+
13
+ ```bash
14
+ dsh plugin --profile web add dsh-voice
15
+ ```
16
+
17
+ ## 配置
18
+
19
+ `voice_tts` 零配置可用;`voice_stt` 需要 ASR 密钥:
20
+
21
+ ```yaml
22
+ - id: voice
23
+ name: 'dsh-voice'
24
+ config:
25
+ asrEngine: groq # groq | openai | custom
26
+ asrModel: whisper-large-v3-turbo # groq 的 whisper 模型
27
+ # asrApiKey: gsk_... # 推荐改用环境变量 DSH_VOICE_ASR_KEY
28
+ ttsVoice: zh-CN-XiaoxiaoNeural # 默认音色
29
+ # proxyUrl: http://127.0.0.1:7890 # ASR 接口需要特殊代理时启用
30
+ ```
31
+
32
+ ## 工具一览
33
+
34
+ | 工具 | 作用 | 关键参数 |
35
+ | :-- | :-- | :-- |
36
+ | `voice_tts` | 文字合成 MP3(免费) | `text` 必填;`voice`/`rate`/`pitch`/`output` 可选 |
37
+ | `voice_stt` | 音频转文字 | `audio` 必填;`engine`/`model`/`language`/`prompt`/`output` 可选 |
38
+ | `voice_list` | 常用音色清单 | 无 |
39
+
40
+ ### 示例
41
+
42
+ ```text
43
+ voice_tts { text: 今天的 AI 早报来了 } # 晓晓女声,输出 voice_output.mp3
44
+ voice_tts { text: hello, voice: en-US-AriaNeural } # 英文女声
45
+ voice_stt { audio: E:\audio\meeting.mp3, language: zh } # 转写会议录音
46
+ voice_list {}
47
+ ```
48
+
49
+ ## 硬核细节
50
+
51
+ - **edge-tts 协议直连**:Sec-MS-GEC 令牌按官方 DRM 算法**本地生成**(SHA256(Windows 文件时间 + TrustedClientToken),5 分钟窗口),WS 传输用 `ws` 库 + permessage-deflate 压缩 + 可选 HTTP CONNECT 代理隧道
52
+ - **零 API 成本**:TTS 完全免费;STT 只花你选的 ASR 接口的钱
53
+ - 文本 ≤5000 字符、音频 ≤25MB 前置校验;输出同名自动加序号
54
+ - 协议对齐开源 edge-tts 当前版本(7.x),不依赖过时的令牌端点
55
+
56
+ ## 开发
57
+
58
+ ```bash
59
+ pnpm install
60
+ pnpm test # 构建 + 31 个测试(含真实合成集成测试)
61
+ ```
62
+
63
+ ## License
64
+
65
+ MIT
@@ -0,0 +1,22 @@
1
+ # dsh-voice 组合包补丁:把语音双件套工具插件行插入 profile。
2
+ # 本补丁按 id 覆盖;用户应在自己 profile 的 cordis.patch.yml 里覆盖本行的整个
3
+ # config(见 README 的配置示例)。配置缺失时插件仍会加载:voice_tts 零配置可用,
4
+ # voice_stt 需要 ASR 接口(Groq / OpenAI 兼容)。
5
+ #
6
+ # 示例(用户 profile 的 cordis.patch.yml):
7
+ # - id: voice
8
+ # name: 'dsh-voice'
9
+ # config:
10
+ # asrEngine: groq # groq | openai | custom(voice_stt 用)
11
+ # # asrApiKey: gsk_... # 推荐改用环境变量 DSH_VOICE_ASR_KEY
12
+ # # asrBaseUrl: https://api.groq.com/openai/v1
13
+ # asrModel: whisper-large-v3-turbo
14
+ # # proxyUrl: http://127.0.0.1:7890 # 需要特殊代理访问 ASR/Bing 时启用
15
+ # ttsVoice: zh-CN-XiaoxiaoNeural
16
+ #
17
+ - insert:
18
+ - id: voice
19
+ name: 'dsh-voice'
20
+ config:
21
+ asrEngine: groq
22
+ asrModel: whisper-large-v3-turbo
@@ -0,0 +1,37 @@
1
+ /**
2
+ * dsh-voice 配置解析:TTS 默认音色与语速、ASR 引擎与密钥、代理。
3
+ *
4
+ * @module dsh-voice/config
5
+ */
6
+ /** 插件行配置(cordis.patch.yml 里的 config 段,可缺省)。 */
7
+ export interface VoiceConfig {
8
+ ttsVoice?: string;
9
+ ttsRate?: string;
10
+ ttsPitch?: string;
11
+ asrEngine?: 'groq' | 'openai' | 'custom';
12
+ asrBaseUrl?: string;
13
+ asrApiKey?: string;
14
+ asrModel?: string;
15
+ proxyUrl?: string;
16
+ timeoutMs?: number;
17
+ overwrite?: boolean;
18
+ }
19
+ /** 解析后的配置。 */
20
+ export interface ResolvedVoiceConfig {
21
+ ttsVoice: string;
22
+ ttsRate: string;
23
+ ttsPitch: string;
24
+ asrEngine: 'groq' | 'openai' | 'custom';
25
+ asrBaseUrl: string;
26
+ asrApiKey: string;
27
+ asrModel: string;
28
+ proxyUrl: string;
29
+ timeoutMs: number;
30
+ overwrite: boolean;
31
+ }
32
+ /** ASR 密钥:配置优先,其次环境变量 DSH_VOICE_ASR_KEY。 */
33
+ export declare function resolveAsrApiKey(config: VoiceConfig | undefined, env?: NodeJS.ProcessEnv): string;
34
+ /**
35
+ * 解析并校验配置。
36
+ */
37
+ export declare function resolveConfig(config: VoiceConfig | undefined | null): ResolvedVoiceConfig;
package/lib/config.js ADDED
@@ -0,0 +1,41 @@
1
+ /**
2
+ * dsh-voice 配置解析:TTS 默认音色与语速、ASR 引擎与密钥、代理。
3
+ *
4
+ * @module dsh-voice/config
5
+ */
6
+ const ENGINE_PRESETS = {
7
+ groq: { baseUrl: 'https://api.groq.com/openai/v1', model: 'whisper-large-v3-turbo' },
8
+ openai: { baseUrl: 'https://api.openai.com/v1', model: 'whisper-1' },
9
+ };
10
+ /** ASR 密钥:配置优先,其次环境变量 DSH_VOICE_ASR_KEY。 */
11
+ export function resolveAsrApiKey(config, env = process.env) {
12
+ const cfg = config ?? {};
13
+ if (typeof cfg.asrApiKey === 'string' && cfg.asrApiKey.trim() !== '')
14
+ return cfg.asrApiKey.trim();
15
+ return env.DSH_VOICE_ASR_KEY?.trim() ?? '';
16
+ }
17
+ /**
18
+ * 解析并校验配置。
19
+ */
20
+ export function resolveConfig(config) {
21
+ const cfg = config ?? {};
22
+ const ttsVoice = typeof cfg.ttsVoice === 'string' && cfg.ttsVoice.trim() !== '' ? cfg.ttsVoice.trim() : 'zh-CN-XiaoxiaoNeural';
23
+ const ttsRate = typeof cfg.ttsRate === 'string' && cfg.ttsRate.trim() !== '' ? cfg.ttsRate.trim() : '+0%';
24
+ const ttsPitch = typeof cfg.ttsPitch === 'string' && cfg.ttsPitch.trim() !== '' ? cfg.ttsPitch.trim() : '+0Hz';
25
+ const asrEngine = cfg.asrEngine === 'openai' ? 'openai' : cfg.asrEngine === 'custom' ? 'custom' : 'groq';
26
+ const preset = asrEngine === 'custom' ? undefined : ENGINE_PRESETS[asrEngine];
27
+ const asrBaseUrl = (typeof cfg.asrBaseUrl === 'string' && cfg.asrBaseUrl.trim() !== '' ? cfg.asrBaseUrl.trim() : preset?.baseUrl ?? '').replace(/\/$/, '');
28
+ const asrModel = typeof cfg.asrModel === 'string' && cfg.asrModel.trim() !== '' ? cfg.asrModel.trim() : preset?.model ?? '';
29
+ const asrApiKey = resolveAsrApiKey(cfg);
30
+ const proxyUrl = (typeof cfg.proxyUrl === 'string' && cfg.proxyUrl.trim() !== '' ? cfg.proxyUrl.trim() : '');
31
+ if (proxyUrl !== '' && !/^https?:\/\//i.test(proxyUrl))
32
+ throw new Error('proxyUrl 必须是 http(s):// 开头的地址,例如 http://127.0.0.1:7890。');
33
+ let timeoutMs = 60000;
34
+ if (cfg.timeoutMs !== undefined) {
35
+ if (typeof cfg.timeoutMs !== 'number' || !Number.isFinite(cfg.timeoutMs) || cfg.timeoutMs <= 0)
36
+ throw new Error('timeoutMs 必须是大于 0 的数字(毫秒)。');
37
+ timeoutMs = Math.min(10 * 60 * 1000, Math.max(5000, Math.round(cfg.timeoutMs)));
38
+ }
39
+ const overwrite = cfg.overwrite === true;
40
+ return { ttsVoice, ttsRate, ttsPitch, asrEngine, asrBaseUrl, asrApiKey, asrModel, proxyUrl, timeoutMs, overwrite };
41
+ }
@@ -0,0 +1,36 @@
1
+ /** 合成选项。 */
2
+ export interface TtsOptions {
3
+ text: string;
4
+ voice: string;
5
+ rate: string;
6
+ pitch: string;
7
+ }
8
+ /** 可注入依赖(测试用假实现)。 */
9
+ export interface TtsDeps {
10
+ webSocketFactory?: WebSocketFactory;
11
+ nowSeconds?: () => number;
12
+ proxyUrl?: string;
13
+ }
14
+ /** 最小的 WebSocket 面。 */
15
+ export interface WebSocketLike {
16
+ addEventListener(type: 'message', listener: (event: {
17
+ data: unknown;
18
+ }) => void): void;
19
+ addEventListener(type: 'open' | 'close' | 'error', listener: (event?: unknown) => void): void;
20
+ send(data: string): void;
21
+ close(): void;
22
+ }
23
+ export type WebSocketFactory = (url: string, options: {
24
+ headers: Record<string, string>;
25
+ }) => WebSocketLike;
26
+ /**
27
+ * 本地生成 Sec-MS-GEC 令牌(对齐 edge-tts DRM 算法)。
28
+ */
29
+ export declare function generateSecMsGec(nowSeconds?: number): string;
30
+ /** 生成 SSML。 */
31
+ export declare function buildSsml(options: TtsOptions): string;
32
+ /**
33
+ * 合成语音,返回 MP3 字节。
34
+ * @throws 文本为空/超长 / 连接失败 / 超时 / 无音频数据时抛中文错误。
35
+ */
36
+ export declare function synthesizeSpeech(options: TtsOptions, deps?: TtsDeps, timeoutMs?: number): Promise<Buffer>;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * edge-tts 协议客户端:原生 WebSocket 直连微软 Edge 朗读服务,零 API 成本。
3
+ * 协议对齐开源 edge-tts 当前版本:Sec-MS-GEC 令牌由本地 DRM 算法生成
4
+ * (SHA256(Windows 文件时间 + TrustedClientToken),5 分钟窗口),不再请求令牌端点。
5
+ *
6
+ * @module dsh-voice/edge-tts
7
+ */
8
+ import { createHash, randomBytes, randomUUID } from 'node:crypto';
9
+ import { HttpsProxyAgent } from 'https-proxy-agent';
10
+ import WebSocket from 'ws';
11
+ const TRUSTED_CLIENT_TOKEN = '6A5AA1D4EAFF4E9FB37E23D68491D6F4';
12
+ const WS_BASE = 'wss://speech.platform.bing.com/consumer/speech/synthesize/readaloud/edge/v1';
13
+ const OUTPUT_FORMAT = 'audio-24khz-48kbitrate-mono-mp3';
14
+ const SEC_MS_GEC_VERSION = '1-143.0.3650.75';
15
+ const WIN_EPOCH_SECONDS = 11644473600;
16
+ const WSS_HEADERS = {
17
+ Pragma: 'no-cache',
18
+ 'Cache-Control': 'no-cache',
19
+ Origin: 'chrome-extension://jdiccldimpdaibmpdkjnbmckianbfold',
20
+ 'Accept-Encoding': 'gzip, deflate, br, zstd',
21
+ 'Accept-Language': 'en-US,en;q=0.9',
22
+ 'Sec-WebSocket-Version': '13',
23
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0',
24
+ };
25
+ /** 直连工厂(ws 库,Node 全版本可用)。 */
26
+ function directWebSocket(url, options) {
27
+ return new WebSocket(url, { headers: options.headers });
28
+ }
29
+ /** 代理工厂:经 HTTP CONNECT 隧道走本地代理(大陆直连会被重置时使用)。 */
30
+ function proxyWebSocket(proxyUrl) {
31
+ const agent = new HttpsProxyAgent(proxyUrl);
32
+ return (url, options) => new WebSocket(url, { headers: options.headers, agent });
33
+ }
34
+ /**
35
+ * 本地生成 Sec-MS-GEC 令牌(对齐 edge-tts DRM 算法)。
36
+ */
37
+ export function generateSecMsGec(nowSeconds = Date.now() / 1000) {
38
+ let ticks = Math.floor(nowSeconds) + WIN_EPOCH_SECONDS;
39
+ ticks -= ticks % 300;
40
+ const windowsTicks = ticks * 10000000;
41
+ const raw = String(windowsTicks) + TRUSTED_CLIENT_TOKEN;
42
+ return createHash('sha256').update(raw, 'ascii').digest('hex').toUpperCase();
43
+ }
44
+ /** XML 转义。 */
45
+ function escapeXml(text) {
46
+ return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
47
+ }
48
+ /** 生成 SSML。 */
49
+ export function buildSsml(options) {
50
+ const lang = /^[a-z]{2,3}(-[A-Z]{2})?/.exec(options.voice)?.[0] ?? 'zh-CN';
51
+ return "<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xml:lang='" + lang + "'><voice name='" + options.voice + "'><prosody pitch='" + options.pitch + "' rate='" + options.rate + "' volume='+0%'>" + escapeXml(options.text) + '</prosody></voice></speak>';
52
+ }
53
+ /** 生成带时间戳与路径的协议消息头。 */
54
+ function protocolHeader(path, extra) {
55
+ const lines = ['X-Timestamp:' + new Date().toISOString()];
56
+ for (const [key, value] of Object.entries(extra))
57
+ lines.push(key + ':' + value);
58
+ lines.push('Path:' + path);
59
+ return lines.join('\r\n') + '\r\n\r\n';
60
+ }
61
+ /**
62
+ * 合成语音,返回 MP3 字节。
63
+ * @throws 文本为空/超长 / 连接失败 / 超时 / 无音频数据时抛中文错误。
64
+ */
65
+ export async function synthesizeSpeech(options, deps = {}, timeoutMs = 30000) {
66
+ if (options.text.trim() === '')
67
+ throw new Error('要合成的文本为空。');
68
+ if (options.text.length > 5000)
69
+ throw new Error('文本过长(超过 5000 字符),请分段合成。');
70
+ const factory = deps.webSocketFactory ?? (deps.proxyUrl !== undefined && deps.proxyUrl !== '' ? proxyWebSocket(deps.proxyUrl) : directWebSocket);
71
+ const token = generateSecMsGec(deps.nowSeconds?.() ?? Date.now() / 1000);
72
+ // 对齐 edge-tts 当前协议:Sec-MS-GEC 走 URL 查询参数,MUID 走 header
73
+ const url = WS_BASE + '?TrustedClientToken=' + TRUSTED_CLIENT_TOKEN + '&ConnectionId=' + randomUUID().replace(/-/g, '') + '&Sec-MS-GEC=' + token + '&Sec-MS-GEC-Version=' + SEC_MS_GEC_VERSION;
74
+ const socket = factory(url, {
75
+ headers: { ...WSS_HEADERS, MUID: randomBytes(16).toString('hex').toUpperCase() },
76
+ });
77
+ const chunks = [];
78
+ let done = false;
79
+ await new Promise((resolvePromise, reject) => {
80
+ const timer = setTimeout(() => {
81
+ try {
82
+ socket.close();
83
+ }
84
+ catch { /* 忽略 */ }
85
+ reject(new Error('语音合成超时(' + timeoutMs + ' 毫秒无完整音频),请重试或检查网络。'));
86
+ }, timeoutMs);
87
+ const finish = () => { clearTimeout(timer); resolvePromise(); };
88
+ socket.addEventListener('open', () => {
89
+ const config = protocolHeader('speech.config', { 'Content-Type': 'application/json; charset=utf-8' })
90
+ + JSON.stringify({
91
+ context: {
92
+ synthesis: {
93
+ audio: { metadataoptions: { sentenceBoundaryEnabled: 'false', wordBoundaryEnabled: 'false' }, outputFormat: OUTPUT_FORMAT },
94
+ },
95
+ },
96
+ });
97
+ const ssml = protocolHeader('ssml', { 'Content-Type': 'application/ssml+xml', 'X-RequestId': randomUUID() }) + buildSsml(options);
98
+ try {
99
+ socket.send(config);
100
+ socket.send(ssml);
101
+ }
102
+ catch (error) {
103
+ clearTimeout(timer);
104
+ reject(error instanceof Error ? error : new Error(String(error)));
105
+ }
106
+ });
107
+ socket.addEventListener('message', (event) => {
108
+ const data = event.data;
109
+ if (typeof data === 'string') {
110
+ if (data.includes('Path:turn.end')) {
111
+ if (chunks.length === 0) {
112
+ clearTimeout(timer);
113
+ reject(new Error('合成结束但没有收到音频数据(服务端可能拒绝了请求)。'));
114
+ }
115
+ else if (!done) {
116
+ done = true;
117
+ finish();
118
+ }
119
+ }
120
+ return;
121
+ }
122
+ // 二进制帧:2 字节大端头长 + 头文本 + 音频数据
123
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
124
+ if (buffer.length > 2) {
125
+ const headerLength = buffer.readUInt16BE(0);
126
+ if (buffer.length > 2 + headerLength) {
127
+ chunks.push(buffer.subarray(2 + headerLength));
128
+ }
129
+ }
130
+ });
131
+ socket.addEventListener('error', () => {
132
+ clearTimeout(timer);
133
+ reject(new Error('edge-tts WebSocket 连接失败。若网络需要特殊代理(梯子),请在 cordis.patch.yml 配置 proxyUrl 后重启。'));
134
+ });
135
+ socket.addEventListener('close', () => {
136
+ if (!done) {
137
+ clearTimeout(timer);
138
+ if (chunks.length > 0) {
139
+ done = true;
140
+ finish();
141
+ }
142
+ else {
143
+ reject(new Error('edge-tts 连接在收到音频前关闭。'));
144
+ }
145
+ }
146
+ });
147
+ });
148
+ const audio = Buffer.concat(chunks);
149
+ if (audio.length === 0)
150
+ throw new Error('合成结果为空。');
151
+ return audio;
152
+ }
package/lib/index.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ /**
2
+ * dsh-voice —— 语音双件套工具插件(node 半身,配置走 cordis.patch.yml)。
3
+ *
4
+ * 插件导出 apply(ctx, config):注册三个面向模型的工具(voice_tts / voice_stt /
5
+ * voice_list)。TTS 走 edge-tts 协议(原生 WebSocket,零 API 成本);STT 走 OpenAI
6
+ * 兼容 ASR 接口(Groq/OpenAI/自定义端点)。
7
+ *
8
+ * @module dsh-voice
9
+ */
10
+ import { type VoiceConfig } from './config.js';
11
+ import { type VoiceToolDefinition } from './tools.js';
12
+ /** cordis 服务注入:apply 里要用 ctx.tools,必须显式声明。 */
13
+ export declare const inject: string[];
14
+ /** 插件所需的最小 ctx 面。 */
15
+ export interface VoicePluginContext {
16
+ tools: {
17
+ register(definition: VoiceToolDefinition): () => void;
18
+ };
19
+ on?(event: string, listener: () => void): () => void;
20
+ }
21
+ /**
22
+ * 插件入口:解析配置并注册三个语音工具。
23
+ */
24
+ export declare function apply(ctx: VoicePluginContext, config?: VoiceConfig | null): void;
25
+ export * from './config.js';
26
+ export * from './edge-tts.js';
27
+ export * from './paths.js';
28
+ export * from './proxy-fetch.js';
29
+ export * from './stt.js';
30
+ export * from './tools.js';
31
+ export * from './voices.js';
package/lib/index.js ADDED
@@ -0,0 +1,43 @@
1
+ /**
2
+ * dsh-voice —— 语音双件套工具插件(node 半身,配置走 cordis.patch.yml)。
3
+ *
4
+ * 插件导出 apply(ctx, config):注册三个面向模型的工具(voice_tts / voice_stt /
5
+ * voice_list)。TTS 走 edge-tts 协议(原生 WebSocket,零 API 成本);STT 走 OpenAI
6
+ * 兼容 ASR 接口(Groq/OpenAI/自定义端点)。
7
+ *
8
+ * @module dsh-voice
9
+ */
10
+ import { resolveConfig } from './config.js';
11
+ import { buildVoiceTools } from './tools.js';
12
+ /** cordis 服务注入:apply 里要用 ctx.tools,必须显式声明。 */
13
+ export const inject = ['tools'];
14
+ /**
15
+ * 插件入口:解析配置并注册三个语音工具。
16
+ */
17
+ export function apply(ctx, config) {
18
+ let cfg;
19
+ try {
20
+ cfg = resolveConfig(config);
21
+ }
22
+ catch (error) {
23
+ console.warn('[dsh-voice] ' + (error instanceof Error ? error.message : String(error)));
24
+ cfg = resolveConfig(null);
25
+ }
26
+ const disposers = [];
27
+ for (const definition of buildVoiceTools(cfg)) {
28
+ disposers.push(ctx.tools.register(definition));
29
+ }
30
+ if (typeof ctx.on === 'function') {
31
+ ctx.on('dispose', () => {
32
+ for (const dispose of disposers)
33
+ dispose();
34
+ });
35
+ }
36
+ }
37
+ export * from './config.js';
38
+ export * from './edge-tts.js';
39
+ export * from './paths.js';
40
+ export * from './proxy-fetch.js';
41
+ export * from './stt.js';
42
+ export * from './tools.js';
43
+ export * from './voices.js';
package/lib/paths.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** 校验音频文件存在且是文件;返回绝对路径。 */
2
+ export declare function assertAudioFile(input: string): string;
3
+ /** 决定输出路径:缺省放在当前工作目录,同名自动加 _1/_2 序号。 */
4
+ export declare function resolveOutputPath(explicit: string | undefined, defaultName: string, overwrite: boolean): string;
package/lib/paths.js ADDED
@@ -0,0 +1,30 @@
1
+ /**
2
+ * 输出路径与音频文件校验。
3
+ *
4
+ * @module dsh-voice/paths
5
+ */
6
+ import { existsSync, statSync } from 'node:fs';
7
+ import { basename, dirname, extname, join, resolve } from 'node:path';
8
+ /** 校验音频文件存在且是文件;返回绝对路径。 */
9
+ export function assertAudioFile(input) {
10
+ const absolute = resolve(input);
11
+ if (!existsSync(absolute) || !statSync(absolute).isFile()) {
12
+ throw new Error('音频文件不存在:' + input);
13
+ }
14
+ return absolute;
15
+ }
16
+ /** 决定输出路径:缺省放在当前工作目录,同名自动加 _1/_2 序号。 */
17
+ export function resolveOutputPath(explicit, defaultName, overwrite) {
18
+ let target = explicit !== undefined && explicit.trim() !== '' ? resolve(explicit.trim()) : resolve(process.cwd(), defaultName);
19
+ if (overwrite || !existsSync(target))
20
+ return target;
21
+ const directory = dirname(target);
22
+ const base = basename(target, extname(target));
23
+ const extension = extname(target);
24
+ for (let index = 1; index < 1000; index++) {
25
+ const candidate = join(directory, base + '_' + index + extension);
26
+ if (!existsSync(candidate))
27
+ return candidate;
28
+ }
29
+ throw new Error('找不到可用的输出文件名(同名文件超过 999 个),请显式指定 output。');
30
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 构造一个把所有请求路由到 proxyUrl 的 fetch。
3
+ */
4
+ export declare function createProxyFetch(proxyUrl: string): typeof globalThis.fetch;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 插件级 HTTP 代理:给 TTS 令牌与 ASR 请求一个走指定代理的 fetch,不影响同进程其他插件。
3
+ */
4
+ import { ProxyAgent, fetch as undiciFetch } from 'undici';
5
+ /**
6
+ * 构造一个把所有请求路由到 proxyUrl 的 fetch。
7
+ */
8
+ export function createProxyFetch(proxyUrl) {
9
+ const agent = new ProxyAgent(proxyUrl);
10
+ return ((input, init) => undiciFetch(input, { ...init, dispatcher: agent }));
11
+ }
package/lib/stt.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ /** 转写选项。 */
2
+ export interface SttOptions {
3
+ audio: Buffer;
4
+ filename: string;
5
+ model: string;
6
+ language?: string;
7
+ prompt?: string;
8
+ }
9
+ /**
10
+ * 调用 OpenAI 兼容 ASR 接口转写音频。
11
+ * @throws 缺密钥 / 文件过大 / HTTP 错误 / 无文本时抛中文错误。
12
+ */
13
+ export declare function transcribe(baseUrl: string, apiKey: string, options: SttOptions, fetchImpl?: typeof fetch, timeoutMs?: number): Promise<{
14
+ text: string;
15
+ model: string;
16
+ }>;
package/lib/stt.js ADDED
@@ -0,0 +1,63 @@
1
+ /**
2
+ * ASR 客户端:OpenAI 兼容 /audio/transcriptions multipart 上传。
3
+ *
4
+ * @module dsh-voice/stt
5
+ */
6
+ import { extname } from 'node:path';
7
+ const MAX_AUDIO_BYTES = 25 * 1024 * 1024;
8
+ /**
9
+ * 调用 OpenAI 兼容 ASR 接口转写音频。
10
+ * @throws 缺密钥 / 文件过大 / HTTP 错误 / 无文本时抛中文错误。
11
+ */
12
+ export async function transcribe(baseUrl, apiKey, options, fetchImpl = globalThis.fetch, timeoutMs = 120000) {
13
+ if (apiKey === '')
14
+ throw new Error('未配置 ASR 密钥:请设置环境变量 DSH_VOICE_ASR_KEY,或在 cordis.patch.yml 的 asrApiKey 配置后重启。');
15
+ if (baseUrl === '')
16
+ throw new Error('未配置 ASR 接口地址(asrEngine=custom 时必须提供 asrBaseUrl)。');
17
+ if (options.audio.length === 0)
18
+ throw new Error('音频文件为空。');
19
+ if (options.audio.length > MAX_AUDIO_BYTES)
20
+ throw new Error('音频超过 25MB 上限(ASR 接口限制),请先用 ffmpeg 压缩。');
21
+ const form = new FormData();
22
+ const mime = mimeOf(options.filename);
23
+ form.append('file', new Blob([options.audio], { type: mime }), options.filename);
24
+ form.append('model', options.model);
25
+ if (options.language !== undefined && options.language !== '')
26
+ form.append('language', options.language);
27
+ if (options.prompt !== undefined && options.prompt !== '')
28
+ form.append('prompt', options.prompt);
29
+ let response;
30
+ try {
31
+ response = await fetchImpl(baseUrl + '/audio/transcriptions', {
32
+ method: 'POST',
33
+ headers: { authorization: 'Bearer ' + apiKey },
34
+ body: form,
35
+ signal: AbortSignal.timeout(timeoutMs),
36
+ });
37
+ }
38
+ catch (error) {
39
+ throw new Error('ASR 请求失败:' + (error instanceof Error ? error.message : String(error)) + '。若接口需要特殊代理(梯子),请在 cordis.patch.yml 配置 proxyUrl 后重启。');
40
+ }
41
+ if (!response.ok) {
42
+ const body = (await response.text()).slice(0, 300);
43
+ throw new Error('ASR 失败:HTTP ' + response.status + '。' + body);
44
+ }
45
+ let json;
46
+ try {
47
+ json = await response.json();
48
+ }
49
+ catch {
50
+ throw new Error('ASR 响应不是合法 JSON。');
51
+ }
52
+ const text = typeof json === 'object' && json !== null && typeof json.text === 'string' ? json.text : '';
53
+ return { text, model: options.model };
54
+ }
55
+ /** 常见音频扩展名 → MIME。 */
56
+ function mimeOf(filename) {
57
+ const ext = extname(filename).toLowerCase();
58
+ const table = {
59
+ '.mp3': 'audio/mpeg', '.wav': 'audio/wav', '.m4a': 'audio/mp4', '.mp4': 'audio/mp4',
60
+ '.ogg': 'audio/ogg', '.opus': 'audio/ogg', '.flac': 'audio/flac', '.webm': 'audio/webm',
61
+ };
62
+ return table[ext] ?? 'application/octet-stream';
63
+ }
package/lib/tools.d.ts ADDED
@@ -0,0 +1,33 @@
1
+ import { type ResolvedVoiceConfig } from './config.js';
2
+ import { synthesizeSpeech } from './edge-tts.js';
3
+ import { transcribe } from './stt.js';
4
+ /** 模型可见的内容块。 */
5
+ export interface ContentBlock {
6
+ type: 'text';
7
+ text: string;
8
+ }
9
+ /** 注册给 ctx.tools.register 的原始工具定义。 */
10
+ export interface VoiceToolDefinition {
11
+ name: string;
12
+ description: string;
13
+ parameters: {
14
+ type: 'object';
15
+ properties: Record<string, unknown>;
16
+ required?: string[];
17
+ };
18
+ output: {
19
+ schema: Record<string, unknown>;
20
+ render(args: unknown, value: unknown): ContentBlock[];
21
+ };
22
+ execute(args: unknown, exec: unknown): Promise<unknown>;
23
+ timeoutMs?: number;
24
+ }
25
+ /** 可注入依赖(测试用假实现)。 */
26
+ export interface VoiceToolDeps {
27
+ tts?: typeof synthesizeSpeech;
28
+ stt?: typeof transcribe;
29
+ }
30
+ /**
31
+ * 构建三个工具定义。
32
+ */
33
+ export declare function buildVoiceTools(config: ResolvedVoiceConfig, deps?: VoiceToolDeps): VoiceToolDefinition[];
package/lib/tools.js ADDED
@@ -0,0 +1,193 @@
1
+ /**
2
+ * 三个面向模型的语音工具:voice_tts / voice_stt / voice_list。
3
+ *
4
+ * @module dsh-voice/tools
5
+ */
6
+ import { readFileSync, writeFileSync } from 'node:fs';
7
+ import { basename } from 'node:path';
8
+ import { synthesizeSpeech } from './edge-tts.js';
9
+ import { assertAudioFile, resolveOutputPath } from './paths.js';
10
+ import { createProxyFetch } from './proxy-fetch.js';
11
+ import { transcribe } from './stt.js';
12
+ import { isValidVoiceId, VOICES } from './voices.js';
13
+ function compileParameters(spec) {
14
+ const properties = {};
15
+ const required = [];
16
+ for (const [key, prop] of Object.entries(spec)) {
17
+ if (prop?.required === true)
18
+ required.push(key);
19
+ const node = {};
20
+ if (typeof prop?.type === 'string')
21
+ node.type = prop.type;
22
+ if (typeof prop?.description === 'string')
23
+ node.description = prop.description;
24
+ properties[key] = node;
25
+ }
26
+ return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
27
+ }
28
+ function asRecord(value) {
29
+ return typeof value === 'object' && value !== null ? value : {};
30
+ }
31
+ function optionalString(args, key) {
32
+ const value = args[key];
33
+ return typeof value === 'string' && value.trim() !== '' ? value.trim() : undefined;
34
+ }
35
+ function requiredString(args, key, label) {
36
+ const value = optionalString(args, key);
37
+ if (value === undefined)
38
+ throw new Error(label + '(参数 ' + key + ')为必填,请提供非空字符串。');
39
+ return value;
40
+ }
41
+ const voiceItemSchema = {
42
+ type: 'object',
43
+ properties: { id: { type: 'string' }, gender: { type: 'string' }, locale: { type: 'string' } },
44
+ additionalProperties: true,
45
+ };
46
+ const listSchema = {
47
+ type: 'object',
48
+ properties: { count: { type: 'integer' }, voices: { type: 'array', items: voiceItemSchema } },
49
+ additionalProperties: true,
50
+ };
51
+ const ttsSchema = {
52
+ type: 'object',
53
+ properties: {
54
+ output: { type: 'string' },
55
+ bytes: { type: 'integer' },
56
+ voice: { type: 'string' },
57
+ rate: { type: 'string' },
58
+ pitch: { type: 'string' },
59
+ textLength: { type: 'integer' },
60
+ },
61
+ additionalProperties: true,
62
+ };
63
+ const sttSchema = {
64
+ type: 'object',
65
+ properties: {
66
+ text: { type: 'string' },
67
+ model: { type: 'string' },
68
+ language: { type: 'string' },
69
+ audio: { type: 'string' },
70
+ transcriptFile: { type: 'string' },
71
+ },
72
+ additionalProperties: true,
73
+ };
74
+ /**
75
+ * 构建三个工具定义。
76
+ */
77
+ export function buildVoiceTools(config, deps = {}) {
78
+ const cfg = config;
79
+ const fetchImpl = cfg.proxyUrl !== '' ? createProxyFetch(cfg.proxyUrl) : undefined;
80
+ const timeout = cfg.timeoutMs;
81
+ const voiceList = {
82
+ name: 'voice_list',
83
+ description: '列出常用语音合成音色(edge-tts 微软神经语音,免费)。含普通话/方言/粤语/英/日/韩/法/德/俄/西。voice_tts 的 voice 参数也接受清单外的合法 edge 音色 id(如 zh-CN-XXXNeural)。',
84
+ parameters: compileParameters({}),
85
+ output: {
86
+ schema: listSchema,
87
+ render: (_args, value) => {
88
+ const rec = asRecord(value);
89
+ const voices = Array.isArray(rec.voices) ? rec.voices : [];
90
+ const lines = ['共 ' + voices.length + ' 个常用音色:'];
91
+ for (const voice of voices) {
92
+ const v = asRecord(voice);
93
+ lines.push('- ' + v.id + '(' + v.locale + ')');
94
+ }
95
+ return [{ type: 'text', text: lines.join('\n') }];
96
+ },
97
+ },
98
+ async execute() {
99
+ return { count: VOICES.length, voices: VOICES };
100
+ },
101
+ };
102
+ const voiceTts = {
103
+ name: 'voice_tts',
104
+ description: '文字转语音:调用 edge-tts(微软 Edge 朗读服务,免费)合成 MP3。text 必填(≤5000 字符,更长请分段);voice 可选(默认 zh-CN-XiaoxiaoNeural,清单见 voice_list);rate/pitch 如 +10%、-2Hz;输出默认 voice_output.mp3(同名自动加序号)。',
105
+ parameters: compileParameters({
106
+ text: { type: 'string', required: true, description: '要合成的文本(必填,≤5000 字符)。' },
107
+ voice: { type: 'string', description: '音色 id(可选,默认配置的 ttsVoice)。' },
108
+ rate: { type: 'string', description: '语速,如 +20% 或 -10%(可选)。' },
109
+ pitch: { type: 'string', description: '音调,如 +2Hz 或 -1Hz(可选)。' },
110
+ output: { type: 'string', description: '输出 MP3 路径(可选)。' },
111
+ }),
112
+ output: {
113
+ schema: ttsSchema,
114
+ render: (_args, value) => {
115
+ const rec = asRecord(value);
116
+ return [{ type: 'text', text: '语音合成完成:' + rec.output + '(' + rec.bytes + ' 字节,音色 ' + rec.voice + ')' }];
117
+ },
118
+ },
119
+ async execute(rawArgs) {
120
+ const args = asRecord(rawArgs);
121
+ const text = requiredString(args, 'text', '要合成的文本');
122
+ if (text.length > 5000)
123
+ throw new Error('文本超过 5000 字符,请分段合成。');
124
+ const voice = optionalString(args, 'voice') ?? cfg.ttsVoice;
125
+ if (!isValidVoiceId(voice))
126
+ throw new Error('音色 id 不合法:' + voice + '。请用 voice_list 查看常用音色,或使用 zh-CN-XXXNeural 形式的 edge 音色。');
127
+ const rate = optionalString(args, 'rate') ?? cfg.ttsRate;
128
+ const pitch = optionalString(args, 'pitch') ?? cfg.ttsPitch;
129
+ const output = resolveOutputPath(optionalString(args, 'output'), 'voice_output.mp3', cfg.overwrite);
130
+ const audio = await (deps.tts ?? synthesizeSpeech)({ text, voice, rate, pitch }, { proxyUrl: cfg.proxyUrl }, timeout);
131
+ writeFileSync(output, audio);
132
+ return { output, bytes: audio.length, voice, rate, pitch, textLength: text.length };
133
+ },
134
+ timeoutMs: timeout + 10000,
135
+ };
136
+ const voiceStt = {
137
+ name: 'voice_stt',
138
+ description: '语音转文字:调用 OpenAI 兼容 ASR 接口(默认 Groq whisper-large-v3-turbo;可切 openai/custom)。audio 为音频文件路径(mp3/wav/m4a/ogg/flac,≤25MB);language/prompt 可选;output 可把转写文本写成 .txt。密钥用环境变量 DSH_VOICE_ASR_KEY 或配置 asrApiKey。',
139
+ parameters: compileParameters({
140
+ audio: { type: 'string', required: true, description: '音频文件路径(必填)。' },
141
+ engine: { type: 'string', description: '引擎:groq / openai / custom(可选,默认配置值)。' },
142
+ model: { type: 'string', description: '模型名(可选,覆盖配置)。' },
143
+ language: { type: 'string', description: '语言提示,如 zh(可选)。' },
144
+ prompt: { type: 'string', description: '提示词(专有名词/术语纠偏,可选)。' },
145
+ output: { type: 'string', description: '把转写文本写成 .txt 的路径(可选)。' },
146
+ }),
147
+ output: {
148
+ schema: sttSchema,
149
+ render: (_args, value) => {
150
+ const rec = asRecord(value);
151
+ return [{ type: 'text', text: '转写完成(模型 ' + rec.model + '):' + rec.text }];
152
+ },
153
+ },
154
+ async execute(rawArgs) {
155
+ const args = asRecord(rawArgs);
156
+ const audioPath = assertAudioFile(requiredString(args, 'audio', '音频文件'));
157
+ const engine = optionalString(args, 'engine') ?? cfg.asrEngine;
158
+ let baseUrl;
159
+ let model;
160
+ if (engine === 'openai') {
161
+ baseUrl = 'https://api.openai.com/v1';
162
+ model = optionalString(args, 'model') ?? 'whisper-1';
163
+ }
164
+ else if (engine === 'custom') {
165
+ baseUrl = cfg.asrBaseUrl;
166
+ model = optionalString(args, 'model') ?? cfg.asrModel;
167
+ }
168
+ else if (engine === 'groq') {
169
+ baseUrl = 'https://api.groq.com/openai/v1';
170
+ model = optionalString(args, 'model') ?? (cfg.asrModel !== '' && cfg.asrEngine === 'groq' ? cfg.asrModel : 'whisper-large-v3-turbo');
171
+ }
172
+ else
173
+ throw new Error('engine 必须是 groq / openai / custom 之一(当前:' + engine + ')。');
174
+ const { text } = await (deps.stt ?? transcribe)(baseUrl, cfg.asrApiKey, {
175
+ audio: readFileSync(audioPath),
176
+ filename: basename(audioPath),
177
+ model,
178
+ language: optionalString(args, 'language'),
179
+ prompt: optionalString(args, 'prompt'),
180
+ }, fetchImpl ?? globalThis.fetch, timeout);
181
+ let transcriptFile = null;
182
+ const output = optionalString(args, 'output');
183
+ if (output !== undefined) {
184
+ const target = resolveOutputPath(output, '', cfg.overwrite);
185
+ writeFileSync(target, text, 'utf8');
186
+ transcriptFile = target;
187
+ }
188
+ return { text, model, language: optionalString(args, 'language') ?? null, audio: audioPath, transcriptFile };
189
+ },
190
+ timeoutMs: timeout + 10000,
191
+ };
192
+ return [voiceList, voiceTts, voiceStt];
193
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 常用音色清单(edge-tts 全量约 500 个,这里收录常用中/英/日/韩/法/德/俄/西音色)。
3
+ *
4
+ * @module dsh-voice/voices
5
+ */
6
+ /** 音色信息。 */
7
+ export interface VoiceInfo {
8
+ id: string;
9
+ gender: 'Female' | 'Male';
10
+ locale: string;
11
+ }
12
+ /** 常用音色。 */
13
+ export declare const VOICES: VoiceInfo[];
14
+ /** 校验音色 id:常用清单内或符合 edge-tts 命名规则。 */
15
+ export declare function isValidVoiceId(id: string): boolean;
package/lib/voices.js ADDED
@@ -0,0 +1,36 @@
1
+ /**
2
+ * 常用音色清单(edge-tts 全量约 500 个,这里收录常用中/英/日/韩/法/德/俄/西音色)。
3
+ *
4
+ * @module dsh-voice/voices
5
+ */
6
+ /** 常用音色。 */
7
+ export const VOICES = [
8
+ { id: 'zh-CN-XiaoxiaoNeural', gender: 'Female', locale: '普通话(晓晓,温柔女声)' },
9
+ { id: 'zh-CN-XiaoyiNeural', gender: 'Female', locale: '普通话(晓伊,活泼女声)' },
10
+ { id: 'zh-CN-YunxiNeural', gender: 'Male', locale: '普通话(云希,青年男声)' },
11
+ { id: 'zh-CN-YunyangNeural', gender: 'Male', locale: '普通话(云扬,新闻男声)' },
12
+ { id: 'zh-CN-YunjianNeural', gender: 'Male', locale: '普通话(云健,浑厚男声)' },
13
+ { id: 'zh-CN-liaoning-XiaobeiNeural', gender: 'Female', locale: '东北话(晓北)' },
14
+ { id: 'zh-CN-shaanxi-XiaoniNeural', gender: 'Female', locale: '陕西话(晓妮)' },
15
+ { id: 'zh-TW-HsiaoChenNeural', gender: 'Female', locale: '台湾腔(晓臻)' },
16
+ { id: 'zh-HK-HiuMaanNeural', gender: 'Female', locale: '粤语(曉曼)' },
17
+ { id: 'en-US-AriaNeural', gender: 'Female', locale: '美式英语(Aria,女声)' },
18
+ { id: 'en-US-JennyNeural', gender: 'Female', locale: '美式英语(Jenny,女声)' },
19
+ { id: 'en-US-GuyNeural', gender: 'Male', locale: '美式英语(Guy,男声)' },
20
+ { id: 'en-US-DavisNeural', gender: 'Male', locale: '美式英语(Davis,男声)' },
21
+ { id: 'en-GB-SoniaNeural', gender: 'Female', locale: '英式英语(Sonia)' },
22
+ { id: 'en-GB-RyanNeural', gender: 'Male', locale: '英式英语(Ryan)' },
23
+ { id: 'ja-JP-NanamiNeural', gender: 'Female', locale: '日语(Nanami,女声)' },
24
+ { id: 'ja-JP-KeitaNeural', gender: 'Male', locale: '日语(Keita,男声)' },
25
+ { id: 'ko-KR-SunHiNeural', gender: 'Female', locale: '韩语(SunHi)' },
26
+ { id: 'fr-FR-DeniseNeural', gender: 'Female', locale: '法语(Denise)' },
27
+ { id: 'de-DE-KatjaNeural', gender: 'Female', locale: '德语(Katja)' },
28
+ { id: 'ru-RU-SvetlanaNeural', gender: 'Female', locale: '俄语(Svetlana)' },
29
+ { id: 'es-ES-ElviraNeural', gender: 'Female', locale: '西班牙语(Elvira)' },
30
+ ];
31
+ /** 校验音色 id:常用清单内或符合 edge-tts 命名规则。 */
32
+ export function isValidVoiceId(id) {
33
+ if (VOICES.some((voice) => voice.id === id))
34
+ return true;
35
+ return /^[a-z]{2,3}(-[A-Z]{2})?-[A-Z][A-Za-z]+Neural$/.test(id);
36
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "dsh-voice",
3
+ "version": "0.1.0",
4
+ "description": "DSH 语音双件套插件:voice_tts(edge-tts 协议零成本微软神经语音合成)/ voice_stt(OpenAI 兼容 ASR 语音转文字)/ voice_list(音色列表),原生 WebSocket + 插件级代理。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./cordis.patch.yml": "./cordis.patch.yml",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib",
18
+ "cordis.patch.yml",
19
+ "README.md",
20
+ "README.en.md"
21
+ ],
22
+ "scripts": {
23
+ "build": "tsc -p tsconfig.json",
24
+ "prepare": "tsc -p tsconfig.json",
25
+ "typecheck": "tsc -p tsconfig.json --noEmit",
26
+ "test": "pnpm run build && node --test \"test/*.test.mjs\"",
27
+ "prepublishOnly": "pnpm run build"
28
+ },
29
+ "dsh": {
30
+ "bundle": {
31
+ "patch": "./cordis.patch.yml"
32
+ }
33
+ },
34
+ "keywords": [
35
+ "dsh",
36
+ "deepseek-harness",
37
+ "plugin",
38
+ "tts",
39
+ "stt",
40
+ "whisper",
41
+ "voice"
42
+ ],
43
+ "license": "MIT",
44
+ "engines": {
45
+ "node": ">=22"
46
+ },
47
+ "dependencies": {
48
+ "https-proxy-agent": "^9.1.0",
49
+ "undici": "^8.10.0",
50
+ "ws": "^8.21.3"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^24.0.0",
54
+ "@types/ws": "^8.18.1",
55
+ "typescript": "^5.6.0"
56
+ },
57
+ "repository": {
58
+ "type": "git",
59
+ "url": "https://github.com/STARDUSTLC666/dsh-voice"
60
+ },
61
+ "bugs": {
62
+ "url": "https://github.com/STARDUSTLC666/dsh-voice/issues"
63
+ },
64
+ "homepage": "https://github.com/STARDUSTLC666/dsh-voice#readme",
65
+ "author": "stardustlc",
66
+ "packageManager": "pnpm@11.7.0"
67
+ }