89 lines
2.8 KiB
Python
89 lines
2.8 KiB
Python
#!/usr/bin/env python3
|
|
"""STT Server persistente (frigate.vpn:8883).
|
|
ASR: whisper.cpp (Vulkan) | Diarization opzionale: pyannote Community-1 (CPU)
|
|
|
|
Endpoints:
|
|
GET /health → {"status": "ok", "model": ...}
|
|
POST /transcribe → multipart: file=<audio> + form: lang, model, diarize
|
|
diarize=false (default): ASR only → {"text": "...", "segments": [...]}
|
|
diarize=true: ASR + diarization → segments con speaker
|
|
|
|
Uso: uvicorn server:app --host 0.0.0.0 --port 8883
|
|
"""
|
|
import asyncio, os, tempfile, time
|
|
from pathlib import Path
|
|
|
|
from fastapi import FastAPI, File, Form, UploadFile
|
|
from fastapi.responses import JSONResponse
|
|
|
|
import transcribe
|
|
|
|
app = FastAPI(title="STT Server", version="1.0")
|
|
|
|
# Semaforo: whisper-cli Vulkan e pyannote non sono thread-safe per uso concorrente
|
|
_lock = asyncio.Lock()
|
|
|
|
# Cache dei modelli già caricati (pyannote pipeline è costosa da caricare)
|
|
_pipeline_cache = {}
|
|
|
|
|
|
def _asr_only(audio_path: str, lang: str, model: str) -> list[dict]:
|
|
"""Path veloce: solo ASR (whisper.cpp Vulkan), niente diarization."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
segs = transcribe.asr(audio_path, model, lang, tmp)
|
|
return segs
|
|
|
|
|
|
def _full_pipeline(audio_path: str, lang: str, model: str) -> list[dict]:
|
|
"""Path completo: ASR + diarization pyannote."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
segs = transcribe.asr(audio_path, model, lang, tmp)
|
|
turns = transcribe.diarize(audio_path, tmp)
|
|
segs = transcribe.merge(segs, turns)
|
|
return segs
|
|
|
|
|
|
@app.get("/health")
|
|
async def health():
|
|
return {"status": "ok", "service": "stt", "model": "large-v3-turbo"}
|
|
|
|
|
|
@app.post("/transcribe")
|
|
async def transcribe_endpoint(
|
|
file: UploadFile = File(...),
|
|
lang: str = Form("it"),
|
|
model: str = Form("large-v3-turbo"),
|
|
diarize: bool = Form(False),
|
|
):
|
|
t0 = time.time()
|
|
# Salva l'upload in un file temporaneo
|
|
suffix = Path(file.filename or "audio.wav").suffix or ".wav"
|
|
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as tmp:
|
|
tmp.write(await file.read())
|
|
audio_path = tmp.name
|
|
try:
|
|
async with _lock:
|
|
loop = asyncio.get_running_loop()
|
|
if diarize:
|
|
segs = await loop.run_in_executor(None, _full_pipeline, audio_path, lang, model)
|
|
else:
|
|
segs = await loop.run_in_executor(None, _asr_only, audio_path, lang, model)
|
|
finally:
|
|
os.unlink(audio_path)
|
|
|
|
text = " ".join(s["text"] for s in segs).strip()
|
|
elapsed = round(time.time() - t0, 2)
|
|
return JSONResponse({
|
|
"text": text,
|
|
"segments": segs,
|
|
"diarize": diarize,
|
|
"lang": lang,
|
|
"model": model,
|
|
"elapsed_s": elapsed,
|
|
})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", "8883")))
|