@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 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 completely offline neural models (Kokoro-82M and F5-TTS).
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 -->|Priority 1| P1[Kokoro / F5-TTS Local Offline]
53
- Chain -.->|Cloud Neural| P2[ElevenLabs / OpenAI / CosyVoice]
54
- Chain -.->|Free Cloud / Edge| P3[EdgeTTS / SiliconFlow]
55
- Chain -.->|System Fallback| P4[Local Piper / eSpeak NG]
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 Local Neural Engines (Kokoro CPU & F5-TTS GPU)
79
- * **Kokoro-82M (CPU)**: 82M-parameter lightweight neural model running locally on CPU via ONNX Runtime. High-speed synthesis with zero cloud dependencies.
80
- * **F5-TTS (GPU)**: Zero-shot diffusion transformer voice synthesis running on NVIDIA GPUs via a local inference daemon.
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* | **100% offline CPU neural synthesis** |
116
- | `f5` | Local F5-TTS GPU Daemon | `F5-TTS` | Default | *None* | **High-fidelity GPU zero-shot voice synthesis** |
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 local Kokoro and F5-TTS model installation states.
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>Многопровайдерная озвучка ответов агента с локальными нейродвижками, потоковым звуком (<300 мс), IT-словарём и интеграцией с мессенджерами для DeepSeek Harness</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-ключи никогда не передаются в браузер: синтез аудио выполняется на стороне хоста через **независимые цепочки отказоустойчивости (фолбеков)**, включая полностью локальные оффлайн-нейросети (Kokoro-82M и F5-TTS).
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 -->|1-й приоритет| P1[Kokoro / F5-TTS Локально]
53
- Chain -.->|Облачные нейросети| P2[ElevenLabs / OpenAI / CosyVoice]
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. 📴 Полностью локальные оффлайн-движки (Kokoro CPU и F5-TTS GPU)
79
- * **Kokoro-82M (CPU)**: Компактная нейросетевая модель (82 млн параметров), работающая локально на CPU через ONNX Runtime. Быстрый синтез без внешних API и интернета.
80
- * **F5-TTS (GPU)**: Диффузионный трансформер нулевого выстрела (zero-shot) на видеокартах NVIDIA через выделенный локальный демон инференса.
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` | Локальный Kokoro-82M ONNX | `hexgrad/Kokoro-82M` | `af_bella` | *Не требуется* | **100% локальный нейросинтез на CPU** |
116
- | `f5` | Локальный F5-TTS GPU | `F5-TTS` | По умолчанию | *Не требуется* | **Качественный zero-shot синтез на NVIDIA GPU** |
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` — Статус установленных локальных моделей (Kokoro, F5-TTS).
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 多引擎语音合成:本地离线神经网络引擎、低延迟流式音频(<300ms)、IT术语词典与即时通讯集成</h3>
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 密钥绝不暴露给前端:音频合成全程在服务端通过**多服务商独立备用链**执行,包括完全离线的本地神经网络模型(Kokoro-82M 和 F5-TTS)。
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 -->|首选| P1[Kokoro / F5-TTS 本地离线]
53
- Chain -.->|云端神经网络| P2[ElevenLabs / OpenAI / CosyVoice]
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. 📴 完全离线本地神经网络引擎(Kokoro CPU 与 F5-TTS GPU)
79
- * **Kokoro-82M (CPU)**:8200万参数轻量级神经网络模型,通过 ONNX Runtime 在 CPU 上本地运行,零云端依赖。
80
- * **F5-TTS (GPU)**:零样本扩散 Transformer 语音合成,通过本地推理守护进程运行于 NVIDIA GPU。
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` | 本地 Kokoro-82M ONNX | `hexgrad/Kokoro-82M` | `af_bella` | *无需密钥* | **100% 离线 CPU 神经网络合成** |
111
- | `f5` | 本地 F5-TTS GPU 守护进程 | `F5-TTS` | 默认 | *无需密钥* | **高保真 GPU 零样本语音合成** |
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` — 查询本地模型安装状态(Kokoro、F5-TTS)。
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
- // значение — ответ провайдера как есть (байты + MIME). Вытеснение самых
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
- // Значок раскрытия — ядровый; свой SVG только как фолбэк для сборок
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: 'gpt-4o-mini-tts',
28
- elevenlabs: 'eleven_multilingual_v2',
29
- google: 'gemini-2.5-flash-preview-tts',
30
- azure: 'region voice, e.g. en-US-JennyNeural',
31
- groq: 'playai-tts',
32
- deepgram: 'aura-asteria-en (no Russian)',
33
- openrouter: 'openai/gpt-4o-mini-tts-2025-12-15',
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-css="' + setCssId + '"]')) {
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
- // Недавние озвучки: тексты последних реплик и избранное (localStorage).
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
- // Переводчик приходит от карточки: слот отдаёт props.t только ей.
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-совместимый адрес, например http://localhost:8880/v1; пусто = выключен.',
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
- // Переводчик приходит от слота, потому что в его записи указан locale.
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
- // loading — ответа хоста ещё нет;
835
- // unavailable — хост ответил, но пространство настроек пока не готово;
836
- // ready — значения на месте.
837
- // При unavailable поле writable берётся из документа и остаётся true,
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, #eab308)',
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, #eab308)' } },
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, #eab308)',
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, #eab308)' } },
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
- // единому канону (issue #19): li в списке ядра, шапка со сбросом темы,
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
- // Слот с `locale` кладёт перевод в props; запасной путь — привязка,
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
- // `locale` в описании слота — то, чем ядро кладёт перевод в props
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
- // уносил с собой весь плагин — в интерфейсе это выглядело как «Failed to
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
- // Подпись раздела рисует боковой список, а не наш компонент: props.t
1489
- // туда не доходит, поэтому берём переводчик, привязанный к namespace.
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
- // Вкладка перебирает пространства настроек и ищет слот по entryKey,
1493
- // равному имени пространства: ключ обязан равняться NS, иначе карточка
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
- // Запасной путь для сборок без settings.plugin.item: прежний раздел
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',