diff --git a/mcp-server/README.md b/mcp-server/README.md new file mode 100644 index 0000000..9d6c244 --- /dev/null +++ b/mcp-server/README.md @@ -0,0 +1,578 @@ +# OpenTryOn MCP Server + +An MCP (Model Context Protocol) server that exposes OpenTryOn's AI-powered fashion tech capabilities to AI agents and applications. + +## Overview + +The OpenTryOn MCP Server provides a standardized interface for AI agents to access: +- **Virtual Try-On** (3 providers): Amazon Nova Canvas, Kling AI, Segmind +- **Image Generation** (6 models): Gemini, FLUX.2, Luma AI +- **Video Generation**: Luma AI Ray models +- **Preprocessing Tools**: Garment segmentation, extraction, human parsing +- **Datasets**: Fashion-MNIST, VITON-HD + +**Total**: 17 tools across 5 categories + +## Quick Start + +### Prerequisites +- Python 3.10+ +- OpenTryOn installed +- At least one API key configured + +### Installation + +```bash +# 1. Install OpenTryOn core library +cd /path/to/opentryon +pip install -e . + +# 2. Install MCP server dependencies +cd mcp-server +pip install -r requirements.txt + +# 3. Configure API keys +cp ../env.template ../.env +# Edit .env with your API keys + +# 4. Test installation +python test_server.py + +# 5. Start server +python server.py +``` + +### Minimum Configuration + +You don't need all API keys. Start with just these: + +```env +# In .env file: +KLING_AI_API_KEY=your_key +KLING_AI_SECRET_KEY=your_secret +GEMINI_API_KEY=your_key +``` + +This gives you: +- ✓ Virtual try-on (Kling AI) +- ✓ Image generation (Gemini) +- ✓ All preprocessing and dataset tools + +## Configuration + +### Configuration Status + +When you start the MCP server, it shows which services are configured: + +``` +OpenTryOn MCP Server Configuration Status: + Amazon Nova Canvas: ✗ Not configured (optional - requires AWS) + Kling AI: ✓ Configured + Segmind: ✓ Configured + Gemini (Nano Banana): ✓ Configured + FLUX.2: ✓ Configured + Luma AI: ✗ Not configured (optional - for video) + U2Net (Preprocessing): ✗ Not configured (optional - for local segmentation) + +✅ Ready! At least one service from each category is configured. +``` + +### Required vs Optional Services + +**Minimum Required** (choose at least ONE from each): +- **Virtual Try-On**: Kling AI OR Segmind (recommended) +- **Image Generation**: Gemini OR FLUX.2 (recommended) + +**Optional Services**: +- **Amazon Nova Canvas**: Only if you want AWS Bedrock (requires AWS account) +- **Luma AI**: Only for video generation and Luma image models +- **U2Net**: Only for local garment preprocessing + +### Getting API Keys + +| Service | URL | Cost | Notes | +|---------|-----|------|-------| +| **Kling AI** | [klingai.com](https://klingai.com/) | Pay-per-use | Best VTON quality | +| **Segmind** | [segmind.com](https://segmind.com/) | Pay-per-use | Fast VTON | +| **Gemini** | [ai.google.dev](https://ai.google.dev/) | Free tier | Good for testing | +| **FLUX.2** | [api.bfl.ml](https://api.bfl.ml/) | Pay-per-use | High quality | +| **Luma AI** | [lumalabs.ai](https://lumalabs.ai/) | Pay-per-use | For video | +| **AWS Bedrock** | [aws.amazon.com/bedrock](https://aws.amazon.com/bedrock/) | Pay-per-use | Requires AWS | + +### Complete Configuration + +For full functionality, add all services to `.env`: + +```env +# Virtual Try-On +AWS_ACCESS_KEY_ID=your_aws_key +AWS_SECRET_ACCESS_KEY=your_aws_secret +AMAZON_NOVA_REGION=us-east-1 + +KLING_AI_API_KEY=your_key +KLING_AI_SECRET_KEY=your_secret +KLING_AI_BASE_URL=https://api-singapore.klingai.com + +SEGMIND_API_KEY=your_key + +# Image Generation +GEMINI_API_KEY=your_key +BFL_API_KEY=your_key +LUMA_AI_API_KEY=your_key + +# Preprocessing (optional) +U2NET_CLOTH_SEG_CHECKPOINT_PATH=/path/to/cloth_segm_u2net_latest.pth +``` + +## Integration Options + +### Option 1: Claude Desktop + +Add to `claude_desktop_config.json`: + +**macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` +**Windows**: `%APPDATA%/Claude/claude_desktop_config.json` + +```json +{ + "mcpServers": { + "opentryon": { + "command": "python", + "args": ["/absolute/path/to/opentryon/mcp-server/server.py"], + "env": { + "PYTHONPATH": "/absolute/path/to/opentryon" + } + } + } +} +``` + +Then restart Claude Desktop and start using OpenTryOn tools! + +### Option 2: Standalone Server + +```bash +cd mcp-server +python server.py +``` + +The server listens for MCP protocol messages via stdio. + +### Option 3: Programmatic Usage + +```python +from tools import virtual_tryon_nova, generate_image_nano_banana + +# Virtual try-on +result = virtual_tryon_nova( + source_image="person.jpg", + reference_image="garment.jpg", + output_dir="outputs" +) + +# Image generation +result = generate_image_nano_banana( + prompt="A fashion model in elegant evening wear", + aspect_ratio="16:9", + output_dir="outputs" +) +``` + +## Available Tools + +### Virtual Try-On (3 tools) + +#### `virtual_tryon_nova` +Amazon Nova Canvas virtual try-on using AWS Bedrock. + +```python +virtual_tryon_nova( + source_image="person.jpg", # Person/model image + reference_image="garment.jpg", # Garment image + mask_type="GARMENT", # GARMENT or IMAGE + garment_class="UPPER_BODY", # UPPER_BODY, LOWER_BODY, FULL_BODY, FOOTWEAR + output_dir="outputs/nova" +) +``` + +#### `virtual_tryon_kling` +Kling AI Kolors-based virtual try-on. + +```python +virtual_tryon_kling( + source_image="person.jpg", + reference_image="garment.jpg", + output_dir="outputs/kling" +) +``` + +#### `virtual_tryon_segmind` +Segmind Try-On Diffusion. + +```python +virtual_tryon_segmind( + model_image="person.jpg", + cloth_image="garment.jpg", + category="Upper body", # "Upper body", "Lower body", "Dress" + num_inference_steps=25, # 20-100 + guidance_scale=2.0, # 1-25 + output_dir="outputs/segmind" +) +``` + +### Image Generation (6 tools) + +#### `generate_image_nano_banana` +Fast 1024px image generation with Gemini 2.5 Flash. + +```python +generate_image_nano_banana( + prompt="A fashion model in elegant evening wear", + aspect_ratio="16:9", # 1:1, 2:3, 3:2, 4:3, 9:16, 16:9, etc. + mode="text_to_image", # text_to_image, edit, compose + output_dir="outputs" +) +``` + +#### `generate_image_nano_banana_pro` +4K image generation with Gemini 3 Pro. + +```python +generate_image_nano_banana_pro( + prompt="Professional fashion photography", + resolution="4K", # 1K, 2K, 4K + aspect_ratio="16:9", + use_search_grounding=True, # Enable Google Search grounding + output_dir="outputs" +) +``` + +#### `generate_image_flux2_pro` +High-quality image generation with FLUX.2 PRO. + +```python +generate_image_flux2_pro( + prompt="A stylish fashion model", + width=1024, + height=1024, + seed=42, # Optional: for reproducibility + mode="text_to_image", # text_to_image, edit, compose + output_dir="outputs" +) +``` + +#### `generate_image_flux2_flex` +Flexible generation with advanced controls. + +```python +generate_image_flux2_flex( + prompt="Fashion model in casual wear", + width=1024, + height=1024, + guidance=7.5, # 1.5-10 + steps=28, # Number of inference steps + prompt_upsampling=False, # Enable prompt enhancement + output_dir="outputs" +) +``` + +#### `generate_image_luma_photon_flash` +Fast and cost-efficient with Luma AI Photon-Flash-1. + +```python +generate_image_luma_photon_flash( + prompt="A model in a studio setting", + aspect_ratio="16:9", + mode="text_to_image", # text_to_image, img_ref, style_ref, char_ref + output_dir="outputs" +) +``` + +#### `generate_image_luma_photon` +High-fidelity professional-grade with Luma AI Photon-1. + +```python +generate_image_luma_photon( + prompt="Professional fashion shoot", + aspect_ratio="16:9", + mode="text_to_image", + output_dir="outputs" +) +``` + +### Video Generation (1 tool) + +#### `generate_video_luma_ray` +Video generation with Luma AI Ray models. + +```python +generate_video_luma_ray( + prompt="A model walking on a runway", + model="ray-2", # ray-1-6, ray-2, ray-flash-2 + mode="text_video", # text_video, image_video + resolution="720p", # 540p, 720p, 1080p, 4k + duration="5s", # 5s, 9s, 10s + aspect_ratio="16:9", + output_dir="outputs/videos" +) +``` + +### Preprocessing (3 tools) + +#### `segment_garment` +Segment garments using U2Net. + +```python +segment_garment( + input_path="garment_images/", + output_dir="outputs/segmented", + garment_class="upper" # upper, lower, all +) +``` + +#### `extract_garment` +Extract and preprocess garments. + +```python +extract_garment( + input_path="garment_images/", + output_dir="outputs/extracted", + garment_class="upper", + resize_width=400 +) +``` + +#### `segment_human` +Segment human subjects from images. + +```python +segment_human( + image_path="person.jpg", + output_dir="outputs/segmented" +) +``` + +### Datasets (2 tools) + +#### `load_fashion_mnist` +Load Fashion-MNIST dataset (60K training, 10K test). + +```python +load_fashion_mnist( + download=True, + normalize=True, + flatten=False +) +``` + +#### `load_viton_hd` +Load VITON-HD dataset (11,647 training, 2,032 test). + +```python +load_viton_hd( + data_dir="/path/to/viton-hd", + split="train", # train, test + batch_size=8 +) +``` + +## Architecture + +``` +┌─────────────────────────────────────────┐ +│ MCP Clients (Claude, etc) │ +└──────────────┬──────────────────────────┘ + │ MCP Protocol (stdio) +┌──────────────▼──────────────────────────┐ +│ OpenTryOn MCP Server │ +│ ┌───────────────────────────────────┐ │ +│ │ Tool Router (server.py) │ │ +│ └──────┬─────────────┬──────────────┘ │ +│ │ │ │ +│ ┌──────▼──────┐ ┌───▼───────┐ │ +│ │ Tools │ │ Config │ │ +│ │ - VTON │ │ - API │ │ +│ │ - Image │ │ Keys │ │ +│ │ - Video │ │ - Env │ │ +│ │ - Process │ │ Vars │ │ +│ │ - Dataset │ └───────────┘ │ +│ └─────────────┘ │ +└─────────────────────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ OpenTryOn Core Library │ +│ (tryon.api, tryon.preprocessing, etc) │ +└──────────────┬──────────────────────────┘ + │ +┌──────────────▼──────────────────────────┐ +│ External APIs & Models │ +│ AWS, Kling, Segmind, Gemini, │ +│ FLUX.2, Luma AI, U2Net │ +└─────────────────────────────────────────┘ +``` + +### Key Components + +1. **Server Core** (`server.py`): Implements MCP protocol, registers tools, routes requests +2. **Tools** (`tools/`): Implements tool logic for each feature +3. **Config** (`config.py`): Manages API keys and environment variables +4. **Utils** (`utils/`): Image handling and validation utilities + +### Data Flow Example (Virtual Try-On) + +``` +1. Client sends request via MCP +2. Server validates request format +3. Router identifies tool (virtual_tryon_nova) +4. Tool validates inputs +5. Tool calls OpenTryOn API adapter +6. Adapter calls external API (AWS Bedrock) +7. API returns generated images +8. Tool saves images to disk +9. Tool returns structured response +10. Server formats and sends to client +``` + +## Troubleshooting + +### Server won't start + +**Problem**: `ModuleNotFoundError: No module named 'mcp'` + +**Solution**: +```bash +pip install -r requirements.txt +``` + +### API errors + +**Problem**: `Error: API key not configured` + +**Solution**: +1. Check `.env` file exists in OpenTryOn root +2. Verify API keys are set correctly +3. Ensure no extra spaces or quotes +4. Restart server after updating `.env` + +### Import errors + +**Problem**: `ModuleNotFoundError: No module named 'tryon'` + +**Solution**: +```bash +cd /path/to/opentryon +pip install -e . +``` + +### Tool execution fails + +**Solutions**: +1. Verify API key is valid and has credits +2. Check network connectivity +3. Verify input file paths exist +4. Check API service status +5. Review server logs for errors + +### Configuration validation + +Run the test suite to check configuration: +```bash +python test_server.py +``` + +This validates: +- Directory structure +- Configuration loading +- Module imports +- OpenTryOn library +- Tool definitions + +## Project Structure + +``` +mcp-server/ +├── server.py # Main MCP server (700+ lines) +├── config.py # Configuration management +├── requirements.txt # Dependencies +├── pyproject.toml # Package config +├── test_server.py # Test suite +│ +├── tools/ # Tool implementations +│ ├── virtual_tryon.py # 3 virtual try-on tools +│ ├── image_gen.py # 6 image generation tools +│ ├── video_gen.py # 1 video generation tool +│ ├── preprocessing.py # 3 preprocessing tools +│ └── datasets.py # 2 dataset tools +│ +├── utils/ # Utilities +│ ├── image_utils.py # Image handling +│ └── validation.py # Input validation +│ +└── examples/ # Usage examples + ├── example_usage.py + └── claude_desktop_config.json +``` + +## Security + +The MCP server implements security best practices: + +- ✅ **API Keys**: Stored in environment variables, never exposed +- ✅ **Path Validation**: Prevents directory traversal attacks +- ✅ **Input Sanitization**: Validates all inputs +- ✅ **File Size Limits**: Prevents resource exhaustion +- ✅ **Temp File Cleanup**: Automatic cleanup of temporary files + +## Testing + +Run the comprehensive test suite: + +```bash +python test_server.py +``` + +Tests validate: +- Configuration loading +- Module imports +- Tool definitions +- Directory structure +- OpenTryOn library integration + +## Examples + +See `examples/example_usage.py` for complete examples of: +- Virtual try-on with all providers +- Image generation with all models +- Video generation +- Preprocessing tools +- Dataset loading + +## Support + +- **Issues**: [GitHub Issues](https://github.com/tryonlabs/opentryon/issues) +- **Discord**: [Join our Discord](https://discord.gg/T5mPpZHxkY) +- **Documentation**: [OpenTryOn Docs](https://tryonlabs.github.io/opentryon/) +- **Email**: contact@tryonlabs.ai + +## Contributing + +Contributions are welcome! Areas to contribute: +1. Add support for new API providers +2. Improve documentation +3. Add more test coverage +4. Fix bugs +5. Implement new features + +## License + +Part of OpenTryOn - Creative Commons BY-NC 4.0 + +See main [LICENSE](../LICENSE) file for details. + +## Version + +**v0.0.1** - First public release + +--- + +Made with ❤️ by [TryOn Labs](https://www.tryonlabs.ai) diff --git a/mcp-server/config.py b/mcp-server/config.py new file mode 100644 index 0000000..0a2007d --- /dev/null +++ b/mcp-server/config.py @@ -0,0 +1,173 @@ +"""Configuration management for OpenTryOn MCP Server.""" + +import os +from pathlib import Path +from typing import Optional +from dotenv import load_dotenv + +# Load environment variables from parent directory +parent_dir = Path(__file__).parent.parent +env_path = parent_dir / ".env" +try: + if env_path.exists(): + load_dotenv(env_path) +except Exception: + # Silently fail if .env cannot be loaded (e.g., permission issues) + pass + + +class Config: + """Configuration for OpenTryOn MCP Server.""" + + # Server settings + SERVER_NAME = "opentryon-mcp" + SERVER_VERSION = "0.0.1" + + # AWS/Amazon Nova Canvas + AWS_ACCESS_KEY_ID: Optional[str] = os.getenv("AWS_ACCESS_KEY_ID") + AWS_SECRET_ACCESS_KEY: Optional[str] = os.getenv("AWS_SECRET_ACCESS_KEY") + AMAZON_NOVA_REGION: str = os.getenv("AMAZON_NOVA_REGION", "us-east-1") + AMAZON_NOVA_MODEL_ID: str = os.getenv("AMAZON_NOVA_MODEL_ID", "amazon.nova-canvas-v1:0") + + # Kling AI + KLING_AI_API_KEY: Optional[str] = os.getenv("KLING_AI_API_KEY") + KLING_AI_SECRET_KEY: Optional[str] = os.getenv("KLING_AI_SECRET_KEY") + KLING_AI_BASE_URL: str = os.getenv("KLING_AI_BASE_URL", "https://api-singapore.klingai.com") + + # Segmind + SEGMIND_API_KEY: Optional[str] = os.getenv("SEGMIND_API_KEY") + + # Google Gemini (Nano Banana) + GEMINI_API_KEY: Optional[str] = os.getenv("GEMINI_API_KEY") + + # BFL API (FLUX.2) + BFL_API_KEY: Optional[str] = os.getenv("BFL_API_KEY") + + # Luma AI + LUMA_AI_API_KEY: Optional[str] = os.getenv("LUMA_AI_API_KEY") + + # U2Net Checkpoint + U2NET_CLOTH_SEG_CHECKPOINT_PATH: Optional[str] = os.getenv("U2NET_CLOTH_SEG_CHECKPOINT_PATH") + + # File handling + MAX_FILE_SIZE_MB = 50 + ALLOWED_IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"} + ALLOWED_VIDEO_EXTENSIONS = {".mp4", ".mov", ".avi"} + + # Temporary files + TEMP_DIR = Path("/tmp/opentryon_mcp") + TEMP_DIR.mkdir(exist_ok=True, parents=True) + + @classmethod + def validate(cls) -> dict[str, bool]: + """Validate configuration and return status of each service.""" + return { + "amazon_nova": bool(cls.AWS_ACCESS_KEY_ID and cls.AWS_SECRET_ACCESS_KEY), + "kling_ai": bool(cls.KLING_AI_API_KEY and cls.KLING_AI_SECRET_KEY), + "segmind": bool(cls.SEGMIND_API_KEY), + "gemini": bool(cls.GEMINI_API_KEY), + "flux2": bool(cls.BFL_API_KEY), + "luma_ai": bool(cls.LUMA_AI_API_KEY), + "u2net": bool(cls.U2NET_CLOTH_SEG_CHECKPOINT_PATH), + } + + @classmethod + def get_status_message(cls) -> str: + """Get a human-readable status message with helpful guidance.""" + status = cls.validate() + lines = ["OpenTryOn MCP Server Configuration Status:"] + + # Virtual Try-On Services + nova_status = "✓ Configured" if status['amazon_nova'] else "✗ Not configured (optional - requires AWS)" + kling_status = "✓ Configured" if status['kling_ai'] else "✗ Not configured (recommended)" + segmind_status = "✓ Configured" if status['segmind'] else "✗ Not configured (recommended)" + + lines.append(f" Amazon Nova Canvas: {nova_status}") + lines.append(f" Kling AI: {kling_status}") + lines.append(f" Segmind: {segmind_status}") + + # Image Generation Services + gemini_status = "✓ Configured" if status['gemini'] else "✗ Not configured (recommended)" + flux_status = "✓ Configured" if status['flux2'] else "✗ Not configured (recommended)" + luma_status = "✓ Configured" if status['luma_ai'] else "✗ Not configured (optional - for video)" + + lines.append(f" Gemini (Nano Banana): {gemini_status}") + lines.append(f" FLUX.2: {flux_status}") + lines.append(f" Luma AI: {luma_status}") + + # Preprocessing + u2net_status = "✓ Configured" if status['u2net'] else "✗ Not configured (optional - for local segmentation)" + lines.append(f" U2Net (Preprocessing): {u2net_status}") + + # Add helpful message + vton_count = sum([status['amazon_nova'], status['kling_ai'], status['segmind']]) + img_count = sum([status['gemini'], status['flux2'], status['luma_ai']]) + + lines.append("") + if vton_count == 0: + lines.append("⚠️ Warning: No virtual try-on service configured!") + lines.append(" Configure at least one: Kling AI (recommended) or Segmind") + if img_count == 0: + lines.append("⚠️ Warning: No image generation service configured!") + lines.append(" Configure at least one: Gemini (recommended) or FLUX.2") + + if vton_count > 0 and img_count > 0: + lines.append("✅ Ready! At least one service from each category is configured.") + lines.append(f" Virtual Try-On: {vton_count}/3 services") + lines.append(f" Image Generation: {img_count}/3 services") + + lines.append("") + lines.append("💡 Tip: Copy env.template to .env and add your API keys") + lines.append("📖 Setup guide: mcp-server/README.md") + + return "\n".join(lines) + + @classmethod + def get_missing_services(cls) -> dict[str, list[str]]: + """Get list of missing services by category.""" + status = cls.validate() + + missing = { + "virtual_tryon": [], + "image_generation": [], + "optional": [] + } + + # Virtual Try-On (at least one required) + if not status['kling_ai']: + missing["virtual_tryon"].append("Kling AI") + if not status['segmind']: + missing["virtual_tryon"].append("Segmind") + if not status['amazon_nova']: + missing["optional"].append("Amazon Nova Canvas (requires AWS)") + + # Image Generation (at least one required) + if not status['gemini']: + missing["image_generation"].append("Gemini (Nano Banana)") + if not status['flux2']: + missing["image_generation"].append("FLUX.2") + + # Optional services + if not status['luma_ai']: + missing["optional"].append("Luma AI (for video generation)") + if not status['u2net']: + missing["optional"].append("U2Net (for local garment segmentation)") + + return missing + + @classmethod + def is_ready(cls) -> bool: + """Check if minimum required services are configured.""" + status = cls.validate() + + # Need at least one virtual try-on service + has_vton = status['kling_ai'] or status['segmind'] or status['amazon_nova'] + + # Need at least one image generation service + has_img_gen = status['gemini'] or status['flux2'] or status['luma_ai'] + + return has_vton and has_img_gen + + +config = Config() + diff --git a/mcp-server/examples/claude_desktop_config.json b/mcp-server/examples/claude_desktop_config.json new file mode 100644 index 0000000..17fd7de --- /dev/null +++ b/mcp-server/examples/claude_desktop_config.json @@ -0,0 +1,14 @@ +{ + "mcpServers": { + "opentryon": { + "command": "python", + "args": [ + "/absolute/path/to/opentryon/mcp-server/server.py" + ], + "env": { + "PYTHONPATH": "/absolute/path/to/opentryon" + } + } + } +} + diff --git a/mcp-server/examples/example_usage.py b/mcp-server/examples/example_usage.py new file mode 100644 index 0000000..0d98c4e --- /dev/null +++ b/mcp-server/examples/example_usage.py @@ -0,0 +1,179 @@ +"""Example usage of OpenTryOn MCP Server tools.""" + +import json +from pathlib import Path +import sys + +# Add parent directory to path to import tools +sys.path.insert(0, str(Path(__file__).parent.parent)) + +from tools import ( + virtual_tryon_nova, + virtual_tryon_kling, + virtual_tryon_segmind, + generate_image_nano_banana, + generate_image_nano_banana_pro, + generate_image_flux2_pro, + generate_video_luma_ray, + segment_garment, + load_fashion_mnist, +) + + +def example_virtual_tryon(): + """Example: Virtual try-on with different providers.""" + print("\n=== Virtual Try-On Examples ===\n") + + # Example 1: Amazon Nova Canvas + print("1. Amazon Nova Canvas Virtual Try-On") + result = virtual_tryon_nova( + source_image="path/to/person.jpg", + reference_image="path/to/garment.jpg", + garment_class="UPPER_BODY", + output_dir="outputs/nova" + ) + print(json.dumps(result, indent=2)) + + # Example 2: Kling AI + print("\n2. Kling AI Virtual Try-On") + result = virtual_tryon_kling( + source_image="path/to/person.jpg", + reference_image="path/to/garment.jpg", + output_dir="outputs/kling" + ) + print(json.dumps(result, indent=2)) + + # Example 3: Segmind + print("\n3. Segmind Virtual Try-On") + result = virtual_tryon_segmind( + model_image="path/to/person.jpg", + cloth_image="path/to/garment.jpg", + category="Upper body", + num_inference_steps=35, + guidance_scale=2.5, + output_dir="outputs/segmind" + ) + print(json.dumps(result, indent=2)) + + +def example_image_generation(): + """Example: Image generation with different models.""" + print("\n=== Image Generation Examples ===\n") + + # Example 1: Nano Banana (Fast) + print("1. Nano Banana - Fast Image Generation") + result = generate_image_nano_banana( + prompt="A professional fashion model wearing elegant evening wear on a runway", + aspect_ratio="16:9", + output_dir="outputs/nano_banana" + ) + print(json.dumps(result, indent=2)) + + # Example 2: Nano Banana Pro (4K) + print("\n2. Nano Banana Pro - 4K Image Generation") + result = generate_image_nano_banana_pro( + prompt="Professional fashion photography of elegant evening wear", + resolution="4K", + aspect_ratio="16:9", + use_search_grounding=True, + output_dir="outputs/nano_banana_pro" + ) + print(json.dumps(result, indent=2)) + + # Example 3: FLUX.2 PRO + print("\n3. FLUX.2 PRO - High-Quality Image Generation") + result = generate_image_flux2_pro( + prompt="A stylish fashion model in modern casual wear", + width=1024, + height=1024, + seed=42, + output_dir="outputs/flux2_pro" + ) + print(json.dumps(result, indent=2)) + + +def example_video_generation(): + """Example: Video generation with Luma AI.""" + print("\n=== Video Generation Examples ===\n") + + # Example 1: Text-to-Video + print("1. Text-to-Video with Ray 2") + result = generate_video_luma_ray( + prompt="A fashion model walking on a runway in elegant evening wear", + model="ray-2", + mode="text_video", + resolution="720p", + duration="5s", + aspect_ratio="16:9", + output_dir="outputs/videos" + ) + print(json.dumps(result, indent=2)) + + # Example 2: Image-to-Video with Keyframes + print("\n2. Image-to-Video with Keyframes") + result = generate_video_luma_ray( + prompt="Model walking gracefully", + model="ray-2", + mode="image_video", + start_image="path/to/start_frame.jpg", + end_image="path/to/end_frame.jpg", + resolution="720p", + duration="5s", + output_dir="outputs/videos" + ) + print(json.dumps(result, indent=2)) + + +def example_preprocessing(): + """Example: Preprocessing tools.""" + print("\n=== Preprocessing Examples ===\n") + + # Example 1: Segment Garment + print("1. Segment Garment") + result = segment_garment( + input_path="path/to/garment_images", + output_dir="outputs/segmented", + garment_class="upper" + ) + print(json.dumps(result, indent=2)) + + +def example_datasets(): + """Example: Dataset loading.""" + print("\n=== Dataset Examples ===\n") + + # Example 1: Fashion-MNIST + print("1. Load Fashion-MNIST Dataset") + result = load_fashion_mnist( + download=True, + normalize=True, + flatten=False + ) + print(json.dumps(result, indent=2)) + + +def main(): + """Run all examples.""" + print("OpenTryOn MCP Server - Example Usage") + print("=" * 50) + + # Note: These examples show the API structure + # Actual execution requires valid image paths and API keys + + print("\nNote: These are example API calls.") + print("Replace paths and ensure API keys are configured in .env file.") + print("\nUncomment the examples you want to run:\n") + + # Uncomment to run examples: + # example_virtual_tryon() + # example_image_generation() + # example_video_generation() + # example_preprocessing() + # example_datasets() + + print("\nExamples complete!") + + +if __name__ == "__main__": + main() + diff --git a/mcp-server/pyproject.toml b/mcp-server/pyproject.toml new file mode 100644 index 0000000..e853749 --- /dev/null +++ b/mcp-server/pyproject.toml @@ -0,0 +1,39 @@ +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" + +[project] +name = "opentryon-mcp-server" +version = "0.0.1" +description = "MCP server for OpenTryOn - AI toolkit for fashion tech and virtual try-on" +authors = [ + {name = "TryOn Labs", email = "contact@tryonlabs.ai"} +] +readme = "README.md" +requires-python = ">=3.10" +license = {text = "CC-BY-NC-4.0"} +keywords = ["mcp", "fashion", "virtual-try-on", "ai", "agents"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", +] + +dependencies = [ + "mcp>=1.0.0", + "pydantic>=2.0.0", + "python-dotenv>=1.0.0", +] + +[project.urls] +Homepage = "https://github.com/tryonlabs/opentryon" +Documentation = "https://tryonlabs.github.io/opentryon/" +Repository = "https://github.com/tryonlabs/opentryon" +Issues = "https://github.com/tryonlabs/opentryon/issues" + +[project.scripts] +opentryon-mcp = "server:main" + diff --git a/mcp-server/requirements.txt b/mcp-server/requirements.txt new file mode 100644 index 0000000..abb7b78 --- /dev/null +++ b/mcp-server/requirements.txt @@ -0,0 +1,8 @@ +# MCP Server Dependencies +mcp>=1.0.0 +pydantic>=2.0.0 +python-dotenv>=1.0.0 + +# OpenTryOn is installed from parent directory +# Make sure to run: pip install -e .. + diff --git a/mcp-server/server.py b/mcp-server/server.py new file mode 100755 index 0000000..d1dff50 --- /dev/null +++ b/mcp-server/server.py @@ -0,0 +1,723 @@ +#!/usr/bin/env python3 +"""OpenTryOn MCP Server - Main server implementation.""" + +import asyncio +import json +import sys +from typing import Any, Optional + +from mcp.server import Server +from mcp.server.stdio import stdio_server +from mcp.types import ( + Tool, + TextContent, + ImageContent, + EmbeddedResource, +) + +from config import config +from tools import ( + virtual_tryon_nova, + virtual_tryon_kling, + virtual_tryon_segmind, + generate_image_nano_banana, + generate_image_nano_banana_pro, + generate_image_flux2_pro, + generate_image_flux2_flex, + generate_image_luma_photon_flash, + generate_image_luma_photon, + generate_video_luma_ray, + segment_garment, + extract_garment, + segment_human, + load_fashion_mnist, + load_viton_hd, +) + + +# Initialize MCP server +app = Server("opentryon-mcp") + + +# Define available tools +TOOLS = [ + # Virtual Try-On Tools + Tool( + name="virtual_tryon_nova", + description="Generate virtual try-on using Amazon Nova Canvas. Combines a person image with a garment image to create realistic try-on results.", + inputSchema={ + "type": "object", + "properties": { + "source_image": { + "type": "string", + "description": "Path or URL to person/model image" + }, + "reference_image": { + "type": "string", + "description": "Path or URL to garment/product image" + }, + "mask_type": { + "type": "string", + "enum": ["GARMENT", "IMAGE"], + "description": "Type of mask to use", + "default": "GARMENT" + }, + "garment_class": { + "type": "string", + "enum": ["UPPER_BODY", "LOWER_BODY", "FULL_BODY", "FOOTWEAR"], + "description": "Garment class for automatic masking", + "default": "UPPER_BODY" + }, + "region": { + "type": "string", + "description": "AWS region (optional)", + }, + "output_dir": { + "type": "string", + "description": "Directory to save results (optional)" + } + }, + "required": ["source_image", "reference_image"] + } + ), + Tool( + name="virtual_tryon_kling", + description="Generate virtual try-on using Kling AI's Kolors API. Fast and high-quality virtual try-on with automatic task polling.", + inputSchema={ + "type": "object", + "properties": { + "source_image": { + "type": "string", + "description": "Path or URL to person/model image" + }, + "reference_image": { + "type": "string", + "description": "Path or URL to garment/product image" + }, + "model": { + "type": "string", + "description": "Model version (optional)" + }, + "output_dir": { + "type": "string", + "description": "Directory to save results (optional)" + } + }, + "required": ["source_image", "reference_image"] + } + ), + Tool( + name="virtual_tryon_segmind", + description="Generate virtual try-on using Segmind's Try-On Diffusion API. Supports upper body, lower body, and dress categories.", + inputSchema={ + "type": "object", + "properties": { + "model_image": { + "type": "string", + "description": "Path or URL to person/model image" + }, + "cloth_image": { + "type": "string", + "description": "Path or URL to garment/cloth image" + }, + "category": { + "type": "string", + "enum": ["Upper body", "Lower body", "Dress"], + "description": "Garment category", + "default": "Upper body" + }, + "num_inference_steps": { + "type": "integer", + "minimum": 20, + "maximum": 100, + "description": "Number of denoising steps", + "default": 25 + }, + "guidance_scale": { + "type": "number", + "minimum": 1, + "maximum": 25, + "description": "Classifier-free guidance scale", + "default": 2.0 + }, + "seed": { + "type": "integer", + "description": "Random seed (-1 for random)", + "default": -1 + }, + "output_dir": { + "type": "string", + "description": "Directory to save results (optional)" + } + }, + "required": ["model_image", "cloth_image"] + } + ), + + # Image Generation Tools + Tool( + name="generate_image_nano_banana", + description="Generate images using Nano Banana (Gemini 2.5 Flash). Fast 1024px image generation with text-to-image, editing, and composition modes.", + inputSchema={ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Text description for image generation" + }, + "aspect_ratio": { + "type": "string", + "enum": ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"], + "description": "Image aspect ratio", + "default": "1:1" + }, + "mode": { + "type": "string", + "enum": ["text_to_image", "edit", "compose"], + "description": "Generation mode", + "default": "text_to_image" + }, + "image": { + "type": "string", + "description": "Input image for edit mode (optional)" + }, + "images": { + "type": "array", + "items": {"type": "string"}, + "description": "Input images for compose mode (optional)" + }, + "output_dir": { + "type": "string", + "description": "Directory to save results (optional)" + } + }, + "required": ["prompt"] + } + ), + Tool( + name="generate_image_nano_banana_pro", + description="Generate high-resolution images using Nano Banana Pro (Gemini 3 Pro). Advanced 4K image generation with search grounding.", + inputSchema={ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Text description for image generation" + }, + "resolution": { + "type": "string", + "enum": ["1K", "2K", "4K"], + "description": "Image resolution", + "default": "1K" + }, + "aspect_ratio": { + "type": "string", + "enum": ["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"], + "description": "Image aspect ratio", + "default": "1:1" + }, + "use_search_grounding": { + "type": "boolean", + "description": "Enable Google Search grounding", + "default": False + }, + "mode": { + "type": "string", + "enum": ["text_to_image", "edit"], + "description": "Generation mode", + "default": "text_to_image" + }, + "image": { + "type": "string", + "description": "Input image for edit mode (optional)" + }, + "output_dir": { + "type": "string", + "description": "Directory to save results (optional)" + } + }, + "required": ["prompt"] + } + ), + Tool( + name="generate_image_flux2_pro", + description="Generate images using FLUX.2 PRO. High-quality image generation with text-to-image, editing, and multi-image composition.", + inputSchema={ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Text description for image generation" + }, + "width": { + "type": "integer", + "minimum": 64, + "description": "Image width in pixels", + "default": 1024 + }, + "height": { + "type": "integer", + "minimum": 64, + "description": "Image height in pixels", + "default": 1024 + }, + "seed": { + "type": "integer", + "description": "Random seed for reproducibility (optional)" + }, + "mode": { + "type": "string", + "enum": ["text_to_image", "edit", "compose"], + "description": "Generation mode", + "default": "text_to_image" + }, + "input_image": { + "type": "string", + "description": "Input image for edit mode (optional)" + }, + "images": { + "type": "array", + "items": {"type": "string"}, + "description": "Input images for compose mode (optional)" + }, + "output_dir": { + "type": "string", + "description": "Directory to save results (optional)" + } + }, + "required": ["prompt"] + } + ), + Tool( + name="generate_image_flux2_flex", + description="Generate images using FLUX.2 FLEX. Flexible generation with advanced controls including guidance scale, steps, and prompt upsampling.", + inputSchema={ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Text description for image generation" + }, + "width": { + "type": "integer", + "minimum": 64, + "description": "Image width in pixels", + "default": 1024 + }, + "height": { + "type": "integer", + "minimum": 64, + "description": "Image height in pixels", + "default": 1024 + }, + "guidance": { + "type": "number", + "minimum": 1.5, + "maximum": 10, + "description": "Guidance scale for prompt adherence", + "default": 7.5 + }, + "steps": { + "type": "integer", + "minimum": 1, + "description": "Number of inference steps", + "default": 28 + }, + "prompt_upsampling": { + "type": "boolean", + "description": "Enable prompt enhancement", + "default": False + }, + "seed": { + "type": "integer", + "description": "Random seed for reproducibility (optional)" + }, + "mode": { + "type": "string", + "enum": ["text_to_image", "edit"], + "description": "Generation mode", + "default": "text_to_image" + }, + "input_image": { + "type": "string", + "description": "Input image for edit mode (optional)" + }, + "output_dir": { + "type": "string", + "description": "Directory to save results (optional)" + } + }, + "required": ["prompt"] + } + ), + Tool( + name="generate_image_luma_photon_flash", + description="Generate images using Luma AI Photon-Flash-1. Fast and cost-efficient image generation with multiple reference modes.", + inputSchema={ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Text description for image generation" + }, + "aspect_ratio": { + "type": "string", + "enum": ["1:1", "3:4", "4:3", "9:16", "16:9", "21:9", "9:21"], + "description": "Image aspect ratio", + "default": "1:1" + }, + "mode": { + "type": "string", + "enum": ["text_to_image", "img_ref", "style_ref", "char_ref", "modify"], + "description": "Generation mode", + "default": "text_to_image" + }, + "images": { + "type": "array", + "items": {"type": "string"}, + "description": "Input images for reference/modify modes (optional)" + }, + "weights": { + "type": "array", + "items": {"type": "number"}, + "description": "Weights for reference images (optional)" + }, + "char_id": { + "type": "string", + "description": "Character ID for character reference mode (optional)" + }, + "char_images": { + "type": "array", + "items": {"type": "string"}, + "description": "Character reference images (optional)" + }, + "output_dir": { + "type": "string", + "description": "Directory to save results (optional)" + } + }, + "required": ["prompt"] + } + ), + Tool( + name="generate_image_luma_photon", + description="Generate images using Luma AI Photon-1. High-fidelity professional-grade image generation with multiple reference modes.", + inputSchema={ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Text description for image generation" + }, + "aspect_ratio": { + "type": "string", + "enum": ["1:1", "3:4", "4:3", "9:16", "16:9", "21:9", "9:21"], + "description": "Image aspect ratio", + "default": "1:1" + }, + "mode": { + "type": "string", + "enum": ["text_to_image", "img_ref", "style_ref", "char_ref", "modify"], + "description": "Generation mode", + "default": "text_to_image" + }, + "images": { + "type": "array", + "items": {"type": "string"}, + "description": "Input images for reference/modify modes (optional)" + }, + "weights": { + "type": "array", + "items": {"type": "number"}, + "description": "Weights for reference images (optional)" + }, + "char_id": { + "type": "string", + "description": "Character ID for character reference mode (optional)" + }, + "char_images": { + "type": "array", + "items": {"type": "string"}, + "description": "Character reference images (optional)" + }, + "output_dir": { + "type": "string", + "description": "Directory to save results (optional)" + } + }, + "required": ["prompt"] + } + ), + + # Video Generation Tools + Tool( + name="generate_video_luma_ray", + description="Generate videos using Luma AI Ray models. Supports text-to-video and image-to-video with keyframe control.", + inputSchema={ + "type": "object", + "properties": { + "prompt": { + "type": "string", + "description": "Text description for video generation" + }, + "model": { + "type": "string", + "enum": ["ray-1-6", "ray-2", "ray-flash-2"], + "description": "Ray model version", + "default": "ray-2" + }, + "mode": { + "type": "string", + "enum": ["text_video", "image_video"], + "description": "Generation mode", + "default": "text_video" + }, + "resolution": { + "type": "string", + "enum": ["540p", "720p", "1080p", "4k"], + "description": "Video resolution", + "default": "720p" + }, + "duration": { + "type": "string", + "enum": ["5s", "9s", "10s"], + "description": "Video duration", + "default": "5s" + }, + "aspect_ratio": { + "type": "string", + "enum": ["1:1", "3:4", "4:3", "9:16", "16:9", "21:9", "9:21"], + "description": "Video aspect ratio", + "default": "16:9" + }, + "start_image": { + "type": "string", + "description": "Start keyframe for image_video mode (optional)" + }, + "end_image": { + "type": "string", + "description": "End keyframe for image_video mode (optional)" + }, + "loop": { + "type": "boolean", + "description": "Enable seamless looping", + "default": False + }, + "output_dir": { + "type": "string", + "description": "Directory to save results (optional)" + } + }, + "required": ["prompt"] + } + ), + + # Preprocessing Tools + Tool( + name="segment_garment", + description="Segment garments from images using U2Net. Supports upper body, lower body, or all garments.", + inputSchema={ + "type": "object", + "properties": { + "input_path": { + "type": "string", + "description": "Path to input image or directory" + }, + "output_dir": { + "type": "string", + "description": "Output directory for segmented images" + }, + "garment_class": { + "type": "string", + "enum": ["upper", "lower", "all"], + "description": "Garment class to segment", + "default": "upper" + } + }, + "required": ["input_path", "output_dir"] + } + ), + Tool( + name="extract_garment", + description="Extract and preprocess garments from images. Includes segmentation and optional resizing.", + inputSchema={ + "type": "object", + "properties": { + "input_path": { + "type": "string", + "description": "Path to input image or directory" + }, + "output_dir": { + "type": "string", + "description": "Output directory for extracted garments" + }, + "garment_class": { + "type": "string", + "enum": ["upper", "lower", "all"], + "description": "Garment class to extract", + "default": "upper" + }, + "resize_width": { + "type": "integer", + "description": "Target width for resizing (optional)", + "default": 400 + } + }, + "required": ["input_path", "output_dir"] + } + ), + Tool( + name="segment_human", + description="Segment human subjects from images using advanced segmentation models.", + inputSchema={ + "type": "object", + "properties": { + "image_path": { + "type": "string", + "description": "Path to input image" + }, + "output_dir": { + "type": "string", + "description": "Output directory for segmented images" + } + }, + "required": ["image_path", "output_dir"] + } + ), + + # Dataset Tools + Tool( + name="load_fashion_mnist", + description="Load Fashion-MNIST dataset. A dataset of Zalando's article images (60K training, 10K test, 10 classes).", + inputSchema={ + "type": "object", + "properties": { + "download": { + "type": "boolean", + "description": "Download dataset if not present", + "default": True + }, + "normalize": { + "type": "boolean", + "description": "Normalize images to [0, 1]", + "default": True + }, + "flatten": { + "type": "boolean", + "description": "Flatten images to 1D arrays", + "default": False + } + }, + "required": [] + } + ), + Tool( + name="load_viton_hd", + description="Load VITON-HD dataset. High-resolution virtual try-on dataset (11,647 training pairs, 2,032 test pairs).", + inputSchema={ + "type": "object", + "properties": { + "data_dir": { + "type": "string", + "description": "Path to VITON-HD dataset directory" + }, + "split": { + "type": "string", + "enum": ["train", "test"], + "description": "Dataset split to load", + "default": "train" + }, + "batch_size": { + "type": "integer", + "minimum": 1, + "description": "Batch size for DataLoader", + "default": 8 + } + }, + "required": ["data_dir"] + } + ), +] + + +@app.list_tools() +async def list_tools() -> list[Tool]: + """List all available tools.""" + return TOOLS + + +@app.call_tool() +async def call_tool(name: str, arguments: Any) -> list[TextContent]: + """Execute a tool with given arguments.""" + try: + # Route to appropriate tool function + if name == "virtual_tryon_nova": + result = virtual_tryon_nova(**arguments) + elif name == "virtual_tryon_kling": + result = virtual_tryon_kling(**arguments) + elif name == "virtual_tryon_segmind": + result = virtual_tryon_segmind(**arguments) + elif name == "generate_image_nano_banana": + result = generate_image_nano_banana(**arguments) + elif name == "generate_image_nano_banana_pro": + result = generate_image_nano_banana_pro(**arguments) + elif name == "generate_image_flux2_pro": + result = generate_image_flux2_pro(**arguments) + elif name == "generate_image_flux2_flex": + result = generate_image_flux2_flex(**arguments) + elif name == "generate_image_luma_photon_flash": + result = generate_image_luma_photon_flash(**arguments) + elif name == "generate_image_luma_photon": + result = generate_image_luma_photon(**arguments) + elif name == "generate_video_luma_ray": + result = generate_video_luma_ray(**arguments) + elif name == "segment_garment": + result = segment_garment(**arguments) + elif name == "extract_garment": + result = extract_garment(**arguments) + elif name == "segment_human": + result = segment_human(**arguments) + elif name == "load_fashion_mnist": + result = load_fashion_mnist(**arguments) + elif name == "load_viton_hd": + result = load_viton_hd(**arguments) + else: + result = { + "success": False, + "error": f"Unknown tool: {name}" + } + + # Format result as text content + return [TextContent( + type="text", + text=json.dumps(result, indent=2) + )] + + except Exception as e: + return [TextContent( + type="text", + text=json.dumps({ + "success": False, + "error": f"Tool execution failed: {str(e)}" + }, indent=2) + )] + + +async def main(): + """Main entry point for the MCP server.""" + # Print configuration status + print(config.get_status_message(), file=sys.stderr) + print("\nStarting OpenTryOn MCP Server...", file=sys.stderr) + + # Run the server + async with stdio_server() as (read_stream, write_stream): + await app.run( + read_stream, + write_stream, + app.create_initialization_options() + ) + + +if __name__ == "__main__": + asyncio.run(main()) + diff --git a/mcp-server/test_server.py b/mcp-server/test_server.py new file mode 100755 index 0000000..a04f7e8 --- /dev/null +++ b/mcp-server/test_server.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python3 +"""Test script for OpenTryOn MCP Server.""" + +import sys +from pathlib import Path + +# Add current directory to path +sys.path.insert(0, str(Path(__file__).parent)) + +from config import config + + +def test_configuration(): + """Test configuration loading and validation.""" + print("=" * 60) + print("Testing Configuration") + print("=" * 60) + + # Print configuration status + print(config.get_status_message()) + + # Validate configuration + status = config.validate() + + print("\nConfiguration Validation:") + for service, is_configured in status.items(): + status_icon = "✓" if is_configured else "✗" + print(f" {status_icon} {service}: {'Configured' if is_configured else 'Not configured'}") + + # Check if at least one service is configured + if any(status.values()): + print("\n✓ At least one service is configured") + return True + else: + print("\n✗ No services are configured") + print("Please configure API keys in .env file") + return False + + +def test_imports(): + """Test that all modules can be imported.""" + print("\n" + "=" * 60) + print("Testing Module Imports") + print("=" * 60) + + modules_to_test = [ + ("config", "Configuration module"), + ("utils", "Utilities module"), + ("utils.image_utils", "Image utilities"), + ("utils.validation", "Validation utilities"), + ("tools", "Tools module"), + ("tools.virtual_tryon", "Virtual try-on tools"), + ("tools.image_gen", "Image generation tools"), + ("tools.video_gen", "Video generation tools"), + ("tools.preprocessing", "Preprocessing tools"), + ("tools.datasets", "Dataset tools"), + ] + + all_passed = True + for module_name, description in modules_to_test: + try: + __import__(module_name) + print(f" ✓ {description}: OK") + except ImportError as e: + print(f" ✗ {description}: FAILED - {e}") + all_passed = False + except Exception as e: + print(f" ✗ {description}: ERROR - {e}") + all_passed = False + + return all_passed + + +def test_tool_definitions(): + """Test that tool definitions are valid.""" + print("\n" + "=" * 60) + print("Testing Tool Definitions") + print("=" * 60) + + try: + from server import TOOLS + + print(f"\nFound {len(TOOLS)} tools:") + + categories = { + "Virtual Try-On": [], + "Image Generation": [], + "Video Generation": [], + "Preprocessing": [], + "Datasets": [] + } + + for tool in TOOLS: + if "virtual_tryon" in tool.name: + categories["Virtual Try-On"].append(tool.name) + elif "generate_image" in tool.name: + categories["Image Generation"].append(tool.name) + elif "generate_video" in tool.name: + categories["Video Generation"].append(tool.name) + elif tool.name in ["segment_garment", "extract_garment", "segment_human"]: + categories["Preprocessing"].append(tool.name) + elif "load_" in tool.name: + categories["Datasets"].append(tool.name) + + for category, tools in categories.items(): + if tools: + print(f"\n{category} ({len(tools)} tools):") + for tool_name in tools: + print(f" - {tool_name}") + + print(f"\n✓ All {len(TOOLS)} tools loaded successfully") + return True + + except Exception as e: + print(f"\n✗ Failed to load tools: {e}") + return False + + +def test_opentryon_imports(): + """Test that OpenTryOn library can be imported.""" + print("\n" + "=" * 60) + print("Testing OpenTryOn Library Imports") + print("=" * 60) + + opentryon_modules = [ + ("tryon.api", "API adapters"), + ("tryon.preprocessing", "Preprocessing"), + ("tryon.datasets", "Datasets"), + ] + + all_passed = True + for module_name, description in opentryon_modules: + try: + __import__(module_name) + print(f" ✓ {description}: OK") + except ImportError as e: + print(f" ✗ {description}: FAILED - {e}") + print(f" Make sure OpenTryOn is installed: pip install -e /path/to/opentryon") + all_passed = False + except Exception as e: + print(f" ✗ {description}: ERROR - {e}") + all_passed = False + + return all_passed + + +def test_directory_structure(): + """Test that required directories exist.""" + print("\n" + "=" * 60) + print("Testing Directory Structure") + print("=" * 60) + + base_dir = Path(__file__).parent + + required_dirs = [ + (".", "MCP server root"), + ("tools", "Tools directory"), + ("utils", "Utils directory"), + ("examples", "Examples directory"), + ] + + required_files = [ + ("server.py", "Main server file"), + ("config.py", "Configuration file"), + ("requirements.txt", "Requirements file"), + ("README.md", "README file"), + ] + + all_passed = True + + print("\nDirectories:") + for dir_path, description in required_dirs: + full_path = base_dir / dir_path + if full_path.exists() and full_path.is_dir(): + print(f" ✓ {description}: {dir_path}") + else: + print(f" ✗ {description}: {dir_path} (missing)") + all_passed = False + + print("\nFiles:") + for file_path, description in required_files: + full_path = base_dir / file_path + if full_path.exists() and full_path.is_file(): + print(f" ✓ {description}: {file_path}") + else: + print(f" ✗ {description}: {file_path} (missing)") + all_passed = False + + return all_passed + + +def main(): + """Run all tests.""" + print("\n" + "=" * 60) + print("OpenTryOn MCP Server - Test Suite") + print("=" * 60) + + tests = [ + ("Directory Structure", test_directory_structure), + ("Configuration", test_configuration), + ("Module Imports", test_imports), + ("OpenTryOn Library", test_opentryon_imports), + ("Tool Definitions", test_tool_definitions), + ] + + results = {} + for test_name, test_func in tests: + try: + results[test_name] = test_func() + except Exception as e: + print(f"\n✗ {test_name} test crashed: {e}") + results[test_name] = False + + # Summary + print("\n" + "=" * 60) + print("Test Summary") + print("=" * 60) + + passed = sum(1 for result in results.values() if result) + total = len(results) + + for test_name, result in results.items(): + status = "✓ PASSED" if result else "✗ FAILED" + print(f" {status}: {test_name}") + + print(f"\nTotal: {passed}/{total} tests passed") + + if passed == total: + print("\n✓ All tests passed! Server is ready to use.") + return 0 + else: + print(f"\n✗ {total - passed} test(s) failed. Please fix the issues above.") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) + diff --git a/mcp-server/tools/__init__.py b/mcp-server/tools/__init__.py new file mode 100644 index 0000000..f0e4c06 --- /dev/null +++ b/mcp-server/tools/__init__.py @@ -0,0 +1,51 @@ +"""Tool implementations for OpenTryOn MCP Server.""" + +from .virtual_tryon import ( + virtual_tryon_nova, + virtual_tryon_kling, + virtual_tryon_segmind, +) +from .image_gen import ( + generate_image_nano_banana, + generate_image_nano_banana_pro, + generate_image_flux2_pro, + generate_image_flux2_flex, + generate_image_luma_photon_flash, + generate_image_luma_photon, +) +from .video_gen import ( + generate_video_luma_ray, +) +from .preprocessing import ( + segment_garment, + extract_garment, + segment_human, +) +from .datasets import ( + load_fashion_mnist, + load_viton_hd, +) + +__all__ = [ + # Virtual Try-On + "virtual_tryon_nova", + "virtual_tryon_kling", + "virtual_tryon_segmind", + # Image Generation + "generate_image_nano_banana", + "generate_image_nano_banana_pro", + "generate_image_flux2_pro", + "generate_image_flux2_flex", + "generate_image_luma_photon_flash", + "generate_image_luma_photon", + # Video Generation + "generate_video_luma_ray", + # Preprocessing + "segment_garment", + "extract_garment", + "segment_human", + # Datasets + "load_fashion_mnist", + "load_viton_hd", +] + diff --git a/mcp-server/tools/datasets.py b/mcp-server/tools/datasets.py new file mode 100644 index 0000000..097f95d --- /dev/null +++ b/mcp-server/tools/datasets.py @@ -0,0 +1,103 @@ +"""Dataset tools for OpenTryOn MCP Server.""" + +from typing import Optional +from pathlib import Path + +from tryon.datasets import FashionMNIST, VITONHD + + + +def load_fashion_mnist( + download: bool = True, + normalize: bool = True, + flatten: bool = False, +) -> dict: + """ + Load Fashion-MNIST dataset. + + Args: + download: Download if not present + normalize: Normalize images + flatten: Flatten images + + Returns: + Dictionary with status and dataset information + """ + try: + dataset = FashionMNIST(download=download) + (train_images, train_labels), (test_images, test_labels) = dataset.load( + normalize=normalize, + flatten=flatten + ) + + return { + "success": True, + "dataset": "fashion_mnist", + "train_size": len(train_images), + "test_size": len(test_images), + "num_classes": 10, + "image_shape": train_images.shape[1:], + "message": f"Loaded Fashion-MNIST: {len(train_images)} training, {len(test_images)} test images" + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + + +def load_viton_hd( + data_dir: str, + split: str = "train", + batch_size: int = 8, +) -> dict: + """ + Load VITON-HD dataset. + + Args: + data_dir: Dataset directory + split: "train" or "test" + batch_size: Batch size for DataLoader + + Returns: + Dictionary with status and dataset information + """ + try: + # Validate split + if split not in ["train", "test"]: + return { + "success": False, + "error": f"Invalid split: {split}. Must be 'train' or 'test'" + } + + # Validate data directory + data_path = Path(data_dir) + if not data_path.exists(): + return { + "success": False, + "error": f"Data directory does not exist: {data_dir}" + } + + dataset = VITONHD(data_dir=str(data_path), download=False) + dataloader = dataset.get_dataloader( + split=split, + batch_size=batch_size, + shuffle=(split == "train") + ) + + return { + "success": True, + "dataset": "viton_hd", + "split": split, + "batch_size": batch_size, + "num_batches": len(dataloader), + "message": f"Loaded VITON-HD {split} split with batch size {batch_size}" + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + diff --git a/mcp-server/tools/image_gen.py b/mcp-server/tools/image_gen.py new file mode 100644 index 0000000..6f96a70 --- /dev/null +++ b/mcp-server/tools/image_gen.py @@ -0,0 +1,531 @@ +"""Image generation tools for OpenTryOn MCP Server.""" + +from typing import Optional, List +from pathlib import Path + +from tryon.api.nano_banana import NanoBananaAdapter, NanoBananaProAdapter +from tryon.api.flux2 import Flux2ProAdapter, Flux2FlexAdapter +from tryon.api.lumaAI import LumaAIAdapter +from config import config +from utils import save_image + + +def generate_image_nano_banana( + prompt: str, + aspect_ratio: str = "1:1", + mode: str = "text_to_image", + image: Optional[str] = None, + images: Optional[List[str]] = None, + output_dir: Optional[str] = None, +) -> dict: + """ + Generate images using Nano Banana (Gemini 2.5 Flash). + + Args: + prompt: Text description + aspect_ratio: Aspect ratio (e.g., "16:9", "1:1") + mode: "text_to_image", "edit", or "compose" + image: Input image for edit mode + images: Input images for compose mode + output_dir: Directory to save results (optional) + + Returns: + Dictionary with status and result information + """ + try: + adapter = NanoBananaAdapter() + + # Generate based on mode + if mode == "text_to_image": + result_images = adapter.generate_text_to_image( + prompt=prompt, + aspect_ratio=aspect_ratio + ) + elif mode == "edit": + if not image: + return {"success": False, "error": "Image required for edit mode"} + result_images = adapter.generate_image_edit( + image=image, + prompt=prompt + ) + elif mode == "compose": + if not images: + return {"success": False, "error": "Images required for compose mode"} + result_images = adapter.generate_multi_image( + images=images, + prompt=prompt + ) + else: + return {"success": False, "error": f"Invalid mode: {mode}"} + + # Save results + output_paths = [] + if output_dir: + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + for idx, img in enumerate(result_images): + save_path = save_image(img, output_path / f"nano_banana_{idx}.png") + output_paths.append(str(save_path)) + else: + for idx, img in enumerate(result_images): + save_path = save_image(img, config.TEMP_DIR / f"nano_banana_{idx}.png") + output_paths.append(str(save_path)) + + return { + "success": True, + "provider": "nano_banana", + "model": "gemini-2.5-flash-image", + "num_images": len(result_images), + "output_paths": output_paths, + "message": f"Generated {len(result_images)} image(s)" + } + + except Exception as e: + return {"success": False, "error": str(e)} + + +def generate_image_nano_banana_pro( + prompt: str, + resolution: str = "1K", + aspect_ratio: str = "1:1", + use_search_grounding: bool = False, + mode: str = "text_to_image", + image: Optional[str] = None, + output_dir: Optional[str] = None, +) -> dict: + """ + Generate high-resolution images using Nano Banana Pro (Gemini 3 Pro). + + Args: + prompt: Text description + resolution: "1K", "2K", or "4K" + aspect_ratio: Aspect ratio (e.g., "16:9", "1:1") + use_search_grounding: Enable Google Search grounding + mode: "text_to_image" or "edit" + image: Input image for edit mode + output_dir: Directory to save results (optional) + + Returns: + Dictionary with status and result information + """ + try: + adapter = NanoBananaProAdapter() + + # Generate based on mode + if mode == "text_to_image": + result_images = adapter.generate_text_to_image( + prompt=prompt, + resolution=resolution, + aspect_ratio=aspect_ratio, + use_search_grounding=use_search_grounding + ) + elif mode == "edit": + if not image: + return {"success": False, "error": "Image required for edit mode"} + result_images = adapter.generate_image_edit( + image=image, + prompt=prompt, + resolution=resolution + ) + else: + return {"success": False, "error": f"Invalid mode: {mode}"} + + # Save results + output_paths = [] + if output_dir: + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + for idx, img in enumerate(result_images): + save_path = save_image(img, output_path / f"nano_banana_pro_{idx}.png") + output_paths.append(str(save_path)) + else: + for idx, img in enumerate(result_images): + save_path = save_image(img, config.TEMP_DIR / f"nano_banana_pro_{idx}.png") + output_paths.append(str(save_path)) + + return { + "success": True, + "provider": "nano_banana_pro", + "model": "gemini-3-pro-image", + "resolution": resolution, + "num_images": len(result_images), + "output_paths": output_paths, + "message": f"Generated {len(result_images)} {resolution} image(s)" + } + + except Exception as e: + return {"success": False, "error": str(e)} + + +def generate_image_flux2_pro( + prompt: str, + width: int = 1024, + height: int = 1024, + seed: Optional[int] = None, + mode: str = "text_to_image", + input_image: Optional[str] = None, + images: Optional[List[str]] = None, + output_dir: Optional[str] = None, +) -> dict: + """ + Generate images using FLUX.2 PRO. + + Args: + prompt: Text description + width: Image width + height: Image height + seed: Random seed (optional) + mode: "text_to_image", "edit", or "compose" + input_image: Input image for edit mode + images: Input images for compose mode + output_dir: Directory to save results (optional) + + Returns: + Dictionary with status and result information + """ + try: + adapter = Flux2ProAdapter() + + # Generate based on mode + if mode == "text_to_image": + result_images = adapter.generate_text_to_image( + prompt=prompt, + width=width, + height=height, + seed=seed + ) + elif mode == "edit": + if not input_image: + return {"success": False, "error": "Input image required for edit mode"} + result_images = adapter.generate_image_edit( + prompt=prompt, + input_image=input_image, + width=width, + height=height + ) + elif mode == "compose": + if not images: + return {"success": False, "error": "Images required for compose mode"} + result_images = adapter.generate_multi_image( + prompt=prompt, + images=images, + width=width, + height=height + ) + else: + return {"success": False, "error": f"Invalid mode: {mode}"} + + # Save results + output_paths = [] + if output_dir: + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + for idx, img in enumerate(result_images): + save_path = save_image(img, output_path / f"flux2_pro_{idx}.png") + output_paths.append(str(save_path)) + else: + for idx, img in enumerate(result_images): + save_path = save_image(img, config.TEMP_DIR / f"flux2_pro_{idx}.png") + output_paths.append(str(save_path)) + + return { + "success": True, + "provider": "flux2_pro", + "model": "flux-2-pro", + "dimensions": f"{width}x{height}", + "num_images": len(result_images), + "output_paths": output_paths, + "message": f"Generated {len(result_images)} image(s)" + } + + except Exception as e: + return {"success": False, "error": str(e)} + + +def generate_image_flux2_flex( + prompt: str, + width: int = 1024, + height: int = 1024, + guidance: float = 7.5, + steps: int = 28, + prompt_upsampling: bool = False, + seed: Optional[int] = None, + mode: str = "text_to_image", + input_image: Optional[str] = None, + output_dir: Optional[str] = None, +) -> dict: + """ + Generate images using FLUX.2 FLEX with advanced controls. + + Args: + prompt: Text description + width: Image width + height: Image height + guidance: Guidance scale (1.5-10) + steps: Number of steps + prompt_upsampling: Enable prompt enhancement + seed: Random seed (optional) + mode: "text_to_image" or "edit" + input_image: Input image for edit mode + output_dir: Directory to save results (optional) + + Returns: + Dictionary with status and result information + """ + try: + adapter = Flux2FlexAdapter() + + # Generate based on mode + if mode == "text_to_image": + result_images = adapter.generate_text_to_image( + prompt=prompt, + width=width, + height=height, + guidance=guidance, + steps=steps, + prompt_upsampling=prompt_upsampling, + seed=seed + ) + elif mode == "edit": + if not input_image: + return {"success": False, "error": "Input image required for edit mode"} + result_images = adapter.generate_image_edit( + prompt=prompt, + input_image=input_image, + width=width, + height=height, + guidance=guidance, + steps=steps, + prompt_upsampling=prompt_upsampling + ) + else: + return {"success": False, "error": f"Invalid mode: {mode}"} + + # Save results + output_paths = [] + if output_dir: + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + for idx, img in enumerate(result_images): + save_path = save_image(img, output_path / f"flux2_flex_{idx}.png") + output_paths.append(str(save_path)) + else: + for idx, img in enumerate(result_images): + save_path = save_image(img, config.TEMP_DIR / f"flux2_flex_{idx}.png") + output_paths.append(str(save_path)) + + return { + "success": True, + "provider": "flux2_flex", + "model": "flux-2-flex", + "dimensions": f"{width}x{height}", + "guidance": guidance, + "steps": steps, + "num_images": len(result_images), + "output_paths": output_paths, + "message": f"Generated {len(result_images)} image(s)" + } + + except Exception as e: + return {"success": False, "error": str(e)} + + +def generate_image_luma_photon_flash( + prompt: str, + aspect_ratio: str = "1:1", + mode: str = "text_to_image", + images: Optional[List[str]] = None, + weights: Optional[List[float]] = None, + char_id: Optional[str] = None, + char_images: Optional[List[str]] = None, + output_dir: Optional[str] = None, +) -> dict: + """ + Generate images using Luma AI Photon-Flash-1. + + Args: + prompt: Text description + aspect_ratio: Aspect ratio (e.g., "16:9", "1:1") + mode: "text_to_image", "img_ref", "style_ref", "char_ref", or "modify" + images: Input images for reference/modify modes + weights: Weights for reference images + char_id: Character ID for character reference + char_images: Character reference images + output_dir: Directory to save results (optional) + + Returns: + Dictionary with status and result information + """ + try: + adapter = LumaAIAdapter(model="photon-flash-1") + + # Generate based on mode + if mode == "text_to_image": + result_images = adapter.generate_text_to_image( + prompt=prompt, + aspect_ratio=aspect_ratio + ) + elif mode == "img_ref": + if not images: + return {"success": False, "error": "Images required for img_ref mode"} + image_ref = [{"url": img, "weight": w} for img, w in zip(images, weights or [0.8] * len(images))] + result_images = adapter.generate_with_image_reference( + prompt=prompt, + aspect_ratio=aspect_ratio, + image_ref=image_ref + ) + elif mode == "style_ref": + if not images: + return {"success": False, "error": "Images required for style_ref mode"} + style_ref = [{"url": img, "weight": w} for img, w in zip(images, weights or [0.75] * len(images))] + result_images = adapter.generate_with_style_reference( + prompt=prompt, + aspect_ratio=aspect_ratio, + style_ref=style_ref + ) + elif mode == "char_ref": + if not char_id or not char_images: + return {"success": False, "error": "Character ID and images required for char_ref mode"} + character_ref = {char_id: {"images": char_images}} + result_images = adapter.generate_with_character_reference( + prompt=prompt, + aspect_ratio=aspect_ratio, + character_ref=character_ref + ) + elif mode == "modify": + if not images or len(images) != 1: + return {"success": False, "error": "Single image required for modify mode"} + result_images = adapter.generate_with_modify_image( + prompt=prompt, + images=images[0], + weights=weights[0] if weights else 0.85, + aspect_ratio=aspect_ratio + ) + else: + return {"success": False, "error": f"Invalid mode: {mode}"} + + # Save results + output_paths = [] + if output_dir: + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + for idx, img in enumerate(result_images): + save_path = save_image(img, output_path / f"luma_photon_flash_{idx}.png") + output_paths.append(str(save_path)) + else: + for idx, img in enumerate(result_images): + save_path = save_image(img, config.TEMP_DIR / f"luma_photon_flash_{idx}.png") + output_paths.append(str(save_path)) + + return { + "success": True, + "provider": "luma_ai", + "model": "photon-flash-1", + "num_images": len(result_images), + "output_paths": output_paths, + "message": f"Generated {len(result_images)} image(s)" + } + + except Exception as e: + return {"success": False, "error": str(e)} + + +def generate_image_luma_photon( + prompt: str, + aspect_ratio: str = "1:1", + mode: str = "text_to_image", + images: Optional[List[str]] = None, + weights: Optional[List[float]] = None, + char_id: Optional[str] = None, + char_images: Optional[List[str]] = None, + output_dir: Optional[str] = None, +) -> dict: + """ + Generate images using Luma AI Photon-1. + + Args: + prompt: Text description + aspect_ratio: Aspect ratio (e.g., "16:9", "1:1") + mode: "text_to_image", "img_ref", "style_ref", "char_ref", or "modify" + images: Input images for reference/modify modes + weights: Weights for reference images + char_id: Character ID for character reference + char_images: Character reference images + output_dir: Directory to save results (optional) + + Returns: + Dictionary with status and result information + """ + try: + adapter = LumaAIAdapter(model="photon-1") + + # Generate based on mode + if mode == "text_to_image": + result_images = adapter.generate_text_to_image( + prompt=prompt, + aspect_ratio=aspect_ratio + ) + elif mode == "img_ref": + if not images: + return {"success": False, "error": "Images required for img_ref mode"} + image_ref = [{"url": img, "weight": w} for img, w in zip(images, weights or [0.8] * len(images))] + result_images = adapter.generate_with_image_reference( + prompt=prompt, + aspect_ratio=aspect_ratio, + image_ref=image_ref + ) + elif mode == "style_ref": + if not images: + return {"success": False, "error": "Images required for style_ref mode"} + style_ref = [{"url": img, "weight": w} for img, w in zip(images, weights or [0.75] * len(images))] + result_images = adapter.generate_with_style_reference( + prompt=prompt, + aspect_ratio=aspect_ratio, + style_ref=style_ref + ) + elif mode == "char_ref": + if not char_id or not char_images: + return {"success": False, "error": "Character ID and images required for char_ref mode"} + character_ref = {char_id: {"images": char_images}} + result_images = adapter.generate_with_character_reference( + prompt=prompt, + aspect_ratio=aspect_ratio, + character_ref=character_ref + ) + elif mode == "modify": + if not images or len(images) != 1: + return {"success": False, "error": "Single image required for modify mode"} + result_images = adapter.generate_with_modify_image( + prompt=prompt, + images=images[0], + weights=weights[0] if weights else 0.85, + aspect_ratio=aspect_ratio + ) + else: + return {"success": False, "error": f"Invalid mode: {mode}"} + + # Save results + output_paths = [] + if output_dir: + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + for idx, img in enumerate(result_images): + save_path = save_image(img, output_path / f"luma_photon_{idx}.png") + output_paths.append(str(save_path)) + else: + for idx, img in enumerate(result_images): + save_path = save_image(img, config.TEMP_DIR / f"luma_photon_{idx}.png") + output_paths.append(str(save_path)) + + return { + "success": True, + "provider": "luma_ai", + "model": "photon-1", + "num_images": len(result_images), + "output_paths": output_paths, + "message": f"Generated {len(result_images)} image(s)" + } + + except Exception as e: + return {"success": False, "error": str(e)} + diff --git a/mcp-server/tools/preprocessing.py b/mcp-server/tools/preprocessing.py new file mode 100644 index 0000000..d3888d5 --- /dev/null +++ b/mcp-server/tools/preprocessing.py @@ -0,0 +1,185 @@ +"""Preprocessing tools for OpenTryOn MCP Server.""" + +from typing import Optional +from pathlib import Path + +from tryon.preprocessing import segment_garment as _segment_garment +from tryon.preprocessing import extract_garment as _extract_garment +from tryon.preprocessing import segment_human as _segment_human + + +def segment_garment( + input_path: str, + output_dir: str, + garment_class: str = "upper", +) -> dict: + """ + Segment garments from images using U2Net. + + Args: + input_path: Path to input image or directory + output_dir: Output directory + garment_class: "upper", "lower", or "all" + + Returns: + Dictionary with status and result information + """ + try: + # Validate garment class + if garment_class not in ["upper", "lower", "all"]: + return { + "success": False, + "error": f"Invalid garment_class: {garment_class}. Must be 'upper', 'lower', or 'all'" + } + + # Validate input path + input_p = Path(input_path) + if not input_p.exists(): + return { + "success": False, + "error": f"Input path does not exist: {input_path}" + } + + # Create output directory + output_p = Path(output_dir) + output_p.mkdir(parents=True, exist_ok=True) + + # Perform segmentation + _segment_garment( + inputs_dir=str(input_p) if input_p.is_dir() else str(input_p.parent), + outputs_dir=str(output_p), + cls=garment_class + ) + + return { + "success": True, + "operation": "segment_garment", + "input_path": str(input_p), + "output_dir": str(output_p), + "garment_class": garment_class, + "message": f"Successfully segmented garments to {output_dir}" + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + + +def extract_garment( + input_path: str, + output_dir: str, + garment_class: str = "upper", + resize_width: Optional[int] = 400, +) -> dict: + """ + Extract and preprocess garments. + + Args: + input_path: Path to input image or directory + output_dir: Output directory + garment_class: "upper", "lower", or "all" + resize_width: Target width for resizing (optional) + + Returns: + Dictionary with status and result information + """ + try: + # Validate garment class + if garment_class not in ["upper", "lower", "all"]: + return { + "success": False, + "error": f"Invalid garment_class: {garment_class}. Must be 'upper', 'lower', or 'all'" + } + + # Validate input path + input_p = Path(input_path) + if not input_p.exists(): + return { + "success": False, + "error": f"Input path does not exist: {input_path}" + } + + # Create output directory + output_p = Path(output_dir) + output_p.mkdir(parents=True, exist_ok=True) + + # Perform extraction + _extract_garment( + inputs_dir=str(input_p) if input_p.is_dir() else str(input_p.parent), + outputs_dir=str(output_p), + cls=garment_class, + resize_to_width=resize_width + ) + + return { + "success": True, + "operation": "extract_garment", + "input_path": str(input_p), + "output_dir": str(output_p), + "garment_class": garment_class, + "resize_width": resize_width, + "message": f"Successfully extracted garments to {output_dir}" + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + + +def segment_human( + image_path: str, + output_dir: str, +) -> dict: + """ + Segment human subjects from images. + + Args: + image_path: Path to input image + output_dir: Output directory + + Returns: + Dictionary with status and result information + """ + try: + # Validate input path + input_p = Path(image_path) + if not input_p.exists(): + return { + "success": False, + "error": f"Input image does not exist: {image_path}" + } + + if not input_p.is_file(): + return { + "success": False, + "error": f"Input path is not a file: {image_path}" + } + + # Create output directory + output_p = Path(output_dir) + output_p.mkdir(parents=True, exist_ok=True) + + # Perform segmentation + _segment_human( + image_path=str(input_p), + output_dir=str(output_p) + ) + + return { + "success": True, + "operation": "segment_human", + "input_path": str(input_p), + "output_dir": str(output_p), + "message": f"Successfully segmented human to {output_dir}" + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + diff --git a/mcp-server/tools/video_gen.py b/mcp-server/tools/video_gen.py new file mode 100644 index 0000000..5d4c33c --- /dev/null +++ b/mcp-server/tools/video_gen.py @@ -0,0 +1,105 @@ +"""Video generation tools for OpenTryOn MCP Server.""" + +from typing import Optional +from pathlib import Path + +from tryon.api.lumaAI import LumaAIVideoAdapter +from config import config + + +def generate_video_luma_ray( + prompt: str, + model: str = "ray-2", + mode: str = "text_video", + resolution: str = "720p", + duration: str = "5s", + aspect_ratio: str = "16:9", + start_image: Optional[str] = None, + end_image: Optional[str] = None, + loop: bool = False, + output_dir: Optional[str] = None, +) -> dict: + """ + Generate videos using Luma AI Ray models. + + Args: + prompt: Text description + model: "ray-1-6", "ray-2", or "ray-flash-2" + mode: "text_video" or "image_video" + resolution: "540p", "720p", "1080p", or "4k" + duration: "5s", "9s", or "10s" + aspect_ratio: Aspect ratio (e.g., "16:9", "1:1") + start_image: Start keyframe for image_video mode + end_image: End keyframe for image_video mode + loop: Enable seamless looping + output_dir: Directory to save results (optional) + + Returns: + Dictionary with status and result information + """ + try: + adapter = LumaAIVideoAdapter() + + # Validate model + if model not in ["ray-1-6", "ray-2", "ray-flash-2"]: + return { + "success": False, + "error": f"Invalid model: {model}. Must be 'ray-1-6', 'ray-2', or 'ray-flash-2'" + } + + # Generate based on mode + if mode == "text_video": + video_bytes = adapter.generate_text_to_video( + prompt=prompt, + resolution=resolution, + duration=duration, + aspect_ratio=aspect_ratio, + loop=loop, + model=model + ) + elif mode == "image_video": + video_bytes = adapter.generate_image_to_video( + prompt=prompt, + start_image=start_image, + end_image=end_image, + resolution=resolution, + duration=duration, + aspect_ratio=aspect_ratio, + loop=loop, + model=model + ) + else: + return { + "success": False, + "error": f"Invalid mode: {mode}. Must be 'text_video' or 'image_video'" + } + + # Save result + if output_dir: + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + video_path = output_path / f"luma_{model}_video.mp4" + else: + video_path = config.TEMP_DIR / f"luma_{model}_video.mp4" + + with open(video_path, "wb") as f: + f.write(video_bytes) + + return { + "success": True, + "provider": "luma_ai", + "model": model, + "mode": mode, + "resolution": resolution, + "duration": duration, + "aspect_ratio": aspect_ratio, + "output_path": str(video_path), + "message": f"Generated video with {model}" + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + diff --git a/mcp-server/tools/virtual_tryon.py b/mcp-server/tools/virtual_tryon.py new file mode 100644 index 0000000..8dba842 --- /dev/null +++ b/mcp-server/tools/virtual_tryon.py @@ -0,0 +1,242 @@ +"""Virtual try-on tools for OpenTryOn MCP Server.""" + +from typing import Optional, List +from pathlib import Path + +from tryon.api import AmazonNovaCanvasVTONAdapter, KlingAIVTONAdapter, SegmindVTONAdapter +from config import config +from utils import validate_image_path, validate_image_url, save_image + + +def virtual_tryon_nova( + source_image: str, + reference_image: str, + mask_type: str = "GARMENT", + garment_class: str = "UPPER_BODY", + region: Optional[str] = None, + output_dir: Optional[str] = None, +) -> dict: + """ + Generate virtual try-on using Amazon Nova Canvas. + + Args: + source_image: Path or URL to person image + reference_image: Path or URL to garment image + mask_type: "GARMENT" or "IMAGE" + garment_class: "UPPER_BODY", "LOWER_BODY", "FULL_BODY", or "FOOTWEAR" + region: AWS region (optional, uses config default) + output_dir: Directory to save results (optional) + + Returns: + Dictionary with status and result information + """ + try: + # Validate inputs + if not (validate_image_path(source_image) or validate_image_url(source_image)): + return { + "success": False, + "error": f"Invalid source image: {source_image}" + } + + if not (validate_image_path(reference_image) or validate_image_url(reference_image)): + return { + "success": False, + "error": f"Invalid reference image: {reference_image}" + } + + # Initialize adapter + adapter = AmazonNovaCanvasVTONAdapter(region=region or config.AMAZON_NOVA_REGION) + + # Generate virtual try-on + images = adapter.generate_and_decode( + source_image=source_image, + reference_image=reference_image, + mask_type=mask_type, + garment_class=garment_class, + ) + + # Save results + output_paths = [] + if output_dir: + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + for idx, image in enumerate(images): + save_path = save_image(image, output_path / f"nova_result_{idx}.png") + output_paths.append(str(save_path)) + else: + # Save to temp directory + for idx, image in enumerate(images): + save_path = save_image(image, config.TEMP_DIR / f"nova_result_{idx}.png") + output_paths.append(str(save_path)) + + return { + "success": True, + "provider": "amazon_nova_canvas", + "num_images": len(images), + "output_paths": output_paths, + "message": f"Generated {len(images)} virtual try-on image(s)" + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + + +def virtual_tryon_kling( + source_image: str, + reference_image: str, + model: Optional[str] = None, + output_dir: Optional[str] = None, +) -> dict: + """ + Generate virtual try-on using Kling AI. + + Args: + source_image: Path or URL to person image + reference_image: Path or URL to garment image + model: Model version (optional, uses API default) + output_dir: Directory to save results (optional) + + Returns: + Dictionary with status and result information + """ + try: + # Validate inputs + if not (validate_image_path(source_image) or validate_image_url(source_image)): + return { + "success": False, + "error": f"Invalid source image: {source_image}" + } + + if not (validate_image_path(reference_image) or validate_image_url(reference_image)): + return { + "success": False, + "error": f"Invalid reference image: {reference_image}" + } + + # Initialize adapter + adapter = KlingAIVTONAdapter() + + # Generate virtual try-on + images = adapter.generate_and_decode( + source_image=source_image, + reference_image=reference_image, + model=model, + ) + + # Save results + output_paths = [] + if output_dir: + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + for idx, image in enumerate(images): + save_path = save_image(image, output_path / f"kling_result_{idx}.png") + output_paths.append(str(save_path)) + else: + # Save to temp directory + for idx, image in enumerate(images): + save_path = save_image(image, config.TEMP_DIR / f"kling_result_{idx}.png") + output_paths.append(str(save_path)) + + return { + "success": True, + "provider": "kling_ai", + "num_images": len(images), + "output_paths": output_paths, + "message": f"Generated {len(images)} virtual try-on image(s)" + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + + +def virtual_tryon_segmind( + model_image: str, + cloth_image: str, + category: str = "Upper body", + num_inference_steps: int = 25, + guidance_scale: float = 2.0, + seed: int = -1, + output_dir: Optional[str] = None, +) -> dict: + """ + Generate virtual try-on using Segmind. + + Args: + model_image: Path or URL to person image + cloth_image: Path or URL to garment image + category: "Upper body", "Lower body", or "Dress" + num_inference_steps: Number of denoising steps (20-100) + guidance_scale: Classifier-free guidance scale (1-25) + seed: Random seed (-1 for random) + output_dir: Directory to save results (optional) + + Returns: + Dictionary with status and result information + """ + try: + # Validate inputs + if not (validate_image_path(model_image) or validate_image_url(model_image)): + return { + "success": False, + "error": f"Invalid model image: {model_image}" + } + + if not (validate_image_path(cloth_image) or validate_image_url(cloth_image)): + return { + "success": False, + "error": f"Invalid cloth image: {cloth_image}" + } + + if category not in ["Upper body", "Lower body", "Dress"]: + return { + "success": False, + "error": f"Invalid category: {category}. Must be 'Upper body', 'Lower body', or 'Dress'" + } + + # Initialize adapter + adapter = SegmindVTONAdapter() + + # Generate virtual try-on + images = adapter.generate_and_decode( + model_image=model_image, + cloth_image=cloth_image, + category=category, + num_inference_steps=num_inference_steps, + guidance_scale=guidance_scale, + seed=seed, + ) + + # Save results + output_paths = [] + if output_dir: + output_path = Path(output_dir) + output_path.mkdir(parents=True, exist_ok=True) + for idx, image in enumerate(images): + save_path = save_image(image, output_path / f"segmind_result_{idx}.png") + output_paths.append(str(save_path)) + else: + # Save to temp directory + for idx, image in enumerate(images): + save_path = save_image(image, config.TEMP_DIR / f"segmind_result_{idx}.png") + output_paths.append(str(save_path)) + + return { + "success": True, + "provider": "segmind", + "num_images": len(images), + "output_paths": output_paths, + "message": f"Generated {len(images)} virtual try-on image(s)" + } + + except Exception as e: + return { + "success": False, + "error": str(e) + } + diff --git a/mcp-server/utils/__init__.py b/mcp-server/utils/__init__.py new file mode 100644 index 0000000..713858e --- /dev/null +++ b/mcp-server/utils/__init__.py @@ -0,0 +1,30 @@ +"""Utility functions for OpenTryOn MCP Server.""" + +from .image_utils import ( + validate_image_path, + validate_image_url, + load_image, + save_image, + encode_image_base64, + decode_image_base64, +) +from .validation import ( + validate_aspect_ratio, + validate_resolution, + validate_garment_class, + validate_category, +) + +__all__ = [ + "validate_image_path", + "validate_image_url", + "load_image", + "save_image", + "encode_image_base64", + "decode_image_base64", + "validate_aspect_ratio", + "validate_resolution", + "validate_garment_class", + "validate_category", +] + diff --git a/mcp-server/utils/image_utils.py b/mcp-server/utils/image_utils.py new file mode 100644 index 0000000..af0f872 --- /dev/null +++ b/mcp-server/utils/image_utils.py @@ -0,0 +1,138 @@ +"""Image handling utilities for OpenTryOn MCP Server.""" + +import base64 +import io +from pathlib import Path +from typing import Union +from urllib.parse import urlparse + +from PIL import Image +import requests + +from config import config + + +def validate_image_path(path: str) -> bool: + """Validate that a file path points to a valid image.""" + try: + p = Path(path) + if not p.exists(): + return False + if not p.is_file(): + return False + if p.suffix.lower() not in config.ALLOWED_IMAGE_EXTENSIONS: + return False + # Check file size + if p.stat().st_size > config.MAX_FILE_SIZE_MB * 1024 * 1024: + return False + return True + except Exception: + return False + + +def validate_image_url(url: str) -> bool: + """Validate that a URL points to a valid image.""" + try: + parsed = urlparse(url) + if not parsed.scheme in ("http", "https"): + return False + # Check if URL ends with image extension + path = parsed.path.lower() + return any(path.endswith(ext) for ext in config.ALLOWED_IMAGE_EXTENSIONS) + except Exception: + return False + + +def load_image(source: str) -> Image.Image: + """ + Load an image from a file path, URL, or base64 string. + + Args: + source: File path, URL, or base64-encoded image string + + Returns: + PIL Image object + + Raises: + ValueError: If the source is invalid or cannot be loaded + """ + # Try as file path first + if Path(source).exists(): + try: + return Image.open(source) + except Exception as e: + raise ValueError(f"Failed to load image from path: {e}") + + # Try as URL + if source.startswith(("http://", "https://")): + try: + response = requests.get(source, timeout=30) + response.raise_for_status() + return Image.open(io.BytesIO(response.content)) + except Exception as e: + raise ValueError(f"Failed to load image from URL: {e}") + + # Try as base64 + try: + # Remove data URL prefix if present + if "," in source: + source = source.split(",", 1)[1] + image_data = base64.b64decode(source) + return Image.open(io.BytesIO(image_data)) + except Exception as e: + raise ValueError(f"Failed to load image from base64: {e}") + + +def save_image(image: Image.Image, output_path: Union[str, Path]) -> Path: + """ + Save a PIL Image to a file. + + Args: + image: PIL Image object + output_path: Destination file path + + Returns: + Path to saved file + """ + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + image.save(output_path) + return output_path + + +def encode_image_base64(image: Union[Image.Image, str, Path]) -> str: + """ + Encode an image as base64 string. + + Args: + image: PIL Image object or path to image file + + Returns: + Base64-encoded image string + """ + if isinstance(image, (str, Path)): + image = Image.open(image) + + buffer = io.BytesIO() + image.save(buffer, format="PNG") + buffer.seek(0) + return base64.b64encode(buffer.read()).decode("utf-8") + + +def decode_image_base64(base64_string: str) -> Image.Image: + """ + Decode a base64 string to PIL Image. + + Args: + base64_string: Base64-encoded image string + + Returns: + PIL Image object + """ + # Remove data URL prefix if present + if "," in base64_string: + base64_string = base64_string.split(",", 1)[1] + + image_data = base64.b64decode(base64_string) + return Image.open(io.BytesIO(image_data)) + diff --git a/mcp-server/utils/validation.py b/mcp-server/utils/validation.py new file mode 100644 index 0000000..bff2257 --- /dev/null +++ b/mcp-server/utils/validation.py @@ -0,0 +1,111 @@ +"""Input validation utilities for OpenTryOn MCP Server.""" + +from typing import Literal + + +# Valid aspect ratios for different models +VALID_ASPECT_RATIOS = { + "1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", + "9:16", "16:9", "21:9", "9:21" +} + +# Valid resolutions +VALID_RESOLUTIONS = { + "1K", "2K", "4K", # Nano Banana Pro + "540p", "720p", "1080p", "4k" # Luma AI Video +} + +# Valid garment classes +VALID_GARMENT_CLASSES = { + "upper", "lower", "all", # Preprocessing + "UPPER_BODY", "LOWER_BODY", "FULL_BODY", "FOOTWEAR" # Amazon Nova +} + +# Valid categories +VALID_CATEGORIES = { + "Upper body", "Lower body", "Dress" # Segmind +} + +# Valid video durations +VALID_DURATIONS = {"5s", "9s", "10s"} + +# Valid Luma AI models +VALID_LUMA_MODELS = { + "photon-1", "photon-flash-1", # Image + "ray-1-6", "ray-2", "ray-flash-2" # Video +} + +# Valid FLUX models +VALID_FLUX_MODELS = {"flux2-pro", "flux2-flex"} + +# Valid image generation modes +VALID_IMAGE_MODES = { + "text_to_image", "edit", "compose", # Nano Banana + "img_ref", "style_ref", "char_ref", "modify" # Luma AI +} + +# Valid video generation modes +VALID_VIDEO_MODES = {"text_video", "image_video"} + + +def validate_aspect_ratio(aspect_ratio: str) -> bool: + """Validate aspect ratio string.""" + return aspect_ratio in VALID_ASPECT_RATIOS + + +def validate_resolution(resolution: str) -> bool: + """Validate resolution string.""" + return resolution in VALID_RESOLUTIONS + + +def validate_garment_class(garment_class: str) -> bool: + """Validate garment class string.""" + return garment_class in VALID_GARMENT_CLASSES + + +def validate_category(category: str) -> bool: + """Validate category string.""" + return category in VALID_CATEGORIES + + +def validate_duration(duration: str) -> bool: + """Validate video duration string.""" + return duration in VALID_DURATIONS + + +def validate_luma_model(model: str) -> bool: + """Validate Luma AI model name.""" + return model in VALID_LUMA_MODELS + + +def validate_flux_model(model: str) -> bool: + """Validate FLUX model name.""" + return model in VALID_FLUX_MODELS + + +def validate_image_mode(mode: str) -> bool: + """Validate image generation mode.""" + return mode in VALID_IMAGE_MODES + + +def validate_video_mode(mode: str) -> bool: + """Validate video generation mode.""" + return mode in VALID_VIDEO_MODES + + +def validate_range(value: float, min_val: float, max_val: float, name: str) -> None: + """ + Validate that a value is within a specified range. + + Args: + value: Value to validate + min_val: Minimum allowed value + max_val: Maximum allowed value + name: Name of the parameter (for error messages) + + Raises: + ValueError: If value is out of range + """ + if not min_val <= value <= max_val: + raise ValueError(f"{name} must be between {min_val} and {max_val}, got {value}") + diff --git a/setup.py b/setup.py index aec09a1..0245929 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ setup( name="opentryon", - version="0.1.0", + version="0.0.1", description="Open-source AI toolkit for fashion tech and virtual try-on", long_description=long_description, long_description_content_type='text/markdown',