Initial voice-assistant-stack: open-webui + spotify-voice-assistant submodules, asr/tts sidecars

- open-webui: submodule, LAN OIDC/Authentik-fronted, GPU-passthrough ollama,
  ollama-auth proxy, asr/tts wired via docker-compose.audio.yaml
- spotify-voice-assistant: submodule, Home Assistant custom integration
- tts-server / asr-server: plain directories, built as sidecars by
  open-webui/docker-stack.sh (no separate git history)
This commit is contained in:
2026-08-31 00:47:35 -04:00
commit c2b92d96cb
14 changed files with 307 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
FROM python:3.11-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
libsndfile1 ffmpeg \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# The Titan Xp (Pascal, sm_61) needs a torch build old enough to still ship
# Pascal kernels; recent default PyPI wheels dropped sm_60/sm_61 entirely.
ENV PIP_EXTRA_INDEX_URL=https://download.pytorch.org/whl/cu124
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8020
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8020"]
+80
View File
@@ -0,0 +1,80 @@
import hmac
import logging
import os
import tempfile
import torch
from fastapi import Depends, FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import JSONResponse, PlainTextResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from transformers import AutoModelForRNNT, AutoProcessor
from transformers.audio_utils import load_audio
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("asr-server")
AUTH_TOKEN = os.environ["ASR_AUTH_TOKEN"]
bearer_scheme = HTTPBearer()
def require_auth(creds: HTTPAuthorizationCredentials = Depends(bearer_scheme)) -> None:
if not hmac.compare_digest(creds.credentials, AUTH_TOKEN):
raise HTTPException(status_code=401, detail="invalid token")
MODEL_ID = os.environ.get("ASR_MODEL_ID", "nvidia/nemotron-3.5-asr-streaming-0.6b")
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
DTYPE = torch.float16 if DEVICE == "cuda" else torch.float32
# The RNN-T decoder's LSTM layers hit cuDNN's fused RNN kernel on `.to(device)`,
# which requires SM >= 7.5 in recent cuDNN builds. This GPU (Pascal, SM 6.1) is
# below that floor, so disable cuDNN and fall back to PyTorch's generic CUDA
# RNN kernels instead.
torch.backends.cudnn.enabled = False
app = FastAPI()
log.info("Loading %s on %s (%s)", MODEL_ID, DEVICE, DTYPE)
processor = AutoProcessor.from_pretrained(MODEL_ID)
model = AutoModelForRNNT.from_pretrained(MODEL_ID, dtype=DTYPE).to(DEVICE).eval()
SAMPLING_RATE = processor.feature_extractor.sampling_rate
log.info("Model ready (sampling_rate=%s)", SAMPLING_RATE)
@app.get("/health")
def health():
return {"status": "ok", "device": DEVICE}
@app.post("/v1/audio/transcriptions", dependencies=[Depends(require_auth)])
async def transcribe(
file: UploadFile = File(...),
model_name: str = Form("nemotron-3.5-asr-streaming-0.6b", alias="model"),
language: str | None = Form(None),
response_format: str = Form("json"),
):
suffix = os.path.splitext(file.filename or "")[1] or ".wav"
data = await file.read()
with tempfile.NamedTemporaryFile(suffix=suffix) as tmp:
tmp.write(data)
tmp.flush()
try:
audio = load_audio(tmp.name, sampling_rate=SAMPLING_RATE)
except Exception as exc:
raise HTTPException(status_code=400, detail=f"could not decode audio: {exc}") from exc
lang = language or "auto"
inputs = processor(audio, sampling_rate=SAMPLING_RATE, language=lang)
inputs = inputs.to(DEVICE, dtype=DTYPE)
with torch.inference_mode():
output = model.generate(**inputs, return_dict_in_generate=True)
text = processor.decode(output.sequences, skip_special_tokens=True)
if isinstance(text, list):
text = text[0] if text else ""
if response_format == "text":
return PlainTextResponse(text)
return JSONResponse({"text": text})
+8
View File
@@ -0,0 +1,8 @@
fastapi==0.115.*
uvicorn[standard]==0.32.*
python-multipart==0.0.*
torch==2.6.0
transformers>=5.13.0
soundfile
librosa
accelerate