| """ |
| Creative Tool - FFmpeg Video/Audio Processing |
| |
| LangChain BaseTool wrapper for FFmpeg operations with AUTONOMOUS-only governance. |
| |
| Supports: |
| - Video trimming, format conversion, thumbnail generation |
| - Audio extraction, volume normalization |
| - Async job processing with progress tracking |
| |
| Governance: AUTONOMOUS maturity level required (file safety) |
| """ |
|
|
| import os |
| from typing import Optional |
| from langchain.tools import BaseTool |
|
|
| from core.creative.ffmpeg_service import FFmpegService |
| from core.governance_cache import GovernanceCache |
| from core.structured_logger import get_logger |
|
|
| logger = get_logger(__name__) |
|
|
|
|
| class FFmpegTool(BaseTool): |
| """ |
| FFmpeg video/audio editing tool for AI agents. |
| |
| **AUTONOMOUS ONLY** - File operations require highest maturity level. |
| |
| Operations: |
| - trim_video: Cut video to specified start time and duration |
| - convert_format: Convert video to different format (MP4, WebM, MOV, AVI) |
| - generate_thumbnail: Extract single frame as JPEG thumbnail |
| - extract_audio: Extract audio track from video (MP3, M4A, WAV, FLAC) |
| - normalize_audio: Normalize audio volume to EBU R128 standard |
| |
| Security: |
| - All file paths validated against allowed directories |
| - AUTONOMOUS maturity required (STUDENT/INTERN/SUPERVISED blocked) |
| - Full audit trail via FFmpegJob database model |
| |
| Examples: |
| - "Trim the screencast from 5:00 to 10:00" |
| - "Convert this video to WebM format" |
| - "Extract the audio from the meeting recording" |
| - "Generate a thumbnail at 30 seconds for each video" |
| - "Normalize the audio volume to -16 LUFS" |
| """ |
|
|
| name: str = "ffmpeg_edit" |
| description: str = """ |
| Edit video and audio files using FFmpeg. Operations include: |
| - trim_video: Cut video to specified time range (start_time, duration) |
| - convert_format: Convert video format (MP4, WebM, MOV, AVI) |
| - generate_thumbnail: Create thumbnail at timestamp (JPEG) |
| - extract_audio: Extract audio from video (MP3, M4A, WAV, FLAC) |
| - normalize_audio: Normalize audio volume to -16 LUFS |
| |
| **AUTONOMOUS maturity level REQUIRED** (file safety). |
| |
| All file paths must be within allowed directories (./data/media, ./data/exports). |
| Operations run asynchronously - returns job_id for tracking. |
| |
| Example inputs: |
| - Action: trim_video, input: /app/data/media/input/video.mp4, output: /app/data/media/output/trimmed.mp4, start_time: 00:00:05, duration: 00:01:00 |
| - Action: convert_format, input: /app/data/media/input.mov, output: /app/data/media/output/video.mp4, format: mp4 |
| - Action: extract_audio, input: /app/data/media/input/meeting.mp4, output: /app/data/exports/meeting_audio.mp3, format: mp3 |
| """ |
| complexity: int = 3 |
| maturity_required: str = "AUTONOMOUS" |
|
|
| def __init__(self): |
| """Initialize FFmpeg tool with service and governance cache.""" |
| super().__init__() |
|
|
| |
| try: |
| self.service = FFmpegService() |
| logger.info("FFmpegTool initialized", service_available=True) |
| except Exception as e: |
| logger.error("Failed to initialize FFmpegService", error=str(e)) |
| self.service = None |
|
|
| |
| self.governance_cache = GovernanceCache() |
|
|
| def _run( |
| self, |
| action: str, |
| input_path: str, |
| output_path: str, |
| agent_id: Optional[str] = None, |
| maturity_level: Optional[str] = None, |
| **kwargs |
| ) -> str: |
| """ |
| Execute FFmpeg operation with governance enforcement. |
| |
| Args: |
| action: Operation to perform (trim_video, convert_format, etc.) |
| input_path: Source file path (within allowed directories) |
| output_path: Destination file path (within allowed directories) |
| agent_id: Agent identifier for governance check |
| maturity_level: Current agent maturity level |
| **kwargs: Additional operation-specific parameters |
| |
| Returns: |
| JSON string with job_id and status |
| |
| Raises: |
| PermissionError: If maturity level is below AUTONOMOUS |
| ValueError: If file paths are outside allowed directories |
| RuntimeError: If FFmpeg binary or service not available |
| """ |
| |
| if not maturity_level or maturity_level != "AUTONOMOUS": |
| error_msg = ( |
| f"FFmpeg editing requires AUTONOMOUS maturity level. " |
| f"Your agent is at {maturity_level or 'UNKNOWN'} maturity. " |
| f"This restriction ensures file safety - video/audio editing can " |
| f"modify or delete user files." |
| ) |
| logger.warning( |
| "FFmpeg permission denied", |
| agent_id=agent_id, |
| maturity_level=maturity_level, |
| required="AUTONOMOUS" |
| ) |
| raise PermissionError(error_msg) |
|
|
| |
| if not self.service: |
| raise RuntimeError( |
| "FFmpeg service not available. " |
| "Install FFmpeg: brew install ffmpeg (macOS) or apt install ffmpeg (Ubuntu)" |
| ) |
|
|
| |
| try: |
| self.service.validate_path(input_path) |
| self.service.validate_path(output_path) |
| except ValueError as e: |
| logger.warning( |
| "Path validation failed", |
| input_path=input_path, |
| output_path=output_path, |
| error=str(e) |
| ) |
| raise ValueError( |
| f"File path outside allowed directory: {e}. " |
| f"Allowed directories: {self.service.allowed_dirs}" |
| ) |
|
|
| |
| try: |
| result = self._execute_operation( |
| action, |
| input_path, |
| output_path, |
| **kwargs |
| ) |
|
|
| |
| logger.info( |
| "FFmpeg operation initiated", |
| agent_id=agent_id, |
| action=action, |
| input_path=input_path, |
| output_path=output_path, |
| job_id=result.get("job_id") |
| ) |
|
|
| return result |
|
|
| except Exception as e: |
| logger.error( |
| "FFmpeg operation failed", |
| agent_id=agent_id, |
| action=action, |
| error=str(e) |
| ) |
| raise RuntimeError(f"FFmpeg operation failed: {e}") |
|
|
| def _execute_operation( |
| self, |
| action: str, |
| input_path: str, |
| output_path: str, |
| **kwargs |
| ) -> dict: |
| """ |
| Execute specific FFmpeg operation. |
| |
| Args: |
| action: Operation type |
| input_path: Source file |
| output_path: Destination file |
| **kwargs: Operation-specific parameters |
| |
| Returns: |
| Dict with job_id and status |
| """ |
| |
| operations = { |
| "trim_video": self._trim_video, |
| "convert_format": self._convert_format, |
| "generate_thumbnail": self._generate_thumbnail, |
| "extract_audio": self._extract_audio, |
| "normalize_audio": self._normalize_audio |
| } |
|
|
| if action not in operations: |
| raise ValueError( |
| f"Unknown action: {action}. " |
| f"Supported: {list(operations.keys())}" |
| ) |
|
|
| |
| import asyncio |
| coro = operations[action](input_path, output_path, **kwargs) |
|
|
| |
| try: |
| loop = asyncio.get_event_loop() |
| except RuntimeError: |
| loop = asyncio.new_event_loop() |
| asyncio.set_event_loop(loop) |
|
|
| return loop.run_until_complete(coro) |
|
|
| |
| |
| |
|
|
| async def _trim_video( |
| self, |
| input_path: str, |
| output_path: str, |
| start_time: str, |
| duration: str |
| ) -> dict: |
| """Trim video to specified time range.""" |
| return await self.service.trim_video( |
| input_path=input_path, |
| output_path=output_path, |
| start_time=start_time, |
| duration=duration |
| ) |
|
|
| async def _convert_format( |
| self, |
| input_path: str, |
| output_path: str, |
| format: str, |
| quality: str = "medium" |
| ) -> dict: |
| """Convert video format.""" |
| return await self.service.convert_format( |
| input_path=input_path, |
| output_path=output_path, |
| format=format, |
| quality=quality |
| ) |
|
|
| async def _generate_thumbnail( |
| self, |
| input_path: str, |
| output_path: str, |
| timestamp: str = "00:00:01" |
| ) -> dict: |
| """Generate thumbnail from video.""" |
| return await self.service.generate_thumbnail( |
| video_path=input_path, |
| thumbnail_path=output_path, |
| timestamp=timestamp |
| ) |
|
|
| |
| |
| |
|
|
| async def _extract_audio( |
| self, |
| input_path: str, |
| output_path: str, |
| format: str = "mp3" |
| ) -> dict: |
| """Extract audio from video.""" |
| return await self.service.extract_audio( |
| video_path=input_path, |
| audio_path=output_path, |
| format=format |
| ) |
|
|
| async def _normalize_audio( |
| self, |
| input_path: str, |
| output_path: str, |
| target_lufs: float = -16.0 |
| ) -> dict: |
| """Normalize audio volume.""" |
| return await self.service.normalize_audio( |
| input_path=input_path, |
| output_path=output_path, |
| target_lufs=target_lufs |
| ) |
|
|
|
|
| |
| |
| |
|
|
| def register_creative_tool(registry): |
| """ |
| Register FFmpeg creative tool with tool registry. |
| |
| Args: |
| registry: ToolRegistry instance |
| """ |
| try: |
| tool_instance = FFmpegTool() |
|
|
| |
| registry.register( |
| name="ffmpeg_edit", |
| function=tool_instance._run, |
| version="1.0.0", |
| description="FFmpeg video/audio editing (AUTONOMOUS only)", |
| category="creative", |
| complexity=3, |
| maturity_required="AUTONOMOUS", |
| dependencies=["ffmpeg-python", "ffmpeg"], |
| tags=["video", "audio", "ffmpeg", "media", "editing", "creative"] |
| ) |
|
|
| logger.info("FFmpeg creative tool registered", category="creative") |
|
|
| except Exception as e: |
| logger.error("Failed to register FFmpeg tool", error=str(e)) |
|
|
|
|
| |
| try: |
| from tools.registry import ToolRegistry |
| _registry = ToolRegistry() |
| register_creative_tool(_registry) |
| except ImportError: |
| logger.warning("Tool registry not available for auto-registration") |
|
|