@vaibhavgupta0786/opencode-voice 0.3.7 → 0.3.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaibhavgupta0786/opencode-voice",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Local-only push-to-talk dictation for OpenCode v2 \u2014 press F9, speak, press F9.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -53,7 +53,7 @@
53
53
  "@opentui/solid": "^0.5.0",
54
54
  "@types/node": "^26.0.0",
55
55
  "solid-js": "^1.9.0",
56
- "typescript": "^5.4.0"
56
+ "typescript": "^7.0.2"
57
57
  },
58
58
  "repository": {
59
59
  "type": "git",
@@ -28,6 +28,39 @@ import mlx_whisper
28
28
  MODEL = os.environ.get("MLX_MODEL", "mlx-community/whisper-small-mlx")
29
29
  PORT = int(os.environ.get("VOICE_STT_PORT", "8080"))
30
30
 
31
+ # No-speech gate: refuse to transcribe clips that don't look spoken.
32
+ # faster-whisper's default path has VAD + no_speech_threshold for this;
33
+ # the mlx path inherits no such guard and hallucinates on room noise
34
+ # (e.g. pure noise decoded into repeating nonsense Syllables).
35
+ # Config: VOICE_NOSPEECH_MIN_MS / VOICE_NOSPEECH_ZCR, "0" disables.
36
+ NOSPEECH_MIN_MS = float(os.environ.get("VOICE_NOSPEECH_MIN_MS", "200"))
37
+ NOSPEECH_ZCR = float(os.environ.get("VOICE_NOSPEECH_ZCR", "0.4"))
38
+
39
+
40
+ def speech_gate(samples: np.ndarray) -> tuple[bool, str]:
41
+ """Cheap numpy voice-likeness gate; (pass, reason).
42
+
43
+ Frames are 20ms hops at 16kHz. A frame counts as voiced when its RMS
44
+ exceeds 2% full scale. White noise has near-50% zero crossings vs
45
+ speech tones well under; noise that is loud enough to be voiced gets
46
+ refused on zero-crossing rate instead.
47
+ """
48
+ frame = int(16000 * 0.02)
49
+ n = len(samples) // frame
50
+ voiced_ms = 0.0
51
+ zcr = 0.0
52
+ if n:
53
+ rms = np.sqrt((samples[: n * frame].reshape(n, frame) ** 2).mean(axis=1))
54
+ voiced = int((rms > 0.02).sum())
55
+ voiced_ms = voiced * frame / 16000 * 1000
56
+ if len(samples) > 1:
57
+ zcr = (samples[1:] * samples[:-1] < 0).mean()
58
+ if NOSPEECH_MIN_MS and voiced_ms < NOSPEECH_MIN_MS:
59
+ return False, f"voiced={voiced_ms:.0f}ms < {NOSPEECH_MIN_MS:.0f}ms"
60
+ if NOSPEECH_ZCR and zcr > NOSPEECH_ZCR:
61
+ return False, f"zcr={zcr:.2f} > {NOSPEECH_ZCR} (noise-like)"
62
+ return True, f"voiced={voiced_ms:.0f}ms zcr={zcr:.2f}"
63
+
31
64
  print(f"loading {MODEL} ...", flush=True)
32
65
  # Warm up by loading weights once; per-request calls reuse them.
33
66
  _MODEL_REF = MODEL
@@ -80,6 +113,12 @@ class Handler(BaseHTTPRequestHandler):
80
113
  self._json(400, {"error": "no file part"})
81
114
  return
82
115
  samples = wav_bytes_to_float32(bytes(audio))
116
+ passed, gate = speech_gate(samples)
117
+ if not passed:
118
+ print(f"no-speech guard: {gate}", flush=True)
119
+ self._json(200, {"text": ""})
120
+ return
121
+ print(f"speech gate: {gate}", flush=True)
83
122
  try:
84
123
  result = mlx_whisper.transcribe(samples, path_or_hf_repo=_MODEL_REF)
85
124
  finally:
@@ -57,12 +57,27 @@ model loads from cache in roughly 10-25 s on a cold start:
57
57
 
58
58
  curl -s -m 3 http://127.0.0.1:8080/health
59
59
 
60
- ## 5. If it still will not start
60
+ ## 5. Memory check
61
+
62
+ Long-running STT servers ratchet memory silently — buffer allocators
63
+ retain everything (measured: the mlx server held 7.7 GB after 25 days
64
+ because each unique audio length adds GPU buffers that are never freed;
65
+ a per-request `mx.clear_cache()` now caps it). Verify after boot:
66
+
67
+ PID=$(lsof -ti:8080 | head -1)
68
+ vmmap --summary $PID 2>/dev/null | grep "Physical footprint"
69
+
70
+ Healthy: mlx server ≈ 0.7–1.5 GB · CPU (faster-whisper) ≈ 0.4–0.9 GB.
71
+ If the footprint is already high (mlx > 2 GB, CPU > 1 GB), return to
72
+ step 3, restart, and re-verify after one real take — the plateau is the
73
+ health signal, not the size.
74
+
75
+ ## 6. If it still will not start
61
76
 
62
77
  Show the last 10-20 lines of the server log (`mlx-server.log` or
63
78
  `stt-server.log`) and report the error you see. Do not guess further.
64
79
 
65
- ## 6. Report
80
+ ## 7. Report
66
81
 
67
- One line: server was up / has been restarted / failed (with the error).
68
- Remind the user to press Fn+F9 to dictate.
82
+ One line: server was up / has been restarted / failed (with the error),
83
+ plus the footprint reading. Remind the user to press Fn+F9 to dictate.