Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
41 changes: 41 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 |
Expand Down
7 changes: 6 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions helm/audio-splitter/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Loading