Add font resources and fix validation issues
- Add font:// MCP resources for available and web-safe fonts - Create mcp_video_test_fonts tool for font testing - Fix return value validation (convert numbers to strings) - Add Union import for type hints - Clients can now discover available fonts before using overlays
This commit is contained in:
parent
3a4c3e8f77
commit
406cc891c3
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
@ -30,6 +31,82 @@ except ImportError:
|
|||||||
mcp = FastMCP("MCP Video Editor")
|
mcp = FastMCP("MCP Video Editor")
|
||||||
|
|
||||||
|
|
||||||
|
def get_available_fonts():
|
||||||
|
"""Get list of available system fonts for text overlays."""
|
||||||
|
fonts = []
|
||||||
|
|
||||||
|
# Try to get system fonts using different methods
|
||||||
|
try:
|
||||||
|
# Method 1: Try matplotlib font manager
|
||||||
|
import matplotlib.font_manager as fm
|
||||||
|
font_paths = fm.findSystemFonts()
|
||||||
|
font_names = []
|
||||||
|
for font_path in font_paths[:50]: # Limit to first 50 fonts
|
||||||
|
try:
|
||||||
|
prop = fm.FontProperties(fname=font_path)
|
||||||
|
name = prop.get_name()
|
||||||
|
if name and name not in font_names:
|
||||||
|
font_names.append(name)
|
||||||
|
fonts.append({
|
||||||
|
"name": name,
|
||||||
|
"path": font_path,
|
||||||
|
"family": prop.get_family()[0] if prop.get_family() else "Unknown"
|
||||||
|
})
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Method 2: Add common web-safe fonts
|
||||||
|
common_fonts = [
|
||||||
|
{"name": "Arial", "family": "sans-serif", "web_safe": True},
|
||||||
|
{"name": "Helvetica", "family": "sans-serif", "web_safe": True},
|
||||||
|
{"name": "Times New Roman", "family": "serif", "web_safe": True},
|
||||||
|
{"name": "Courier New", "family": "monospace", "web_safe": True},
|
||||||
|
{"name": "Verdana", "family": "sans-serif", "web_safe": True},
|
||||||
|
{"name": "Georgia", "family": "serif", "web_safe": True},
|
||||||
|
{"name": "Comic Sans MS", "family": "cursive", "web_safe": True},
|
||||||
|
{"name": "Impact", "family": "fantasy", "web_safe": True},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add common fonts if not already found
|
||||||
|
existing_names = [f["name"] for f in fonts]
|
||||||
|
for common_font in common_fonts:
|
||||||
|
if common_font["name"] not in existing_names:
|
||||||
|
fonts.append(common_font)
|
||||||
|
|
||||||
|
return fonts[:30] # Return max 30 fonts
|
||||||
|
|
||||||
|
|
||||||
|
# Add font resources to MCP server
|
||||||
|
@mcp.resource("font://available")
|
||||||
|
def get_available_fonts_resource():
|
||||||
|
"""List all available fonts for text overlays."""
|
||||||
|
fonts = get_available_fonts()
|
||||||
|
return {
|
||||||
|
"description": "Available fonts for video text overlays",
|
||||||
|
"total_fonts": len(fonts),
|
||||||
|
"fonts": fonts
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.resource("font://web-safe")
|
||||||
|
def get_websafe_fonts_resource():
|
||||||
|
"""List web-safe fonts guaranteed to work."""
|
||||||
|
web_safe_fonts = [
|
||||||
|
{"name": "Arial", "family": "sans-serif", "description": "Clean, modern sans-serif"},
|
||||||
|
{"name": "Helvetica", "family": "sans-serif", "description": "Professional sans-serif"},
|
||||||
|
{"name": "Times New Roman", "family": "serif", "description": "Classic serif font"},
|
||||||
|
{"name": "Courier New", "family": "monospace", "description": "Fixed-width font"},
|
||||||
|
{"name": "Verdana", "family": "sans-serif", "description": "Web-optimized sans-serif"},
|
||||||
|
{"name": "Georgia", "family": "serif", "description": "Web-optimized serif"},
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"description": "Web-safe fonts guaranteed to work across platforms",
|
||||||
|
"fonts": web_safe_fonts
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _check_moviepy_dependency():
|
def _check_moviepy_dependency():
|
||||||
"""Check if MoviePy is available for video processing."""
|
"""Check if MoviePy is available for video processing."""
|
||||||
if not MOVIEPY_AVAILABLE:
|
if not MOVIEPY_AVAILABLE:
|
||||||
@ -385,8 +462,8 @@ def mcp_video_add_overlay(
|
|||||||
"output_path": output_path,
|
"output_path": output_path,
|
||||||
"overlay_type": overlay_type,
|
"overlay_type": overlay_type,
|
||||||
"position": position,
|
"position": position,
|
||||||
"start_time": start_time,
|
"start_time": str(start_time),
|
||||||
"duration": duration or (clip.duration - start_time),
|
"duration": str(duration or (clip.duration - start_time)),
|
||||||
"status": "success",
|
"status": "success",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -769,6 +846,85 @@ def mcp_video_resolution_optimizer(
|
|||||||
return {"error": f"Resolution optimization failed: {e!s}"}
|
return {"error": f"Resolution optimization failed: {e!s}"}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
def mcp_video_test_fonts(
|
||||||
|
output_path: str,
|
||||||
|
test_text: str = "InterNACHI Expert Agent",
|
||||||
|
font_names: list[str] = None
|
||||||
|
) -> dict[str, Union[str, int, list]]:
|
||||||
|
"""
|
||||||
|
Create a video showcasing different fonts for testing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
output_path: Output video file path
|
||||||
|
test_text: Text to display with each font
|
||||||
|
font_names: List of font names to test (uses web-safe fonts if not provided)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict with output path and font test results
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not MOVIEPY_AVAILABLE:
|
||||||
|
return {"error": "MoviePy is not available for video processing"}
|
||||||
|
|
||||||
|
# Use web-safe fonts if none provided
|
||||||
|
if not font_names:
|
||||||
|
font_names = ["Arial", "Times New Roman", "Courier New", "Verdana"]
|
||||||
|
|
||||||
|
# Create font test clips
|
||||||
|
test_clips = []
|
||||||
|
working_fonts = []
|
||||||
|
failed_fonts = []
|
||||||
|
|
||||||
|
for i, font_name in enumerate(font_names):
|
||||||
|
try:
|
||||||
|
# Try to create text clip with this font
|
||||||
|
text_clip = TextClip(
|
||||||
|
text=f"{test_text} - {font_name}",
|
||||||
|
font_size=48,
|
||||||
|
color='white',
|
||||||
|
duration=2
|
||||||
|
).with_position('center')
|
||||||
|
|
||||||
|
# Create background for contrast
|
||||||
|
bg_clip = ColorClip(size=(1280, 720), color=(50, 50, 50)).with_duration(2)
|
||||||
|
|
||||||
|
# Composite text on background
|
||||||
|
composite = CompositeVideoClip([bg_clip, text_clip])
|
||||||
|
test_clips.append(composite)
|
||||||
|
working_fonts.append(font_name)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
failed_fonts.append({"font": font_name, "error": str(e)})
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not test_clips:
|
||||||
|
return {"error": "No fonts worked for testing"}
|
||||||
|
|
||||||
|
# Concatenate all test clips
|
||||||
|
final_video = concatenate_videoclips(test_clips)
|
||||||
|
final_video.write_videofile(output_path, fps=24)
|
||||||
|
|
||||||
|
# Cleanup
|
||||||
|
for clip in test_clips:
|
||||||
|
clip.close()
|
||||||
|
final_video.close()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"output_path": output_path,
|
||||||
|
"total_fonts_tested": len(font_names),
|
||||||
|
"working_fonts": working_fonts,
|
||||||
|
"failed_fonts": failed_fonts,
|
||||||
|
"working_count": len(working_fonts),
|
||||||
|
"failed_count": len(failed_fonts),
|
||||||
|
"video_duration": len(working_fonts) * 2,
|
||||||
|
"status": "success"
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": f"Font testing failed: {str(e)}"}
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main entry point for the MCP Video Editor server."""
|
"""Main entry point for the MCP Video Editor server."""
|
||||||
mcp.run()
|
mcp.run()
|
||||||
|
2
uv.lock
generated
2
uv.lock
generated
@ -562,6 +562,7 @@ version = "0.1.0"
|
|||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "fastmcp" },
|
{ name = "fastmcp" },
|
||||||
|
{ name = "moviepy" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
@ -582,6 +583,7 @@ video = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "fastmcp", specifier = ">=2.12.2" },
|
{ name = "fastmcp", specifier = ">=2.12.2" },
|
||||||
{ name = "ffmpeg-python", marker = "extra == 'video'", specifier = ">=0.2.0" },
|
{ name = "ffmpeg-python", marker = "extra == 'video'", specifier = ">=0.2.0" },
|
||||||
|
{ name = "moviepy", specifier = ">=2.2.1" },
|
||||||
{ name = "moviepy", marker = "extra == 'video'", specifier = ">=1.0.3" },
|
{ name = "moviepy", marker = "extra == 'video'", specifier = ">=1.0.3" },
|
||||||
{ name = "numpy", marker = "extra == 'video'", specifier = ">=1.24.0" },
|
{ name = "numpy", marker = "extra == 'video'", specifier = ">=1.24.0" },
|
||||||
{ name = "opencv-python", marker = "extra == 'video'", specifier = ">=4.8.0" },
|
{ name = "opencv-python", marker = "extra == 'video'", specifier = ">=4.8.0" },
|
||||||
|
Loading…
x
Reference in New Issue
Block a user