Add local TTS/STT wrapper servers (chatterbox, faster-whisper) as openai_compatible providers
Wraps the locally installed chatterbox-tts and faster-whisper packages in thin FastAPI servers implementing OpenAI's audio API shape, since neither Ollama nor OpenRouter support speech. Pinned to GPU 2 to avoid contending with Ollama's resident models on GPU 1. Requires UFW rules for 8901/8902 (same pattern as the existing 11434 rule) since UFW defaults to deny-incoming. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
114dfacba8
commit
fb1fbcfabe
4 changed files with 198 additions and 0 deletions
64
services/stt_server.py
Normal file
64
services/stt_server.py
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
#!/usr/bin/env python3
|
||||
"""OpenAI-compatible STT wrapper around faster-whisper, for Open Notebook.
|
||||
|
||||
Implements exactly the endpoint esperanto's OpenAICompatibleSpeechToTextModel calls:
|
||||
POST /audio/transcriptions (multipart: file, model, language?, prompt?) -> {"text": ...}
|
||||
|
||||
Start:
|
||||
python3 stt_server.py
|
||||
|
||||
Env vars:
|
||||
STT_HOST (default 0.0.0.0), STT_PORT (default 8902)
|
||||
WHISPER_MODEL (default large-v3), WHISPER_COMPUTE_TYPE (default float16)
|
||||
CUDA_VISIBLE_DEVICES should be set by the caller (e.g. "1,2") to keep GPU 0 free.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
from faster_whisper import WhisperModel
|
||||
from fastapi import FastAPI, File, Form, UploadFile
|
||||
|
||||
app = FastAPI(title="faster-whisper STT (OpenAI-compatible)", version="1.0")
|
||||
|
||||
_MODEL_SIZE = os.environ.get("WHISPER_MODEL", "large-v3")
|
||||
_COMPUTE_TYPE = os.environ.get("WHISPER_COMPUTE_TYPE", "float16")
|
||||
_model = WhisperModel(_MODEL_SIZE, device="cuda", compute_type=_COMPUTE_TYPE)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "model": _MODEL_SIZE}
|
||||
|
||||
|
||||
@app.post("/audio/transcriptions")
|
||||
async def transcriptions(
|
||||
file: UploadFile = File(...),
|
||||
model: str = Form(default="whisper-1"),
|
||||
language: str | None = Form(default=None),
|
||||
prompt: str | None = Form(default=None),
|
||||
):
|
||||
suffix = Path(file.filename or "audio").suffix or ".wav"
|
||||
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
|
||||
tmp.write(await file.read())
|
||||
tmp_path = tmp.name
|
||||
|
||||
try:
|
||||
segments, _info = _model.transcribe(
|
||||
tmp_path,
|
||||
language=language,
|
||||
initial_prompt=prompt,
|
||||
)
|
||||
text = "".join(segment.text for segment in segments).strip()
|
||||
finally:
|
||||
os.unlink(tmp_path)
|
||||
|
||||
return {"text": text}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host=os.environ.get("STT_HOST", "0.0.0.0"),
|
||||
port=int(os.environ.get("STT_PORT", "8902")))
|
||||
Loading…
Add table
Add a link
Reference in a new issue