Files
LLM-infa/api_legacy/route/mp3_ffmpeg_stream.py
jze9 2697e01714 Рабочий пайплайн: YouTube -> транскрипция -> выжимка -> Postgres
- новый api/: /pipeline/process (yt-dlp, субтитры или Vosk, LLM/экстрактивная
  суммаризация), /videos, /llm; старый код перенесён в api_legacy/
- web/: Flet UI (flet 0.28.3 + flet-web, порт 8550)
- Dockerfile.api: ffmpeg слоем из mwader/static-ffmpeg, pip с кэш-маунтом
- requirements/pyproject: убраны moviepy, imageio-ffmpeg, битый asyncio
- README с инструкцией запуска и загрузкой Vosk-модели

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 11:12:46 +05:00

139 lines
4.2 KiB
Python

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