#!/usr/bin/env python3 """Audio post-processing wrapper around ffmpeg. Subcommands: convert — WAV to OGG (libvorbis, quality 6) normalize — LUFS normalize to -16 LUFS (broadcast standard) trim — Remove leading/trailing silence pipeline — trim + normalize + convert (full post-processing chain) All operations write to a new file (never overwrites input). """ import argparse import os import subprocess import sys import shutil import json def check_ffmpeg(): if not shutil.which("ffmpeg"): print(json.dumps({"ok": False, "error": "ffmpeg not found in PATH"})) sys.exit(1) def run_ffmpeg(args, description): """Run ffmpeg, capture output, return success.""" cmd = ["ffmpeg", "-y", "-hide_banner", "-loglevel", "error"] + args print(f" {description}", file=sys.stderr) result = subprocess.run(cmd, capture_output=True, text=True) if result.returncode != 0: print(json.dumps({ "ok": False, "error": f"ffmpeg failed: {result.stderr.strip()}", "command": " ".join(cmd) })) sys.exit(1) return True def cmd_convert(args): """Convert WAV to OGG (libvorbis).""" output = args.output or args.input.rsplit(".", 1)[0] + ".ogg" run_ffmpeg( ["-i", args.input, "-c:a", "libvorbis", "-q:a", str(args.quality), output], f"converting {os.path.basename(args.input)} → {os.path.basename(output)}" ) size = os.path.getsize(output) print(json.dumps({"ok": True, "output": output, "size_bytes": size})) def cmd_normalize(args): """LUFS normalize audio file.""" output = args.output or _suffixed(args.input, "_norm") run_ffmpeg( ["-i", args.input, "-af", f"loudnorm=I={args.lufs}:LRA=11:TP=-1", output], f"normalizing to {args.lufs} LUFS" ) print(json.dumps({"ok": True, "output": output})) def cmd_trim(args): """Trim leading/trailing silence.""" output = args.output or _suffixed(args.input, "_trimmed") af = ( "silenceremove=start_periods=1:start_silence=0.05" f":start_threshold={args.threshold}dB," "areverse," "silenceremove=start_periods=1:start_silence=0.05" f":start_threshold={args.threshold}dB," "areverse" ) run_ffmpeg( ["-i", args.input, "-af", af, output], f"trimming silence (threshold: {args.threshold}dB)" ) print(json.dumps({"ok": True, "output": output})) def cmd_pipeline(args): """Full post-processing: trim → normalize → convert to OGG.""" base = args.input.rsplit(".", 1)[0] trimmed = base + "_trimmed.wav" normalized = base + "_norm.wav" output = args.output or base + ".ogg" # Step 1: trim af_trim = ( "silenceremove=start_periods=1:start_silence=0.05" f":start_threshold={args.threshold}dB," "areverse," "silenceremove=start_periods=1:start_silence=0.05" f":start_threshold={args.threshold}dB," "areverse" ) run_ffmpeg( ["-i", args.input, "-af", af_trim, trimmed], "step 1/3: trimming silence" ) # Step 2: normalize run_ffmpeg( ["-i", trimmed, "-af", f"loudnorm=I={args.lufs}:LRA=11:TP=-1", normalized], f"step 2/3: normalizing to {args.lufs} LUFS" ) # Step 3: convert run_ffmpeg( ["-i", normalized, "-c:a", "libvorbis", "-q:a", str(args.quality), output], "step 3/3: converting to OGG" ) # Clean up intermediates os.remove(trimmed) os.remove(normalized) size = os.path.getsize(output) print(json.dumps({"ok": True, "output": output, "size_bytes": size})) def _suffixed(path, suffix): base, ext = os.path.splitext(path) return base + suffix + ext def main(): parser = argparse.ArgumentParser(description="Audio post-processing (ffmpeg wrapper)") sub = parser.add_subparsers(dest="command", required=True) # convert p = sub.add_parser("convert", help="WAV → OGG") p.add_argument("input", help="Input WAV file") p.add_argument("--output", "-o", help="Output file (default: same name .ogg)") p.add_argument("--quality", "-q", type=int, default=6, help="Vorbis quality 0-10 (default: 6)") p.set_defaults(func=cmd_convert) # normalize p = sub.add_parser("normalize", help="LUFS normalize") p.add_argument("input", help="Input audio file") p.add_argument("--output", "-o", help="Output file") p.add_argument("--lufs", type=float, default=-16, help="Target LUFS (default: -16)") p.set_defaults(func=cmd_normalize) # trim p = sub.add_parser("trim", help="Trim silence") p.add_argument("input", help="Input audio file") p.add_argument("--output", "-o", help="Output file") p.add_argument("--threshold", type=int, default=-50, help="Silence threshold in dB (default: -50)") p.set_defaults(func=cmd_trim) # pipeline p = sub.add_parser("pipeline", help="Full post-processing: trim + normalize + convert") p.add_argument("input", help="Input WAV file") p.add_argument("--output", "-o", help="Output OGG file") p.add_argument("--quality", "-q", type=int, default=6, help="Vorbis quality 0-10 (default: 6)") p.add_argument("--lufs", type=float, default=-16, help="Target LUFS (default: -16)") p.add_argument("--threshold", type=int, default=-50, help="Silence threshold in dB (default: -50)") p.set_defaults(func=cmd_pipeline) args = parser.parse_args() check_ffmpeg() args.func(args) if __name__ == "__main__": main()