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, not0.0.0.0. It should never be reachable from the network — only from PHP on the same machine. - FFmpeg timeout — The
timeout=20prevents 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.