@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 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 midY = h / 2
1095
- for (let i = 0; i < levels.length && i * 7 < w; i++) {
1096
- const level = levels[levels.length - 1 - i]
1097
- const age = i / levels.length
1098
- const x = w - 10 - i * 7
1099
- const hh = Math.max(2.5, level * (h - 6) * 0.5 * (1 - age * 0.35))
1100
- g.globalAlpha = 1 - age * 0.75
1101
- g.fillStyle = voice.waveColor
1102
- g.fillRect(x, midY - hh, 3.5, hh * 2)
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
- g.globalAlpha = 1
1105
- }, 50)
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.11",
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",