Files
suno-mcp-server/NexusAI/sidecar/stt_server.py
T
OpenCode 02fc536e85 Add NexusAI: universal MCP service (TTS/STT/image/video/music/web-search)
Monorepo-lite with shared @nexusai/core and @nexusai/mcp-server (stdio).
Provider-agnostic tools with explicit provider selection; NordRouter adapter
(media generate/poll/download, estimate, upload, models) and search via
perplexity/sonar. Local STT via faster-whisper uv sidecar. SQLite journal of
every generation for usage stats and future admin. 10 MCP tools.
2026-08-17 17:06:47 +07:00

132 lines
4.4 KiB
Python

#!/usr/bin/env python3
# /// script
# requires-python = ">=3.9"
# dependencies = [
# "faster-whisper>=1.0.0",
# ]
# ///
"""
NexusAI local STT sidecar (faster-whisper).
Invoked by the TypeScript MCP server as:
uv run sidecar/stt_server.py '<json-request>'
Request JSON:
{ "audio": "/path/to/file", "model": "small", "device": "auto",
"compute_type": "auto", "language": "ru" }
Prints exactly ONE JSON line on stdout:
{ "ok": true, "text": "...", "language": "ru", "duration": 12.3,
"segments": [ { "start": 0.0, "end": 2.1, "text": "..." } ] }
On error:
{ "ok": false, "error": "message" }
All diagnostics go to stderr so stdout stays a clean single JSON line.
faster-whisper decodes audio via bundled ffmpeg/pyav, so no manual conversion
is required for common formats (mp3/wav/ogg/m4a/webm...).
"""
import json
import sys
def log(msg: str) -> None:
print(msg, file=sys.stderr, flush=True)
def resolve_device(device: str, compute_type: str):
if device and device != "auto":
ct = compute_type if compute_type and compute_type != "auto" else (
"float16" if device == "cuda" else "int8"
)
return device, ct
# auto-detect: try cuda, fall back to cpu
try:
import ctranslate2 # noqa: F401
cuda_count = 0
try:
cuda_count = ctranslate2.get_cuda_device_count()
except Exception:
cuda_count = 0
if cuda_count > 0:
return "cuda", (compute_type if compute_type != "auto" else "float16")
except Exception:
pass
return "cpu", (compute_type if compute_type != "auto" else "int8")
def main() -> int:
if len(sys.argv) < 2:
print(json.dumps({"ok": False, "error": "missing request argument"}))
return 1
try:
req = json.loads(sys.argv[1])
except Exception as exc:
print(json.dumps({"ok": False, "error": f"invalid request json: {exc}"}))
return 1
audio = req.get("audio")
if not audio:
print(json.dumps({"ok": False, "error": "no audio path provided"}))
return 1
model_size = req.get("model") or "small"
device, compute_type = resolve_device(req.get("device", "auto"), req.get("compute_type", "auto"))
language = req.get("language") or None
try:
from faster_whisper import WhisperModel
except Exception as exc:
print(json.dumps({"ok": False, "error": f"faster-whisper not available: {exc}"}))
return 1
# Allow pointing at a local model dir (offline) or a mirror via env.
# NEXUS_STT_MODEL may be a whisper size name OR an absolute path to a
# pre-downloaded CTranslate2 model directory.
import os
model_ref = model_size
local_only = os.environ.get("NEXUS_STT_LOCAL_ONLY", "").lower() in ("1", "true", "yes")
download_root = os.environ.get("NEXUS_STT_DOWNLOAD_ROOT") or None
try:
log(f"loading model={model_ref} device={device} compute={compute_type} local_only={local_only}")
model = WhisperModel(
model_ref,
device=device,
compute_type=compute_type,
download_root=download_root,
local_files_only=local_only,
)
segments_iter, info = model.transcribe(audio, language=language, vad_filter=True)
segments = []
parts = []
for seg in segments_iter:
parts.append(seg.text)
segments.append({"start": round(seg.start, 3), "end": round(seg.end, 3), "text": seg.text.strip()})
result = {
"ok": True,
"text": "".join(parts).strip(),
"language": info.language,
"duration": round(getattr(info, "duration", 0.0) or 0.0, 3),
"segments": segments,
}
print(json.dumps(result, ensure_ascii=False))
return 0
except Exception as exc:
hint = ""
msg = str(exc)
if "connect" in msg.lower() or "internet" in msg.lower() or "Hub" in msg:
hint = (
" | Model download failed. Options: set HF_ENDPOINT to a reachable mirror, "
"pre-download the model and set NEXUS_STT_MODEL to its local path with "
"NEXUS_STT_LOCAL_ONLY=1, or set NEXUS_STT_DOWNLOAD_ROOT to a cache dir."
)
print(json.dumps({"ok": False, "error": f"transcription failed: {exc}{hint}"}, ensure_ascii=False))
return 1
if __name__ == "__main__":
sys.exit(main())