@goodandready/dsh-voice 0.9.8 → 0.9.9

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/lib/client.js CHANGED
@@ -922,7 +922,6 @@ window.__ModuleLoader__.load({
922
922
  }
923
923
  })
924
924
  }
925
-
926
925
  // ------------------------------------------------------------ recording
927
926
  function teardown(rec) {
928
927
  if (!rec) return
@@ -932,7 +931,6 @@ window.__ModuleLoader__.load({
932
931
  if (rec.analyser) { try { rec.analyser.disconnect() } catch (e) { /* already disconnected */ } }
933
932
  if (rec.audioCtx) { try { rec.audioCtx.close() } catch (e) { /* already closed */ } }
934
933
  }
935
-
936
934
  if (typeof window !== 'undefined') {
937
935
  const onPageUnload = () => {
938
936
  if (voice.rec) teardown(voice.rec)
@@ -943,12 +941,10 @@ window.__ModuleLoader__.load({
943
941
  window.addEventListener('pagehide', onPageUnload)
944
942
  window.addEventListener('beforeunload', onPageUnload)
945
943
  }
946
-
947
944
  function waitStop(recorder) {
948
945
  if (!recorder || recorder.state === 'inactive') return Promise.resolve()
949
946
  return new Promise((resolve) => recorder.addEventListener('stop', resolve, { once: true }))
950
947
  }
951
-
952
948
  async function openMic(mode) {
953
949
  if (typeof navigator === 'undefined' || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
954
950
  throw new Error(t('micUnavailable'))
@@ -982,7 +978,6 @@ window.__ModuleLoader__.load({
982
978
  if (rec.audioCtx.state === 'suspended') { rec.audioCtx.resume().catch(() => {}) }
983
979
  const src = rec.audioCtx.createMediaStreamSource(stream)
984
980
  rec.srcNode = src
985
-
986
981
  let lastNode = src
987
982
  try {
988
983
  const filter = rec.audioCtx.createBiquadFilter()
@@ -993,7 +988,6 @@ window.__ModuleLoader__.load({
993
988
  lastNode = filter
994
989
  rec.filterNode = filter
995
990
  } catch (e) { /* audio filter fallback */ }
996
-
997
991
  rec.analyser = rec.audioCtx.createAnalyser()
998
992
  rec.analyser.fftSize = 128
999
993
  lastNode.connect(rec.analyser)
@@ -1002,7 +996,6 @@ window.__ModuleLoader__.load({
1002
996
  recorder.start()
1003
997
  return rec
1004
998
  }
1005
-
1006
999
  function currentLevel(rec) {
1007
1000
  if (!rec || !rec.analyser) return 0
1008
1001
  const data = new Uint8Array(rec.analyser.frequencyBinCount)
@@ -1010,7 +1003,6 @@ window.__ModuleLoader__.load({
1010
1003
  let sum = 0
1011
1004
  for (let i = 0; i < data.length; i++) sum += data[i]
1012
1005
  const raw = Math.min(1, (sum / data.length / 255) * 2.2)
1013
-
1014
1006
  const gateDb = Number(voice.settings && voice.settings.noiseGateDb !== undefined ? voice.settings.noiseGateDb : -45)
1015
1007
  if (gateDb > -90) {
1016
1008
  const gateAmp = Math.pow(10, gateDb / 20) * 2.2
@@ -1018,11 +1010,9 @@ window.__ModuleLoader__.load({
1018
1010
  }
1019
1011
  return raw
1020
1012
  }
1021
-
1022
1013
  // Cut the current phrase: stop the recorder, send the finished file and
1023
1014
  // immediately start a new recording with the same recorder.
1024
1015
  let dictationQueue = Promise.resolve()
1025
-
1026
1016
  function cutPhrase() {
1027
1017
  const rec = voice.rec
1028
1018
  if (!rec || rec.cutting || rec.closing) return
@@ -1037,6 +1027,7 @@ window.__ModuleLoader__.load({
1037
1027
  return
1038
1028
  }
1039
1029
  stopped.then(async () => {
1030
+ if (rec.cancelled || voice.activeOpId !== opId) return
1040
1031
  const blob = new Blob(rec.chunks, { type: rec.mime })
1041
1032
  rec.chunks = []
1042
1033
  rec.silenceMs = 0
@@ -1107,6 +1098,7 @@ window.__ModuleLoader__.load({
1107
1098
  // Browser recognition instead of file recording. Returns false when the
1108
1099
  // browser cannot do it — then fall back to the normal path.
1109
1100
  function startBrowserLeg(mode, language) {
1101
+ if (voice.settings && voice.settings.localOnly) return false
1110
1102
  if (!browserRecognitionAvailable()) return false
1111
1103
  const finals = []
1112
1104
  voice.caption = ''
@@ -1163,6 +1155,7 @@ window.__ModuleLoader__.load({
1163
1155
 
1164
1156
  function startRecording(mode) {
1165
1157
  if (voice.phase !== 'idle' && voice.phase !== 'error') return
1158
+ voice.activeOpId = (voice.activeOpId || 0) + 1
1166
1159
  if (voice.isTtsSpeaking) {
1167
1160
  if (voice.settings.bargeIn) {
1168
1161
  triggerBargeIn()
@@ -1326,6 +1319,7 @@ window.__ModuleLoader__.load({
1326
1319
  }
1327
1320
 
1328
1321
  function cancelCurrent() {
1322
+ voice.activeOpId = (voice.activeOpId || 0) + 1
1329
1323
  announceVoice('end')
1330
1324
  focusComposer()
1331
1325
  if (voice.browser) {
@@ -1372,6 +1366,8 @@ window.__ModuleLoader__.load({
1372
1366
  const rec = voice.rec
1373
1367
  if (!rec || rec.closing) return
1374
1368
  rec.closing = true
1369
+ voice.activeOpId = (voice.activeOpId || 0) + 1
1370
+ const opId = voice.activeOpId
1375
1371
  const mode = rec.mode
1376
1372
  const stopped = waitStop(rec.recorder)
1377
1373
  try {
@@ -1391,6 +1387,7 @@ window.__ModuleLoader__.load({
1391
1387
  voice.set({ phase: 'processing' })
1392
1388
  try {
1393
1389
  const out = await sendAudio(blob, rec.mime, mode)
1390
+ if (rec.cancelled || voice.activeOpId !== opId || voice.phase === 'idle') return
1394
1391
  if (out.command) { runSessionCommand(out.command); return }
1395
1392
  const text = out.text || ''
1396
1393
  const action = out.action || null
@@ -1429,6 +1426,7 @@ window.__ModuleLoader__.load({
1429
1426
  voice.set({ phase: 'idle' })
1430
1427
  }
1431
1428
  } catch (e) {
1429
+ if (rec.cancelled || voice.activeOpId !== opId || voice.phase === 'idle') return
1432
1430
  voice.set({ phase: 'error', error: String(e && e.message ? e.message : e) })
1433
1431
  }
1434
1432
  })
@@ -1980,6 +1978,7 @@ window.__ModuleLoader__.load({
1980
1978
  // The composer needs fresh settings without opening the card.
1981
1979
  Object.assign(voice.settings, {
1982
1980
  beep: !!(data && data.beep),
1981
+ localOnly: !!(data && data.localOnly),
1983
1982
  micDeviceId: String((data && data.micDeviceId) || ''),
1984
1983
  historyLimit: Number(data && data.historyLimit),
1985
1984
  voiceCommands: !!(data && data.voiceCommands),
@@ -2673,7 +2672,8 @@ window.__ModuleLoader__.load({
2673
2672
  const draft = props.draft
2674
2673
  const setTop = props.setTop
2675
2674
  const writable = props.writable
2676
- const [jargonInput, setJargonInput] = React.useState(null)
2675
+ const jargonInput = props.jargonInput
2676
+ const setJargonInput = props.setJargonInput
2677
2677
 
2678
2678
  return React.createElement('div', { className: 'cb-section-card' },
2679
2679
  React.createElement('div', { className: 'cb-section-title' },
@@ -2694,14 +2694,14 @@ window.__ModuleLoader__.load({
2694
2694
  rows: 3,
2695
2695
  disabled: !writable,
2696
2696
  placeholder: t('jargonPlaceholder'),
2697
- value: jargonInput !== null
2697
+ value: jargonInput !== null && jargonInput !== undefined
2698
2698
  ? jargonInput
2699
2699
  : (Array.isArray(draft && draft.jargonDictionary)
2700
2700
  ? draft.jargonDictionary.map((item) => item && item.from ? `${item.from} -> ${item.to}` : '').filter(Boolean).join('\n')
2701
2701
  : ''),
2702
- onChange: (e) => setJargonInput(e.target.value),
2702
+ onChange: (e) => setJargonInput && setJargonInput(e.target.value),
2703
2703
  onBlur: () => {
2704
- if (jargonInput === null) return
2704
+ if (jargonInput === null || jargonInput === undefined) return
2705
2705
  const lines = jargonInput.split('\n')
2706
2706
  const parsed = lines
2707
2707
  .map((line) => {
@@ -2883,6 +2883,7 @@ window.__ModuleLoader__.load({
2883
2883
 
2884
2884
  // Hotkey picker: not a name field — press the key you want.
2885
2885
  const [catching, setCatching] = React.useState(false)
2886
+ const [jargonInput, setJargonInput] = React.useState(null)
2886
2887
  React.useEffect(() => {
2887
2888
  if (!catching) return undefined
2888
2889
  const onKey = (event) => {
@@ -2956,6 +2957,7 @@ window.__ModuleLoader__.load({
2956
2957
  vadSilenceMs: Number(val.dictation && val.dictation.vadSilenceMs) || 700,
2957
2958
  autoSendMs: Number(val.message && val.message.autoSendMs) || 4000,
2958
2959
  beep: !!val.beep,
2960
+ localOnly: !!val.localOnly,
2959
2961
  micDeviceId: String(val.micDeviceId || ''),
2960
2962
  historyLimit: Number(val.historyLimit) || 20,
2961
2963
  voiceCommands: !!val.voiceCommands,
@@ -3099,7 +3101,7 @@ window.__ModuleLoader__.load({
3099
3101
  })
3100
3102
  try { window.dispatchEvent(new CustomEvent('dsh-voice:settings-saved')) } catch (noEvents) { /* nobody */ }
3101
3103
 
3102
- setSaved(true)
3104
+ setSaved(true); setJargonInput(null)
3103
3105
  setTimeout(() => setSaved(false), 2000)
3104
3106
  fetchStatus()
3105
3107
  }
@@ -3356,6 +3358,8 @@ window.__ModuleLoader__.load({
3356
3358
  draft: draft,
3357
3359
  setTop: setTop,
3358
3360
  writable: writable,
3361
+ jargonInput: jargonInput,
3362
+ setJargonInput: setJargonInput,
3359
3363
  }),
3360
3364
 
3361
3365
  // 10. Provider Telemetry & Latency Dashboard
package/lib/polish.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { isLoopback } from './http-util.js'
1
2
  // Transcript polish. Pure factory: inject fetch, key resolver, LLM, config.
2
3
  // On any failure return the original text — polish must never block input.
3
4
 
@@ -14,6 +15,15 @@ export function createPolishText({ resolveKey, fetchImpl, getConfig, llm, getAge
14
15
  const enable = modeCfg && (modeCfg.polish === true || modeCfg.polishSend === true)
15
16
  if (!enable || !text) return text
16
17
  const cfg = getConfig() || {}
18
+ if (cfg.localOnly) {
19
+ if (!cfg.polishBaseUrl) return text
20
+ try {
21
+ const u = new URL(cfg.polishBaseUrl)
22
+ if (!isLoopback(u.hostname)) return text
23
+ } catch {
24
+ return text
25
+ }
26
+ }
17
27
  const ask =
18
28
  'Fix the punctuation and spelling of this dictated text and split it into '
19
29
  + 'paragraphs where the speaker changes topic. Remove filler words ("um", "uh", '
package/lib/updater.js CHANGED
@@ -198,23 +198,25 @@ export function checkProfileLock(profileDir) {
198
198
  const lockPath = resolve(profileDir, 'package.json.lock');
199
199
  if (!existsSync(lockPath)) return { locked: false };
200
200
  const pid = readLockPid(lockPath);
201
- if (pid && isProcessAlive(pid)) {
202
- return { locked: true, pid };
203
- }
204
- // Stale lock: PID is dead or unreadable; clean it up to prevent permanent blockage
205
- try {
206
- unlinkSync(lockPath);
207
- return { locked: false, cleanedStale: true };
208
- } catch {
209
- return { locked: false };
201
+ if (pid !== null) {
202
+ if (isProcessAlive(pid)) {
203
+ return { locked: true, pid };
204
+ }
205
+ try {
206
+ unlinkSync(lockPath);
207
+ return { locked: false, cleanedStale: true };
208
+ } catch {
209
+ return { locked: false };
210
+ }
210
211
  }
212
+ return { locked: true, pid: null };
211
213
  }
212
214
 
213
215
  async function installExact(target, packageSpec, options) {
214
216
  if (target.cliEntry === undefined) throw new Error('Automatic update is unavailable in this runtime.');
215
217
  const lockCheck = checkProfileLock(target.profileDir);
216
218
  if (lockCheck.locked) {
217
- throw new Error(`Another package installation is currently in progress (PID ${lockCheck.pid}).`);
219
+ throw new Error(`Another package installation is currently in progress${lockCheck.pid ? ` (PID ${lockCheck.pid})` : ''}.`);
218
220
  }
219
221
  await new Promise((resolvePromise, reject) => {
220
222
  const child = spawn(process.execPath, [
@@ -228,28 +230,34 @@ async function installExact(target, packageSpec, options) {
228
230
  env: { ...process.env, NO_COLOR: '1' },
229
231
  });
230
232
  let detail = '';
233
+ let timedOut = false;
234
+ let killTimer = null;
231
235
  child.stdout?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000); });
232
236
  child.stderr?.on('data', chunk => { detail = (detail + String(chunk)).slice(-4_000); });
233
- const timer = setTimeout(() => {
234
- child.kill('SIGTERM');
235
- setTimeout(() => {
236
- if (!child.killed) {
237
- try { child.kill('SIGKILL'); } catch { /* ignore */ }
238
- }
239
- checkProfileLock(target.profileDir);
240
- }, 1000).unref?.();
241
- reject(new Error('Update timed out; use the normal DSH update flow.'));
237
+ const timeoutTimer = setTimeout(() => {
238
+ timedOut = true;
239
+ try { child.kill('SIGTERM'); } catch { /* ignore */ }
240
+ killTimer = setTimeout(() => {
241
+ try { child.kill('SIGKILL'); } catch { /* ignore */ }
242
+ }, 1000);
242
243
  }, UPDATE_TIMEOUT_MS);
243
244
  child.once('error', error => {
244
- clearTimeout(timer);
245
+ clearTimeout(timeoutTimer);
246
+ if (killTimer) clearTimeout(killTimer);
245
247
  checkProfileLock(target.profileDir);
246
248
  reject(error);
247
249
  });
248
250
  child.once('exit', code => {
249
- clearTimeout(timer);
251
+ clearTimeout(timeoutTimer);
252
+ if (killTimer) clearTimeout(killTimer);
250
253
  checkProfileLock(target.profileDir);
251
- if (code === 0) resolvePromise();
252
- else reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`));
254
+ if (timedOut) {
255
+ reject(new Error('Update timed out; use the normal DSH update flow.'));
256
+ } else if (code === 0) {
257
+ resolvePromise();
258
+ } else {
259
+ reject(new Error(detail.trim() || `Update exited with code ${String(code)}.`));
260
+ }
253
261
  });
254
262
  });
255
263
  }
@@ -293,7 +301,7 @@ export function registerPluginUpdater(ctx, options) {
293
301
  if (installing || lockStatus.locked) {
294
302
  json(response, 409, {
295
303
  error: lockStatus.locked
296
- ? `Another package installation is currently in progress (PID ${lockStatus.pid}).`
304
+ ? `Another package installation is currently in progress${lockStatus.pid ? ` (PID ${lockStatus.pid})` : ''}.`
297
305
  : 'This plugin is already updating.',
298
306
  });
299
307
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-voice",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
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",