media-agent-mcp 2.6.14__tar.gz → 2.7.2__tar.gz
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-2.6.14 → media_agent_mcp-2.7.2}/PKG-INFO +2 -1
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/pyproject.toml +2 -1
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/__init__.py +2 -2
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/ai_models/omni_human.py +4 -3
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/async_server.py +10 -292
- media_agent_mcp-2.7.2/src/media_agent_mcp/be/__pycache__/__init__.cpython-312.pyc +0 -0
- media_agent_mcp-2.7.2/src/media_agent_mcp/be/__pycache__/app.cpython-312.pyc +0 -0
- media_agent_mcp-2.7.2/src/media_agent_mcp/be/__pycache__/routes_media.cpython-312.pyc +0 -0
- media_agent_mcp-2.7.2/src/media_agent_mcp/be/__pycache__/routes_omni_human.cpython-312.pyc +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/__pycache__/routes_subtitles.cpython-312.pyc +0 -0
- media_agent_mcp-2.7.2/src/media_agent_mcp/be/__pycache__/utils.cpython-312.pyc +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/app.py +2 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/routes_media.py +7 -4
- media_agent_mcp-2.7.2/src/media_agent_mcp/be/routes_omni_human.py +34 -0
- media_agent_mcp-2.7.2/src/media_agent_mcp/video/omni_human.py +118 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp.egg-info/PKG-INFO +2 -1
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp.egg-info/SOURCES.txt +3 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp.egg-info/requires.txt +1 -0
- media_agent_mcp-2.6.14/src/media_agent_mcp/be/__pycache__/__init__.cpython-312.pyc +0 -0
- media_agent_mcp-2.6.14/src/media_agent_mcp/be/__pycache__/app.cpython-312.pyc +0 -0
- media_agent_mcp-2.6.14/src/media_agent_mcp/be/__pycache__/routes_media.cpython-312.pyc +0 -0
- media_agent_mcp-2.6.14/src/media_agent_mcp/be/__pycache__/utils.cpython-312.pyc +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/README.md +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/setup.cfg +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/ai_models/__init__.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/ai_models/openaiedit.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/ai_models/seed16.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/ai_models/seedance.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/ai_models/seededit.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/ai_models/seedream.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/ai_models/tts.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/async_wrapper.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/audio/combiner.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/audio/speed_controller.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/audio/tts.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/README.md +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/__init__.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/__pycache__/routes_omni.cpython-312.pyc +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/fonts/en/EduNSWACTCursive-VariableFont_wght.ttf +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/fonts/en/MozillaText-VariableFont_wght.ttf +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/fonts/en/Roboto_Condensed-Regular.ttf +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/fonts/zh/MaShanZheng-Regular.ttf +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/fonts/zh/NotoSerifSC-VariableFont_wght.ttf +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/fonts/zh/ZCOOLXiaoWei-Regular.ttf +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/pyproject.toml +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/routes_subtitles.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/utils.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/be/uv.lock +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/install_tools/__init__.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/install_tools/installer.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/media_selectors/__init__.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/media_selectors/image_selector.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/media_selectors/video_selector.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/storage/__init__.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/storage/tos_client.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/video/__init__.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/video/processor.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/video/stack.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/video/subtitle.py +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp.egg-info/dependency_links.txt +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp.egg-info/entry_points.txt +0 -0
- {media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: media-agent-mcp
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.7.2
|
4
4
|
Summary: A Model Context Protocol server for media processing with AI tools
|
5
5
|
Author-email: Media Agent Team <team@mediaagent.com>
|
6
6
|
Keywords: mcp,ai,media,video,image,processing
|
@@ -26,6 +26,7 @@ Requires-Dist: loguru>=0.7.3
|
|
26
26
|
Requires-Dist: imageio-ffmpeg>=0.4.0
|
27
27
|
Requires-Dist: Flask>=3.0.0
|
28
28
|
Requires-Dist: pydub>=0.25.1
|
29
|
+
Requires-Dist: gunicorn>=22.0.0
|
29
30
|
Requires-Dist: audioop-lts; python_version >= "3.13"
|
30
31
|
Requires-Dist: google-genai>=1.33.0
|
31
32
|
Requires-Dist: mcp==1.11.0
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "media-agent-mcp"
|
3
|
-
version = "2.
|
3
|
+
version = "2.7.2"
|
4
4
|
description = "A Model Context Protocol server for media processing with AI tools"
|
5
5
|
readme = "README.md"
|
6
6
|
requires-python = ">=3.12"
|
@@ -31,6 +31,7 @@ dependencies = [
|
|
31
31
|
"imageio-ffmpeg>=0.4.0",
|
32
32
|
"Flask>=3.0.0",
|
33
33
|
"pydub>=0.25.1",
|
34
|
+
"gunicorn>=22.0.0",
|
34
35
|
"audioop-lts; python_version >= '3.13'",
|
35
36
|
"google-genai>=1.33.0",
|
36
37
|
"mcp==1.11.0",
|
@@ -1,8 +1,8 @@
|
|
1
1
|
"""Media Agent MCP Server - A Model Context Protocol server for media processing."""
|
2
2
|
|
3
3
|
from . import ai_models, media_selectors, storage, video
|
4
|
-
from .async_server import main
|
4
|
+
from .async_server import main as async_main
|
5
5
|
from . import async_wrapper
|
6
6
|
|
7
7
|
__version__ = "0.1.0"
|
8
|
-
__all__ = ['ai_models', 'media_selectors', 'storage', 'video', 'main', 'async_wrapper']
|
8
|
+
__all__ = ['ai_models', 'media_selectors', 'storage', 'video', 'main', 'async_main', 'async_wrapper']
|
{media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/ai_models/omni_human.py
RENAMED
@@ -122,8 +122,9 @@ if __name__ == '__main__':
|
|
122
122
|
time1 = time.time()
|
123
123
|
|
124
124
|
print(generate_video_from_omni_human(
|
125
|
-
image_url="https://
|
126
|
-
audio_url="https://carey.tos-ap-southeast-1.bytepluses.com/media_agent/2025-
|
125
|
+
image_url="https://carey.tos-ap-southeast-1.bytepluses.com/Art%20Portrait/Art%20Portrait/Art%20Portrait/Art%20Portrait%20(1).jpg",
|
126
|
+
audio_url="https://carey.tos-ap-southeast-1.bytepluses.com/media_agent/2025-09-02/66620bed2a5f4b2cbc641559ff93a2ed.mp3"
|
127
127
|
))
|
128
128
|
|
129
|
-
print(time.time() - time1)
|
129
|
+
print(time.time() - time1)
|
130
|
+
# 16452282602934202024
|
@@ -26,12 +26,6 @@ from dotenv import load_dotenv
|
|
26
26
|
import uvicorn
|
27
27
|
import anyio
|
28
28
|
from functools import wraps
|
29
|
-
import uuid
|
30
|
-
import weakref
|
31
|
-
from starlette.applications import Starlette
|
32
|
-
from starlette.middleware.base import BaseHTTPMiddleware
|
33
|
-
from starlette.requests import Request
|
34
|
-
from starlette.responses import Response
|
35
29
|
|
36
30
|
def async_retry(max_retries=3, delay=2):
|
37
31
|
def decorator(func):
|
@@ -50,20 +44,6 @@ def async_retry(max_retries=3, delay=2):
|
|
50
44
|
await asyncio.sleep(delay)
|
51
45
|
continue
|
52
46
|
return result
|
53
|
-
except anyio.ClosedResourceError as e:
|
54
|
-
logger.warning(f"ClosedResourceError in {func.__name__} (attempt {attempt + 1}): {e}")
|
55
|
-
# For ClosedResourceError, we should handle it gracefully
|
56
|
-
if attempt < max_retries - 1:
|
57
|
-
logger.info(f"Retrying {func.__name__} after ClosedResourceError...")
|
58
|
-
await asyncio.sleep(delay)
|
59
|
-
continue
|
60
|
-
else:
|
61
|
-
# On final attempt, return a structured error response
|
62
|
-
return {
|
63
|
-
"status": "error",
|
64
|
-
"data": None,
|
65
|
-
"message": f"Session expired during {func.__name__} execution. Please retry with a new session."
|
66
|
-
}
|
67
47
|
except Exception as e:
|
68
48
|
last_exception = str(e)
|
69
49
|
logger.error(f"Attempt {attempt + 1} of {max_retries} failed for {func.__name__} with exception: {e}. Retrying in {delay}s...")
|
@@ -77,46 +57,6 @@ def async_retry(max_retries=3, delay=2):
|
|
77
57
|
return wrapper
|
78
58
|
return decorator
|
79
59
|
|
80
|
-
def session_aware_retry(max_retries=3, delay=2):
|
81
|
-
"""Enhanced retry decorator that handles session expiration specifically."""
|
82
|
-
def decorator(func):
|
83
|
-
@wraps(func)
|
84
|
-
async def wrapper(*args, **kwargs):
|
85
|
-
for attempt in range(max_retries):
|
86
|
-
try:
|
87
|
-
result = await func(*args, **kwargs)
|
88
|
-
return result
|
89
|
-
except anyio.ClosedResourceError as e:
|
90
|
-
logger.warning(f"Session expired during {func.__name__} execution (attempt {attempt + 1}): {e}")
|
91
|
-
|
92
|
-
if attempt < max_retries - 1:
|
93
|
-
# Generate new session for retry
|
94
|
-
new_session_id = session_manager.generate_session_id()
|
95
|
-
logger.info(f"Generated new session {new_session_id} for retry of {func.__name__}")
|
96
|
-
await asyncio.sleep(delay)
|
97
|
-
continue
|
98
|
-
else:
|
99
|
-
return {
|
100
|
-
"status": "error",
|
101
|
-
"data": None,
|
102
|
-
"message": f"Session expired during {func.__name__} execution. A new session has been generated. Please retry your request."
|
103
|
-
}
|
104
|
-
except Exception as e:
|
105
|
-
logger.error(f"Unexpected error in {func.__name__} (attempt {attempt + 1}): {e}")
|
106
|
-
if attempt < max_retries - 1:
|
107
|
-
await asyncio.sleep(delay)
|
108
|
-
continue
|
109
|
-
else:
|
110
|
-
return {
|
111
|
-
"status": "error",
|
112
|
-
"data": None,
|
113
|
-
"message": f"Function {func.__name__} failed: {str(e)}"
|
114
|
-
}
|
115
|
-
|
116
|
-
return {"status": "error", "data": None, "message": f"Function {func.__name__} failed after {max_retries} retries"}
|
117
|
-
return wrapper
|
118
|
-
return decorator
|
119
|
-
|
120
60
|
from mcp.server.fastmcp import FastMCP
|
121
61
|
|
122
62
|
# Import async wrappers
|
@@ -141,133 +81,6 @@ from media_agent_mcp.async_wrapper import (
|
|
141
81
|
logging.basicConfig(level=logging.INFO)
|
142
82
|
logger = logging.getLogger(__name__)
|
143
83
|
|
144
|
-
# Session management for handling expired sessions
|
145
|
-
class SessionManager:
|
146
|
-
def __init__(self):
|
147
|
-
self._sessions = {} # Changed from WeakValueDictionary to regular dict
|
148
|
-
self._session_routes = {}
|
149
|
-
self._session_timestamps = {} # Track session creation times
|
150
|
-
|
151
|
-
def generate_session_id(self) -> str:
|
152
|
-
"""Generate a new unique session ID."""
|
153
|
-
return str(uuid.uuid4()).replace('-', '')
|
154
|
-
|
155
|
-
def register_session(self, session_id: str, session_obj):
|
156
|
-
"""Register a session object."""
|
157
|
-
import time
|
158
|
-
self._sessions[session_id] = session_obj
|
159
|
-
self._session_timestamps[session_id] = time.time()
|
160
|
-
logger.info(f"Registered session: {session_id}")
|
161
|
-
|
162
|
-
def get_session(self, session_id: str):
|
163
|
-
"""Get session object by ID."""
|
164
|
-
return self._sessions.get(session_id)
|
165
|
-
|
166
|
-
def remove_session(self, session_id: str, remove_route_mapping: bool = True):
|
167
|
-
"""Remove a session."""
|
168
|
-
if session_id in self._sessions:
|
169
|
-
del self._sessions[session_id]
|
170
|
-
if remove_route_mapping and session_id in self._session_routes:
|
171
|
-
del self._session_routes[session_id]
|
172
|
-
if session_id in self._session_timestamps:
|
173
|
-
del self._session_timestamps[session_id]
|
174
|
-
logger.info(f"Removed session: {session_id} (route_mapping_removed: {remove_route_mapping})")
|
175
|
-
|
176
|
-
def cleanup_expired_sessions(self, max_age_seconds: int = 3600):
|
177
|
-
"""Clean up sessions older than max_age_seconds."""
|
178
|
-
import time
|
179
|
-
current_time = time.time()
|
180
|
-
expired_sessions = []
|
181
|
-
|
182
|
-
for session_id, timestamp in self._session_timestamps.items():
|
183
|
-
if current_time - timestamp > max_age_seconds:
|
184
|
-
expired_sessions.append(session_id)
|
185
|
-
|
186
|
-
for session_id in expired_sessions:
|
187
|
-
self.remove_session(session_id)
|
188
|
-
logger.info(f"Cleaned up expired session: {session_id}")
|
189
|
-
|
190
|
-
return len(expired_sessions)
|
191
|
-
|
192
|
-
def get_session_count(self) -> int:
|
193
|
-
"""Get the number of active sessions."""
|
194
|
-
return len(self._sessions)
|
195
|
-
|
196
|
-
def get_route_count(self) -> int:
|
197
|
-
"""Get the number of route mappings."""
|
198
|
-
return len(self._session_routes)
|
199
|
-
|
200
|
-
def add_route_mapping(self, old_session_id: str, new_session_id: str):
|
201
|
-
"""Add route mapping for session forwarding."""
|
202
|
-
self._session_routes[old_session_id] = new_session_id
|
203
|
-
logger.info(f"Added route mapping: {old_session_id} -> {new_session_id}")
|
204
|
-
|
205
|
-
def get_route_mapping(self, session_id: str) -> Optional[str]:
|
206
|
-
"""Get route mapping for a session."""
|
207
|
-
return self._session_routes.get(session_id)
|
208
|
-
|
209
|
-
# Global session manager
|
210
|
-
session_manager = SessionManager()
|
211
|
-
|
212
|
-
def _extract_session_id_from_url(url: str) -> Optional[str]:
|
213
|
-
"""Extract session_id from URL."""
|
214
|
-
import re
|
215
|
-
from urllib.parse import urlparse, parse_qs
|
216
|
-
|
217
|
-
# Parse the URL
|
218
|
-
parsed = urlparse(url)
|
219
|
-
|
220
|
-
# Try to get from query parameters
|
221
|
-
query_params = parse_qs(parsed.query)
|
222
|
-
if 'session_id' in query_params:
|
223
|
-
return query_params['session_id'][0]
|
224
|
-
|
225
|
-
# Try to extract from URL path using regex
|
226
|
-
match = re.search(r'session_id=([a-f0-9]+)', url)
|
227
|
-
if match:
|
228
|
-
return match.group(1)
|
229
|
-
|
230
|
-
return None
|
231
|
-
|
232
|
-
# Global exception handler for ClosedResourceError
|
233
|
-
async def handle_closed_resource_error(request, exc):
|
234
|
-
"""Global handler for ClosedResourceError exceptions."""
|
235
|
-
logger.error(f"Global ClosedResourceError handler triggered: {exc}")
|
236
|
-
|
237
|
-
# Extract session_id from request if possible
|
238
|
-
session_id = None
|
239
|
-
if hasattr(request, 'query_params'):
|
240
|
-
session_id = request.query_params.get('session_id')
|
241
|
-
|
242
|
-
if session_id:
|
243
|
-
# Generate new session ID
|
244
|
-
new_session_id = session_manager.generate_session_id()
|
245
|
-
session_manager.add_route_mapping(session_id, new_session_id)
|
246
|
-
session_manager.remove_session(session_id, remove_route_mapping=False)
|
247
|
-
|
248
|
-
logger.info(f"Global handler: Generated new session {new_session_id} to replace {session_id}")
|
249
|
-
|
250
|
-
from starlette.responses import JSONResponse
|
251
|
-
return JSONResponse(
|
252
|
-
status_code=410, # Gone - indicates the resource is no longer available
|
253
|
-
content={
|
254
|
-
"error": "session_expired",
|
255
|
-
"message": "Session has expired. A new session has been generated.",
|
256
|
-
"old_session_id": session_id,
|
257
|
-
"new_session_id": new_session_id,
|
258
|
-
"action": "retry_with_new_session"
|
259
|
-
}
|
260
|
-
)
|
261
|
-
|
262
|
-
from starlette.responses import JSONResponse
|
263
|
-
return JSONResponse(
|
264
|
-
status_code=500,
|
265
|
-
content={
|
266
|
-
"error": "internal_server_error",
|
267
|
-
"message": "An internal server error occurred. Please try again."
|
268
|
-
}
|
269
|
-
)
|
270
|
-
|
271
84
|
# Initialize FastMCP server (will be configured in main function)
|
272
85
|
load_dotenv()
|
273
86
|
mcp = FastMCP("Media-Agent-MCP-Async")
|
@@ -623,76 +436,6 @@ async def tts_tool(text: str, speaker_id: str) -> dict:
|
|
623
436
|
return result
|
624
437
|
|
625
438
|
|
626
|
-
@mcp.tool()
|
627
|
-
async def get_session_status() -> dict:
|
628
|
-
"""
|
629
|
-
Get current session management status and statistics.
|
630
|
-
|
631
|
-
Returns:
|
632
|
-
Dictionary with session statistics and status information
|
633
|
-
"""
|
634
|
-
try:
|
635
|
-
active_sessions = session_manager.get_session_count()
|
636
|
-
route_mappings = session_manager.get_route_count()
|
637
|
-
|
638
|
-
# Clean up expired sessions (older than 1 hour)
|
639
|
-
cleaned_sessions = session_manager.cleanup_expired_sessions(3600)
|
640
|
-
|
641
|
-
return {
|
642
|
-
"status": "success",
|
643
|
-
"data": {
|
644
|
-
"active_sessions": active_sessions,
|
645
|
-
"route_mappings": route_mappings,
|
646
|
-
"cleaned_sessions": cleaned_sessions,
|
647
|
-
"session_manager_enabled": True,
|
648
|
-
"features": [
|
649
|
-
"automatic_session_recovery",
|
650
|
-
"closed_resource_error_handling",
|
651
|
-
"request_forwarding",
|
652
|
-
"session_route_mapping",
|
653
|
-
"automatic_session_cleanup"
|
654
|
-
]
|
655
|
-
},
|
656
|
-
"message": f"Session management is active. Cleaned {cleaned_sessions} expired sessions."
|
657
|
-
}
|
658
|
-
except Exception as e:
|
659
|
-
logger.error(f"Error getting session status: {e}")
|
660
|
-
return {
|
661
|
-
"status": "error",
|
662
|
-
"data": None,
|
663
|
-
"message": f"Failed to get session status: {str(e)}"
|
664
|
-
}
|
665
|
-
|
666
|
-
|
667
|
-
@mcp.tool()
|
668
|
-
async def generate_new_session() -> dict:
|
669
|
-
"""
|
670
|
-
Manually generate a new session ID for testing or recovery purposes.
|
671
|
-
|
672
|
-
Returns:
|
673
|
-
Dictionary with new session ID
|
674
|
-
"""
|
675
|
-
try:
|
676
|
-
new_session_id = session_manager.generate_session_id()
|
677
|
-
logger.info(f"Manually generated new session: {new_session_id}")
|
678
|
-
|
679
|
-
return {
|
680
|
-
"status": "success",
|
681
|
-
"data": {
|
682
|
-
"session_id": new_session_id,
|
683
|
-
"timestamp": asyncio.get_event_loop().time()
|
684
|
-
},
|
685
|
-
"message": f"New session generated: {new_session_id}"
|
686
|
-
}
|
687
|
-
except Exception as e:
|
688
|
-
logger.error(f"Error generating new session: {e}")
|
689
|
-
return {
|
690
|
-
"status": "error",
|
691
|
-
"data": None,
|
692
|
-
"message": f"Failed to generate new session: {str(e)}"
|
693
|
-
}
|
694
|
-
|
695
|
-
|
696
439
|
def main():
|
697
440
|
"""Main entry point for the Async MCP server."""
|
698
441
|
import os
|
@@ -706,8 +449,6 @@ def main():
|
|
706
449
|
help='Host for SSE transport (default: 127.0.0.1)')
|
707
450
|
parser.add_argument('--port', type=int, default=8000,
|
708
451
|
help='Port for SSE transport (default: 8000)')
|
709
|
-
parser.add_argument('--version', action='store_true',
|
710
|
-
help='Show version information')
|
711
452
|
parser.add_argument('--run-be', action='store_true',
|
712
453
|
help='Run the backend server')
|
713
454
|
parser.add_argument('--be-host', type=str, default='0.0.0.0',
|
@@ -717,10 +458,6 @@ def main():
|
|
717
458
|
|
718
459
|
args = parser.parse_args()
|
719
460
|
|
720
|
-
if args.version:
|
721
|
-
print("Async Media Agent MCP Server v0.1.0")
|
722
|
-
return
|
723
|
-
|
724
461
|
if args.run_be:
|
725
462
|
logger.info(f"Starting backend server on {args.be_host}:{args.be_port}...")
|
726
463
|
be_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'be'))
|
@@ -730,22 +467,20 @@ def main():
|
|
730
467
|
try:
|
731
468
|
subprocess.run(
|
732
469
|
[
|
733
|
-
|
734
|
-
"
|
735
|
-
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
args.be_host,
|
742
|
-
str(args.be_port),
|
470
|
+
"gunicorn",
|
471
|
+
"--workers",
|
472
|
+
"4",
|
473
|
+
"--bind",
|
474
|
+
f"{args.be_host}:{args.be_port}",
|
475
|
+
"--log-level",
|
476
|
+
"debug",
|
477
|
+
"app:app",
|
743
478
|
],
|
744
479
|
cwd=be_path,
|
745
480
|
check=True
|
746
481
|
)
|
747
482
|
except FileNotFoundError:
|
748
|
-
logger.error("`
|
483
|
+
logger.error("`gunicorn` command not found. Please ensure it is installed in your environment.")
|
749
484
|
except subprocess.CalledProcessError as e:
|
750
485
|
logger.error(f"Failed to start backend server: {e}")
|
751
486
|
return
|
@@ -754,11 +489,6 @@ def main():
|
|
754
489
|
logger.info(f"Transport: {args.transport}")
|
755
490
|
if args.transport == 'sse':
|
756
491
|
logger.info(f"SSE Server will run on {args.host}:{args.port}")
|
757
|
-
logger.info("Session management features enabled:")
|
758
|
-
logger.info(" - Automatic session expiration detection")
|
759
|
-
logger.info(" - Auto-generation of new session IDs")
|
760
|
-
logger.info(" - Request forwarding with route mapping")
|
761
|
-
logger.info(" - ClosedResourceError handling")
|
762
492
|
|
763
493
|
logger.info("Available async tools:")
|
764
494
|
logger.info(" 1. video_last_frame_tool_async - Extract last frame from video and upload to TOS")
|
@@ -776,11 +506,8 @@ def main():
|
|
776
506
|
logger.info(" 13. install_tools_plugin_async - Install development tools (ffmpeg and ffprobe)")
|
777
507
|
logger.info(" 14. omni_human_tool_async - Generate a video using Omni Human AI model")
|
778
508
|
logger.info(" 15. google_edit_tool_async - Edit images with Google Gemini (async)")
|
779
|
-
logger.info(" 16. get_session_status - Get current session management status and statistics")
|
780
|
-
logger.info(" 17. generate_new_session - Manually generate a new session ID")
|
781
509
|
logger.info("")
|
782
510
|
logger.info("All tools support concurrent execution using asyncio.gather() or run_multiple_tools_concurrently()")
|
783
|
-
logger.info("Session management tools (16-17) help monitor and manage connection sessions")
|
784
511
|
|
785
512
|
try:
|
786
513
|
# Start the server with specified transport
|
@@ -788,18 +515,9 @@ def main():
|
|
788
515
|
logger.info(f"Starting async SSE server on {args.host}:{args.port}")
|
789
516
|
mcp.settings.host = args.host
|
790
517
|
mcp.settings.port = args.port
|
791
|
-
|
792
|
-
# Get the SSE app and add global exception handler
|
793
|
-
sse_app = mcp.sse_app()
|
794
|
-
|
795
|
-
# Add global exception handler for ClosedResourceError
|
796
|
-
sse_app.add_exception_handler(anyio.ClosedResourceError, handle_closed_resource_error)
|
797
|
-
|
798
|
-
logger.info("Added global ClosedResourceError handler for automatic session recovery")
|
799
|
-
|
800
518
|
# Use uvicorn to run SSE app with extended keep-alive timeout (5 minutes)
|
801
519
|
uvicorn.run(
|
802
|
-
sse_app,
|
520
|
+
mcp.sse_app(),
|
803
521
|
host=args.host,
|
804
522
|
port=args.port,
|
805
523
|
timeout_keep_alive=300
|
Binary file
|
Binary file
|
Binary file
|
@@ -6,6 +6,7 @@ from flask import Flask
|
|
6
6
|
# Register blueprints from modularized routes
|
7
7
|
from media_agent_mcp.be.routes_media import media_bp
|
8
8
|
from media_agent_mcp.be.routes_subtitles import subtitles_bp
|
9
|
+
from media_agent_mcp.be.routes_omni_human import omni_human_bp
|
9
10
|
|
10
11
|
|
11
12
|
logger = logging.getLogger(__name__)
|
@@ -14,6 +15,7 @@ app = Flask(__name__)
|
|
14
15
|
# Register blueprints (keep original paths unchanged)
|
15
16
|
app.register_blueprint(media_bp)
|
16
17
|
app.register_blueprint(subtitles_bp)
|
18
|
+
app.register_blueprint(omni_human_bp)
|
17
19
|
|
18
20
|
|
19
21
|
if __name__ == "__main__":
|
@@ -5,6 +5,7 @@ import subprocess
|
|
5
5
|
import tempfile
|
6
6
|
from pathlib import Path
|
7
7
|
from typing import List, Optional, Dict, Any
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
8
9
|
|
9
10
|
from flask import Blueprint, jsonify, request, send_file, after_this_request
|
10
11
|
|
@@ -160,13 +161,15 @@ def concat_videos():
|
|
160
161
|
pass
|
161
162
|
return response
|
162
163
|
|
163
|
-
logger.info('Downloading videos')
|
164
|
+
logger.info('Downloading videos concurrently')
|
164
165
|
video_paths = []
|
165
|
-
|
166
|
-
|
166
|
+
with ThreadPoolExecutor(max_workers=min(len(video_urls), 5)) as executor:
|
167
|
+
download_results = list(executor.map(download_video_from_url, video_urls))
|
168
|
+
|
169
|
+
for dl in download_results:
|
167
170
|
if dl.get("status") == "error":
|
168
171
|
return jsonify(dl), 400
|
169
|
-
path = Path(dl["data"]["file_path"])
|
172
|
+
path = Path(dl["data"]["file_path"])
|
170
173
|
temp_files.append(path)
|
171
174
|
video_paths.append(path)
|
172
175
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from flask import Blueprint, request, jsonify
|
2
|
+
from media_agent_mcp.video.omni_human import generate_video_from_omni_human
|
3
|
+
|
4
|
+
omni_human_bp = Blueprint("omni_human", __name__)
|
5
|
+
|
6
|
+
@omni_human_bp.post("/generate-video-from-omni-human")
|
7
|
+
def generate_video():
|
8
|
+
"""
|
9
|
+
Generates a video from an image and audio using the Omni Human API.
|
10
|
+
"""
|
11
|
+
try:
|
12
|
+
data = request.get_json(silent=True) or {}
|
13
|
+
image_url = data.get("image_url")
|
14
|
+
audio_url = data.get("audio_url")
|
15
|
+
|
16
|
+
if not image_url or not audio_url:
|
17
|
+
return jsonify({
|
18
|
+
"status": "error",
|
19
|
+
"data": None,
|
20
|
+
"message": "Fields image_url and audio_url are required"
|
21
|
+
}), 400
|
22
|
+
|
23
|
+
video_url = generate_video_from_omni_human(image_url, audio_url)
|
24
|
+
|
25
|
+
return jsonify({
|
26
|
+
"status": "success",
|
27
|
+
"data": {
|
28
|
+
"video_url": video_url
|
29
|
+
},
|
30
|
+
"message": "Video generated successfully"
|
31
|
+
})
|
32
|
+
|
33
|
+
except Exception as e:
|
34
|
+
return jsonify({"status": "error", "data": None, "message": str(e)}), 500
|
@@ -0,0 +1,118 @@
|
|
1
|
+
import hashlib
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import random
|
5
|
+
import time
|
6
|
+
from typing import Dict, Any
|
7
|
+
import requests
|
8
|
+
|
9
|
+
|
10
|
+
def _generate_signature(nonce: int, timestamp: int, security_key: str) -> str:
|
11
|
+
"""
|
12
|
+
Generates a signature for the API request.
|
13
|
+
"""
|
14
|
+
keys = [str(nonce), str(security_key), str(timestamp)]
|
15
|
+
keys.sort()
|
16
|
+
key_str = "".join(keys).encode("utf-8")
|
17
|
+
signature = hashlib.sha1(key_str).hexdigest()
|
18
|
+
return signature.lower()
|
19
|
+
|
20
|
+
|
21
|
+
def _submit_task(image_url: str, audio_url: str, api_key: str, security_key: str) -> str:
|
22
|
+
"""
|
23
|
+
Submits a video generation task.
|
24
|
+
"""
|
25
|
+
submit_task_url = "https://cv-api.byteintlapi.com/api/common/v2/submit_task"
|
26
|
+
timestamp = int(time.time())
|
27
|
+
nonce = random.randint(0, (1 << 31) - 1)
|
28
|
+
signature = _generate_signature(nonce, timestamp, security_key)
|
29
|
+
|
30
|
+
params = {
|
31
|
+
"api_key": api_key,
|
32
|
+
"timestamp": str(timestamp),
|
33
|
+
"nonce": str(nonce),
|
34
|
+
"sign": signature,
|
35
|
+
}
|
36
|
+
headers = {"Content-Type": "application/json"}
|
37
|
+
body = {
|
38
|
+
"req_key": "realman_avatar_picture_omni_cv",
|
39
|
+
"image_url": image_url,
|
40
|
+
"audio_url": audio_url,
|
41
|
+
}
|
42
|
+
|
43
|
+
response = requests.post(submit_task_url, params=params, headers=headers, json=body)
|
44
|
+
response.raise_for_status()
|
45
|
+
data = response.json()
|
46
|
+
if data["code"] != 10000:
|
47
|
+
raise Exception(f"Failed to submit task: {data['message']}")
|
48
|
+
return data["data"]["task_id"]
|
49
|
+
|
50
|
+
|
51
|
+
def _get_task_result(task_id: str, api_key: str, security_key: str) -> Dict[str, Any]:
|
52
|
+
"""
|
53
|
+
Gets the result of a video generation task.
|
54
|
+
"""
|
55
|
+
get_result_url = "https://cv-api.byteintlapi.com/api/common/v2/get_result"
|
56
|
+
timestamp = int(time.time())
|
57
|
+
nonce = random.randint(0, (1 << 31) - 1)
|
58
|
+
signature = _generate_signature(nonce, timestamp, security_key)
|
59
|
+
|
60
|
+
params = {
|
61
|
+
"api_key": api_key,
|
62
|
+
"timestamp": str(timestamp),
|
63
|
+
"nonce": str(nonce),
|
64
|
+
"sign": signature,
|
65
|
+
}
|
66
|
+
headers = {"Content-Type": "application/json"}
|
67
|
+
body = {
|
68
|
+
"req_key": "realman_avatar_picture_omni_cv",
|
69
|
+
"task_id": task_id,
|
70
|
+
}
|
71
|
+
|
72
|
+
response = requests.post(get_result_url, params=params, headers=headers, json=body)
|
73
|
+
print(response.text)
|
74
|
+
response.raise_for_status()
|
75
|
+
return response.json()
|
76
|
+
|
77
|
+
|
78
|
+
def generate_video_from_omni_human(image_url: str, audio_url: str) -> str:
|
79
|
+
"""
|
80
|
+
Generates a video from an image and audio using the Omni Human API.
|
81
|
+
|
82
|
+
Args:
|
83
|
+
image_url: The URL of the portrait image.
|
84
|
+
audio_url: The URL of the audio.
|
85
|
+
|
86
|
+
Returns:
|
87
|
+
The URL of the generated video.
|
88
|
+
"""
|
89
|
+
api_key = os.environ.get("OMNI_HUMAN_AK")
|
90
|
+
security_key = os.environ.get("OMNI_HUMAN_SK")
|
91
|
+
|
92
|
+
if not api_key or not security_key:
|
93
|
+
raise ValueError("OMNI_HUMAN_AK and OMNI_HUMAN_SK environment variables must be set")
|
94
|
+
|
95
|
+
task_id = _submit_task(image_url, audio_url, api_key, security_key)
|
96
|
+
print('Submitted task, task_id:', task_id)
|
97
|
+
while True:
|
98
|
+
result = _get_task_result(task_id, api_key, security_key)
|
99
|
+
if result["code"] != 10000:
|
100
|
+
raise Exception(f"Failed to get task result: {result['message']}")
|
101
|
+
|
102
|
+
status = result.get("data", {}).get("status")
|
103
|
+
if status == "done":
|
104
|
+
# Parse resp_data JSON string to get video_url
|
105
|
+
resp_data_str = result["data"].get("resp_data", "{}")
|
106
|
+
try:
|
107
|
+
resp_data = json.loads(resp_data_str)
|
108
|
+
video_url = resp_data.get("video_url")
|
109
|
+
if video_url:
|
110
|
+
return video_url
|
111
|
+
else:
|
112
|
+
raise Exception(f"No video_url found in response: {resp_data}")
|
113
|
+
except json.JSONDecodeError as e:
|
114
|
+
raise Exception(f"Failed to parse resp_data JSON: {e}")
|
115
|
+
elif status in ["failed", "error"]:
|
116
|
+
raise Exception(f"Video generation failed: {result}")
|
117
|
+
|
118
|
+
time.sleep(5)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: media-agent-mcp
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.7.2
|
4
4
|
Summary: A Model Context Protocol server for media processing with AI tools
|
5
5
|
Author-email: Media Agent Team <team@mediaagent.com>
|
6
6
|
Keywords: mcp,ai,media,video,image,processing
|
@@ -26,6 +26,7 @@ Requires-Dist: loguru>=0.7.3
|
|
26
26
|
Requires-Dist: imageio-ffmpeg>=0.4.0
|
27
27
|
Requires-Dist: Flask>=3.0.0
|
28
28
|
Requires-Dist: pydub>=0.25.1
|
29
|
+
Requires-Dist: gunicorn>=22.0.0
|
29
30
|
Requires-Dist: audioop-lts; python_version >= "3.13"
|
30
31
|
Requires-Dist: google-genai>=1.33.0
|
31
32
|
Requires-Dist: mcp==1.11.0
|
@@ -25,6 +25,7 @@ src/media_agent_mcp/be/__init__.py
|
|
25
25
|
src/media_agent_mcp/be/app.py
|
26
26
|
src/media_agent_mcp/be/pyproject.toml
|
27
27
|
src/media_agent_mcp/be/routes_media.py
|
28
|
+
src/media_agent_mcp/be/routes_omni_human.py
|
28
29
|
src/media_agent_mcp/be/routes_subtitles.py
|
29
30
|
src/media_agent_mcp/be/utils.py
|
30
31
|
src/media_agent_mcp/be/uv.lock
|
@@ -32,6 +33,7 @@ src/media_agent_mcp/be/__pycache__/__init__.cpython-312.pyc
|
|
32
33
|
src/media_agent_mcp/be/__pycache__/app.cpython-312.pyc
|
33
34
|
src/media_agent_mcp/be/__pycache__/routes_media.cpython-312.pyc
|
34
35
|
src/media_agent_mcp/be/__pycache__/routes_omni.cpython-312.pyc
|
36
|
+
src/media_agent_mcp/be/__pycache__/routes_omni_human.cpython-312.pyc
|
35
37
|
src/media_agent_mcp/be/__pycache__/routes_subtitles.cpython-312.pyc
|
36
38
|
src/media_agent_mcp/be/__pycache__/utils.cpython-312.pyc
|
37
39
|
src/media_agent_mcp/be/fonts/en/EduNSWACTCursive-VariableFont_wght.ttf
|
@@ -48,6 +50,7 @@ src/media_agent_mcp/media_selectors/video_selector.py
|
|
48
50
|
src/media_agent_mcp/storage/__init__.py
|
49
51
|
src/media_agent_mcp/storage/tos_client.py
|
50
52
|
src/media_agent_mcp/video/__init__.py
|
53
|
+
src/media_agent_mcp/video/omni_human.py
|
51
54
|
src/media_agent_mcp/video/processor.py
|
52
55
|
src/media_agent_mcp/video/stack.py
|
53
56
|
src/media_agent_mcp/video/subtitle.py
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
File without changes
|
File without changes
|
File without changes
|
{media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/ai_models/openaiedit.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/audio/speed_controller.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/install_tools/__init__.py
RENAMED
File without changes
|
{media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/install_tools/installer.py
RENAMED
File without changes
|
{media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp/media_selectors/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp.egg-info/dependency_links.txt
RENAMED
File without changes
|
{media_agent_mcp-2.6.14 → media_agent_mcp-2.7.2}/src/media_agent_mcp.egg-info/entry_points.txt
RENAMED
File without changes
|
File without changes
|