diff --git a/.woodpecker/build-amd64.yml b/.woodpecker/build-amd64.yml index b3ce7c8..816daa1 100644 --- a/.woodpecker/build-amd64.yml +++ b/.woodpecker/build-amd64.yml @@ -7,7 +7,9 @@ steps: image: bash commands: - attic login lounge-rocks https://cache.lounge.rocks $ATTIC_KEY --set-default - secrets: [attic_key] + environment: + ATTIC_KEY: + from_secret: attic_key - name: build whisper_api image: bash diff --git a/Dockerfile b/Dockerfile index fa301e5..65a520d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,8 @@ # newest version: # https://hub.docker.com/r/nvidia/cuda/tags?page=1&name=-base-ubuntu22.04&ordering=name -ARG CUDA_VERSION=12.4.1 -FROM nvidia/cuda:${CUDA_VERSION}-base-ubuntu22.04 - -ARG PYTHON_VERSION=3.10 +ARG CUDA_VERSION=12.6.2 +FROM nvidia/cuda:${CUDA_VERSION}-base-ubuntu24.04 WORKDIR /workspace @@ -12,14 +10,13 @@ RUN export DEBIAN_FRONTEND=noninteractive && \ apt-get -qq update && \ apt-get -qq install --no-install-recommends \ ffmpeg \ - python${PYTHON_VERSION} \ - python${PYTHON_VERSION}-venv \ + python3 \ + python3-venv \ python3-pip && \ - rm -rf /var/lib/apt/lists/* && \ - pip3 install --upgrade pip + rm -rf /var/lib/apt/lists/* COPY requirements.txt requirements.txt -RUN pip3 install -r requirements.txt +RUN pip3 install -r requirements.txt --break-system-packages # disabled by default since GitHub Actions do not have enough space ARG PREFETCH_MODEL=0 @@ -32,7 +29,7 @@ RUN if [ "$PREFETCH_MODEL" != 0 ]; then \ COPY . /workspace/code RUN cd /workspace/code && \ - pip3 install . + pip3 install . --break-system-packages ENV PORT=3001 \ LISTEN=0.0.0.0 \ diff --git a/flake.lock b/flake.lock index 873d3e5..59b740a 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1730200266, - "narHash": "sha256-l253w0XMT8nWHGXuXqyiIC/bMvh1VRszGXgdpQlfhvU=", + "lastModified": 1767892417, + "narHash": "sha256-dhhvQY67aboBk8b0/u0XB6vwHdgbROZT3fJAjyNh5Ww=", "owner": "nixos", "repo": "nixpkgs", - "rev": "807e9154dcb16384b1b765ebe9cd2bba2ac287fd", + "rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 072e060..ae371b1 100644 --- a/flake.nix +++ b/flake.nix @@ -33,8 +33,8 @@ (system: nixpkgsFor.${system}.nixpkgs-fmt); overlays.default = final: prev: { - devShell = final.callPackage nixos/devShell { inherit self; }; - whisper_api = final.callPackage nixos/pkgs/whisper_api { inherit self; }; + devShell = final.python3Packages.callPackage nixos/devShell { inherit self; }; + whisper_api = final.python3Packages.callPackage nixos/pkgs/whisper_api { inherit self; }; # Our code is not compatible with pydantic version 2 yet. python3 = prev.python3.override { packageOverrides = python-self: python-super: { diff --git a/nixos/pkgs/whisper_api/default.nix b/nixos/pkgs/whisper_api/default.nix index de2e887..ef1db10 100644 --- a/nixos/pkgs/whisper_api/default.nix +++ b/nixos/pkgs/whisper_api/default.nix @@ -1,9 +1,24 @@ -{ self -, lib -, python3 -, +{ + self, + lib, + buildPythonApplication, + + # build-system + setuptools, + pythonRelaxDepsHook, + + # dependencies + fastapi, + ffmpeg-python, + openai-whisper, + python-multipart, + uvicorn, + + # tests + unittestCheckHook, + httpx, }: -python3.pkgs.buildPythonApplication { +buildPythonApplication { pname = "whisper_api"; @@ -18,12 +33,12 @@ python3.pkgs.buildPythonApplication { pythonRelaxDeps = [ ]; - nativeBuildInputs = with python3.pkgs; [ + nativeBuildInputs = [ setuptools pythonRelaxDepsHook ]; - propagatedBuildInputs = with python3.pkgs; [ + propagatedBuildInputs = [ fastapi ffmpeg-python openai-whisper @@ -31,7 +46,7 @@ python3.pkgs.buildPythonApplication { uvicorn ]; - nativeCheckInputs = with python3.pkgs; [ + nativeCheckInputs = [ unittestCheckHook httpx ]; diff --git a/pyproject.toml b/pyproject.toml index 2717a79..7f6abd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "whisper_api" dynamic = ["dependencies", "version"] description = "A simple API for whisper" readme = "README.md" -requires-python = ">=3.10" +requires-python = "~=3.12" license = { text = "" } authors = [ { name = "MayNiklas", email = "info@niklas-steffen.de" }, diff --git a/requirements.txt b/requirements.txt index 139d147..822f1e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -fastapi==0.112.0 +fastapi==0.121.1 ffmpeg-python==0.2.0 -openai-whisper==20240930 -pydantic==2.8.2 -python-multipart==0.0.9 -uvicorn==0.29.0 +openai-whisper==20250625 +pydantic==2.12.4 +python-multipart==0.0.20 +uvicorn==0.38.0 diff --git a/src/whisper_api/__init__.py b/src/whisper_api/__init__.py index ee2b9cd..a3ff0f1 100644 --- a/src/whisper_api/__init__.py +++ b/src/whisper_api/__init__.py @@ -1,3 +1,4 @@ +import multiprocessing import os import sys @@ -6,10 +7,19 @@ path = os.path.realpath(os.path.abspath(__file__)) sys.path.insert(0, os.path.dirname(os.path.dirname(path))) -__version__ = "20241105" +__version__ = "20251214" -from whisper_api.main import app # isort: skip -from whisper_api.main import start + +from whisper_api.main import am_i_main_process +def start(): + """Mock Version of start() that is overwritten in the main process by the import below.""" + name = multiprocessing.current_process().name + raise ImportError(f"You're trying to run start() from another process than Main " + f"(you are: '{name}'). This is currently not supported.") + +if am_i_main_process(): + from whisper_api.main import app # isort: skip + from whisper_api.main import start if __name__ == "__main__": start() diff --git a/src/whisper_api/main.py b/src/whisper_api/main.py index a44a72b..2df6bf6 100644 --- a/src/whisper_api/main.py +++ b/src/whisper_api/main.py @@ -47,20 +47,25 @@ from whisper_api.log_setup import logger from whisper_api.log_setup import uuid_log_format -IS_MAIN_PROCESS = multiprocessing.current_process().name == "MainProcess" + +def am_i_process(name: str) -> bool: + return multiprocessing.current_process().name == name + +def am_i_main_process() -> bool: + return am_i_process("MainProcess") description = """ Whisper API transcribes audio files. """ -if IS_MAIN_PROCESS: +if am_i_main_process(): print(description) """ init global variables """ -if IS_MAIN_PROCESS: +if am_i_main_process(): # TODO: can tasks get GCed before they finish if queue is too long? task_dict: TempDict[uuid_hex_t, Task] = TempDict( expiration_time_m=DELETE_RESULTS_AFTER_M, @@ -284,7 +289,7 @@ async def lifespan(_app: FastAPI): """ Init API """ -if IS_MAIN_PROCESS: +if am_i_main_process(): app = FastAPI( title="Whisper API", description=description, @@ -346,18 +351,19 @@ async def log_requests(req: Request, call_next): return resp -""" -Hook for uvicorn -""" + """ + Hook for uvicorn + """ -def start(): - import uvicorn + def start(): + """Entrypoint to start the API (note: this version is only imported on the MainProcess (see __init.py__)""" + import uvicorn - # TODO: - # forwarded_allow_ips= should be set via env var - # proxy_headers=True only when needed - uvicorn.run(app, host=API_LISTEN, port=API_PORT, proxy_headers=True, forwarded_allow_ips="*", log_level="warning") + # TODO: + # forwarded_allow_ips= should be set via env var + # proxy_headers=True only when needed + uvicorn.run(app, host=API_LISTEN, port=API_PORT, proxy_headers=True, forwarded_allow_ips="*", log_level="warning") if __name__ == "__main__": diff --git a/test/test_import.py b/test/test_import.py index 036e367..2bad8cb 100644 --- a/test/test_import.py +++ b/test/test_import.py @@ -9,4 +9,29 @@ def test_import_whisper_api(self): assert whisper_api except ImportError: - unittest.fail("Failed to import whisper_api") + unittest.fail("Failed to 'import whisper_api'") + + def test_from_import_app(self): + try: + from whisper_api import app + + assert app + + except ImportError: + unittest.fail("Failed to 'from whisper_api import app'") + + + def test_from_import_start(self): + try: + from whisper_api import start + + assert start + + assert "Mock Version" not in start.__doc__ + + except ImportError: + unittest.fail("Failed to 'from whisper_api import real implementation of start()' on main process") + + + + # TODO: write a test that attempt to import from a a mp.Process and assert that app does not exist \ No newline at end of file