@goodandready/dsh-tts 0.3.23 → 0.4.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.md +12 -12
- package/README.ru.md +13 -10
- package/README.zh.md +10 -10
- package/lib/cache.js +4 -5
- package/lib/client.js +69 -81
- package/lib/engines/f5.js +45 -33
- package/lib/engines/kokoro.js +21 -29
- package/lib/engines/types.js +26 -13
- package/lib/http-util.js +27 -0
- package/lib/index.js +41 -455
- package/lib/providers.js +29 -18
- package/lib/routes.js +400 -0
- package/lib/stream-hub.js +31 -0
- package/lib/text.js +19 -19
- package/package.json +5 -2
- package/scripts/f5_daemon.py +40 -0
package/README.md
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
**`dsh-tts`** provides robust, lifelike spoken voice synthesis for assistant replies in the **DeepSeek Harness** Web UI. When **Speak agent replies** is enabled, each finished assistant turn or real-time streaming chunk is synthesized on the host and streamed directly to the browser.
|
|
31
31
|
|
|
32
|
-
API keys never reach client browsers: synthesis is executed entirely on the host backend across **independent multi-provider fallback chains**, including
|
|
32
|
+
API keys never reach client browsers: synthesis is executed entirely on the host backend across **independent multi-provider fallback chains**, including local system engines (Edge TTS, Piper, eSpeak). Kokoro-82M and F5-TTS weights can be downloaded for future runtime support, but neural inference is **not bundled** in this package — those providers fail honestly and the chain continues.
|
|
33
33
|
|
|
34
34
|
```mermaid
|
|
35
35
|
graph LR
|
|
@@ -49,10 +49,10 @@ graph LR
|
|
|
49
49
|
|
|
50
50
|
subgraph Fallback [TTS Provider Fallback Chain]
|
|
51
51
|
LRU -->|Cache Miss| Chain{Active Chain}
|
|
52
|
-
Chain -->|
|
|
53
|
-
Chain -.->|Cloud Neural| P2[ElevenLabs /
|
|
54
|
-
Chain -.->|
|
|
55
|
-
Chain -.->|
|
|
52
|
+
Chain -->|Offline| P1[Edge TTS / Piper / eSpeak]
|
|
53
|
+
Chain -.->|Cloud Neural| P2[OpenAI / ElevenLabs / Google / Azure / Groq]
|
|
54
|
+
Chain -.->|OpenAI-compatible| P3[SiliconFlow / DeepInfra / Fireworks / OpenRouter]
|
|
55
|
+
Chain -.->|Other| P4[MiMo / MiniMax / Custom]
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
subgraph Output [Delivery & Integrations]
|
|
@@ -75,9 +75,9 @@ graph LR
|
|
|
75
75
|
|
|
76
76
|
## 🚀 Key Features
|
|
77
77
|
|
|
78
|
-
### 1. 📴 Offline
|
|
79
|
-
* **
|
|
80
|
-
* **F5-TTS
|
|
78
|
+
### 1. 📴 Offline system engines + optional future neural runtimes
|
|
79
|
+
* **Edge TTS / Piper / eSpeak**: fully offline or free local/system synthesis without cloud API keys (Edge needs the `edge-tts` CLI).
|
|
80
|
+
* **Kokoro-82M / F5-TTS**: weight download and status UI only. **Neural inference is not bundled** in this package — those providers fail with a clear reason and the fallback chain continues. Do not enable them expecting speech until a supported runtime is wired.
|
|
81
81
|
* **ModelManager UI**: Direct manual installation in settings with real-time download progress bar, SHA-256 validation, and deletion. No silent or automatic multi-gigabyte downloads.
|
|
82
82
|
|
|
83
83
|
### 2. ⚡ Real-Time Streaming Audio (< 300 ms Latency)
|
|
@@ -112,8 +112,8 @@ graph LR
|
|
|
112
112
|
|
|
113
113
|
| Provider Key | Service Backend | Default Model | Default Voice | Credential Ref | Features & Notes |
|
|
114
114
|
|---|---|---|---|---|---|
|
|
115
|
-
| `kokoro` | Local Kokoro-82M ONNX | `hexgrad/Kokoro-82M` | `af_bella` | *None* | **
|
|
116
|
-
| `f5` | Local F5-TTS GPU Daemon | `F5-TTS` | Default | *None* | **
|
|
115
|
+
| `kokoro` | Local Kokoro-82M ONNX | `hexgrad/Kokoro-82M` | `af_bella` | *None* | Weights downloadable; **ONNX inference not bundled** — fails honestly |
|
|
116
|
+
| `f5` | Local F5-TTS GPU Daemon | `F5-TTS` | Default | *None* | Daemon ping only; **GPU inference not bundled** — fails honestly |
|
|
117
117
|
| `elevenlabs` | ElevenLabs API | `eleven_multilingual_v2` | `Rachel` | `ELEVENLABS_API_KEY` | Ultra-realistic, emotional nuance |
|
|
118
118
|
| `openai` | OpenAI Audio | `gpt-4o-mini-tts` / `tts-1` | `alloy` | `OPENAI_API_KEY` | High-quality industry standard |
|
|
119
119
|
| `edge` | Microsoft Edge Online | `ru-RU-SvetlanaNeural` | `ru-RU-SvetlanaNeural` | *None* | **Free, high-fidelity neural TTS without API keys** |
|
|
@@ -170,8 +170,8 @@ dsh-tts:
|
|
|
170
170
|
cacheMaxMb: 150
|
|
171
171
|
autoDetect: true
|
|
172
172
|
chain:
|
|
173
|
-
- provider: kokoro
|
|
174
173
|
- provider: edge
|
|
174
|
+
- provider: espeak
|
|
175
175
|
voice: ru-RU-SvetlanaNeural
|
|
176
176
|
- provider: openai
|
|
177
177
|
model: tts-1
|
|
@@ -192,7 +192,7 @@ dsh-tts:
|
|
|
192
192
|
* `GET /dsh-tts/stream` — Real-time Server-Sent Events (SSE) audio streaming.
|
|
193
193
|
* `POST /dsh-tts/speak` — `{ text, voice?, model? }` → Returns synthesized audio.
|
|
194
194
|
* `POST /dsh-tts/preview` — `{ provider, model, voice, text? }` → Test voice playback in UI.
|
|
195
|
-
* `GET /dsh-tts/models/status` — Reports
|
|
195
|
+
* `GET /dsh-tts/models/status` — Reports Kokoro/F5 weight installation states (download only; inference not bundled).
|
|
196
196
|
* `POST /dsh-tts/models/install` — `{ engine: 'kokoro' | 'f5' }` → Starts HuggingFace model download.
|
|
197
197
|
* `DELETE /dsh-tts/models/delete` — `{ engine: 'kokoro' | 'f5' }` → Removes local model files.
|
|
198
198
|
* `GET /dsh-tts/integrations` — Status of sibling plugins (`dsh-voice`, `dsh-messenger-gateway`).
|
package/README.ru.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
-
<h3>Многопровайдерная озвучка ответов
|
|
5
|
+
<h3>Многопровайдерная озвучка ответов агента: облачные и системные offline-движки, потоковый звук, IT-словарь и интеграция с мессенджерами для DeepSeek Harness</h3>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<a href="https://www.npmjs.com/package/@goodandready/dsh-tts"><img src="https://img.shields.io/npm/v/@goodandready/dsh-tts.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
@@ -27,9 +27,12 @@
|
|
|
27
27
|
|
|
28
28
|
## ⚡ Обзор
|
|
29
29
|
|
|
30
|
+
> **Честно про локальные нейросети:** Kokoro/F5 — только статус и загрузка весов. Inference **не входит** в пакет; эти провайдеры падают с понятной причиной, цепочка идёт дальше. Рабочий offline: Edge TTS, Piper, eSpeak.
|
|
31
|
+
|
|
32
|
+
|
|
30
33
|
**`dsh-tts`** обеспечивает качественное голосовое озвучивание ответов ассистента в веб-интерфейсе **DeepSeek Harness**. При включённой опции **«Читать ответы агента»**, каждая готовая реплика или потоковый фрагмент синтезируется на хосте и мгновенно воспроизводится в браузере.
|
|
31
34
|
|
|
32
|
-
API-ключи никогда не передаются в браузер: синтез аудио выполняется на стороне хоста через **независимые цепочки отказоустойчивости (фолбеков)**, включая
|
|
35
|
+
API-ключи никогда не передаются в браузер: синтез аудио выполняется на стороне хоста через **независимые цепочки отказоустойчивости (фолбеков)**, включая системные offline-движки (Edge TTS, Piper, eSpeak). Kokoro/F5 — только загрузка весов; inference не входит в пакет.
|
|
33
36
|
|
|
34
37
|
```mermaid
|
|
35
38
|
graph LR
|
|
@@ -49,8 +52,8 @@ graph LR
|
|
|
49
52
|
|
|
50
53
|
subgraph Fallback [Цепочка фолбеков TTS]
|
|
51
54
|
LRU -->|Промах кэша| Chain{Активная цепочка}
|
|
52
|
-
Chain -->|
|
|
53
|
-
Chain
|
|
55
|
+
Chain -->|Offline| P1[Edge TTS / Piper / eSpeak]
|
|
56
|
+
Chain -.->|Cloud| P2[OpenAI / ElevenLabs / Google / Azure / Groq]
|
|
54
57
|
Chain -.->|Бесплатные облака| P3[EdgeTTS / SiliconFlow]
|
|
55
58
|
Chain -.->|Системный фолбек| P4[Локальный Piper / eSpeak NG]
|
|
56
59
|
end
|
|
@@ -75,9 +78,9 @@ graph LR
|
|
|
75
78
|
|
|
76
79
|
## 🚀 Ключевые возможности
|
|
77
80
|
|
|
78
|
-
### 1. 📴
|
|
79
|
-
* **
|
|
80
|
-
* **F5-TTS
|
|
81
|
+
### 1. 📴 Offline-системные движки и честный статус нейросетей
|
|
82
|
+
* **Edge TTS / Piper / eSpeak**: рабочий offline/системный синтез без облачных API-ключей.
|
|
83
|
+
* **Kokoro-82M / F5-TTS**: только UI и загрузка весов. **Inference не входит в пакет** — провайдер падает с понятной причиной, цепочка идёт дальше.
|
|
81
84
|
* **ModelManager в UI**: Ручная загрузка моделей прямо из настроек плагина с отображением процентов и прогресс-бара, проверкой SHA-256 и кнопкой удаления. Никаких скрытых или автозагрузок гигабайтных весов.
|
|
82
85
|
|
|
83
86
|
### 2. ⚡ Потоковое воспроизведение звука с задержкой < 300 мс
|
|
@@ -112,8 +115,8 @@ graph LR
|
|
|
112
115
|
|
|
113
116
|
| Ключ провайдера | Сервис | Модель по умолчанию | Голос по умолчанию | Имя ключа (credential) | Особенности |
|
|
114
117
|
|---|---|---|---|---|---|
|
|
115
|
-
| `kokoro` |
|
|
116
|
-
| `f5` |
|
|
118
|
+
| `kokoro` | Веса Kokoro-82M (ONNX) | `hexgrad/Kokoro-82M` | `af_bella` | *Не требуется* | Веса скачать можно; **inference не bundled** — честный fail |
|
|
119
|
+
| `f5` | Демон F5 (ping) | `F5-TTS` | По умолчанию | *Не требуется* | Только ping; **GPU inference не bundled** — честный fail |
|
|
117
120
|
| `elevenlabs` | ElevenLabs API | `eleven_multilingual_v2` | `Rachel` | `ELEVENLABS_API_KEY` | Реалистичный эмоциональный синтез |
|
|
118
121
|
| `openai` | OpenAI Audio | `gpt-4o-mini-tts` / `tts-1` | `alloy` | `OPENAI_API_KEY` | Эталонное качество индустрии |
|
|
119
122
|
| `edge` | Microsoft Edge Online | `ru-RU-SvetlanaNeural` | `ru-RU-SvetlanaNeural` | *Не требуется* | **Бесплатный нейросинтез без API-ключей** |
|
|
@@ -182,7 +185,7 @@ dsh-tts:
|
|
|
182
185
|
* `GET /dsh-tts/stream` — Потоковая передача аудио по SSE в реальном времени.
|
|
183
186
|
* `POST /dsh-tts/speak` — `{ text, voice?, model? }` → Возвращает аудиофайл для воспроизведения.
|
|
184
187
|
* `POST /dsh-tts/preview` — `{ provider, model, voice, text? }` → Тестовое прослушивание голоса в UI.
|
|
185
|
-
* `GET /dsh-tts/models/status` — Статус
|
|
188
|
+
* `GET /dsh-tts/models/status` — Статус загрузки весов Kokoro/F5 (inference не в комплекте).
|
|
186
189
|
* `POST /dsh-tts/models/install` — `{ engine: 'kokoro' | 'f5' }` → Запуск загрузки модели с HuggingFace.
|
|
187
190
|
* `DELETE /dsh-tts/models/delete` — `{ engine: 'kokoro' | 'f5' }` → Удаление локальной модели.
|
|
188
191
|
* `GET /dsh-tts/integrations` — Проверка статуса связанных плагинов (`dsh-voice`, `dsh-messenger-gateway`).
|
package/README.zh.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<div align="center">
|
|
4
4
|
|
|
5
|
-
<h3>DeepSeek Harness
|
|
5
|
+
<h3>DeepSeek Harness 多引擎语音合成:云端与系统离线引擎、流式音频、IT 术语词典与即时通讯集成</h3>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<a href="https://www.npmjs.com/package/@goodandready/dsh-tts"><img src="https://img.shields.io/npm/v/@goodandready/dsh-tts.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
**`dsh-tts`** 为 **DeepSeek Harness** Web 界面提供高保真智能体回复语音朗读服务。开启 **朗读智能体回复** 后,每条生成的助手回复或实时流式片段均由服务端合成并即时推流至浏览器播放。
|
|
31
31
|
|
|
32
|
-
API
|
|
32
|
+
API 密钥绝不暴露给前端:音频合成全程在服务端通过**多服务商独立备用链**执行,包括系统离线引擎(Edge TTS、Piper、eSpeak)。Kokoro/F5 仅可下载权重,**推理未捆绑**。
|
|
33
33
|
|
|
34
34
|
```mermaid
|
|
35
35
|
graph LR
|
|
@@ -49,8 +49,8 @@ graph LR
|
|
|
49
49
|
|
|
50
50
|
subgraph Fallback [TTS 引擎备用链]
|
|
51
51
|
LRU -->|未命中| Chain{生效备用链}
|
|
52
|
-
Chain
|
|
53
|
-
Chain
|
|
52
|
+
Chain -->|离线| P1[Edge TTS / Piper / eSpeak]
|
|
53
|
+
Chain -.->|云端| P2[OpenAI / ElevenLabs / Google / Azure / Groq]
|
|
54
54
|
Chain -.->|免费云端| P3[EdgeTTS / SiliconFlow]
|
|
55
55
|
Chain -.->|系统兜底| P4[本地 Piper / eSpeak NG]
|
|
56
56
|
end
|
|
@@ -75,9 +75,9 @@ graph LR
|
|
|
75
75
|
|
|
76
76
|
## 🚀 核心功能
|
|
77
77
|
|
|
78
|
-
### 1. 📴
|
|
79
|
-
* **
|
|
80
|
-
* **F5-TTS
|
|
78
|
+
### 1. 📴 离线系统引擎与诚实的神经引擎状态
|
|
79
|
+
* **Edge TTS / Piper / eSpeak**:可用的离线/系统合成,无需云 API Key。
|
|
80
|
+
* **Kokoro-82M / F5-TTS**:仅权重下载与状态展示。**推理未捆绑在本包中**,provider 会明确失败并继续 fallback。
|
|
81
81
|
* **ModelManager 管理界面**:在设置中手动安装模型,实时显示下载进度条、SHA-256 校验和一键删除。无任何静默或自动下载。
|
|
82
82
|
|
|
83
83
|
### 2. ⚡ 实时流式音频播放(延迟 < 300ms)
|
|
@@ -107,8 +107,8 @@ graph LR
|
|
|
107
107
|
|
|
108
108
|
| 服务商 Key | 对应引擎 | 默认模型 | 默认发音人 | 凭证变量名 | 说明与亮点 |
|
|
109
109
|
|---|---|---|---|---|---|
|
|
110
|
-
| `kokoro` |
|
|
111
|
-
| `f5` |
|
|
110
|
+
| `kokoro` | Kokoro 权重(ONNX) | `hexgrad/Kokoro-82M` | `af_bella` | *无需密钥* | 可下载权重;**推理未捆绑** — 明确失败 |
|
|
111
|
+
| `f5` | F5 守护进程(ping) | `F5-TTS` | 默认 | *无需密钥* | 仅 ping;**GPU 推理未捆绑** — 明确失败 |
|
|
112
112
|
| `elevenlabs` | ElevenLabs API | `eleven_multilingual_v2` | `Rachel` | `ELEVENLABS_API_KEY` | 极致拟人情感音色 |
|
|
113
113
|
| `openai` | OpenAI Audio | `gpt-4o-mini-tts` / `tts-1` | `alloy` | `OPENAI_API_KEY` | 经典高清发音 |
|
|
114
114
|
| `edge` | 微软 Edge 在线 | `zh-CN-XiaoxiaoNeural` | `zh-CN-XiaoxiaoNeural` | *无需密钥* | **免费免 Key 高保真神经网络语音** |
|
|
@@ -177,7 +177,7 @@ dsh-tts:
|
|
|
177
177
|
* `GET /dsh-tts/stream` — 实时 SSE 音频流推送。
|
|
178
178
|
* `POST /dsh-tts/speak` — `{ text, voice?, model? }` → 返回合成音频。
|
|
179
179
|
* `POST /dsh-tts/preview` — `{ provider, model, voice, text? }` → UI 中试听语音。
|
|
180
|
-
* `GET /dsh-tts/models/status` —
|
|
180
|
+
* `GET /dsh-tts/models/status` — 查询 Kokoro/F5 权重下载状态(推理未捆绑)。
|
|
181
181
|
* `POST /dsh-tts/models/install` — `{ engine: 'kokoro' | 'f5' }` → 启动模型下载。
|
|
182
182
|
* `DELETE /dsh-tts/models/delete` — `{ engine: 'kokoro' | 'f5' }` → 删除本地模型。
|
|
183
183
|
* `GET /dsh-tts/integrations` — 查询关联插件状态(`dsh-voice`、`dsh-messenger-gateway`)。
|
package/lib/cache.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// cacheMaxMb и может меняться на живую, поэтому приходит функцией.
|
|
1
|
+
// Disk speech cache: key is a hash of (text, provider, model, voice),
|
|
2
|
+
// value is the provider response as-is (bytes + MIME). LRU eviction;
|
|
3
|
+
// cacheMaxMb is live config so the limit is passed as a function.
|
|
5
4
|
import { createHash } from 'node:crypto'
|
|
6
5
|
import fs from 'node:fs'
|
|
7
6
|
import path from 'node:path'
|
|
@@ -68,7 +67,7 @@ export function createSpeechCache({ root, maxBytes }) {
|
|
|
68
67
|
try {
|
|
69
68
|
const meta = JSON.parse(await fs.promises.readFile(path.join(dir, key + '.json'), 'utf8'))
|
|
70
69
|
const audio = await fs.promises.readFile(path.join(dir, key + AUDIO_EXT))
|
|
71
|
-
//
|
|
70
|
+
// Touch LRU asynchronously so reads are never blocked.
|
|
72
71
|
fs.promises.writeFile(path.join(dir, key + '.json'), JSON.stringify({ mime: meta.mime, at: Date.now() })).catch(() => {})
|
|
73
72
|
return { mime: meta.mime || 'audio/mpeg', audio }
|
|
74
73
|
} catch (miss) {
|
package/lib/client.js
CHANGED
|
@@ -5,9 +5,8 @@ window.__ModuleLoader__.load({
|
|
|
5
5
|
var exports = module.exports
|
|
6
6
|
const React = require('react')
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// поворачивает её классом.
|
|
8
|
+
// Chevron comes from core UI primitives; local SVG is a fallback for
|
|
9
|
+
// builds without the package. Icon always points down; open state rotates.
|
|
11
10
|
let ChevronIcon = null
|
|
12
11
|
try {
|
|
13
12
|
const primitives = require('@deepseek-ai/dsh-client-ui-primitives')
|
|
@@ -23,25 +22,27 @@ window.__ModuleLoader__.load({
|
|
|
23
22
|
const CLOUD = {
|
|
24
23
|
openai: 1, elevenlabs: 1, google: 1, azure: 1, groq: 1, deepgram: 1, openrouter: 1, custom: 1,
|
|
25
24
|
}
|
|
25
|
+
// Hints stay generic: concrete provider model IDs rot quickly.
|
|
26
26
|
const MODEL_HINT = {
|
|
27
|
-
openai: '
|
|
28
|
-
elevenlabs: '
|
|
29
|
-
google: '
|
|
30
|
-
azure: 'region voice
|
|
31
|
-
groq: '
|
|
32
|
-
deepgram: '
|
|
33
|
-
openrouter: '
|
|
34
|
-
edge: 'ru-RU-SvetlanaNeural',
|
|
27
|
+
openai: 'provider default',
|
|
28
|
+
elevenlabs: 'provider default',
|
|
29
|
+
google: 'provider default',
|
|
30
|
+
azure: 'region voice name',
|
|
31
|
+
groq: 'provider default',
|
|
32
|
+
deepgram: 'provider default (English-only voices)',
|
|
33
|
+
openrouter: 'provider/model id',
|
|
34
|
+
edge: 'BCP-47 voice, e.g. ru-RU-SvetlanaNeural',
|
|
35
35
|
piper: 'path to .onnx',
|
|
36
|
-
espeak: 'ru',
|
|
36
|
+
espeak: 'language code, e.g. ru',
|
|
37
|
+
kokoro: 'not bundled — offline neural runtime unavailable',
|
|
38
|
+
f5: 'not bundled — offline neural runtime unavailable',
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
const SET_CSS =
|
|
40
42
|
'.dts-wrap{display:flex;flex-direction:column;gap:22px;padding:4px 0;max-width:720px}' +
|
|
41
43
|
'.dts-block{display:flex;flex-direction:column;gap:10px}' +
|
|
42
44
|
'.dts-h{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}' +
|
|
43
|
-
//
|
|
44
|
-
// (issue #19), цвета только переменными темы.
|
|
45
|
+
// Plugin-settings card matches the core card pattern; theme variables only.
|
|
45
46
|
'.dts-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none}' +
|
|
46
47
|
'.dts-head{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;display:flex;align-items:center;gap:12px;padding:14px 16px}' +
|
|
47
48
|
'.dts-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}' +
|
|
@@ -54,7 +55,7 @@ window.__ModuleLoader__.load({
|
|
|
54
55
|
'.dts-input{height:34px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px}' +
|
|
55
56
|
'.dts-foot{border-top:1px solid var(--dsw-alias-border-l2);display:flex;justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px}' +
|
|
56
57
|
'.dts-save{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 14px;font-size:13px;background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}' +
|
|
57
|
-
//
|
|
58
|
+
// Provider-chain form helpers.
|
|
58
59
|
'.dts-entry{display:flex;flex-direction:column;gap:8px;padding:10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px}' +
|
|
59
60
|
'.dts-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}' +
|
|
60
61
|
'.dts-row .dts-model{flex:1;min-width:120px}' +
|
|
@@ -67,20 +68,19 @@ window.__ModuleLoader__.load({
|
|
|
67
68
|
'.dts-link{background:none;border:none;color:var(--dsw-alias-label-secondary);cursor:pointer;font-size:12px;padding:0}'
|
|
68
69
|
|
|
69
70
|
const setCssId = 'dsh-tts/settings.module.css'
|
|
70
|
-
if (typeof document !== 'undefined' && !document.querySelector('style[data-plugin
|
|
71
|
+
if (typeof document !== 'undefined' && !document.querySelector('style[data-dsh-plugin="dsh-tts"]')) {
|
|
71
72
|
const tag = document.createElement('style')
|
|
72
73
|
tag.textContent = SET_CSS
|
|
73
|
-
tag.setAttribute('data-plugin', 'dsh-tts')
|
|
74
|
+
tag.setAttribute('data-dsh-plugin', 'dsh-tts')
|
|
74
75
|
tag.dataset.pluginCss = setCssId
|
|
75
76
|
document.head.appendChild(tag)
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
//
|
|
79
|
+
// Player.
|
|
79
80
|
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
// закончилось предыдущее.
|
|
81
|
+
// Each incoming chunk used to replace the previous one immediately; while reading
|
|
82
|
+
// For full replies this was invisible; with speak-as-it-goes only the last
|
|
83
|
+
// chunk would be heard. Now items queue: the next starts after the previous ends.
|
|
84
84
|
const player = {
|
|
85
85
|
audio: null,
|
|
86
86
|
after: '',
|
|
@@ -111,8 +111,7 @@ window.__ModuleLoader__.load({
|
|
|
111
111
|
return player
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
//
|
|
115
|
-
// хранить и настраивать.
|
|
114
|
+
// Short chime is synthesized in-browser: no file to download, store, or configure.
|
|
116
115
|
function playChime(kind) {
|
|
117
116
|
return new Promise((resolve) => {
|
|
118
117
|
try {
|
|
@@ -139,8 +138,8 @@ window.__ModuleLoader__.load({
|
|
|
139
138
|
})
|
|
140
139
|
}
|
|
141
140
|
|
|
142
|
-
//
|
|
143
|
-
//
|
|
141
|
+
// Provider failed — fall back to the browser voice. Worse quality, but silence
|
|
142
|
+
// is worse: the user would not know the reply is ready.
|
|
144
143
|
function speakInBrowser(text) {
|
|
145
144
|
return new Promise((resolve) => {
|
|
146
145
|
try {
|
|
@@ -203,8 +202,7 @@ window.__ModuleLoader__.load({
|
|
|
203
202
|
audio.onended = done
|
|
204
203
|
audio.onerror = done
|
|
205
204
|
audio.play().catch(() => {
|
|
206
|
-
|
|
207
|
-
// поломкой, просто ничего не звучит.
|
|
205
|
+
// Autoplay without a user gesture is blocked — not a bug; just no sound.
|
|
208
206
|
done()
|
|
209
207
|
})
|
|
210
208
|
} catch (cannotDecode) {
|
|
@@ -272,11 +270,10 @@ window.__ModuleLoader__.load({
|
|
|
272
270
|
}
|
|
273
271
|
}
|
|
274
272
|
|
|
275
|
-
//
|
|
273
|
+
// User started talking — stop playback.
|
|
276
274
|
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
// окно, а не чужую службу, ровно поэтому.
|
|
275
|
+
// The event may come from a voice-input plugin, but there is no hard dependency:
|
|
276
|
+
// without that plugin the event simply never fires. We listen on window on purpose.
|
|
280
277
|
function listenForVoice() {
|
|
281
278
|
if (typeof window === 'undefined') return () => {}
|
|
282
279
|
const onSpeaking = (event) => {
|
|
@@ -306,8 +303,8 @@ window.__ModuleLoader__.load({
|
|
|
306
303
|
const data = await res.json()
|
|
307
304
|
const items = data && Array.isArray(data.items) ? data.items : []
|
|
308
305
|
for (const item of items) {
|
|
309
|
-
|
|
310
|
-
|
|
306
|
+
// Queue slot reserved but synthesis still running — wait for the next poll
|
|
307
|
+
// or phrase order would break.
|
|
311
308
|
if (item.kind === 'reserved') break
|
|
312
309
|
player.after = item.id
|
|
313
310
|
if (seenIds.has(item.id)) continue
|
|
@@ -323,7 +320,7 @@ window.__ModuleLoader__.load({
|
|
|
323
320
|
} catch (hostRestarting) { /* хост перезапускается */ }
|
|
324
321
|
}
|
|
325
322
|
|
|
326
|
-
//
|
|
323
|
+
// Recent utterances: last reply texts and favorites (localStorage).
|
|
327
324
|
function recentStore() {
|
|
328
325
|
const KEY = 'dsh-tts/recent'
|
|
329
326
|
let st
|
|
@@ -349,7 +346,7 @@ window.__ModuleLoader__.load({
|
|
|
349
346
|
}
|
|
350
347
|
const recent = recentStore()
|
|
351
348
|
|
|
352
|
-
//
|
|
349
|
+
// One-shot playback outside the queue: replay from recents.
|
|
353
350
|
function playStandalone(text) {
|
|
354
351
|
fetch('/dsh-tts/speak', {
|
|
355
352
|
method: 'POST',
|
|
@@ -370,7 +367,7 @@ window.__ModuleLoader__.load({
|
|
|
370
367
|
}
|
|
371
368
|
|
|
372
369
|
function ChainEditor(props) {
|
|
373
|
-
//
|
|
370
|
+
// Translator comes from the card: the slot passes props.t only to it.
|
|
374
371
|
const t = props.t || ((key) => key)
|
|
375
372
|
const rows = Array.isArray(props.value) ? props.value : []
|
|
376
373
|
const change = (i, patch) => {
|
|
@@ -437,9 +434,8 @@ window.__ModuleLoader__.load({
|
|
|
437
434
|
)
|
|
438
435
|
}
|
|
439
436
|
|
|
440
|
-
//
|
|
441
|
-
//
|
|
442
|
-
// на него же приходится откат, если перевода нет.
|
|
437
|
+
// Card strings live in the locale registry so a separate package can translate
|
|
438
|
+
// them without touching this plugin. English is the source language and fallback.
|
|
443
439
|
const en = {
|
|
444
440
|
'general': 'General',
|
|
445
441
|
'loading': 'Loading\u2026',
|
|
@@ -673,7 +669,7 @@ window.__ModuleLoader__.load({
|
|
|
673
669
|
'removeRegex': 'Регулярка вырезания',
|
|
674
670
|
'removeRegexHint': 'Глобальная регулярка: совпадения удаляются перед синтезом.',
|
|
675
671
|
'customBaseUrl': 'Свой TTS endpoint',
|
|
676
|
-
'customBaseUrlHint': 'OpenAI
|
|
672
|
+
'customBaseUrlHint': 'OpenAI-compatible base URL, e.g. http://localhost:8880/v1; empty disables it.',
|
|
677
673
|
'customKeyEnv': 'Имя ключа для endpoint',
|
|
678
674
|
'customKeyEnvHint': 'Имя credential для Bearer-ключа своего endpointа.',
|
|
679
675
|
|
|
@@ -804,7 +800,7 @@ window.__ModuleLoader__.load({
|
|
|
804
800
|
}
|
|
805
801
|
|
|
806
802
|
function TtsSection(props) {
|
|
807
|
-
//
|
|
803
|
+
// Translator comes from the slot because its registration sets locale.
|
|
808
804
|
const t = (props && props.t) || ((key) => key)
|
|
809
805
|
const [draft, setDraft] = React.useState(null)
|
|
810
806
|
const [credentials, setCredentials] = React.useState({})
|
|
@@ -830,12 +826,12 @@ window.__ModuleLoader__.load({
|
|
|
830
826
|
return () => { alive = false }
|
|
831
827
|
}, [scope])
|
|
832
828
|
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
829
|
+
// Snapshot status matters more than the value:
|
|
830
|
+
// loading — host has not answered yet;
|
|
831
|
+
// unavailable — host answered but the settings namespace is not ready;
|
|
832
|
+
// ready — values are present.
|
|
833
|
+
// On unavailable, writable defaults true; without a status check the card
|
|
834
|
+
// looks like a working empty form.
|
|
839
835
|
const ready = !scope || (!!snap && snap.status === 'ready')
|
|
840
836
|
const writable = !scope ? true : (ready && snap.writable !== false)
|
|
841
837
|
|
|
@@ -975,7 +971,7 @@ window.__ModuleLoader__.load({
|
|
|
975
971
|
}),
|
|
976
972
|
React.createElement('span', { className: 'dts-sub' }, hint))
|
|
977
973
|
|
|
978
|
-
//
|
|
974
|
+
// The card header draws the title — do not duplicate a second title block.
|
|
979
975
|
const titleLine = props && props.compact ? null : React.createElement('div', { className: 'dts-h' }, t('title'))
|
|
980
976
|
return React.createElement('div', { className: 'dts-wrap' },
|
|
981
977
|
React.createElement('div', { className: 'dts-block' },
|
|
@@ -1125,7 +1121,7 @@ window.__ModuleLoader__.load({
|
|
|
1125
1121
|
{ from: 'RAM', to: 'рам', whole: true, lang: 'ru' },
|
|
1126
1122
|
]
|
|
1127
1123
|
|
|
1128
|
-
//
|
|
1124
|
+
// Pronunciation dictionary editor: rules apply top-down.
|
|
1129
1125
|
function PronEditor(props) {
|
|
1130
1126
|
const t = props.t || ((key) => key)
|
|
1131
1127
|
const rows = Array.isArray(props.value) ? props.value : []
|
|
@@ -1258,7 +1254,7 @@ window.__ModuleLoader__.load({
|
|
|
1258
1254
|
style: {
|
|
1259
1255
|
padding: '10px 14px',
|
|
1260
1256
|
borderRadius: '8px',
|
|
1261
|
-
border: '1px solid var(--dsw-alias-state-warning-primary
|
|
1257
|
+
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
1262
1258
|
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1263
1259
|
display: 'flex',
|
|
1264
1260
|
flexDirection: 'column',
|
|
@@ -1266,7 +1262,7 @@ window.__ModuleLoader__.load({
|
|
|
1266
1262
|
fontSize: '13px',
|
|
1267
1263
|
},
|
|
1268
1264
|
},
|
|
1269
|
-
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary
|
|
1265
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
1270
1266
|
'⚠️ ' + t('voiceNotInstalledTitle')
|
|
1271
1267
|
),
|
|
1272
1268
|
React.createElement('span', { className: 'dts-sub' },
|
|
@@ -1300,7 +1296,7 @@ window.__ModuleLoader__.load({
|
|
|
1300
1296
|
style: {
|
|
1301
1297
|
padding: '10px 14px',
|
|
1302
1298
|
borderRadius: '8px',
|
|
1303
|
-
border: '1px solid var(--dsw-alias-state-warning-primary
|
|
1299
|
+
border: '1px solid var(--dsw-alias-state-warning-primary)',
|
|
1304
1300
|
background: 'var(--dsw-alias-bg-layer-2)',
|
|
1305
1301
|
display: 'flex',
|
|
1306
1302
|
flexDirection: 'column',
|
|
@@ -1308,7 +1304,7 @@ window.__ModuleLoader__.load({
|
|
|
1308
1304
|
fontSize: '13px',
|
|
1309
1305
|
},
|
|
1310
1306
|
},
|
|
1311
|
-
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary
|
|
1307
|
+
React.createElement('span', { style: { fontWeight: 600, color: 'var(--dsw-alias-state-warning-primary)' } },
|
|
1312
1308
|
'⚠️ ' + t('messengerNotInstalledTitle')
|
|
1313
1309
|
),
|
|
1314
1310
|
React.createElement('span', { className: 'dts-sub' },
|
|
@@ -1330,7 +1326,7 @@ window.__ModuleLoader__.load({
|
|
|
1330
1326
|
)
|
|
1331
1327
|
}
|
|
1332
1328
|
|
|
1333
|
-
//
|
|
1329
|
+
// Synthesis stats panel: live host counters.
|
|
1334
1330
|
function StatsPanel(props) {
|
|
1335
1331
|
const t = props.t || ((key) => key)
|
|
1336
1332
|
const [data, setData] = React.useState(null)
|
|
@@ -1351,9 +1347,8 @@ window.__ModuleLoader__.load({
|
|
|
1351
1347
|
)
|
|
1352
1348
|
}
|
|
1353
1349
|
|
|
1354
|
-
//
|
|
1355
|
-
//
|
|
1356
|
-
// тело с чертой-разделителем. Шеврон — наш, ядро его не даёт.
|
|
1350
|
+
// Plugin-settings tab card follows the shared card pattern: li in the core list,
|
|
1351
|
+
// theme-reset header, body with a divider. Chevron is ours; core does not provide it.
|
|
1357
1352
|
function TtsCard(props) {
|
|
1358
1353
|
const t = (props && props.t) || ((key) => key)
|
|
1359
1354
|
const [open, setOpen] = React.useState(false)
|
|
@@ -1386,12 +1381,10 @@ window.__ModuleLoader__.load({
|
|
|
1386
1381
|
)
|
|
1387
1382
|
}
|
|
1388
1383
|
|
|
1389
|
-
//
|
|
1390
|
-
// открыть недавние озвучки для реплая.
|
|
1384
|
+
// Input-dock button: shows what is playing, stop control, and recents for replay.
|
|
1391
1385
|
function SpeakerControl(props) {
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
// не донесёт: без него кнопки остались бы без подписей вовсе.
|
|
1386
|
+
// A slot with `locale` injects translations into props; the fallback is the
|
|
1387
|
+
// binder created at registration, in case the slot drops translations.
|
|
1395
1388
|
const t = (props && props.t) || fallbackDockText
|
|
1396
1389
|
const p = usePlayer()
|
|
1397
1390
|
const [showList, setShowList] = React.useState(false)
|
|
@@ -1450,8 +1443,8 @@ window.__ModuleLoader__.load({
|
|
|
1450
1443
|
let fallbackDockText = (key) => key
|
|
1451
1444
|
|
|
1452
1445
|
function registerSpeaker(ctx) {
|
|
1453
|
-
|
|
1454
|
-
|
|
1446
|
+
// `locale` in the slot descriptor is how core injects translations into props.
|
|
1447
|
+
// Without it the dock would show raw keys only.
|
|
1455
1448
|
fallbackDockText = ctx.locale.bind(NS)
|
|
1456
1449
|
ctx.slots.inject('conversation.input.dock', () => ctx.slots.register(
|
|
1457
1450
|
{
|
|
@@ -1466,14 +1459,10 @@ window.__ModuleLoader__.load({
|
|
|
1466
1459
|
}
|
|
1467
1460
|
|
|
1468
1461
|
function registerSettings(ctx) {
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
// load plugins» с перечнем ни в чём не повинных соседей.
|
|
1474
|
-
//
|
|
1475
|
-
// Поэтому каждый язык объявляется отдельно и по-хорошему: заняли до нас —
|
|
1476
|
-
// уступаем, свой английский при этом всё равно встаёт на место.
|
|
1462
|
+
// Dictionary packages may also register languages for other namespaces.
|
|
1463
|
+
// Core throws on duplicate namespace+language; an unguarded call used to
|
|
1464
|
+
// take down the whole plugin ("Failed to load plugins"). Register each
|
|
1465
|
+
// language separately: skip if taken; English still lands.
|
|
1477
1466
|
const addLocale = (locale, dictionary) => {
|
|
1478
1467
|
try {
|
|
1479
1468
|
return ctx.locale.register(NS, locale, dictionary)
|
|
@@ -1485,13 +1474,12 @@ window.__ModuleLoader__.load({
|
|
|
1485
1474
|
const undo = [addLocale('en', en), addLocale('ru', ru)]
|
|
1486
1475
|
return () => { for (const off of undo) off() }
|
|
1487
1476
|
}, 'dsh-tts: словари')
|
|
1488
|
-
|
|
1489
|
-
|
|
1477
|
+
// The sidebar draws the section label, not our component: props.t never
|
|
1478
|
+
// reaches it, so bind the translator to the namespace ourselves.
|
|
1490
1479
|
const t = ctx.locale.bind(NS)
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
// не появится молча, без единой ошибки в журнале.
|
|
1480
|
+
// Primary settings home is the plugin-settings tab card. The tab looks up
|
|
1481
|
+
// a slot by entryKey equal to the namespace name: key must equal NS or
|
|
1482
|
+
// the card never appears, with no log error.
|
|
1495
1483
|
const tryPluginItem = () => {
|
|
1496
1484
|
try {
|
|
1497
1485
|
const res = ctx.slots.inject('settings.plugin.item', () => ctx.slots.register(
|
|
@@ -1510,8 +1498,8 @@ window.__ModuleLoader__.load({
|
|
|
1510
1498
|
}
|
|
1511
1499
|
}
|
|
1512
1500
|
if (tryPluginItem()) return
|
|
1513
|
-
|
|
1514
|
-
|
|
1501
|
+
// Fallback for builds without settings.plugin.item: keep the sidebar section
|
|
1502
|
+
// so settings cannot disappear.
|
|
1515
1503
|
ctx.slots.inject('settings.section', () => ctx.slots.register(
|
|
1516
1504
|
{
|
|
1517
1505
|
name: 'settings.section',
|