@vaibhavgupta0786/opencode-voice 0.3.6 → 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/README.md CHANGED
@@ -44,11 +44,14 @@ plugin only touches two documented-stable APIs —
44
44
  OpenTUI 0.5 → 0.6 migration doesn't affect it. Newer `2.x` releases are
45
45
  expected to work; if one doesn't, file an issue.
46
46
 
47
- Manifest note: `@opencode/plugin` is capped at `>=2.0.12 <2.0.21`, since
48
- SDK `2.0.21+` raises its OpenTUI peer floor beyond this plugin's pinned
49
- `0.5.12` runtime deps and would break fresh `npm install` resolutions.
50
- This affects only local npm resolution — the host resolves the import at
51
- runtime, so OpenCode host versions above are unaffected.
47
+ Manifest note: `@opencode/plugin` is pinned `>=2.0.12 <2.1.0`. SDK
48
+ `2.1.0+` peers `@opentui/core >=0.6.6` and would break fresh
49
+ `npm install` resolutions against this plugin's `0.5.17` line; everything
50
+ up to `2.0.32` resolves cleanly since the `@opentui/*` pins moved to
51
+ `0.5.17` (whose own peer chain fixes `solid-js` at `1.9.12` — that exact
52
+ pin is contract, don't loosen it). This only shapes local npm trees —
53
+ the host resolves the import at runtime, so OpenCode host versions above
54
+ are unaffected. A full OpenTUI 0.6 migration unlocks `2.1.x+`.
52
55
 
53
56
  ## Usage
54
57
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaibhavgupta0786/opencode-voice",
3
- "version": "0.3.6",
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",
@@ -43,17 +43,17 @@
43
43
  "typecheck": "tsc --noEmit"
44
44
  },
45
45
  "dependencies": {
46
- "@opencode/plugin": ">=2.0.12 <2.0.21",
46
+ "@opencode/plugin": ">=2.0.12 <2.1.0",
47
47
  "solid-js": "1.9.12",
48
- "@opentui/solid": "0.5.12",
49
- "@opentui/core": "0.5.12"
48
+ "@opentui/solid": "0.5.17",
49
+ "@opentui/core": "0.5.17"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@opencode/client": "2.0.12",
53
53
  "@opentui/solid": "^0.5.0",
54
- "@types/node": "^22.0.0",
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.