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.
- media_agent_mcp/__init__.py +7 -0
- media_agent_mcp/ai_models/__init__.py +17 -0
- media_agent_mcp/ai_models/seed16.py +151 -0
- media_agent_mcp/ai_models/seedance.py +258 -0
- media_agent_mcp/ai_models/seededit.py +94 -0
- media_agent_mcp/ai_models/seedream.py +136 -0
- media_agent_mcp/media_selectors/__init__.py +9 -0
- media_agent_mcp/media_selectors/image_selector.py +119 -0
- media_agent_mcp/media_selectors/video_selector.py +159 -0
- media_agent_mcp/server.py +405 -0
- media_agent_mcp/storage/__init__.py +8 -0
- media_agent_mcp/storage/tos_client.py +98 -0
- media_agent_mcp/video/__init__.py +9 -0
- media_agent_mcp/video/processor.py +337 -0
- media_agent_mcp-0.1.0.dist-info/METADATA +495 -0
- media_agent_mcp-0.1.0.dist-info/RECORD +18 -0
- media_agent_mcp-0.1.0.dist-info/WHEEL +4 -0
- media_agent_mcp-0.1.0.dist-info/entry_points.txt +2 -0
@@ -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}")
|