@vaibhavgupta0786/opencode-voice 0.0.0-stage → 0.3.1
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/LICENSE +21 -0
- package/NOTICE +12 -0
- package/README.md +138 -2
- package/bin/SHA256SUMS +3 -0
- package/bin/darwin-arm64/opencode-voice-recorder +0 -0
- package/bin/darwin-x64/opencode-voice-recorder +0 -0
- package/bin/linux-x64/opencode-voice-recorder +0 -0
- package/docs/STT.md +64 -0
- package/docs/TROUBLESHOOTING.md +82 -0
- package/docs/VOICE-MODE.md +42 -0
- package/logic.ts +171 -0
- package/package.json +34 -4
- package/servers/mlx_server.py +94 -0
- package/skills/voice-check/SKILL.md +68 -0
- package/tui.tsx +733 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Minimal OpenAI-compatible speech-to-text server on Apple Silicon GPU.
|
|
3
|
+
|
|
4
|
+
Same API as the faster-whisper reference server, so opencode-voice needs no
|
|
5
|
+
changes: POST /v1/audio/transcriptions (multipart file + model) -> {"text"}.
|
|
6
|
+
|
|
7
|
+
Differences: mlx-whisper on the Metal GPU (~2x faster than CPU
|
|
8
|
+
faster-whisper/small on this class of machine) and zero temp files — the
|
|
9
|
+
uploaded WAV is decoded in memory with the stdlib wave module.
|
|
10
|
+
|
|
11
|
+
Config (env):
|
|
12
|
+
MLX_MODEL HF repo (default: mlx-community/whisper-small-mlx)
|
|
13
|
+
VOICE_STT_PORT listen port (default: 8080)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import io
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import wave
|
|
20
|
+
from email.parser import BytesParser
|
|
21
|
+
from email.policy import default as email_policy
|
|
22
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
import mlx_whisper
|
|
26
|
+
|
|
27
|
+
MODEL = os.environ.get("MLX_MODEL", "mlx-community/whisper-small-mlx")
|
|
28
|
+
PORT = int(os.environ.get("VOICE_STT_PORT", "8080"))
|
|
29
|
+
|
|
30
|
+
print(f"loading {MODEL} ...", flush=True)
|
|
31
|
+
# Warm up by loading weights once; per-request calls reuse them.
|
|
32
|
+
_MODEL_REF = MODEL
|
|
33
|
+
print("ready", flush=True)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def wav_bytes_to_float32(raw: bytes) -> np.ndarray:
|
|
37
|
+
with wave.open(io.BytesIO(raw), "rb") as w:
|
|
38
|
+
if w.getframerate() != 16_000 or w.getnchannels() != 1 or w.getsampwidth() != 2:
|
|
39
|
+
raise ValueError(
|
|
40
|
+
f"expected 16kHz mono s16, got {w.getframerate()}Hz "
|
|
41
|
+
f"{w.getnchannels()}ch {w.getsampwidth() * 8}bit"
|
|
42
|
+
)
|
|
43
|
+
frames = w.readframes(w.getnframes())
|
|
44
|
+
return np.frombuffer(frames, dtype=np.int16).astype(np.float32) / 32768.0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class Handler(BaseHTTPRequestHandler):
|
|
48
|
+
def _json(self, code: int, payload: dict) -> None:
|
|
49
|
+
body = json.dumps(payload).encode()
|
|
50
|
+
self.send_response(code)
|
|
51
|
+
self.send_header("Content-Type", "application/json")
|
|
52
|
+
self.send_header("Content-Length", str(len(body)))
|
|
53
|
+
self.end_headers()
|
|
54
|
+
self.wfile.write(body)
|
|
55
|
+
|
|
56
|
+
def do_GET(self): # noqa: N802
|
|
57
|
+
if self.path == "/health":
|
|
58
|
+
self._json(200, {"status": "ok"})
|
|
59
|
+
else:
|
|
60
|
+
self._json(404, {"error": "not found"})
|
|
61
|
+
|
|
62
|
+
def do_POST(self): # noqa: N802
|
|
63
|
+
if not self.path.rstrip("/").endswith("/audio/transcriptions"):
|
|
64
|
+
self._json(404, {"error": "not found"})
|
|
65
|
+
return
|
|
66
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
67
|
+
raw = self.rfile.read(length)
|
|
68
|
+
ctype = self.headers.get("Content-Type", "")
|
|
69
|
+
try:
|
|
70
|
+
msg = BytesParser(policy=email_policy).parsebytes(
|
|
71
|
+
f"Content-Type: {ctype}\r\nMIME-Version: 1.0\r\n\r\n".encode() + raw
|
|
72
|
+
)
|
|
73
|
+
audio: bytes | None = None
|
|
74
|
+
for part in msg.iter_parts():
|
|
75
|
+
if part.get_filename():
|
|
76
|
+
audio = part.get_content()
|
|
77
|
+
break
|
|
78
|
+
if audio is None:
|
|
79
|
+
self._json(400, {"error": "no file part"})
|
|
80
|
+
return
|
|
81
|
+
samples = wav_bytes_to_float32(bytes(audio))
|
|
82
|
+
result = mlx_whisper.transcribe(samples, path_or_hf_repo=_MODEL_REF)
|
|
83
|
+
self._json(200, {"text": (result.get("text") or "").strip()})
|
|
84
|
+
except Exception as error: # keep the endpoint fail-closed with a reason
|
|
85
|
+
self._json(500, {"error": f"{type(error).__name__}: {error}"})
|
|
86
|
+
|
|
87
|
+
def log_message(self, *args): # quieter logs
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
|
|
93
|
+
print(f"listening on 127.0.0.1:{PORT} model={MODEL}", flush=True)
|
|
94
|
+
server.serve_forever()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Voice Check
|
|
3
|
+
description: Check and restart the local whisper STT server that the opencode-voice dictation plugin uses on port 8080. Use when voice dictation fails, an "STT failed" error appears, or the user asks to fix, prepare, check, or bring back voice input.
|
|
4
|
+
slash: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Voice STT server check
|
|
8
|
+
|
|
9
|
+
The opencode-voice plugin transcribes dictation through a local whisper
|
|
10
|
+
server at http://127.0.0.1:8080. When that server is down, every take
|
|
11
|
+
fails with "STT failed". Work these steps in order; stop at the first one
|
|
12
|
+
that resolves the situation.
|
|
13
|
+
|
|
14
|
+
## 1. Health check
|
|
15
|
+
|
|
16
|
+
curl -s -m 3 http://127.0.0.1:8080/health
|
|
17
|
+
|
|
18
|
+
If it prints `{"status": "ok"}`, the server is up — report that dictation
|
|
19
|
+
should work (press Fn+F9) and stop. Nothing is broken.
|
|
20
|
+
|
|
21
|
+
## 2. Locate the server install
|
|
22
|
+
|
|
23
|
+
Check these directories in order; the server dir is the first that
|
|
24
|
+
contains a server file and a Python virtualenv:
|
|
25
|
+
|
|
26
|
+
1. `~/.local/share/opencode/voice-stt` — canonical location (see the
|
|
27
|
+
opencode-voice repo's `docs/STT.md`)
|
|
28
|
+
2. `~/.local/share/opencode/opencode-dictate` — legacy location
|
|
29
|
+
|
|
30
|
+
Inside the server dir, expect one of:
|
|
31
|
+
|
|
32
|
+
- `mlx_server.py` + `mlx-env/` — mlx-whisper on the Apple Silicon GPU
|
|
33
|
+
(preferred), or
|
|
34
|
+
- `server.py` + `stt-env/` — faster-whisper on CPU
|
|
35
|
+
|
|
36
|
+
If neither directory exists, the STT server was never installed. Point the
|
|
37
|
+
user to the opencode-voice repo's `docs/STT.md` and stop.
|
|
38
|
+
|
|
39
|
+
## 3. Restart the server
|
|
40
|
+
|
|
41
|
+
Clear anything still holding the port (ignore errors):
|
|
42
|
+
|
|
43
|
+
lsof -ti:8080 | xargs kill -9
|
|
44
|
+
|
|
45
|
+
Start detached. If `mlx_server.py` exists:
|
|
46
|
+
|
|
47
|
+
cd <server-dir> && nohup ./mlx-env/bin/python3 mlx_server.py > mlx-server.log 2>&1 &
|
|
48
|
+
|
|
49
|
+
otherwise:
|
|
50
|
+
|
|
51
|
+
cd <server-dir> && WHISPER_MODEL=small nohup ./stt-env/bin/python3 server.py > stt-server.log 2>&1 &
|
|
52
|
+
|
|
53
|
+
## 4. Wait for boot, then re-verify
|
|
54
|
+
|
|
55
|
+
Poll the health endpoint every 5 seconds, up to 30 seconds total — the
|
|
56
|
+
model loads from cache in roughly 10-25 s on a cold start:
|
|
57
|
+
|
|
58
|
+
curl -s -m 3 http://127.0.0.1:8080/health
|
|
59
|
+
|
|
60
|
+
## 5. If it still will not start
|
|
61
|
+
|
|
62
|
+
Show the last 10-20 lines of the server log (`mlx-server.log` or
|
|
63
|
+
`stt-server.log`) and report the error you see. Do not guess further.
|
|
64
|
+
|
|
65
|
+
## 6. Report
|
|
66
|
+
|
|
67
|
+
One line: server was up / has been restarted / failed (with the error).
|
|
68
|
+
Remind the user to press Fn+F9 to dictate.
|