File size: 11,545 Bytes
a37e6db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
"""
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  # HIGH - Modifies user files
    maturity_required: str = "AUTONOMOUS"

    def __init__(self):
        """Initialize FFmpeg tool with service and governance cache."""
        super().__init__()

        # Initialize FFmpeg service
        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

        # Governance cache for permission checks
        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
        """
        # Governance check - AUTONOMOUS ONLY
        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)

        # Check FFmpeg service availability
        if not self.service:
            raise RuntimeError(
                "FFmpeg service not available. "
                "Install FFmpeg: brew install ffmpeg (macOS) or apt install ffmpeg (Ubuntu)"
            )

        # Validate file paths (security boundary)
        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}"
            )

        # Route to appropriate operation
        try:
            result = self._execute_operation(
                action,
                input_path,
                output_path,
                **kwargs
            )

            # Log successful operation for audit trail
            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
        """
        # Route to appropriate async method
        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())}"
            )

        # Execute operation (async)
        import asyncio
        coro = operations[action](input_path, output_path, **kwargs)

        # Run async operation in event loop
        try:
            loop = asyncio.get_event_loop()
        except RuntimeError:
            loop = asyncio.new_event_loop()
            asyncio.set_event_loop(loop)

        return loop.run_until_complete(coro)

    # ========================================================================
    # Video Operations
    # ========================================================================

    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
        )

    # ========================================================================
    # Audio Operations
    # ========================================================================

    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
        )


# ============================================================================
# Tool Registration
# ============================================================================

def register_creative_tool(registry):
    """
    Register FFmpeg creative tool with tool registry.

    Args:
        registry: ToolRegistry instance
    """
    try:
        tool_instance = FFmpegTool()

        # Register with metadata
        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))


# Auto-register on import
try:
    from tools.registry import ToolRegistry
    _registry = ToolRegistry()
    register_creative_tool(_registry)
except ImportError:
    logger.warning("Tool registry not available for auto-registration")