Files
LLM-infa/api/route/convert_from_mp3.py
2026-02-19 16:43:48 +05:00

186 lines
6.3 KiB
Python

from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from fastapi.responses import FileResponse
import os
from pathlib import Path
import uuid
from typing import List
import asyncio
import subprocess
import shutil
import uuid
# moviepy is imported lazily inside the conversion function to avoid
# failing module import at application startup when the package is missing.
router = APIRouter(prefix="/convert", tags=["convert"])
@router.get("/list-audio", response_model=List[str])
async def list_audio_files():
base_dir = Path(__file__).resolve().parent.parent.parent / "data" / "audio"
if not base_dir.exists():
return []
files = [f.name for f in base_dir.iterdir() if f.is_file()]
return files
class DownloadAudioRequest(BaseModel):
filename: str
@router.post("/download-audio")
async def download_audio_file(data: DownloadAudioRequest):
base_dir = Path(__file__).resolve().parent.parent.parent / "data" / "audio"
src = (base_dir / data.filename).resolve()
try:
src.relative_to(base_dir.parent)
except Exception:
raise HTTPException(status_code=400, detail="Invalid filename")
if not src.exists():
raise HTTPException(status_code=404, detail="File not found")
return FileResponse(str(src), filename=src.name, media_type="audio/mpeg")
# New: convert a video file (located in ./data) to mp3 using bundled imageio-ffmpeg
class VideoToMp3Request(BaseModel):
filename: str
def _run_moviepy(src: Path, dest: Path):
from moviepy import VideoFileClip
clip = VideoFileClip(str(src))
try:
if clip.audio is None:
raise RuntimeError("Source file has no audio stream")
clip.audio.write_audiofile(str(dest), codec="libmp3lame", bitrate="192k")
finally:
clip.close()
@router.post("/video-to-mp3")
async def video_to_mp3(req: VideoToMp3Request):
# ensure MoviePy is available before doing work, return clear 503 if not
try:
import moviepy # noqa: F401
except Exception:
raise HTTPException(status_code=503, detail="moviepy is not installed in the runtime. Rebuild image or install moviepy/imageio-ffmpeg")
data_dir = Path(__file__).resolve().parent.parent.parent / "data"
src = (data_dir / req.filename).resolve()
try:
src.relative_to(data_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")
out_dir = data_dir / "audio"
out_dir.mkdir(parents=True, exist_ok=True)
dest = out_dir / f"{src.stem}.mp3"
try:
# run blocking MoviePy conversion in threadpool to avoid blocking event loop
await asyncio.get_running_loop().run_in_executor(None, _run_moviepy, src, dest)
except Exception as e:
raise HTTPException(status_code=500, detail=f"conversion failed: {e}")
return FileResponse(str(dest), filename=dest.name, media_type="audio/mpeg")
# Convert an existing MP3 (in data/audio) to WAV and run transcription
class Mp3ToTextRequest(BaseModel):
filename: str
model_path: str
def _run_ffmpeg(src: Path, dest: Path):
# produce mono 16k WAV suitable for Vosk
cmd = [
"ffmpeg",
"-y",
"-i",
str(src),
"-ac",
"1",
"-ar",
"16000",
"-vn",
"-f",
"wav",
str(dest),
]
subprocess.run(cmd, check=True)
@router.post("/mp3-to-text")
async def mp3_to_text(req: Mp3ToTextRequest):
logger = __import__("logging").getLogger("convert.mp3-to-text")
try:
if shutil.which("ffmpeg") is None:
raise HTTPException(status_code=503, detail="ffmpeg is not available in the runtime. Install ffmpeg in the environment/container.")
except Exception:
raise HTTPException(status_code=503, detail="ffmpeg is not available in the runtime. Install ffmpeg in the environment/container.")
data_dir = Path(__file__).resolve().parent.parent.parent / "data"
audio_dir = data_dir / "audio"
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")
audio_dir.mkdir(parents=True, exist_ok=True)
dest = audio_dir / f"{src.stem}_{uuid.uuid4().hex}.wav"
try:
logger.info("Starting ffmpeg conversion %s -> %s", src.name, dest.name)
await asyncio.get_running_loop().run_in_executor(None, _run_ffmpeg, src, dest)
logger.info("ffmpeg conversion finished")
except subprocess.CalledProcessError as e:
raise HTTPException(status_code=500, detail=f"ffmpeg conversion failed: {e}")
except Exception as e:
raise HTTPException(status_code=500, detail=f"conversion failed: {e}")
# Use class-based transcriber
try:
from api.lib.audio_processing import VoskModelManager
except Exception as e:
try:
dest.unlink()
except Exception:
pass
raise HTTPException(status_code=500, detail=f"Internal import error: {e}")
try:
logger.info("Starting transcription stage for %s", dest.name)
# Resolve model path (relative to project root or absolute) and ensure it's inside ./models
project_root = Path(__file__).resolve().parent.parent.parent
models_dir = project_root / "models"
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}")
mgr = VoskModelManager(model_path)
text = mgr.transcribe_wav(dest)
logger.info("Transcription stage finished for %s", dest.name)
finally:
try:
dest.unlink()
except Exception:
pass
return {"text": text}
# Note: this endpoint uses the bundled ffmpeg provided by `imageio-ffmpeg`.