diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9c72807..8baf75e 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -24,3 +24,11 @@ updates: day: "monday" commit-message: prefix: "chore(docker)" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + commit-message: + prefix: "ci" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c7aadeb --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +jobs: + docker-smoke: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v7 + + - name: Build image + run: docker build -t audio-splitter:ci . + + - name: Start container (read-only, like the Helm chart) + run: | + docker run -d --name app --read-only \ + --tmpfs /tmp:uid=1000,gid=1000 -e TMPDIR=/tmp \ + --cap-drop ALL --security-opt no-new-privileges \ + -p 8000:8000 audio-splitter:ci + + - name: Smoke test + shell: bash # -eo pipefail: a failing curl fails the step, not just the assert + run: | + for _ in $(seq 30); do curl -sf localhost:8000/health && break; sleep 1; done + curl -sS --fail-with-body localhost:8000/ready + docker run --rm --entrypoint ffmpeg audio-splitter:ci -v error \ + -f lavfi -i sine=frequency=440:duration=20 -b:a 128k -f mp3 - > tone.mp3 + curl -sS --fail-with-body -F file=@tone.mp3 -F chunk_size_mb=0.1 localhost:8000/split \ + | python3 -c 'import json, sys; c = json.load(sys.stdin); assert len(c) > 1, c; print(len(c), "chunks")' + + - name: Container logs + if: always() + run: docker logs app diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d3f3e9..d2fe2c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Local Development Setup -**Prerequisites:** Python 3.13, FFmpeg (with `ffprobe`) installed and on `PATH`. +**Prerequisites:** Python 3.14, FFmpeg (with `ffprobe`) installed and on `PATH`. ```bash # Create and activate a virtual environment diff --git a/README.md b/README.md index d3cde92..5adfe42 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Audio Splitter [![License: MPL 2.0](https://img.shields.io/badge/License-MPL_2.0-brightgreen.svg)](https://opensource.org/licenses/MPL-2.0) -[![Python](https://img.shields.io/badge/python-3.13-blue.svg)](https://www.python.org/downloads/) +[![Python](https://img.shields.io/badge/python-3.14-blue.svg)](https://www.python.org/downloads/) [![FastAPI](https://img.shields.io/badge/FastAPI-0.142.2-009688.svg)](https://fastapi.tiangolo.com) A FastAPI-based HTTP microservice that splits large audio files into smaller chunks using FFmpeg. Designed for integration with workflow automation tools such as n8n, and deployable as a Docker container or Kubernetes workload. @@ -31,7 +31,7 @@ The service is now available at `http://localhost:8000`. ### Local Development -**Prerequisites:** Python 3.13, FFmpeg installed and on `PATH`. +**Prerequisites:** Python 3.14, FFmpeg installed and on `PATH`. ```bash # Create and activate a virtual environment @@ -61,7 +61,7 @@ Splits an uploaded audio file into chunks of a specified size. | `chunk_size_mb` | float | `10.0` | Target size of each output chunk in MB (min: 0.1, max: 500) | | `output_prefix` | string | `chunk` | Filename prefix for generated chunks (alphanumeric, hyphens, underscores only) | | `same_as_input` | bool | `true` | Keep the same container format as the input | -| `output_format` | string | `null` | Output container (e.g. `m4a` for AAC input, `ogg` for Opus input). Audio is copied, not re-encoded, so the container must support the input codec, otherwise the request fails with `500`. Only used when `same_as_input` is `false` | +| `output_format` | string | `null` | Output container, case-insensitive (e.g. `m4a` for AAC input, `ogg` for Opus input). Audio is copied, not re-encoded, so the container must support the input codec, otherwise the request fails with `500`. Only used when `same_as_input` is `false` | | `memory_mode` | string | `auto` | Upload strategy: `auto` (same as `streaming`), `streaming` (writes the upload to disk in 1 MB chunks), or `buffered` (reads it into memory) | #### Response @@ -89,7 +89,7 @@ Returns a JSON array. Each element represents one chunk. The `duration` field is | Status | Condition | |---|---| -| `400` | Unsupported file format, invalid `chunk_size_mb` range, invalid `output_format`, or too many chunks generated | +| `400` | Unsupported file format, invalid `chunk_size_mb` range, invalid `output_format`, audio bitrate below 1 kbps, or too many chunks generated | | `413` | File exceeds `MAX_FILE_SIZE_MB` | | `422` | Request validation error (e.g. non-numeric `chunk_size_mb`, unknown `memory_mode`, missing `file`); `detail` is a list of field errors | | `500` | FFmpeg could not process the audio (corrupt input, or `output_format` incompatible with the input codec), processing timed out (`FFMPEG_TIMEOUT_SECONDS`), or unexpected server error | diff --git a/app/main.py b/app/main.py index 6d32d4f..bd4b9ba 100644 --- a/app/main.py +++ b/app/main.py @@ -110,6 +110,11 @@ async def split_audio( ) -> list[str]: """Split audio file using FFmpeg segment muxer.""" bitrate = await get_audio_bitrate(input_path) + if bitrate < 1: # no real audio is under 1 kbps; segmenting would divide by zero + raise HTTPException( + status_code=400, + detail="Unsupported audio: bitrate is below 1 kbps" + ) segment_time = calculate_segment_time(chunk_size_mb, bitrate) output_pattern = os.path.join(output_dir, f"{output_prefix}_%03d{output_ext}") @@ -280,7 +285,7 @@ async def split_audio_endpoint( output_ext = ext if not same_as_input and output_format: - output_ext = f".{output_format.lstrip('.')}" + output_ext = f".{output_format.lstrip('.').lower()}" if output_ext not in SUPPORTED_FORMATS: raise HTTPException( status_code=400, diff --git a/helm/audio-splitter/Chart.yaml b/helm/audio-splitter/Chart.yaml index cc2c8e9..8db04a1 100644 --- a/helm/audio-splitter/Chart.yaml +++ b/helm/audio-splitter/Chart.yaml @@ -2,5 +2,5 @@ apiVersion: v2 name: audio-splitter description: HTTP service for splitting audio files into chunks using FFmpeg type: application -version: 0.1.0 -appVersion: "1.0.0" +version: 0.1.1 +appVersion: "1.0.1"