@goodandready/dsh-voice 0.8.11 → 0.8.13
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 +1 -0
- package/README.ru.md +1 -0
- package/README.zh.md +1 -0
- package/lib/client.js +151 -12
- package/lib/index.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,6 +76,7 @@ graph LR
|
|
|
76
76
|
* 🖥️ **Offline Local Whisper Server**: Automatically boots and manages [whisper.cpp](https://github.com/ggerganov/whisper.cpp) (`whisper-server`) with on-the-fly `ffmpeg` transcode.
|
|
77
77
|
* ⚡ **SenseVoice-ONNX / Sherpa-ONNX** *(0.8.11)*: Ultra-fast (~50–100ms) non-autoregressive local STT engine with automatic emotion/event tag stripping. Supports both Sherpa-ONNX HTTP and OpenAI-compatible endpoints.
|
|
78
78
|
* 🌐 **Realtime Audio Streaming** *(0.8.11)*: Low-latency WebSocket bridge (`/dsh-voice/realtime`) for OpenAI Realtime API or local Sherpa-ONNX streaming. API keys stay securely on the host.
|
|
79
|
+
* 🌊 **Liquid Wave & Dynamic Orb Visualizer** *(0.8.12)*: Smooth animated audio visualization in the recording pill with real-time mic volume reactivity. Switch between organic multi-layer liquid waves, pulsating radiant orb, classic bars, or off.
|
|
79
80
|
|
|
80
81
|
---
|
|
81
82
|
|
package/README.ru.md
CHANGED
|
@@ -76,6 +76,7 @@ graph LR
|
|
|
76
76
|
* 🖥️ **Автозапуск локального whisper.cpp**: управление жизненным циклом `whisper-server` с авто-конвертацией через `ffmpeg`.
|
|
77
77
|
* ⚡ **SenseVoice-ONNX / Sherpa-ONNX** *(0.8.11)*: сверхбыстрый (~50–100мс) неавторегрессивный локальный STT с автоматической очисткой тегов эмоций/событий. Поддержка Sherpa-ONNX HTTP и OpenAI-совместимых эндпоинтов.
|
|
78
78
|
* 🌐 **Потоковое аудио в реальном времени** *(0.8.11)*: низколатентный WebSocket-мост (`/dsh-voice/realtime`) для OpenAI Realtime API или локального Sherpa-ONNX. API-ключи остаются на хосте.
|
|
79
|
+
* 🌊 **Анимированные визуализаторы Liquid Wave и Dynamic Orb** *(0.8.12)*: живая интерактивная анимация звуковой волны в полоске записи с откликом на громкость микрофона. Выбор стиля: текучая волна, пульсирующая сфера, классические столбики или выключен.
|
|
79
80
|
|
|
80
81
|
---
|
|
81
82
|
|
package/README.zh.md
CHANGED
|
@@ -76,6 +76,7 @@ graph LR
|
|
|
76
76
|
* 🖥️ **本地 whisper.cpp 服务端直连**:自动管理 `whisper-server` 进程,结合 `ffmpeg` 实现实时音频转码。
|
|
77
77
|
* ⚡ **SenseVoice-ONNX / Sherpa-ONNX** *(0.8.11)*:超快速(~50–100ms)非自回归本地语音识别引擎,自动清除情绪/事件标签。支持 Sherpa-ONNX HTTP 和 OpenAI 兼容端点。
|
|
78
78
|
* 🌐 **实时音频流** *(0.8.11)*:低延迟 WebSocket 桥接(`/dsh-voice/realtime`),支持 OpenAI Realtime API 或本地 Sherpa-ONNX 流式传输。API 密钥安全保留在服务端。
|
|
79
|
+
* 🌊 **Liquid Wave 与 Dynamic Orb 动态可视化** *(0.8.12)*:录音面板内的生动波形动态可视化,实时响应麦克风音量。可选平滑流动波浪、发光脉冲球体、经典频条或关闭。
|
|
79
80
|
|
|
80
81
|
---
|
|
81
82
|
|
package/lib/client.js
CHANGED
|
@@ -160,6 +160,12 @@ window.__ModuleLoader__.load({
|
|
|
160
160
|
'realtimeActive': 'Streaming live…',
|
|
161
161
|
'realtimeConnect': 'Start streaming',
|
|
162
162
|
'realtimeDisconnect': 'Stop streaming',
|
|
163
|
+
'visualizerStyle': 'Audio visualizer style',
|
|
164
|
+
'visualizerStyleHint': 'Waveform animation inside the recording pill',
|
|
165
|
+
'visLiquidWave': 'Liquid Wave',
|
|
166
|
+
'visDynamicOrb': 'Dynamic Orb',
|
|
167
|
+
'visBars': 'Classic Bars',
|
|
168
|
+
'visOff': 'Off',
|
|
163
169
|
}
|
|
164
170
|
const ru = {
|
|
165
171
|
'saveFailed': 'Часть полей не сохранилась —',
|
|
@@ -299,6 +305,12 @@ window.__ModuleLoader__.load({
|
|
|
299
305
|
'realtimeActive': 'Стриминг идёт…',
|
|
300
306
|
'realtimeConnect': 'Начать стриминг',
|
|
301
307
|
'realtimeDisconnect': 'Остановить стриминг',
|
|
308
|
+
'visualizerStyle': 'Стиль визуализатора звука',
|
|
309
|
+
'visualizerStyleHint': 'Анимация волны внутри полоски записи',
|
|
310
|
+
'visLiquidWave': 'Жидкая волна (Liquid Wave)',
|
|
311
|
+
'visDynamicOrb': 'Динамическая сфера (Dynamic Orb)',
|
|
312
|
+
'visBars': 'Классические столбики',
|
|
313
|
+
'visOff': 'Выключен',
|
|
302
314
|
}
|
|
303
315
|
|
|
304
316
|
// Строки нужны и вне компонентов — в обработчиках записи, в подписях
|
|
@@ -1026,6 +1038,108 @@ window.__ModuleLoader__.load({
|
|
|
1026
1038
|
)
|
|
1027
1039
|
}
|
|
1028
1040
|
|
|
1041
|
+
// ----------------------------------------------------------- visualizers
|
|
1042
|
+
// 1. Liquid Wave: органическая текучая многослойная волна
|
|
1043
|
+
function drawLiquidWave(g, w, h, levels, color, time) {
|
|
1044
|
+
const midY = h / 2
|
|
1045
|
+
const curLevel = levels.length ? levels[levels.length - 1] : 0
|
|
1046
|
+
const smoothLevel = Math.max(0.04, Math.min(1, curLevel * 1.6))
|
|
1047
|
+
|
|
1048
|
+
const layers = [
|
|
1049
|
+
{ amp: smoothLevel * (h * 0.42), freq: 0.024, speed: 0.08, alpha: 0.45 },
|
|
1050
|
+
{ amp: smoothLevel * (h * 0.36), freq: 0.038, speed: -0.06, alpha: 0.75 },
|
|
1051
|
+
{ amp: smoothLevel * (h * 0.28), freq: 0.052, speed: 0.11, alpha: 0.95 },
|
|
1052
|
+
]
|
|
1053
|
+
|
|
1054
|
+
for (let layerIdx = 0; layerIdx < layers.length; layerIdx++) {
|
|
1055
|
+
const lyr = layers[layerIdx]
|
|
1056
|
+
g.beginPath()
|
|
1057
|
+
g.globalAlpha = lyr.alpha
|
|
1058
|
+
|
|
1059
|
+
const grad = g.createLinearGradient(0, 0, w, 0)
|
|
1060
|
+
grad.addColorStop(0, color)
|
|
1061
|
+
grad.addColorStop(0.5, '#38bdf8')
|
|
1062
|
+
grad.addColorStop(1, color)
|
|
1063
|
+
g.strokeStyle = grad
|
|
1064
|
+
g.lineWidth = layerIdx === 2 ? 2.5 : 1.5
|
|
1065
|
+
|
|
1066
|
+
g.moveTo(0, midY)
|
|
1067
|
+
const step = 6
|
|
1068
|
+
for (let x = 0; x <= w; x += step) {
|
|
1069
|
+
const edgeDist = Math.min(x, w - x) / (w * 0.25)
|
|
1070
|
+
const envelope = Math.min(1, Math.max(0, edgeDist))
|
|
1071
|
+
const phase = time * lyr.speed + x * lyr.freq
|
|
1072
|
+
const dy = Math.sin(phase) * lyr.amp * envelope + Math.cos(phase * 0.5) * (lyr.amp * 0.35) * envelope
|
|
1073
|
+
g.lineTo(x, midY + dy)
|
|
1074
|
+
}
|
|
1075
|
+
g.stroke()
|
|
1076
|
+
}
|
|
1077
|
+
g.globalAlpha = 1
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// 2. Dynamic Orb: интерактивная пульсирующая сфера по центру
|
|
1081
|
+
function drawDynamicOrb(g, w, h, levels, color, time) {
|
|
1082
|
+
const cx = w / 2
|
|
1083
|
+
const cy = h / 2
|
|
1084
|
+
const curLevel = levels.length ? levels[levels.length - 1] : 0
|
|
1085
|
+
const smoothLevel = Math.max(0.05, Math.min(1, curLevel * 2.0))
|
|
1086
|
+
|
|
1087
|
+
g.beginPath()
|
|
1088
|
+
g.globalAlpha = 0.25
|
|
1089
|
+
g.strokeStyle = color
|
|
1090
|
+
g.lineWidth = 1
|
|
1091
|
+
g.moveTo(10, cy)
|
|
1092
|
+
g.lineTo(cx - 35, cy)
|
|
1093
|
+
g.moveTo(cx + 35, cy)
|
|
1094
|
+
g.lineTo(w - 10, cy)
|
|
1095
|
+
g.stroke()
|
|
1096
|
+
|
|
1097
|
+
const rRing = 14 + smoothLevel * 14 + Math.sin(time * 0.08) * 3
|
|
1098
|
+
g.beginPath()
|
|
1099
|
+
g.arc(cx, cy, rRing, 0, Math.PI * 2)
|
|
1100
|
+
g.strokeStyle = '#38bdf8'
|
|
1101
|
+
g.globalAlpha = 0.35 + smoothLevel * 0.4
|
|
1102
|
+
g.lineWidth = 1.5
|
|
1103
|
+
g.stroke()
|
|
1104
|
+
|
|
1105
|
+
if (smoothLevel > 0.25) {
|
|
1106
|
+
g.beginPath()
|
|
1107
|
+
g.arc(cx, cy, rRing + 7 + Math.cos(time * 0.06) * 4, 0, Math.PI * 2)
|
|
1108
|
+
g.strokeStyle = color
|
|
1109
|
+
g.globalAlpha = 0.2 + smoothLevel * 0.3
|
|
1110
|
+
g.lineWidth = 1
|
|
1111
|
+
g.stroke()
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const rCore = 6 + smoothLevel * 8 + Math.sin(time * 0.12) * 1.5
|
|
1115
|
+
const radial = g.createRadialGradient(cx, cy, 1, cx, cy, rCore + 4)
|
|
1116
|
+
radial.addColorStop(0, '#ffffff')
|
|
1117
|
+
radial.addColorStop(0.4, '#38bdf8')
|
|
1118
|
+
radial.addColorStop(1, color)
|
|
1119
|
+
g.beginPath()
|
|
1120
|
+
g.arc(cx, cy, rCore, 0, Math.PI * 2)
|
|
1121
|
+
g.fillStyle = radial
|
|
1122
|
+
g.globalAlpha = 0.95
|
|
1123
|
+
g.fill()
|
|
1124
|
+
|
|
1125
|
+
g.globalAlpha = 1
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// 3. Classic Bars: классические вертикальные столбики
|
|
1129
|
+
function drawClassicBars(g, w, h, levels, color) {
|
|
1130
|
+
const midY = h / 2
|
|
1131
|
+
for (let i = 0; i < levels.length && i * 7 < w; i++) {
|
|
1132
|
+
const level = levels[levels.length - 1 - i]
|
|
1133
|
+
const age = i / levels.length
|
|
1134
|
+
const x = w - 10 - i * 7
|
|
1135
|
+
const hh = Math.max(2.5, level * (h - 6) * 0.5 * (1 - age * 0.35))
|
|
1136
|
+
g.globalAlpha = 1 - age * 0.75
|
|
1137
|
+
g.fillStyle = color
|
|
1138
|
+
g.fillRect(x, midY - hh, 3.5, hh * 2)
|
|
1139
|
+
}
|
|
1140
|
+
g.globalAlpha = 1
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1029
1143
|
function RecordPill(props) {
|
|
1030
1144
|
const v = useVoice()
|
|
1031
1145
|
const canvasRef = React.useRef(null)
|
|
@@ -1078,9 +1192,10 @@ window.__ModuleLoader__.load({
|
|
|
1078
1192
|
return () => dispose()
|
|
1079
1193
|
}, [v.phase])
|
|
1080
1194
|
|
|
1081
|
-
//
|
|
1195
|
+
// Анимация осциллограммы / визуализатора.
|
|
1082
1196
|
React.useEffect(() => {
|
|
1083
1197
|
if (v.phase !== 'recording') return
|
|
1198
|
+
let frame = 0
|
|
1084
1199
|
const dispose = ctx.interval(() => {
|
|
1085
1200
|
const canvas = canvasRef.current
|
|
1086
1201
|
if (!canvas) return
|
|
@@ -1091,18 +1206,30 @@ window.__ModuleLoader__.load({
|
|
|
1091
1206
|
try { voice.waveColor = getComputedStyle(canvas).color || '#fff' } catch (e) { voice.waveColor = '#fff' }
|
|
1092
1207
|
}
|
|
1093
1208
|
const levels = voice.levels
|
|
1094
|
-
const
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
g.
|
|
1101
|
-
g.
|
|
1102
|
-
g.
|
|
1209
|
+
const style = (voice.settings && voice.settings.visualizerStyle) || 'liquid-wave'
|
|
1210
|
+
frame++
|
|
1211
|
+
|
|
1212
|
+
if (style === 'off') {
|
|
1213
|
+
g.beginPath()
|
|
1214
|
+
g.globalAlpha = 0.25
|
|
1215
|
+
g.strokeStyle = voice.waveColor
|
|
1216
|
+
g.lineWidth = 1
|
|
1217
|
+
g.moveTo(10, h / 2)
|
|
1218
|
+
g.lineTo(w - 10, h / 2)
|
|
1219
|
+
g.stroke()
|
|
1220
|
+
g.globalAlpha = 1
|
|
1221
|
+
return
|
|
1103
1222
|
}
|
|
1104
|
-
|
|
1105
|
-
|
|
1223
|
+
|
|
1224
|
+
if (style === 'dynamic-orb') {
|
|
1225
|
+
drawDynamicOrb(g, w, h, levels, voice.waveColor, frame)
|
|
1226
|
+
} else if (style === 'bars') {
|
|
1227
|
+
drawClassicBars(g, w, h, levels, voice.waveColor)
|
|
1228
|
+
} else {
|
|
1229
|
+
// default: liquid-wave
|
|
1230
|
+
drawLiquidWave(g, w, h, levels, voice.waveColor, frame)
|
|
1231
|
+
}
|
|
1232
|
+
}, 33)
|
|
1106
1233
|
return () => dispose()
|
|
1107
1234
|
}, [v.phase])
|
|
1108
1235
|
|
|
@@ -1273,6 +1400,7 @@ window.__ModuleLoader__.load({
|
|
|
1273
1400
|
sessionCommands: !!(data && data.modes && data.modes.message && data.modes.message.sessionCommands),
|
|
1274
1401
|
noiseSuppression: data && data.noiseSuppression !== false,
|
|
1275
1402
|
contextGlossary: data && data.contextGlossary !== false,
|
|
1403
|
+
visualizerStyle: (data && data.visualizerStyle) || 'liquid-wave',
|
|
1276
1404
|
})
|
|
1277
1405
|
})
|
|
1278
1406
|
.catch(() => { /* без подсказки хоста клавиши просто не будет */ })
|
|
@@ -1612,6 +1740,7 @@ window.__ModuleLoader__.load({
|
|
|
1612
1740
|
vadAdapt: Number(draft.dictation && draft.dictation.vadAdapt) || 0,
|
|
1613
1741
|
noiseSuppression: draft.noiseSuppression !== false,
|
|
1614
1742
|
contextGlossary: draft.contextGlossary !== false,
|
|
1743
|
+
visualizerStyle: draft.visualizerStyle || 'liquid-wave',
|
|
1615
1744
|
})
|
|
1616
1745
|
// Композер держит обработчик клавиши: пусть перечитает настройку,
|
|
1617
1746
|
// иначе новая клавиша заработает только после перезагрузки страницы.
|
|
@@ -1792,6 +1921,16 @@ window.__ModuleLoader__.load({
|
|
|
1792
1921
|
type: 'checkbox', checked: draft ? draft.contextGlossary !== false : true, disabled: !writable,
|
|
1793
1922
|
onChange: (e) => setTop('contextGlossary', e.target.checked),
|
|
1794
1923
|
})),
|
|
1924
|
+
React.createElement('label', { className: 'dvs-field', title: t('visualizerStyleHint') }, t('visualizerStyle'),
|
|
1925
|
+
React.createElement('select', {
|
|
1926
|
+
value: String((draft && draft.visualizerStyle) || 'liquid-wave'), disabled: !writable,
|
|
1927
|
+
onChange: (e) => setTop('visualizerStyle', e.target.value),
|
|
1928
|
+
},
|
|
1929
|
+
React.createElement('option', { value: 'liquid-wave' }, t('visLiquidWave')),
|
|
1930
|
+
React.createElement('option', { value: 'dynamic-orb' }, t('visDynamicOrb')),
|
|
1931
|
+
React.createElement('option', { value: 'bars' }, t('visBars')),
|
|
1932
|
+
React.createElement('option', { value: 'off' }, t('visOff')),
|
|
1933
|
+
)),
|
|
1795
1934
|
React.createElement('label', { className: 'dvs-field' }, t('vocabulary'),
|
|
1796
1935
|
React.createElement('textarea', {
|
|
1797
1936
|
rows: 3, disabled: !writable,
|
package/lib/index.js
CHANGED
|
@@ -158,6 +158,8 @@ export const Config = z.object({
|
|
|
158
158
|
.description('Realtime streaming provider: "openai" (OpenAI Realtime API) or "sherpa-onnx" (local streaming).'),
|
|
159
159
|
realtimeModel: z.string().default('gpt-4o-realtime-preview')
|
|
160
160
|
.description('Realtime model identifier.'),
|
|
161
|
+
visualizerStyle: z.union(['liquid-wave', 'dynamic-orb', 'bars', 'off']).default('liquid-wave')
|
|
162
|
+
.description('Audio visualizer animation style in the recording pill: "liquid-wave", "dynamic-orb", "bars", or "off".'),
|
|
161
163
|
})
|
|
162
164
|
|
|
163
165
|
const MIME_BY_EXT = {
|
|
@@ -439,6 +441,7 @@ export function apply(ctx, baseConfig) {
|
|
|
439
441
|
realtimeStreaming: !!cfg.realtimeStreaming,
|
|
440
442
|
realtimeProvider: cfg.realtimeProvider || 'openai',
|
|
441
443
|
realtimeModel: cfg.realtimeModel || 'gpt-4o-realtime-preview',
|
|
444
|
+
visualizerStyle: cfg.visualizerStyle || 'liquid-wave',
|
|
442
445
|
providerStats: getProviderStats(),
|
|
443
446
|
})
|
|
444
447
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-voice",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.13",
|
|
4
4
|
"description": "Voice input for DeepSeek Harness: dictation chunked by pauses and voice messages, each with its own provider fallback chain (Deepgram, Groq, HuggingFace, local whisper.cpp, plus any OpenAI-compatible API of your own).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|