Series 7 — Part 5 of 6

The audio converter is a Python HTTP microservice: it accepts a WAV file upload, converts it to OGG/OPUS via FFmpeg, and returns the OGG bytes. Using Python's stdlib http.server means zero dependencies and minimal attack surface. This article covers the complete implementation.

The Microservice

#!/usr/bin/env python3
"""
converter/server.py — WAV → OGG/OPUS conversion microservice
Runs on port 8882. No third-party dependencies.
"""

import http.server
import os
import subprocess
import tempfile
import cgi
import json


class ConversionHandler(http.server.BaseHTTPRequestHandler):

    def log_message(self, format, *args):
        pass  # Suppress default request logging; use structured logging instead

    def do_GET(self):
        if self.path == "/health":
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(b'{"status":"ok"}')
        else:
            self.send_error(404)

    def do_POST(self):
        if self.path != "/convert":
            self.send_error(404)
            return

        ctype, pdict = cgi.parse_header(self.headers.get("Content-Type", ""))
        if ctype != "multipart/form-data":
            self.send_error(400, "multipart/form-data required")
            return

        pdict["boundary"] = bytes(pdict["boundary"], "utf-8")
        fields = cgi.parse_multipart(self.rfile, pdict)
        file_data = fields.get("file", [None])[0]

        if file_data is None:
            self.send_error(400, "No file field in request")
            return

        wav_tmp = ogg_tmp = None
        try:
            # Write WAV to temp file
            with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
                f.write(file_data)
                wav_tmp = f.name

            # Convert with FFmpeg
            ogg_tmp = wav_tmp.replace(".wav", ".ogg")
            result = subprocess.run(
                ["ffmpeg", "-y", "-i", wav_tmp,
                 "-c:a", "libopus", "-b:a", "48k", "-vbr", "on",
                 "-ar", "48000", "-ac", "1", "-f", "ogg", ogg_tmp],
                capture_output=True, timeout=20
            )

            if result.returncode != 0:
                self.send_error(500, "FFmpeg conversion failed")
                return

            with open(ogg_tmp, "rb") as f:
                ogg_bytes = f.read()

            self.send_response(200)
            self.send_header("Content-Type", "audio/ogg")
            self.send_header("Content-Length", str(len(ogg_bytes)))
            self.end_headers()
            self.wfile.write(ogg_bytes)

        except subprocess.TimeoutExpired:
            self.send_error(504, "FFmpeg timeout")
        finally:
            for path in [wav_tmp, ogg_tmp]:
                if path and os.path.exists(path):
                    os.unlink(path)


if __name__ == "__main__":
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument("--port", type=int, default=8882)
    args = parser.parse_args()

    server = http.server.HTTPServer(("localhost", args.port), ConversionHandler)
    print(f"Converter running on port {args.port}")
    server.serve_forever()

What to Watch For

  • Bind to localhost only — The service binds to localhost, not 0.0.0.0. It should never be reachable from the network — only from PHP on the same machine.
  • FFmpeg timeout — The timeout=20 prevents runaway conversions from blocking the handler. Return 504 so the PHP caller knows to fall back to text.
  • Port conflict on startup — Before starting, check ss -tlnp | grep :9012. The startup script from the Running Background Services article handles this already.