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>
64 lines
1.9 KiB
Python
64 lines
1.9 KiB
Python
#!/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")))
|