media-agent-mcp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,98 @@
1
+ """TOS (Temporary Object Storage) client module.
2
+
3
+ This module provides functionality for uploading files to TOS and managing storage operations.
4
+ """
5
+
6
+ import datetime
7
+ import logging
8
+ import os
9
+ import uuid
10
+ from typing import Optional, Dict, Any
11
+
12
+ import tos
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def get_tos_client():
18
+ """
19
+ Creates and returns a TOS client using environment variables for authentication.
20
+
21
+ Returns:
22
+ A TOS client instance.
23
+ """
24
+ try:
25
+ # Retrieve AKSK information from environment variables
26
+ ak = os.getenv('TOS_ACCESS_KEY')
27
+ sk = os.getenv('TOS_SECRET_KEY')
28
+ endpoint = os.getenv('TOS_ENDPOINT', "tos-ap-southeast-1.bytepluses.com")
29
+ region = os.getenv('TOS_REGION', "ap-southeast-1")
30
+
31
+ if not ak or not sk:
32
+ raise ValueError("TOS_ACCESS_KEY and TOS_SECRET_KEY environment variables must be set")
33
+
34
+ client = tos.TosClientV2(ak, sk, endpoint, region)
35
+ return client
36
+ except Exception as e:
37
+ logger.error(f"Error creating TOS client: {str(e)}")
38
+ raise
39
+
40
+
41
+ def upload_to_tos(file_path: str, object_key: Optional[str] = None) -> Dict[str, Any]:
42
+ """
43
+ Uploads a file to TOS and returns the URL.
44
+
45
+ Args:
46
+ file_path: Path to the file to upload.
47
+ object_key: Optional key to use for the object in TOS. If not provided, a UUID will be generated.
48
+
49
+ Returns:
50
+ JSON response with status, data (URL), and message.
51
+ """
52
+ try:
53
+ client = get_tos_client()
54
+ bucket_name = os.getenv('TOS_BUCKET_NAME')
55
+
56
+ if not bucket_name:
57
+ return {
58
+ "status": "error",
59
+ "data": None,
60
+ "message": "TOS_BUCKET_NAME environment variable must be set"
61
+ }
62
+
63
+ if not object_key:
64
+ # Generate a unique object key if not provided
65
+ file_extension = os.path.splitext(file_path)[1]
66
+ date_str = datetime.datetime.now().strftime("%Y-%m-%d")
67
+ object_key = f"media_agent/{date_str}/{uuid.uuid4().hex}{file_extension}"
68
+
69
+ # Upload the file
70
+ with open(file_path, 'rb') as f:
71
+ client.put_object(bucket_name, object_key, content=f)
72
+
73
+ # Construct the URL
74
+ endpoint = os.getenv('TOS_ENDPOINT', "tos-ap-southeast-1.bytepluses.com")
75
+ url = f"https://{bucket_name}.{endpoint}/{object_key}"
76
+
77
+ return {
78
+ "status": "success",
79
+ "data": {"url": url},
80
+ "message": "File uploaded successfully to TOS"
81
+ }
82
+ except Exception as e:
83
+ logger.error(f"Error uploading to TOS: {str(e)}")
84
+ return {
85
+ "status": "error",
86
+ "data": None,
87
+ "message": f"Failed to upload file to TOS: {str(e)}"
88
+ }
89
+
90
+
91
+ if __name__ == '__main__':
92
+ # Example usage
93
+ try:
94
+ file_path = '/Users/bytedance/CodeQ3/Meida_Agent_MCP/media-agent-mcp/seedeidt_api' # Replace with your file path
95
+ url = upload_to_tos(file_path)
96
+ print(f"File uploaded successfully: {url}")
97
+ except Exception as e:
98
+ print(f"Error: {str(e)}")
@@ -0,0 +1,9 @@
1
+ """Video processing module for Media Agent MCP.
2
+
3
+ This module provides video processing functionality including concatenation,
4
+ frame extraction, and video selection.
5
+ """
6
+
7
+ from .processor import concat_videos, extract_last_frame
8
+
9
+ __all__ = ['concat_videos', 'extract_last_frame']
@@ -0,0 +1,337 @@
1
+ """Video processing module.
2
+
3
+ This module provides video processing functionality including concatenation and frame extraction.
4
+ """
5
+
6
+ import asyncio
7
+ import json
8
+ import logging
9
+ import os
10
+ import tempfile
11
+ import uuid
12
+ from typing import Optional, Dict, Any
13
+ from urllib.parse import urlparse
14
+
15
+ import cv2
16
+ import requests
17
+
18
+ from media_agent_mcp.storage.tos_client import upload_to_tos
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def download_video_from_url(url: str) -> Dict[str, Any]:
24
+ """Download video from URL to a temporary file.
25
+
26
+ Args:
27
+ url: URL of the video to download
28
+
29
+ Returns:
30
+ JSON response with status, data (file path), and message
31
+ """
32
+ try:
33
+ # Parse URL to get file extension
34
+ parsed_url = urlparse(url)
35
+ file_extension = os.path.splitext(parsed_url.path)[1] or '.mp4'
36
+
37
+ # Create temporary file
38
+ temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=file_extension)
39
+ temp_path = temp_file.name
40
+ temp_file.close()
41
+
42
+ # Download the video
43
+ logger.info(f"Downloading video from {url}...")
44
+ response = requests.get(url, stream=True, timeout=300)
45
+ response.raise_for_status()
46
+
47
+ with open(temp_path, 'wb') as f:
48
+ for chunk in response.iter_content(chunk_size=8192):
49
+ if chunk:
50
+ f.write(chunk)
51
+
52
+ logger.info(f"Video downloaded to {temp_path}")
53
+ return {
54
+ "status": "success",
55
+ "data": {"file_path": temp_path},
56
+ "message": "Video downloaded successfully"
57
+ }
58
+
59
+ except Exception as e:
60
+ logger.error(f"Error downloading video from {url}: {e}")
61
+ return {
62
+ "status": "error",
63
+ "data": None,
64
+ "message": f"Error downloading video from {url}: {e}"
65
+ }
66
+
67
+
68
+ def concat_videos(video_urls: list, output_path: Optional[str] = None) -> Dict[str, Any]:
69
+ """Concatenate multiple videos into one.
70
+
71
+ Args:
72
+ video_urls: List of URLs or paths to video files to concatenate in order
73
+ output_path: Optional output path for the concatenated video
74
+
75
+ Returns:
76
+ JSON response with status, data (TOS URL), and message
77
+ """
78
+ temp_files = []
79
+ video_captures = []
80
+
81
+ try:
82
+ if not video_urls or len(video_urls) == 0:
83
+ return {
84
+ "status": "error",
85
+ "data": None,
86
+ "message": "No video URLs provided"
87
+ }
88
+
89
+ if not output_path:
90
+ output_path = f"concatenated_{int(asyncio.get_event_loop().time())}.mp4"
91
+
92
+ # Download videos if they are URLs and prepare local paths
93
+ video_paths = []
94
+ for video_input in video_urls:
95
+ if video_input.startswith(('http://', 'https://')):
96
+ download_result = download_video_from_url(video_input)
97
+ if download_result["status"] == "error":
98
+ return download_result
99
+ video_path = download_result["data"]["file_path"]
100
+ temp_files.append(video_path)
101
+ video_paths.append(video_path)
102
+ elif os.path.exists(video_input):
103
+ video_paths.append(video_input)
104
+ else:
105
+ return {
106
+ "status": "error",
107
+ "data": None,
108
+ "message": f"Video file {video_input} not found"
109
+ }
110
+
111
+ # Open all video captures
112
+ for video_path in video_paths:
113
+ cap = cv2.VideoCapture(video_path)
114
+ if not cap.isOpened():
115
+ return {
116
+ "status": "error",
117
+ "data": None,
118
+ "message": f"Could not open video {video_path}"
119
+ }
120
+ video_captures.append(cap)
121
+
122
+ # Get video properties from the first video
123
+ first_cap = video_captures[0]
124
+ fps = int(first_cap.get(cv2.CAP_PROP_FPS))
125
+ width = int(first_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
126
+ height = int(first_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
127
+
128
+ # Create video writer with H.264 codec for better compatibility
129
+ fourcc = cv2.VideoWriter_fourcc(*'avc1') # H.264 codec
130
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
131
+
132
+ # Check if video writer was initialized successfully
133
+ if not out.isOpened():
134
+ # Fallback to mp4v if avc1 fails
135
+ fourcc = cv2.VideoWriter_fourcc(*'mp4v')
136
+ out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
137
+ if not out.isOpened():
138
+ return {
139
+ "status": "error",
140
+ "data": None,
141
+ "message": "Could not initialize video writer"
142
+ }
143
+
144
+ # Write frames from all videos in order
145
+ for i, cap in enumerate(video_captures):
146
+ logger.info(f"Processing video {i+1}/{len(video_captures)}...")
147
+ while True:
148
+ ret, frame = cap.read()
149
+ if not ret:
150
+ break
151
+ # Resize frame if dimensions don't match the first video
152
+ if frame.shape[:2] != (height, width):
153
+ frame = cv2.resize(frame, (width, height))
154
+ out.write(frame)
155
+
156
+ # Release everything
157
+ for cap in video_captures:
158
+ cap.release()
159
+ out.release()
160
+
161
+ logger.info(f"Videos concatenated successfully: {output_path}")
162
+
163
+ # Upload concatenated video to TOS
164
+ try:
165
+ tos_url = upload_to_tos(output_path)
166
+ logger.info(f"Video uploaded to TOS: {tos_url}")
167
+
168
+ # Clean up local concatenated file
169
+ try:
170
+ os.unlink(output_path)
171
+ logger.info(f"Cleaned up local concatenated file: {output_path}")
172
+ except Exception as e:
173
+ logger.warning(f"Failed to clean up local file {output_path}: {e}")
174
+
175
+ return {
176
+ "status": "success",
177
+ "data": {"tos_url": tos_url},
178
+ "message": "Videos concatenated and uploaded successfully"
179
+ }
180
+ except Exception as e:
181
+ logger.error(f"Error uploading to TOS: {e}")
182
+ return {
183
+ "status": "error",
184
+ "data": None,
185
+ "message": f"Error uploading to TOS: {str(e)}"
186
+ }
187
+
188
+ except Exception as e:
189
+ logger.error(f"Error concatenating videos: {e}")
190
+ return {
191
+ "status": "error",
192
+ "data": None,
193
+ "message": f"Error concatenating videos: {str(e)}"
194
+ }
195
+ finally:
196
+ # Release any remaining video captures
197
+ for cap in video_captures:
198
+ if cap.isOpened():
199
+ cap.release()
200
+
201
+ # Clean up temporary files
202
+ for temp_file in temp_files:
203
+ try:
204
+ if os.path.exists(temp_file):
205
+ os.unlink(temp_file)
206
+ logger.info(f"Cleaned up temporary file: {temp_file}")
207
+ except Exception as e:
208
+ logger.warning(f"Failed to clean up temporary file {temp_file}: {e}")
209
+
210
+
211
+ def extract_last_frame(video_input: str, output_path: Optional[str] = None) -> Dict[str, Any]:
212
+ """Extract the last frame from a video as an image and upload to TOS.
213
+
214
+ Args:
215
+ video_input: URL or path to the video file
216
+ output_path: Optional output path for the extracted frame
217
+
218
+ Returns:
219
+ JSON response with status, data (TOS URL), and message
220
+ """
221
+ temp_video_file = None
222
+
223
+ try:
224
+ # Handle URL or local file path
225
+ if video_input.startswith(('http://', 'https://')):
226
+ # Download video from URL
227
+ download_result = download_video_from_url(video_input)
228
+ if download_result["status"] == "error":
229
+ return download_result
230
+ video_path = download_result["data"]["file_path"]
231
+ temp_video_file = video_path
232
+ elif os.path.exists(video_input):
233
+ video_path = video_input
234
+ else:
235
+ return {
236
+ "status": "error",
237
+ "data": None,
238
+ "message": f"Video file {video_input} not found"
239
+ }
240
+
241
+ if not output_path:
242
+ output_path = f"last_frame_{uuid.uuid4().hex}.jpg"
243
+
244
+ # Open video
245
+ cap = cv2.VideoCapture(video_path)
246
+
247
+ if not cap.isOpened():
248
+ return {
249
+ "status": "error",
250
+ "data": None,
251
+ "message": f"Could not open video {video_path}"
252
+ }
253
+
254
+ # Get total number of frames
255
+ total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
256
+
257
+ if total_frames <= 0:
258
+ cap.release()
259
+ return {
260
+ "status": "error",
261
+ "data": None,
262
+ "message": "Video has no frames"
263
+ }
264
+
265
+ # Set position to last frame
266
+ cap.set(cv2.CAP_PROP_POS_FRAMES, total_frames - 1)
267
+
268
+ # Read the last frame
269
+ ret, frame = cap.read()
270
+
271
+ if ret:
272
+ # Save the frame
273
+ cv2.imwrite(output_path, frame)
274
+ cap.release()
275
+ logger.info(f"Last frame extracted: {output_path}")
276
+
277
+ # Upload frame to TOS
278
+ try:
279
+ tos_url = upload_to_tos(output_path)
280
+ logger.info(f"Frame uploaded to TOS: {tos_url}")
281
+
282
+ # Clean up local frame file
283
+ try:
284
+ os.unlink(output_path)
285
+ logger.info(f"Cleaned up local frame file: {output_path}")
286
+ except Exception as e:
287
+ logger.warning(f"Failed to clean up local file {output_path}: {e}")
288
+
289
+ return {
290
+ "status": "success",
291
+ "data": {"tos_url": tos_url},
292
+ "message": "Last frame extracted and uploaded successfully"
293
+ }
294
+ except Exception as e:
295
+ logger.error(f"Error uploading frame to TOS: {e}")
296
+ return {
297
+ "status": "error",
298
+ "data": None,
299
+ "message": f"Error uploading to TOS: {str(e)}"
300
+ }
301
+ else:
302
+ cap.release()
303
+ return {
304
+ "status": "error",
305
+ "data": None,
306
+ "message": "Could not read the last frame"
307
+ }
308
+
309
+ except Exception as e:
310
+ logger.error(f"Error extracting last frame: {e}")
311
+ return {
312
+ "status": "error",
313
+ "data": None,
314
+ "message": f"Error extracting last frame: {str(e)}"
315
+ }
316
+ finally:
317
+ # Clean up temporary video file if downloaded
318
+ if temp_video_file and os.path.exists(temp_video_file):
319
+ try:
320
+ os.unlink(temp_video_file)
321
+ logger.info(f"Cleaned up temporary video file: {temp_video_file}")
322
+ except Exception as e:
323
+ logger.warning(f"Failed to clean up temporary video file {temp_video_file}: {e}")
324
+
325
+
326
+ if __name__ == '__main__':
327
+ # Example usage
328
+ video_urls = [
329
+ "https://carey.tos-ap-southeast-1.bytepluses.com/demo/02175205870921200000000000000000000ffffc0a85094bda733.mp4",
330
+ "https://carey.tos-ap-southeast-1.bytepluses.com/demo/02175205817458400000000000000000000ffffc0a850948120ae.mp4"
331
+ ]
332
+
333
+ concatenated_video = concat_videos(video_urls)
334
+ print(f"Concatenated video URL: {concatenated_video}")
335
+
336
+ last_frame_url = extract_last_frame(video_urls[0])
337
+ print(f"Last frame URL: {last_frame_url}")