Upload app.py
Browse files
app.py
CHANGED
|
@@ -446,6 +446,19 @@ async def render_animation_endpoint(
|
|
| 446 |
)
|
| 447 |
|
| 448 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
@app.get("/media/{request_id}/{filename}", tags=["media"])
|
| 450 |
async def download_file(request_id: str, filename: str):
|
| 451 |
"""
|
|
@@ -494,7 +507,10 @@ async def download_file(request_id: str, filename: str):
|
|
| 494 |
filename=filename,
|
| 495 |
headers={
|
| 496 |
"Cache-Control": "public, max-age=3600",
|
| 497 |
-
"Content-Disposition": f'inline; filename="{filename}"'
|
|
|
|
|
|
|
|
|
|
| 498 |
}
|
| 499 |
)
|
| 500 |
|
|
@@ -621,18 +637,18 @@ def get_render_log_tail(request_id: str, max_chars: int = 2000) -> Optional[str]
|
|
| 621 |
if not log_path.exists():
|
| 622 |
return None
|
| 623 |
|
| 624 |
-
try:
|
| 625 |
-
max_bytes = max_chars * 4
|
| 626 |
-
with log_path.open("rb") as handle:
|
| 627 |
-
handle.seek(0, os.SEEK_END)
|
| 628 |
-
size = handle.tell()
|
| 629 |
-
read_size = min(size, max_bytes)
|
| 630 |
-
handle.seek(-read_size, os.SEEK_END)
|
| 631 |
-
data = handle.read()
|
| 632 |
-
text = data.decode("utf-8", errors="replace").replace("\r", "\n")
|
| 633 |
-
if len(text) > max_chars:
|
| 634 |
-
text = text[-max_chars:]
|
| 635 |
-
return text.strip()
|
| 636 |
except OSError:
|
| 637 |
return None
|
| 638 |
|
|
@@ -694,21 +710,21 @@ async def run_mcp_render(
|
|
| 694 |
return False, error_message, {}
|
| 695 |
|
| 696 |
# Convert to requested formats
|
| 697 |
-
formats_to_convert = []
|
| 698 |
-
output_format = arguments.get("output_format", "mp4")
|
| 699 |
-
|
| 700 |
-
if output_format == "all":
|
| 701 |
-
formats_to_convert = ["gif", "webm"]
|
| 702 |
-
elif output_format != "mp4":
|
| 703 |
-
formats_to_convert = [output_format]
|
| 704 |
-
|
| 705 |
-
conversion_results = {"mp4": output_mp4}
|
| 706 |
-
if formats_to_convert:
|
| 707 |
-
conversion_results.update(await convert_all_formats(
|
| 708 |
-
input_mp4=output_mp4,
|
| 709 |
-
output_formats=formats_to_convert,
|
| 710 |
-
request_id=request_id
|
| 711 |
-
))
|
| 712 |
|
| 713 |
# Generate download URLs
|
| 714 |
base_url = os.getenv("SPACE_HOST", "https://euler314-manim-mcp.hf.space")
|
|
@@ -894,78 +910,78 @@ Use `check_render_status` to see progress and get the output when ready."""
|
|
| 894 |
# Calculate expiration
|
| 895 |
expires_at = datetime.now(timezone.utc) + timedelta(hours=1)
|
| 896 |
|
| 897 |
-
# Format response for ChatGPT
|
| 898 |
-
url_list = "\n".join([f" - {fmt.upper()}: {url}" for fmt, url in urls.items()])
|
| 899 |
-
log_tail = get_render_log_tail(request_id)
|
| 900 |
-
log_block = ""
|
| 901 |
-
if log_tail:
|
| 902 |
-
log_block = f"""
|
| 903 |
-
Recent output:
|
| 904 |
-
```
|
| 905 |
-
{log_tail}
|
| 906 |
-
```"""
|
| 907 |
-
|
| 908 |
-
response_text = f"""Animation rendered successfully.
|
| 909 |
-
|
| 910 |
-
Scene: {scene_name}
|
| 911 |
-
Quality: {arguments.get('quality', '720p')}
|
| 912 |
-
Request ID: `{request_id}`
|
| 913 |
-
|
| 914 |
-
Download URLs:
|
| 915 |
-
{url_list}
|
| 916 |
-
|
| 917 |
-
Files expire at: {expires_at.strftime('%Y-%m-%d %H:%M:%S UTC')}
|
| 918 |
-
{log_block}
|
| 919 |
-
|
| 920 |
-
Your animation is ready. Use the video player below or download via the links.
|
| 921 |
-
"""
|
| 922 |
|
| 923 |
# Get base URL for resources
|
| 924 |
resource_base_url = os.getenv("SPACE_HOST", "https://euler314-manim-mcp.hf.space")
|
| 925 |
if not resource_base_url.startswith("http"):
|
| 926 |
resource_base_url = f"https://{resource_base_url}"
|
| 927 |
|
| 928 |
-
content_items = [
|
| 929 |
-
{
|
| 930 |
-
"type": "text",
|
| 931 |
-
"text": response_text
|
| 932 |
-
}
|
| 933 |
-
]
|
| 934 |
-
|
| 935 |
-
mp4_url = urls.get("mp4")
|
| 936 |
-
if mp4_url:
|
| 937 |
-
content_items.append({
|
| 938 |
-
"type": "resource",
|
| 939 |
-
"resource": {
|
| 940 |
-
"uri": mp4_url,
|
| 941 |
-
"mimeType": "video/mp4"
|
| 942 |
-
}
|
| 943 |
-
})
|
| 944 |
-
|
| 945 |
-
# Return with both text content and UI component
|
| 946 |
-
return {
|
| 947 |
-
"content": content_items + [{
|
| 948 |
-
"type": "resource",
|
| 949 |
-
"resource": {
|
| 950 |
-
"uri": f"{resource_base_url}/mcp/resources/video-player",
|
| 951 |
-
"mimeType": "text/html",
|
| 952 |
-
"text": response_text
|
| 953 |
-
}
|
| 954 |
-
}],
|
| 955 |
-
"isError": False,
|
| 956 |
-
"_meta": {
|
| 957 |
-
"openai/outputTemplate": f"{resource_base_url}/mcp/resources/video-player"
|
| 958 |
-
},
|
| 959 |
-
"structuredContent": {
|
| 960 |
-
"urls": urls,
|
| 961 |
-
"scene_name": scene_name,
|
| 962 |
-
"quality": arguments.get("quality", "720p"),
|
| 963 |
-
"request_id": request_id,
|
| 964 |
-
"expires_at": expires_at.isoformat(),
|
| 965 |
-
"log_tail": log_tail,
|
| 966 |
-
"status": "ready"
|
| 967 |
-
}
|
| 968 |
-
}
|
| 969 |
|
| 970 |
except Exception as e:
|
| 971 |
logger.exception(f"MCP render error: {e}")
|
|
@@ -1004,22 +1020,22 @@ async def get_render_status_for_mcp(arguments: dict) -> dict:
|
|
| 1004 |
}
|
| 1005 |
|
| 1006 |
# Check if expired
|
| 1007 |
-
if await cleanup_service.check_if_expired(request_id):
|
| 1008 |
-
return {
|
| 1009 |
-
"content": [{
|
| 1010 |
-
"type": "text",
|
| 1011 |
-
"text": f"""Render expired.
|
| 1012 |
|
| 1013 |
Request ID: `{request_id}`
|
| 1014 |
-
|
| 1015 |
-
This render has expired (files are deleted after 1 hour). You'll need to re-render the animation."""
|
| 1016 |
-
}],
|
| 1017 |
-
"isError": False,
|
| 1018 |
-
"structuredContent": {
|
| 1019 |
-
"request_id": request_id,
|
| 1020 |
-
"status": "expired"
|
| 1021 |
-
}
|
| 1022 |
-
}
|
| 1023 |
|
| 1024 |
status_info = read_render_status(request_id)
|
| 1025 |
if status_info:
|
|
@@ -1035,40 +1051,40 @@ Recent output:
|
|
| 1035 |
{log_tail}
|
| 1036 |
```"""
|
| 1037 |
|
| 1038 |
-
if status == "error":
|
| 1039 |
-
return {
|
| 1040 |
-
"content": [{
|
| 1041 |
-
"type": "text",
|
| 1042 |
-
"text": f"""Render failed.
|
| 1043 |
-
|
| 1044 |
-
Request ID: `{request_id}`
|
| 1045 |
-
|
| 1046 |
-
{message}{log_block}"""
|
| 1047 |
-
}],
|
| 1048 |
-
"isError": True,
|
| 1049 |
-
"structuredContent": {
|
| 1050 |
-
"request_id": request_id,
|
| 1051 |
-
"status": "error",
|
| 1052 |
-
"log_tail": log_tail
|
| 1053 |
-
}
|
| 1054 |
-
}
|
| 1055 |
-
if status == "processing":
|
| 1056 |
-
return {
|
| 1057 |
-
"content": [{
|
| 1058 |
-
"type": "text",
|
| 1059 |
-
"text": f"""Render in progress.
|
| 1060 |
-
|
| 1061 |
-
Request ID: `{request_id}`
|
| 1062 |
-
|
| 1063 |
-
The render is still processing. Please wait and try again.{log_block}"""
|
| 1064 |
-
}],
|
| 1065 |
-
"isError": False,
|
| 1066 |
-
"structuredContent": {
|
| 1067 |
-
"request_id": request_id,
|
| 1068 |
-
"status": "processing",
|
| 1069 |
-
"log_tail": log_tail
|
| 1070 |
-
}
|
| 1071 |
-
}
|
| 1072 |
|
| 1073 |
# Get render info
|
| 1074 |
render_info = await cleanup_service.get_render_info(request_id)
|
|
@@ -1086,38 +1102,38 @@ Recent output:
|
|
| 1086 |
```
|
| 1087 |
{log_tail}
|
| 1088 |
```"""
|
| 1089 |
-
return {
|
| 1090 |
-
"content": [{
|
| 1091 |
-
"type": "text",
|
| 1092 |
-
"text": f"""Render in progress.
|
| 1093 |
-
|
| 1094 |
-
Request ID: `{request_id}`
|
| 1095 |
-
|
| 1096 |
-
The render is still processing. Please wait and try again.{log_block}"""
|
| 1097 |
-
}],
|
| 1098 |
-
"isError": False,
|
| 1099 |
-
"structuredContent": {
|
| 1100 |
-
"request_id": request_id,
|
| 1101 |
-
"status": "processing",
|
| 1102 |
-
"log_tail": log_tail
|
| 1103 |
-
}
|
| 1104 |
-
}
|
| 1105 |
-
else:
|
| 1106 |
-
return {
|
| 1107 |
-
"content": [{
|
| 1108 |
-
"type": "text",
|
| 1109 |
-
"text": f"""Render not found.
|
| 1110 |
|
| 1111 |
Request ID: `{request_id}`
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
}],
|
| 1115 |
-
"isError":
|
| 1116 |
-
"structuredContent": {
|
| 1117 |
-
"request_id": request_id,
|
| 1118 |
-
"status": "
|
| 1119 |
-
|
| 1120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1121 |
|
| 1122 |
# Render exists - provide details
|
| 1123 |
base_url = os.getenv("SPACE_HOST", "https://euler314-manim-mcp.hf.space")
|
|
@@ -1133,56 +1149,56 @@ No render found with this ID. It may have expired or never existed."""
|
|
| 1133 |
time_remaining = expires_at - datetime.now(timezone.utc)
|
| 1134 |
minutes_remaining = int(time_remaining.total_seconds() / 60)
|
| 1135 |
|
| 1136 |
-
url_list = "\n".join([f" - {fmt.upper()}: {url}" for fmt, url in urls.items()])
|
| 1137 |
-
log_tail = get_render_log_tail(request_id)
|
| 1138 |
-
log_block = ""
|
| 1139 |
-
if log_tail:
|
| 1140 |
-
log_block = f"""
|
| 1141 |
-
Recent output:
|
| 1142 |
-
```
|
| 1143 |
-
{log_tail}
|
| 1144 |
-
```"""
|
| 1145 |
-
|
| 1146 |
-
status_text = f"""Render ready.
|
| 1147 |
-
|
| 1148 |
-
Request ID: `{request_id}`
|
| 1149 |
-
Available Formats: {', '.join(render_info['formats'])}
|
| 1150 |
-
File Size: {render_info.get('size_bytes', 0) / 1024 / 1024:.2f} MB
|
| 1151 |
-
Expires In: {minutes_remaining} minutes
|
| 1152 |
-
|
| 1153 |
-
Download URLs:
|
| 1154 |
-
{url_list}
|
| 1155 |
-
|
| 1156 |
-
Expiration: {expires_at.strftime('%Y-%m-%d %H:%M:%S UTC')}
|
| 1157 |
-
{log_block}
|
| 1158 |
-
"""
|
| 1159 |
-
|
| 1160 |
-
content_items = [{
|
| 1161 |
-
"type": "text",
|
| 1162 |
-
"text": status_text
|
| 1163 |
-
}]
|
| 1164 |
-
|
| 1165 |
-
mp4_url = urls.get("mp4")
|
| 1166 |
-
if mp4_url:
|
| 1167 |
-
content_items.append({
|
| 1168 |
-
"type": "resource",
|
| 1169 |
-
"resource": {
|
| 1170 |
-
"uri": mp4_url,
|
| 1171 |
-
"mimeType": "video/mp4"
|
| 1172 |
-
}
|
| 1173 |
-
})
|
| 1174 |
-
|
| 1175 |
-
return {
|
| 1176 |
-
"content": content_items,
|
| 1177 |
-
"isError": False,
|
| 1178 |
-
"structuredContent": {
|
| 1179 |
-
"request_id": request_id,
|
| 1180 |
-
"status": "ready",
|
| 1181 |
-
"urls": urls,
|
| 1182 |
-
"expires_at": expires_at.isoformat(),
|
| 1183 |
-
"log_tail": log_tail
|
| 1184 |
-
}
|
| 1185 |
-
}
|
| 1186 |
|
| 1187 |
except Exception as e:
|
| 1188 |
logger.exception(f"Error checking render status: {e}")
|
|
|
|
| 446 |
)
|
| 447 |
|
| 448 |
|
| 449 |
+
@app.options("/media/{request_id}/{filename}", tags=["media"])
|
| 450 |
+
async def download_file_options(request_id: str, filename: str):
|
| 451 |
+
"""Handle CORS preflight requests for media files"""
|
| 452 |
+
return JSONResponse(
|
| 453 |
+
content={},
|
| 454 |
+
headers={
|
| 455 |
+
"Access-Control-Allow-Origin": "*",
|
| 456 |
+
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
|
| 457 |
+
"Access-Control-Allow-Headers": "*",
|
| 458 |
+
}
|
| 459 |
+
)
|
| 460 |
+
|
| 461 |
+
|
| 462 |
@app.get("/media/{request_id}/{filename}", tags=["media"])
|
| 463 |
async def download_file(request_id: str, filename: str):
|
| 464 |
"""
|
|
|
|
| 507 |
filename=filename,
|
| 508 |
headers={
|
| 509 |
"Cache-Control": "public, max-age=3600",
|
| 510 |
+
"Content-Disposition": f'inline; filename="{filename}"',
|
| 511 |
+
"Access-Control-Allow-Origin": "*",
|
| 512 |
+
"Access-Control-Allow-Methods": "GET, HEAD, OPTIONS",
|
| 513 |
+
"Access-Control-Allow-Headers": "*"
|
| 514 |
}
|
| 515 |
)
|
| 516 |
|
|
|
|
| 637 |
if not log_path.exists():
|
| 638 |
return None
|
| 639 |
|
| 640 |
+
try:
|
| 641 |
+
max_bytes = max_chars * 4
|
| 642 |
+
with log_path.open("rb") as handle:
|
| 643 |
+
handle.seek(0, os.SEEK_END)
|
| 644 |
+
size = handle.tell()
|
| 645 |
+
read_size = min(size, max_bytes)
|
| 646 |
+
handle.seek(-read_size, os.SEEK_END)
|
| 647 |
+
data = handle.read()
|
| 648 |
+
text = data.decode("utf-8", errors="replace").replace("\r", "\n")
|
| 649 |
+
if len(text) > max_chars:
|
| 650 |
+
text = text[-max_chars:]
|
| 651 |
+
return text.strip()
|
| 652 |
except OSError:
|
| 653 |
return None
|
| 654 |
|
|
|
|
| 710 |
return False, error_message, {}
|
| 711 |
|
| 712 |
# Convert to requested formats
|
| 713 |
+
formats_to_convert = []
|
| 714 |
+
output_format = arguments.get("output_format", "mp4")
|
| 715 |
+
|
| 716 |
+
if output_format == "all":
|
| 717 |
+
formats_to_convert = ["gif", "webm"]
|
| 718 |
+
elif output_format != "mp4":
|
| 719 |
+
formats_to_convert = [output_format]
|
| 720 |
+
|
| 721 |
+
conversion_results = {"mp4": output_mp4}
|
| 722 |
+
if formats_to_convert:
|
| 723 |
+
conversion_results.update(await convert_all_formats(
|
| 724 |
+
input_mp4=output_mp4,
|
| 725 |
+
output_formats=formats_to_convert,
|
| 726 |
+
request_id=request_id
|
| 727 |
+
))
|
| 728 |
|
| 729 |
# Generate download URLs
|
| 730 |
base_url = os.getenv("SPACE_HOST", "https://euler314-manim-mcp.hf.space")
|
|
|
|
| 910 |
# Calculate expiration
|
| 911 |
expires_at = datetime.now(timezone.utc) + timedelta(hours=1)
|
| 912 |
|
| 913 |
+
# Format response for ChatGPT
|
| 914 |
+
url_list = "\n".join([f" - {fmt.upper()}: {url}" for fmt, url in urls.items()])
|
| 915 |
+
log_tail = get_render_log_tail(request_id)
|
| 916 |
+
log_block = ""
|
| 917 |
+
if log_tail:
|
| 918 |
+
log_block = f"""
|
| 919 |
+
Recent output:
|
| 920 |
+
```
|
| 921 |
+
{log_tail}
|
| 922 |
+
```"""
|
| 923 |
+
|
| 924 |
+
response_text = f"""Animation rendered successfully.
|
| 925 |
+
|
| 926 |
+
Scene: {scene_name}
|
| 927 |
+
Quality: {arguments.get('quality', '720p')}
|
| 928 |
+
Request ID: `{request_id}`
|
| 929 |
+
|
| 930 |
+
Download URLs:
|
| 931 |
+
{url_list}
|
| 932 |
+
|
| 933 |
+
Files expire at: {expires_at.strftime('%Y-%m-%d %H:%M:%S UTC')}
|
| 934 |
+
{log_block}
|
| 935 |
+
|
| 936 |
+
Your animation is ready. Use the video player below or download via the links.
|
| 937 |
+
"""
|
| 938 |
|
| 939 |
# Get base URL for resources
|
| 940 |
resource_base_url = os.getenv("SPACE_HOST", "https://euler314-manim-mcp.hf.space")
|
| 941 |
if not resource_base_url.startswith("http"):
|
| 942 |
resource_base_url = f"https://{resource_base_url}"
|
| 943 |
|
| 944 |
+
content_items = [
|
| 945 |
+
{
|
| 946 |
+
"type": "text",
|
| 947 |
+
"text": response_text
|
| 948 |
+
}
|
| 949 |
+
]
|
| 950 |
+
|
| 951 |
+
mp4_url = urls.get("mp4")
|
| 952 |
+
if mp4_url:
|
| 953 |
+
content_items.append({
|
| 954 |
+
"type": "resource",
|
| 955 |
+
"resource": {
|
| 956 |
+
"uri": mp4_url,
|
| 957 |
+
"mimeType": "video/mp4"
|
| 958 |
+
}
|
| 959 |
+
})
|
| 960 |
+
|
| 961 |
+
# Return with both text content and UI component
|
| 962 |
+
return {
|
| 963 |
+
"content": content_items + [{
|
| 964 |
+
"type": "resource",
|
| 965 |
+
"resource": {
|
| 966 |
+
"uri": f"{resource_base_url}/mcp/resources/video-player",
|
| 967 |
+
"mimeType": "text/html",
|
| 968 |
+
"text": response_text
|
| 969 |
+
}
|
| 970 |
+
}],
|
| 971 |
+
"isError": False,
|
| 972 |
+
"_meta": {
|
| 973 |
+
"openai/outputTemplate": f"{resource_base_url}/mcp/resources/video-player"
|
| 974 |
+
},
|
| 975 |
+
"structuredContent": {
|
| 976 |
+
"urls": urls,
|
| 977 |
+
"scene_name": scene_name,
|
| 978 |
+
"quality": arguments.get("quality", "720p"),
|
| 979 |
+
"request_id": request_id,
|
| 980 |
+
"expires_at": expires_at.isoformat(),
|
| 981 |
+
"log_tail": log_tail,
|
| 982 |
+
"status": "ready"
|
| 983 |
+
}
|
| 984 |
+
}
|
| 985 |
|
| 986 |
except Exception as e:
|
| 987 |
logger.exception(f"MCP render error: {e}")
|
|
|
|
| 1020 |
}
|
| 1021 |
|
| 1022 |
# Check if expired
|
| 1023 |
+
if await cleanup_service.check_if_expired(request_id):
|
| 1024 |
+
return {
|
| 1025 |
+
"content": [{
|
| 1026 |
+
"type": "text",
|
| 1027 |
+
"text": f"""Render expired.
|
| 1028 |
|
| 1029 |
Request ID: `{request_id}`
|
| 1030 |
+
|
| 1031 |
+
This render has expired (files are deleted after 1 hour). You'll need to re-render the animation."""
|
| 1032 |
+
}],
|
| 1033 |
+
"isError": False,
|
| 1034 |
+
"structuredContent": {
|
| 1035 |
+
"request_id": request_id,
|
| 1036 |
+
"status": "expired"
|
| 1037 |
+
}
|
| 1038 |
+
}
|
| 1039 |
|
| 1040 |
status_info = read_render_status(request_id)
|
| 1041 |
if status_info:
|
|
|
|
| 1051 |
{log_tail}
|
| 1052 |
```"""
|
| 1053 |
|
| 1054 |
+
if status == "error":
|
| 1055 |
+
return {
|
| 1056 |
+
"content": [{
|
| 1057 |
+
"type": "text",
|
| 1058 |
+
"text": f"""Render failed.
|
| 1059 |
+
|
| 1060 |
+
Request ID: `{request_id}`
|
| 1061 |
+
|
| 1062 |
+
{message}{log_block}"""
|
| 1063 |
+
}],
|
| 1064 |
+
"isError": True,
|
| 1065 |
+
"structuredContent": {
|
| 1066 |
+
"request_id": request_id,
|
| 1067 |
+
"status": "error",
|
| 1068 |
+
"log_tail": log_tail
|
| 1069 |
+
}
|
| 1070 |
+
}
|
| 1071 |
+
if status == "processing":
|
| 1072 |
+
return {
|
| 1073 |
+
"content": [{
|
| 1074 |
+
"type": "text",
|
| 1075 |
+
"text": f"""Render in progress.
|
| 1076 |
+
|
| 1077 |
+
Request ID: `{request_id}`
|
| 1078 |
+
|
| 1079 |
+
The render is still processing. Please wait and try again.{log_block}"""
|
| 1080 |
+
}],
|
| 1081 |
+
"isError": False,
|
| 1082 |
+
"structuredContent": {
|
| 1083 |
+
"request_id": request_id,
|
| 1084 |
+
"status": "processing",
|
| 1085 |
+
"log_tail": log_tail
|
| 1086 |
+
}
|
| 1087 |
+
}
|
| 1088 |
|
| 1089 |
# Get render info
|
| 1090 |
render_info = await cleanup_service.get_render_info(request_id)
|
|
|
|
| 1102 |
```
|
| 1103 |
{log_tail}
|
| 1104 |
```"""
|
| 1105 |
+
return {
|
| 1106 |
+
"content": [{
|
| 1107 |
+
"type": "text",
|
| 1108 |
+
"text": f"""Render in progress.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1109 |
|
| 1110 |
Request ID: `{request_id}`
|
| 1111 |
+
|
| 1112 |
+
The render is still processing. Please wait and try again.{log_block}"""
|
| 1113 |
+
}],
|
| 1114 |
+
"isError": False,
|
| 1115 |
+
"structuredContent": {
|
| 1116 |
+
"request_id": request_id,
|
| 1117 |
+
"status": "processing",
|
| 1118 |
+
"log_tail": log_tail
|
| 1119 |
+
}
|
| 1120 |
+
}
|
| 1121 |
+
else:
|
| 1122 |
+
return {
|
| 1123 |
+
"content": [{
|
| 1124 |
+
"type": "text",
|
| 1125 |
+
"text": f"""Render not found.
|
| 1126 |
+
|
| 1127 |
+
Request ID: `{request_id}`
|
| 1128 |
+
|
| 1129 |
+
No render found with this ID. It may have expired or never existed."""
|
| 1130 |
+
}],
|
| 1131 |
+
"isError": True,
|
| 1132 |
+
"structuredContent": {
|
| 1133 |
+
"request_id": request_id,
|
| 1134 |
+
"status": "not_found"
|
| 1135 |
+
}
|
| 1136 |
+
}
|
| 1137 |
|
| 1138 |
# Render exists - provide details
|
| 1139 |
base_url = os.getenv("SPACE_HOST", "https://euler314-manim-mcp.hf.space")
|
|
|
|
| 1149 |
time_remaining = expires_at - datetime.now(timezone.utc)
|
| 1150 |
minutes_remaining = int(time_remaining.total_seconds() / 60)
|
| 1151 |
|
| 1152 |
+
url_list = "\n".join([f" - {fmt.upper()}: {url}" for fmt, url in urls.items()])
|
| 1153 |
+
log_tail = get_render_log_tail(request_id)
|
| 1154 |
+
log_block = ""
|
| 1155 |
+
if log_tail:
|
| 1156 |
+
log_block = f"""
|
| 1157 |
+
Recent output:
|
| 1158 |
+
```
|
| 1159 |
+
{log_tail}
|
| 1160 |
+
```"""
|
| 1161 |
+
|
| 1162 |
+
status_text = f"""Render ready.
|
| 1163 |
+
|
| 1164 |
+
Request ID: `{request_id}`
|
| 1165 |
+
Available Formats: {', '.join(render_info['formats'])}
|
| 1166 |
+
File Size: {render_info.get('size_bytes', 0) / 1024 / 1024:.2f} MB
|
| 1167 |
+
Expires In: {minutes_remaining} minutes
|
| 1168 |
+
|
| 1169 |
+
Download URLs:
|
| 1170 |
+
{url_list}
|
| 1171 |
+
|
| 1172 |
+
Expiration: {expires_at.strftime('%Y-%m-%d %H:%M:%S UTC')}
|
| 1173 |
+
{log_block}
|
| 1174 |
+
"""
|
| 1175 |
+
|
| 1176 |
+
content_items = [{
|
| 1177 |
+
"type": "text",
|
| 1178 |
+
"text": status_text
|
| 1179 |
+
}]
|
| 1180 |
+
|
| 1181 |
+
mp4_url = urls.get("mp4")
|
| 1182 |
+
if mp4_url:
|
| 1183 |
+
content_items.append({
|
| 1184 |
+
"type": "resource",
|
| 1185 |
+
"resource": {
|
| 1186 |
+
"uri": mp4_url,
|
| 1187 |
+
"mimeType": "video/mp4"
|
| 1188 |
+
}
|
| 1189 |
+
})
|
| 1190 |
+
|
| 1191 |
+
return {
|
| 1192 |
+
"content": content_items,
|
| 1193 |
+
"isError": False,
|
| 1194 |
+
"structuredContent": {
|
| 1195 |
+
"request_id": request_id,
|
| 1196 |
+
"status": "ready",
|
| 1197 |
+
"urls": urls,
|
| 1198 |
+
"expires_at": expires_at.isoformat(),
|
| 1199 |
+
"log_tail": log_tail
|
| 1200 |
+
}
|
| 1201 |
+
}
|
| 1202 |
|
| 1203 |
except Exception as e:
|
| 1204 |
logger.exception(f"Error checking render status: {e}")
|