From 564c9c2d2f9f67f7f208c16a0bf8b627e08b081e Mon Sep 17 00:00:00 2001 From: jze9 Date: Sun, 1 Mar 2026 21:42:05 +0500 Subject: [PATCH] large update --- Dockerfile.api | 36 +++++++++----- api/main.py | 3 +- api/route/media_convert.py | 2 + api/route/moviepy.py | 97 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 api/route/moviepy.py diff --git a/Dockerfile.api b/Dockerfile.api index 94b4164..aa9b826 100644 --- a/Dockerfile.api +++ b/Dockerfile.api @@ -4,11 +4,11 @@ FROM python:3.14-slim AS builder WORKDIR /app -RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - +#RUN apt-get update && apt-get install -y --no-install-recommends \ +# build-essential \ +# ca-certificates \ +# && rm -rf /var/lib/apt/lists/* +# COPY requirements.txt ./ # Build wheels into /wheels (use cache for pip downloads) RUN --mount=type=cache,target=/root/.cache/pip \ @@ -19,18 +19,32 @@ RUN --mount=type=cache,target=/root/.cache/pip \ FROM python:3.14-slim AS final WORKDIR /app -# minimal runtime deps -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - aria2 \ - ffmpeg \ - && rm -rf /var/lib/apt/lists/* +## minimal runtime deps (use static ffmpeg to avoid many apt deps) +#RUN apt-get update && apt-get install -y --no-install-recommends \ +# ca-certificates \ +# curl \ +# && rm -rf /var/lib/apt/lists/* # Copy wheels from builder and install COPY --from=builder /wheels /wheels RUN --mount=type=cache,target=/root/.cache/pip \ pip install --no-cache-dir /wheels/* || pip install --no-cache-dir /wheels/* --no-deps +# Download a static ffmpeg build (faster than installing via apt and avoids many deps) +RUN set -eux; \ + arch="$(dpkg --print-architecture)"; \ + if [ "$arch" = "amd64" ]; then \ + FFMPEG_URL="https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-amd64-static.tar.xz"; \ + else \ + echo "Unsupported arch $arch"; exit 1; \ + fi; \ + curl -fsSL "$FFMPEG_URL" -o /tmp/ffmpeg.tar.xz; \ + tar -xJf /tmp/ffmpeg.tar.xz -C /tmp; \ + cp /tmp/ffmpeg-*-amd64-static/ffmpeg /usr/local/bin/; \ + cp /tmp/ffmpeg-*-amd64-static/ffprobe /usr/local/bin/; \ + chmod +x /usr/local/bin/ffmpeg /usr/local/bin/ffprobe; \ + rm -rf /tmp/ffmpeg* + # Copy application code (exclude models/data via .dockerignore) COPY . ./api diff --git a/api/main.py b/api/main.py index 58389f7..3edfa15 100644 --- a/api/main.py +++ b/api/main.py @@ -2,7 +2,7 @@ import asyncio from fastapi import FastAPI from api.route import default, subscription # renamed route modules for clarity -from api.route import users, installing, media_convert, mp3_ffmpeg_stream +from api.route import users, installing, media_convert, mp3_ffmpeg_stream, moviepy #from api.db.subscription import init_db #from api.db.connection import wait_for_db @@ -19,3 +19,4 @@ app.include_router(default.router) app.include_router(installing.router) app.include_router(media_convert.router) app.include_router(mp3_ffmpeg_stream.router) +app.include_router(moviepy.router) diff --git a/api/route/media_convert.py b/api/route/media_convert.py index 292841d..1337929 100644 --- a/api/route/media_convert.py +++ b/api/route/media_convert.py @@ -127,6 +127,8 @@ async def video_to_wav(req: VideoToWavRequest): return FileResponse(str(dest), filename=dest.name, media_type="audio/wav") +# MoviePy video-to-audio endpoints moved to api/route/moviepy.py + class Mp3ToTextRequest(BaseModel): filename: str diff --git a/api/route/moviepy.py b/api/route/moviepy.py new file mode 100644 index 0000000..c60e2a9 --- /dev/null +++ b/api/route/moviepy.py @@ -0,0 +1,97 @@ +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel +from fastapi.responses import FileResponse +from pathlib import Path +import asyncio + +router = APIRouter(prefix="/moviepy", tags=["moviepy"]) + + +class VideoToMp3Request(BaseModel): + filename: str + + +class VideoToWavRequest(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() + + +def _run_moviepy_to_wav(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), fps=16000, nbytes=2) + finally: + clip.close() + + +@router.post("/video-to-mp3") +async def video_to_mp3(req: VideoToMp3Request): + try: + import moviepy # noqa: F401 + except Exception: + raise HTTPException(status_code=503, detail="moviepy is not installed in the runtime. Install moviepy/imageio-ffmpeg") + + project_root = Path(__file__).resolve().parent.parent.parent + data_dir = project_root / "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: + 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") + + +@router.post("/video-to-wav") +async def video_to_wav(req: VideoToWavRequest): + try: + import moviepy # noqa: F401 + except Exception: + raise HTTPException(status_code=503, detail="moviepy is not installed in the runtime. Install moviepy/imageio-ffmpeg") + + project_root = Path(__file__).resolve().parent.parent.parent + data_dir = project_root / "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}.wav" + + try: + await asyncio.get_running_loop().run_in_executor(None, _run_moviepy_to_wav, 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/wav")