from fastapi import APIRouter, HTTPException from pydantic import BaseModel from pathlib import Path import subprocess import shutil import json import logging try: from vosk import Model, KaldiRecognizer except Exception: Model = None KaldiRecognizer = None router = APIRouter(prefix="/convert", tags=["convert"]) class Mp3ToTextRequest(BaseModel): filename: str model_path: str @router.post("/mp3-to-text-ffmpeg") async def mp3_to_text_ffmpeg(req: Mp3ToTextRequest): """Convert MP3 -> PCM via ffmpeg (stdout) and transcribe with Vosk. This endpoint requires `ffmpeg` available in PATH. It decodes the input MP3 to raw PCM s16le 16k mono and streams it into Vosk recognizer. """ logger = logging.getLogger("convert.mp3") if Model is None or KaldiRecognizer is None: raise HTTPException(status_code=503, detail="vosk is not installed in runtime") project_root = Path(__file__).resolve().parent.parent.parent audio_dir = project_root / "data" / "audio" models_dir = project_root / "models" src = (audio_dir / req.filename).resolve() try: src.relative_to(audio_dir) except Exception: raise HTTPException(status_code=400, detail="Invalid filename") if not src.exists() or not src.is_file(): raise HTTPException(status_code=404, detail="Source file not found") model_path = Path(req.model_path) if not model_path.is_absolute(): model_path = project_root / req.model_path model_path = model_path.resolve() try: model_path.relative_to(models_dir) except Exception: raise HTTPException(status_code=400, detail="model_path must point inside the project's models directory") if not model_path.exists(): raise HTTPException(status_code=400, detail=f"Model path not found: {model_path}") if shutil.which("ffmpeg") is None: raise HTTPException(status_code=503, detail="ffmpeg is not available in the runtime. Install ffmpeg to use this endpoint.") try: from api.lib.audio_processing import FFmpegConverter, VoskModelManager except Exception as e: raise HTTPException(status_code=500, detail=f"Internal import error: {e}") if shutil.which("ffmpeg") is None: raise HTTPException(status_code=503, detail="ffmpeg is not available in the runtime. Install ffmpeg to use this endpoint.") # Stream decode via ffmpeg to temporary process and feed recognizer try: mgr = VoskModelManager(model_path) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to load Vosk model: {e}") cmd = [ "ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(src), "-f", "s16le", "-acodec", "pcm_s16le", "-ac", "1", "-ar", "16000", "-", ] logger.info("Starting ffmpeg streaming decode") try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to start ffmpeg: {e}") if proc.stdout is None: proc.kill() raise HTTPException(status_code=500, detail="ffmpeg did not provide stdout") recognizer = None try: recognizer = KaldiRecognizer(mgr.model, 16000) try: recognizer.SetWords(True) except Exception: pass bytes_read = 0 total = None try: total = src.stat().st_size except Exception: total = None last_pct = 0 while True: chunk = proc.stdout.read(4000) if not chunk: break recognizer.AcceptWaveform(chunk) bytes_read += len(chunk) if total: pct = int(bytes_read * 100 / total) if pct - last_pct >= 5: last_pct = pct logger.info("Decoding+transcription progress: %s%%", pct) result = json.loads(recognizer.FinalResult()) text = result.get("text", "") logger.info("Transcription finished; result length=%d", len(text)) return {"text": text} finally: try: proc.kill() except Exception: pass