from __future__ import annotations from typing import Any from core.tools.base import BaseTool, ToolContext from core.tools.registry import registry from core.events import bus from core.subprocess import run_command class FFmpegTool(BaseTool): name = "ffmpeg" description = "Media processing using FFmpeg" # ========================================================= # ROUTER # ========================================================= def execute(self, payload: dict[str, Any], ctx: ToolContext): action = str(payload.get("action", "")).strip() bus.log( "FFMPEG", "ffmpeg_execute", "INFO", {"action": action} ) match action: case "convert": return self.convert(payload, ctx) case "extract_audio": return self.extract_audio(payload, ctx) case "trim": return self.trim(payload, ctx) case "merge": return self.merge(payload, ctx) case "probe": return self.probe(payload) case "thumbnail": return self.thumbnail(payload, ctx) case _: raise ValueError(f"Unknown ffmpeg action: {action}") # ========================================================= # CONVERT # ========================================================= def convert(self, payload: dict[str, Any], ctx: ToolContext): input_file = payload.get("input") output_file = payload.get("output") codec = payload.get("codec") # optional if not isinstance(input_file, str) or not isinstance(output_file, str): raise ValueError("input/output must be strings") cmd = ["ffmpeg", "-y", "-i", input_file] if isinstance(codec, str): cmd += ["-c:v", codec] cmd.append(output_file) if ctx.dry_run: return {"dry_run": True, "command": cmd} result = run_command(cmd=cmd) return { "action": "convert", "status": "ok" if result.get("return_code") == 0 else "error", "stdout": result.get("stdout", ""), "stderr": result.get("stderr", "") } # ========================================================= # EXTRACT AUDIO # ========================================================= def extract_audio(self, payload: dict[str, Any], ctx: ToolContext): input_file = payload.get("input") output_file = payload.get("output") if not isinstance(input_file, str) or not isinstance(output_file, str): raise ValueError("input/output must be strings") cmd = [ "ffmpeg", "-y", "-i", input_file, "-vn", "-acodec", "copy", output_file ] if ctx.dry_run: return {"dry_run": True, "command": cmd} result = run_command(cmd=cmd) return { "action": "extract_audio", "status": "ok" if result.get("return_code") == 0 else "error", "stdout": result.get("stdout", ""), "stderr": result.get("stderr", "") } # ========================================================= # TRIM # ========================================================= def trim(self, payload: dict[str, Any], ctx: ToolContext): input_file = payload.get("input") output_file = payload.get("output") start = payload.get("start", "00:00:00") duration = payload.get("duration") if not isinstance(input_file, str) or not isinstance(output_file, str): raise ValueError("input/output must be strings") cmd = [ "ffmpeg", "-y", "-i", input_file, "-ss", str(start), ] if duration: cmd += ["-t", str(duration)] cmd.append(output_file) if ctx.dry_run: return {"dry_run": True, "command": cmd} result = run_command(cmd=cmd) return { "action": "trim", "status": "ok" if result.get("return_code") == 0 else "error", "stdout": result.get("stdout", ""), "stderr": result.get("stderr", "") } # ========================================================= # MERGE FILES # ========================================================= def merge(self, payload: dict[str, Any], ctx: ToolContext): inputs = payload.get("inputs") output = payload.get("output") if not isinstance(inputs, list) or not isinstance(output, str): raise ValueError("inputs must be list, output must be string") # ffmpeg concat demuxer style file_list = "ffmpeg_concat.txt" with open(file_list, "w", encoding="utf-8") as f: for item in inputs: f.write(f"file '{item}'\n") cmd = [ "ffmpeg", "-y", "-f", "concat", "-safe", "0", "-i", file_list, "-c", "copy", output ] if ctx.dry_run: return {"dry_run": True, "command": cmd} result = run_command(cmd=cmd) return { "action": "merge", "status": "ok" if result.get("return_code") == 0 else "error", "stdout": result.get("stdout", ""), "stderr": result.get("stderr", "") } # ========================================================= # PROBE # ========================================================= def probe(self, payload: dict[str, Any]): input_file = payload.get("input") if not isinstance(input_file, str): raise ValueError("input must be string") cmd = [ "ffprobe", "-v", "error", "-show_format", "-show_streams", input_file ] result = run_command(cmd=cmd) return { "action": "probe", "data": result.get("stdout", ""), "error": result.get("stderr", "") } # ========================================================= # THUMBNAIL # ========================================================= def thumbnail(self, payload: dict[str, Any], ctx: ToolContext): input_file = payload.get("input") output_file = payload.get("output") time = payload.get("time", "00:00:01") if not isinstance(input_file, str) or not isinstance(output_file, str): raise ValueError("input/output must be strings") cmd = [ "ffmpeg", "-y", "-ss", str(time), "-i", input_file, "-vframes", "1", output_file ] if ctx.dry_run: return {"dry_run": True, "command": cmd} result = run_command(cmd=cmd) return { "action": "thumbnail", "status": "ok" if result.get("return_code") == 0 else "error", "stdout": result.get("stdout", ""), "stderr": result.get("stderr", "") } # ========================================================= # REGISTER # ========================================================= registry.register(FFmpegTool())