- новый 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>
139 lines
4.2 KiB
Python
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
|