Skill: zhy2015/hidream-model-gen (version 1.0.5)

--- name: hidream-model-gen description: Generate images and videos using Vivago AI (智小象) platform. Supports text-to-image, image-to-image, image-to-video, and keyframe-to-video generation. Use when the user wants to create AI-generated images or videos, transform existing images, or perform image style transfer through the Vivago AI API. dependencies: - requests - pillow requires: env: - HIDREAM_AUTHORIZATION --- # Vivago AI Skill Integration with Vivago AI (智小象) platform for AI-powered image and video generation. ## Supported Features ### Image Generation - **Text to Image** (`txt2img`): Generate images from text descriptions - **Image to Image** (`img2img`): Transform existing images based on prompts, including style transfer, image editing, and multi-image fusion ### Video Generation - **Text to Video** (`txt2vid`): Generate videos from text descriptions - **Image to Video** (`img2vid`): Generate videos from static images - **Keyframe to Video** (`keyframe_to_video`): Generate transition videos from start and end keyframes - **Video Templates** (`template_to_video`): 181 pre-defined video effects - Supports multiple model versions (v3Pro, v3L, kling-video-o1) ### Additional Features - Image upload to Vivago storage - Batch generation (up to 4 images) - Multiple aspect ratios (1:1, 4:3, 3:4, 16:9, 9:16) - Automatic retry with polling ## Architecture ### Core Modules ``` scripts/ ├── vivago_client.py # Main API client ├── template_manager.py # Template management ├── config_loader.py # Configuration loading ├── enums.py # Type enums (TaskStatus, AspectRatio, etc.) ├── exceptions.py # Structured exceptions └── config/ # Modular configuration files ``` ### Code Quality - **Type Safety**: Complete type annotations and enums - **Exception Handling**: Structured exception hierarchy - **CI/CD**: GitHub Actions for automated testing - **Modular Config**: Split configuration files for maintainability ## Setup ### Prerequisites Before using this skill, you need to obtain a Vivago.ai API Token: #### Step 1: Login to Vivago.ai 1. Visit [https://vivago.ai/](https://vivago.ai/) and log in to your account 2. Check your remaining credits and consider subscribing to a suitable plan if needed #### Step 2: Obtain Your Token 1. After logging in, visit [https://vivago.ai/prod-api/user/token](https://vivago.ai/prod-api/user/token) 2. The page will return your API Token (in JWT format) 3. Copy this Token for configuration > **Security Note**: The Token is your credential for accessing the API. Please keep it secure and do not share it with others. ### Environment Variables **Security Note:** For secure deployments and AI Agents, the system requires the token to be passed strictly via the `HIDREAM_AUTHORIZATION` environment variable. Export it securely in your current session: ```bash export HIDREAM_AUTHORIZATION="your_vivago_api_token" ``` > **Note:** `STORAGE_AK` and `STORAGE_SK` are deprecated and removed. The image upload uses secure pre-signed URLs provided by the Vivago API. ### File Output Configuration **Important:** By default, all generated resources (JSON results, downloaded images, and videos) will be output to the `assets/` directory within the current working folder. Ensure this directory exists or the system has permission to create it. ### Installation ```bash pip install -r requirements.txt ``` ## Usage ### Python API ```python from scripts import create_client, VivagoClient from scripts.enums import AspectRatio, PortName, TaskStatus from scripts.exceptions import TaskFailedError, TaskTimeoutError # Create client client = create_client() # Text to image results = client.text_to_image( prompt="a beautiful sunset over mountains", port=PortName.KLING_IMAGE, # or PortName.NANO_BANANA wh_ratio=AspectRatio.RATIO_16_9, batch_size=2 ) # Image to video (using local image) results = client.image_to_video( prompt="camera slowly zooming out", image_uuid=client.upload_image("/path/to/image.jpg"), port=PortName.V3PRO, wh_ratio=AspectRatio.RATIO_16_9, duration=5 ) # Keyframe to video (using start and end images) results = client.keyframe_to_video( prompt="smooth transition from start to end", start_image_uuid=client.upload_image("/path/to/start.jpg"), end_image_uuid=client.upload_image("/path/to/end.jpg"), port=PortName.V3PRO, wh_ratio=AspectRatio.RATIO_16_9, duration=5 ) # Video Templates - use pre-defined effects results = client.template_to_video( image_uuid=client.upload_image("/path/to/image.jpg"), template="ghibli", # See available templates below wh_ratio=AspectRatio.RATIO_9_16 ) ``` ### Error Handling ```python from scripts.exceptions import ( TaskFailedError, TaskRejectedError, TaskTimeoutError, InvalidPortError ) try: results = client.image_to_video(...) except TaskFailedError as e: print(f"Task failed: {e.task_id}") except TaskRejectedError as e: print(f"Content rejected: {e.reason}") except TaskTimeoutError as e: print(f"Timeout after {e.timeout_seconds}s") except InvalidPortError as e: print(f"Invalid port: {e.port}, available: {e.available}") ``` ### Command Line (Best for AI Agents) **For AI Agents:** The easiest way to use this skill is through the provided CLI scripts. They automatically handle API communication, polling, and result parsing. By default, they use **HiDream's native models**. **Text to Image:** ```bash python3 scripts/txt2img.py \ --prompt "a futuristic city" \ --wh-ratio 16:9 \ --batch-size 2 \ --output ./assets/results.json ``` *Note: This defaults to the `hidream-txt2img` model.* **Text to Video:** ```bash python3 scripts/txt2vid.py \ --prompt "a cybernetic dragon flying over a futuristic city" \ --wh-ratio 16:9 \ --duration 5 \ --output ./assets/video_results.json ``` *Note: This defaults to the `v3Pro` model.* **Image to Video:** ```bash python3 scripts/img2video.py \ --prompt "slow motion falling leaves" \ --image ./assets/source_image.jpg \ --duration 5 \ --output ./assets/video.json ``` ## API Reference ### Enums ```python from scripts.enums import ( TaskStatus, # PENDING, COMPLETED, PROCESSING, FAILED, REJECTED AspectRatio, # RATIO_1_1, RATIO_4_3, RATIO_16_9, etc. PortCategory, # TEXT_TO_IMAGE, IMAGE_TO_VIDEO, etc. PortName # KLING_IMAGE, V3PRO, NANO_BANANA, etc. ) ``` ### Models | Feature | Available Versions | Default | |---------|-------------------|---------| | Text to Image | v3L (HiDream), kling-image-o1 | **v3L** (via port `hidream-txt2img`) | | Image to Video | v3Pro, v3L, kling-video-o1 | **v3Pro** | | Keyframe to Video | v3Pro, v3L | **v3Pro** | > **Note for AI Agents:** By default, all CLI tools (`txt2img.py`, `txt2vid.py`) are pre-configured to use HiDream's native models (`hidream-txt2img` for images, `v3Pro` for videos). You don't need to specify the model unless explicitly requested by the user. ### Aspect Ratios - `1:1` - Square - `4:3` - Standard - `3:4` - Portrait - `16:9` - Widescreen - `9:16` - Mobile/Vertical ### Task Status Codes ```python from scripts.enums import TaskStatus TaskStatus.PENDING # 0 - Pending TaskStatus.COMPLETED # 1 - Completed TaskStatus.PROCESSING # 2 - Processing TaskStatus.FAILED # 3 - Failed TaskStatus.REJECTED # 4 - Rejected (content review) ``` ## File Structure ``` vivago-ai-skill/ ├── scripts/ │ ├── __init__.py # Package exports │ ├── vivago_client.py # Core API client │ ├── template_manager.py # Template management │ ├── config_loader.py # Configuration loader │ ├── enums.py # Type enums │ ├── exceptions.py # Exception classes │ ├── logging_config.py # Logging configuration │ └── config/ # Modular config files │ ├── base.json │ ├── text_to_image.json │ ├── image_to_video.json │ └── ... ├── tests/ │ ├── conftest.py # Pytest configuration │ ├── archive/ # Archived tests │ └── ... ├── docs/ # Documentation ├── .github/workflows/ # CI configuration ├── requirements.txt ├── README.md └── SKILL.md # This file ``` ## Important Notes ### Feishu Channel Messaging Guidelines When sending generated content through Feishu (飞书) channel: | Content Type | Send Method | Example | |-------------|-------------|---------| | **Images** | ✅ Direct file upload | Attach image file directly | | **Videos** | ❌ **Must send as link** | `https://media.vivago.ai/{video_uuid}` | **⚠️ Critical**: Videos **CANNOT** be sent as file attachments in Feishu. Always construct and send the direct media URL: ``` https://media.vivago.ai/b1268f08-ac32-4b83-863f-a419797d768e.mp4 ``` **Why**: Feishu does not support playable video attachments. Sending video files directly will result in delivery failure or unplayable content. ### Image Download Images can be downloaded using the correct URL format: ``` https://storage.vivago.ai/image/{image_name}.jpg ``` **Example:** ```python from scripts import create_client import requests client = create_client() # Generate image results = client.text_to_image(prompt="a cute cat") image_name = results[0].get('image', '') # Download image image_url = f"https://storage.vivago.ai/image/{image_name}.jpg" response = requests.get(image_url) with open("output.jpg", "wb") as f: f.write(response.content) ``` **Sending via Feishu:** ```python # Download and send through Feishu image_data = requests.get(image_url).content # Then send image_data as file attachment via Feishu API ``` ### Asynchronous Processing - API calls are asynchronous with automatic polling - Images are automatically resized to max 1024px on longest side before upload - Video generation supports 5 or 10 second durations - Batch size for images: 1-4, for videos: 1 - All API calls include automatic retry logic ## Error Handling The client handles common errors: - Network timeouts (with retry) - Rate limiting (with exponential backoff) - Invalid parameters (validation before API call) - Task failures (structured exceptions) ### Exception Hierarchy ``` VivagoError (base) ├── VivagoAPIError ├── MissingCredentialError ├── InvalidPortError ├── ImageUploadError ├── TemplateNotFoundError └── TaskError ├── TaskFailedError ├── TaskRejectedError └── TaskTimeoutError ``` ## Video Templates Reference The following **181 video templates** are available via `template_to_video()`: ### Quick Categories | Category | Count | Example Templates | |----------|-------|-------------------| | **Style Transfer** | 20+ | ghibli, 1930s-2000s vintage styles | | **Harry Potter** | 4 | magic_reveal_ravenclaw, gryffindor, hufflepuff, slytherin | | **Wings/Fantasy** | 10+ | angel_wings, phoenix_wings, crystal_wings, fire_wings | | **Superheroes** | 5+ | iron_man, cat_woman, ghost_rider | | **Dance** | 10+ | apt, dadada, dance, limbo_dance | | **Effects** | 15+ | ash_out, metallic_liquid, flash_flood | | **Thanksgiving** | 10+ | turkey_chasing, autumn_feast, gratitude_photo | | **Comics/Cartoon** | 8+ | gta_star, anime_figure, bring_comics_to_life | | **Products** | 8+ | glasses_display, music_box, food_product_display | | **Scenes** | 20+ | romantic_kiss, graduation, starship_chef | ### Popular Templates | Template ID | Description | |-------------|-------------| | `ghibli` / `ghibli2` | Studio Ghibli animation style | | `magic_reveal_ravenclaw` | Harry Potter Ravenclaw transformation | | `magic_reveal_gryffindor` | Harry Potter Gryffindor transformation | | `magic_reveal_hufflepuff` | Harry Potter Hufflepuff transformation | | `magic_reveal_slytherin` | Harry Potter Slytherin transformation | | `iron_man` | Iron Man armor assembly | | `angel_wings` / `phoenix_wings` / `crystal_wings` / `fire_wings` | Wing transformations | | `cat_woman` | Cat Woman style | | `ghost_rider` | Ghost Rider flaming skull | | `joker` | Joker villain style | | `mermaid` | Mermaid underwater scene | | `snow_white` | Snow White princess | | `barbie` | Barbie princess transformation | | `me_in_hand` | Miniature figure in hand | | `music_box` | Rotating figure on music box | | `anime_figure` | Transform into anime figure | | `gta_star` | GTA game style transformation | | `apt` / `dadada` / `dance` | Dance templates | | `ash_out` | Disintegrate into ashes | | `eye_of_the_storm` | Thunder god awakening | | `metallic_liquid` | Metal mask transformation | | `flash_flood` | Water/flood effect | | `turkey_chasing` / `turkey_away` / `turkey_giant` | Thanksgiving turkey scenes | | `autumn_feast` / `autumn_stroll` | Autumn scenes | | `renovation_of_old_photos` | Colorize B&W photos | | `graduation` | Graduation ceremony | | `glasses` / `glasses_display` | Glasses/eyewear showcase | | `bikini` / `sexy_man` / `sexy_pants` | Fashion/beach | | `romantic_kiss` / `boyfriends_rose` / `girlfriends_rose` | Romantic scenes | | `ai_archaeologist` / `starship_chef` / `cyber_cooker` | Sci-fi characters | | `jungle_reign` / `panther_queen` / `roar_of_the_dustlands` / `tiger_snuggle` | Animal companions | | `instant_sadness` / `headphone_vibe` / `relax` | Emotion/reaction | | `frost_alert` | Cold/freeze effect | | `bald_me` | Bald transformation | | `boom_hair` / `curl_pop` / `long_hair` | Hair transformations | | `muscles` | Muscle transformation | | `face_punch` / `gun_point` | Action effects | | `static_shot` / `tracking_shot` / `orbit_shot` / `push_in` / `zoom_out` / `handheld_shot` | Camera movements | | `earth_zoom_in` / `earth_zoom_out` | Earth zoom effects | ### View All Templates ```python from scripts.template_manager import get_template_manager manager = get_template_manager() templates = manager.list_templates() print(f"Total templates: {len(templates)}") for tid, name in sorted(templates.items()): print(f" {tid}: {name}") ``` ### Usage Example ```python from scripts import create_client client = create_client() # Upload image image_uuid = client.upload_image("/path/to/photo.jpg") # Apply Ghibli style template results = client.template_to_video( image_uuid=image_uuid, template="ghibli", wh_ratio="9:16" ) # Harry Potter transformation results = client.template_to_video( image_uuid=image_uuid, template="magic_reveal_ravenclaw", wh_ratio="9:16" ) ``` ## Changelog ### v0.9.0 (2026-03-09) - ✅ Code review complete (P0-P3) - ✅ Added GitHub Actions CI - ✅ Added type safety module (enums.py) - ✅ Added structured exceptions (exceptions.py) - ✅ Split configuration into modular files - ✅ Archived redundant code and tests - ✅ Pinned dependency versions ### v0.8.2 (2026-03-08) - ✅ Template testing: 44 templates, 40 passed (90.9%) - ✅ Fixed metallic_liquid naming issue - ✅ Marked long_hair as deprecated ### v0.8.0 (2026-03-07) - ✅ Completed Tier 1-4 testing - ✅ Established smart test optimization system

path: requirements.txt
content: # Core dependencies
requests>=2.31.0
Pillow>=10.0.0  # Replaces opencv-python for lighter image processing
tqdm>=4.66.0    # For progress bars

# Configuration
python-dotenv>=1.0.0

# Testing
pytest>=7.4.4
pytest-cov>=4.1.0
responses>=0.24.1

# Code quality
black>=23.12.1
flake8>=7.0.0
mypy>=1.8.0

# Type stubs for better IDE support
types-requests>=2.31.0


path: scripts/image_processor.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Image Processor Module
Handles image resizing and compression using Pillow instead of OpenCV.
"""

import os
import io
import logging
from typing import Tuple, Optional
from PIL import Image

logger = logging.getLogger(__name__)

class ImageProcessor:
    """Handles image processing operations (resize, compress)"""
    
    @staticmethod
    def process_for_upload(
        image_path: str, 
        max_side: int = 1024, 
        quality: int = 80
    ) -> bytes:
        """
        Resize and compress image for upload.
        
        Args:
            image_path: Path to the source image
            max_side: Maximum dimension for width or height
            quality: JPEG compression quality (1-100)
            
        Returns:
            bytes: Processed image data in JPEG format
            
        Raises:
            FileNotFoundError: If image file does not exist
            IOError: If image cannot be opened or processed
        """
        if not os.path.exists(image_path):
            raise FileNotFoundError(f"Image not found: {image_path}")
            
        try:
            with Image.open(image_path) as img:
                # Convert to RGB if necessary (e.g. for PNG with transparency)
                if img.mode in ('RGBA', 'P'):
                    img = img.convert('RGB')
                
                # Calculate new dimensions
                width, height = img.size
                scale = 1.0
                
                if width > height:
                    if width > max_side:
                        scale = max_side / width
                else:
                    if height > max_side:
                        scale = max_side / height
                
                if scale < 1.0:
                    new_width = int(width * scale)
                    new_height = int(height * scale)
                    img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
                    logger.info(f"Resized image from {width}x{height} to {new_width}x{new_height}")
                
                # Compress to JPEG
                output = io.BytesIO()
                img.save(output, format='JPEG', quality=quality)
                return output.getvalue()
                
        except Exception as e:
            logger.error(f"Failed to process image {image_path}: {e}")
            raise IOError(f"Image processing failed: {e}")

    @staticmethod
    def get_image_ratio(image_path: str) -> float:
        """
        Get image aspect ratio (width / height).
        
        Args:
            image_path: Path to the image file
            
        Returns:
            float: Aspect ratio
        """
        try:
            with Image.open(image_path) as img:
                width, height = img.size
                return float(width) / float(height)
        except Exception as e:
            logger.error(f"Failed to get image ratio: {e}")
            return 1.0


path: scripts/vivago_client.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Vivago AI API Client - v0.3.1
支持层级架构：一级功能 -> 二级端口
"""

import json
import os
import time
import uuid
import logging
import warnings
import tempfile
from typing import Optional, Dict, Any, List, Tuple
import requests

from .enums import TaskStatus, AspectRatio, PortCategory, PortName, ModuleName
from .exceptions import (
    VivagoAPIError, InvalidPortError, MissingCredentialError,
    TaskFailedError, TaskRejectedError, TaskTimeoutError, ImageUploadError
)
from .config_loader import load_ports_config
from .image_processor import ImageProcessor

logger = logging.getLogger(__name__)

# 视频模板支持的宽高比
SUPPORTED_RATIOS = ["16:9", "1:1", "9:16", "4:3", "3:4"]

def parse_ratio(ratio_str: str) -> float:
    """解析宽高比字符串，返回宽/高的数值"""
    try:
        w, h = ratio_str.split(":")
        return float(w) / float(h)
    except:
        return 1.0

def find_closest_ratio(image_ratio: float) -> str:
    """找到与图片比例最接近的支持的宽高比"""
    min_diff = float('inf')
    closest = "1:1"
    
    for ratio in SUPPORTED_RATIOS:
        r = parse_ratio(ratio)
        diff = abs(image_ratio - r)
        if diff < min_diff:
            min_diff = diff
            closest = ratio
    
    return closest


class VivagoClient:
    """
    Vivago AI API Client
    
    层级架构：
    - 一级功能：text_to_image, image_to_video, image_to_image 等
    - 二级端口：kling-image, v3Pro, img2img 等具体API端点
    """
    
    def __init__(
        self, 
        token: str, 
        ports_config_path: Optional[str] = None
    ):
        """
        Initialize Vivago client.
        
        Args:
            [REDACTED_SECRET] API Bearer token
            ports_config_path: Path to api_ports.json (optional)
        """
        self.token = token
        self.headers = {
            "Authorization": f"Bearer {token}",
            "X-accept-language": "en",
        }
        
        # Load ports configuration
        self.ports_config = self._load_ports_config(ports_config_path)
        self.base_url = self.ports_config.get("base_url", "https://vivago.ai/api/gw")
    
    
    def _load_ports_config(self, config_path: Optional[str] = None) -> Dict:
        """Load API ports configuration
        
        优先使用新的分体式配置加载器，支持 config/ 目录或回退到 api_ports.json
        """
        try:
            # 使用新的配置加载器
            if config_path:
                # 如果指定了路径，使用旧的方式加载
                with open(config_path, 'r', encoding='utf-8') as f:
                    return json.load(f)
            else:
                # 使用新的分体式配置加载
                return load_ports_config()
        except Exception as e:
            logger.warning(f"Failed to load ports config: {e}, using defaults")
            return self._default_ports_config()
    
    def _default_ports_config(self) -> Dict:
        """Default ports configuration"""
        return {
            "base_url": "https://vivago.ai/api/gw",
            "categories": {
                "text_to_image": {
                    "default_port": "kling-image",
                    "ports": {
                        "kling-image": {
                            "endpoint": "/v3/image/image_gen_kling/async",
                            "result_endpoint": "/v3/image/txt2img/async/results",
                            "version": "kling-image-o1"
                        }
                    }
                },
                "image_to_video": {
                    "default_port": "v3Pro",
                    "ports": {
                        "v3Pro": {
                            "endpoint": "/v3/video/video_diffusion_img2vid/async",
                            "result_endpoint": "/v3/video/video_diffusion/async/results",
                            "version": "v3Pro"
                        }
                    }
                }
            }
        }
    
    def _get_port_config(self, category: str, port: Optional[str] = None) -> Tuple[Dict, str]:
        """
        Get port configuration for a category.
        
        Args:
            category: 一级功能名称，如 "text_to_image"
            port: 二级端口名称，如 "kling-image" (None则使用默认)
            
        Returns:
            (port_config, port_name)
        """
        cat_config = self.ports_config.get("categories", {}).get(category)
        if not cat_config:
            raise InvalidPortError(f"Unknown category: {category}")
        
        ports = cat_config.get("ports", {})
        
        if port is None:
            port = cat_config.get("default_port")
            if not port:
                raise InvalidPortError(f"No default port for category: {category}")
        
        if port not in ports:
            available = list(ports.keys())
            raise InvalidPortError(port, category, available)
        
        return ports[port], port
    
    def list_categories(self) -> Dict[str, Dict[str, Any]]:
        """List all available categories (一级功能)"""
        categories = {}
        for cat_id, config in self.ports_config.get("categories", {}).items():
            categories[cat_id] = {
                "name": config.get("name", cat_id),
                "name_en": config.get("name_en", cat_id),
                "status": config.get("status", "unknown"),
                "default_port": config.get("default_port"),
                "description": config.get("description", "")
            }
        return categories
    
    def list_ports(self, category: str) -> Dict[str, Any]:
        """List all available ports for a category (二级端口)"""
        cat_config = self.ports_config.get("categories", {}).get(category)
        if not cat_config:
            raise InvalidPortError(f"Unknown category: {category}")
        
        ports = {}
        for port_id, config in cat_config.get("ports", {}).items():
            ports[port_id] = {
                "name": config.get("name", port_id),
                "display_name": config.get("display_name", port_id),
                "status": config.get("status", "unknown"),
                "tested": config.get("tested", False),
                "speed": config.get("speed", "unknown"),
                "quality": config.get("quality", "unknown"),
                "notes": config.get("notes", "")
            }
        return ports
    
    # ==================== File Upload ====================
    
    def upload_image(self, image_path: str, max_side: int = 1024, quality: int = 80) -> str:
        """
        上传图片到 Vivago 存储 (默认使用 v2 方式)
        
        这是新的默认实现，使用预签名 URL 方式上传。
        旧版 S3 上传方式仍可通过 upload_image_legacy 访问，但已标记为废弃。
        
        Args:
            image_path: 图片文件路径
            max_side: 图片最大边长，超过则缩放 (默认1024)
            quality: JPEG 压缩质量 (默认80)
            
        Returns:
            image_uuid: 上传后的图片 UUID (格式: j_xxxx)
            
        Raises:
            ImageUploadError: 上传失败
        """
        return self.upload_image_v2(image_path, max_side, quality)
    
    def upload_image_v2(self, image_path: str, max_side: int = 1024, quality: int = 80) -> str:
        """
        上传图片到 Vivago 存储 (v2 - 新方式，使用预签名 URL)
        
        新流程:
        1. GET /prod-api/user/google_key/hidreamai-image → 获取预签名 URL
        2. PUT 预签名 URL + 图片二进制数据 → 完成上传
        
        Args:
            image_path: 图片文件路径
            max_side: 图片最大边长，超过则缩放 (默认1024)
            quality: JPEG 压缩质量 (默认80)
            
        Returns:
            image_uuid: 上传后的图片 UUID (格式: j_xxxx)
            
        Raises:
            ImageUploadError: 上传失败
        """
        # 生成唯一的图片 UUID
        image_uuid = f"j_{uuid.uuid4()}"
        logger.info(f"Uploading image {image_path} -> {image_uuid} (v2)")
        
        # 步骤1: 处理图片
        try:
            image_data = ImageProcessor.process_for_upload(image_path, max_side, quality)
        except Exception as e:
            raise ImageUploadError(image_path, str(e))
        
        # 步骤2: 获取预签名上传 URL
        base_url = "https://vivago.ai"
        endpoint = "/prod-api/user/google_key/hidreamai-image"
        
        headers = {
            "Authorization": f"Bearer {self.token}",
            "Content-Type": "application/json"
        }
        
        params = {
            "filename": image_uuid,
            "content_type": "image/jpeg"
        }
        
        try:
            response = requests.get(
                f"{base_url}{endpoint}",
                headers=headers,
                params=params,
                timeout=30
            )
            response.raise_for_status()
            
            result = response.json()
            if result.get('code') != 0:
                raise ImageUploadError(
                    image_path, 
                    f"Failed to get upload URL: {result.get('message')}"
                )
            
            presigned_url = result.get('result')
            if not presigned_url:
                raise ImageUploadError(image_path, "No presigned URL in response")
            
            logger.info(f"Got presigned URL (length: {len(presigned_url)})")
            
        except requests.RequestException as e:
            raise ImageUploadError(image_path, f"Failed to get upload URL: {e}")
        
        # 步骤3: 使用预签名 URL 上传图片
        try:
            upload_response = requests.put(
                presigned_url,
                data=image_data,
                headers={"Content-Type": "image/jpeg"},
                timeout=60
            )
            upload_response.raise_for_status()
            
            logger.info(f"Image uploaded successfully: {image_uuid}")
            return image_uuid
            
        except requests.RequestException as e:
            raise ImageUploadError(image_path, f"Failed to upload image: {e}")
    
    def preprocess_image_for_template(self, image_path: str, target_ratio: str = None) -> Tuple[str, str]:
        """
        为视频模板预处理图片
        
        1. 检测图片实际比例
        2. 如果图片比例在支持列表中，直接使用
        3. 如果不在，裁剪到最近的支持比例
        4. 上传处理后的图片 (使用新的 v2 上传方式)
        
        Args:
            image_path: 原始图片路径
            target_ratio: 指定的目标比例（可选），如果为None则自动检测
            
        Returns:
            (image_uuid, actual_wh_ratio): 上传后的图片UUID和实际使用的宽高比
        """
        # 读取图片比例
        actual_ratio = ImageProcessor.get_image_ratio(image_path)
        
        # 如果指定了目标比例，使用它；否则找到最接近的支持比例
        if target_ratio and target_ratio in SUPPORTED_RATIOS:
            closest_ratio = target_ratio
        else:
            closest_ratio = find_closest_ratio(actual_ratio)
        
        # 由于 ImageProcessor 目前不支持复杂裁剪，这里简化处理：
        # 直接上传并让后端/模板处理，或者后续在 ImageProcessor 中添加裁剪逻辑
        # 目前直接调用 upload_image_v2，它会进行缩放但不会裁剪
        
        # TODO: 在 ImageProcessor 中实现 crop_to_ratio
        
        image_uuid = self.upload_image_v2(image_path)
        logger.info(f"Uploaded image {image_path} -> {image_uuid} (ratio: {closest_ratio})")
            
        return image_uuid, closest_ratio
    
    # ==================== Core API Call ====================
    
    # 默认超时配置：30分钟（1800秒），防止重复计费
    DEFAULT_MAX_RETRIES = 360  # 30分钟 / 5秒 = 360次
    DEFAULT_RETRY_DELAY = 5    # 5秒轮询间隔
    
    def call_api(
        self,
        endpoint: str,
        data: Dict[str, Any],
        result_endpoint: str,
        max_retries: int = None,
        retry_delay: int = None
    ) -> Optional[List[Dict]]:
        # 使用默认超时配置
        if max_retries is None:
            max_retries = self.DEFAULT_MAX_RETRIES
        if retry_delay is None:
            retry_delay = self.DEFAULT_RETRY_DELAY
        """
        Generic API call with async task submission and polling.
        
        Args:
            endpoint: API submission endpoint
            data: Request body
            result_endpoint: Result polling endpoint
            max_retries: Maximum polling attempts
            retry_delay: Delay between polls (seconds)
        """
        try:
            url = f"{self.base_url}{endpoint}"
            headers_post = {**self.headers, "Content-Type": "application/json"}
            
            response = requests.post(url, json=data, headers=headers_post, timeout=1800)  # 30分钟
            
            if response.status_code != 200:
                logger.error(f"API error: {response.status_code} - {response.text}")
                return None
            
            result = response.json()
            
            if result.get("code") != 0:
                logger.error(f"API error: {result.get('message')}")
                return result
            
            task_id = result.get("result", {}).get("task_id")
            if not task_id:
                logger.error("No task_id in response")
                return None
            
            logger.info(f"Task submitted: {task_id}")
            
            return self._poll_results(task_id, result_endpoint, max_retries, retry_delay)
            
        except Exception as e:
            logger.error(f"API call error: {e}")
            return None
    
    def _poll_results(
        self,
        task_id: str,
        result_endpoint: str,
        max_retries: int = None,
        retry_delay: int = None
    ) -> Optional[List[Dict]]:
        """Poll for task completion"""
        # 使用默认超时配置
        if max_retries is None:
            max_retries = self.DEFAULT_MAX_RETRIES
        if retry_delay is None:
            retry_delay = self.DEFAULT_RETRY_DELAY
        
        url = f"{self.base_url}{result_endpoint}?task_id={task_id}"
        headers_get = {"Authorization": self.headers["Authorization"]}
        
        for attempt in range(max_retries):
            try:
                response = requests.get(url, headers=headers_get, timeout=1800)  # 30分钟
                
                if response.status_code != 200:
                    logger.warning(f"Poll failed: {response.status_code}")
                    time.sleep(retry_delay)
                    continue
                
                result = response.json()
                
                if result.get("code") != 0:
                    logger.error(f"API error: {result.get('message')}")
                    return None
                
                sub_results = result.get("result", {}).get("sub_task_results", [])
                
                # 使用TaskStatus枚举代替魔法数字
                if sub_results and all(
                    r.get("task_status") in {
                        TaskStatus.COMPLETED.value,
                        TaskStatus.FAILED.value,
                        TaskStatus.REJECTED.value
                    } for r in sub_results
                ):
                    status = sub_results[0].get("task_status")
                    if status == TaskStatus.COMPLETED.value:
                        logger.info(f"Task completed: {task_id}")
                    elif status == TaskStatus.FAILED.value:
                        logger.warning(f"Task failed: {task_id}")
                        raise TaskFailedError(task_id)
                    elif status == TaskStatus.REJECTED.value:
                        logger.warning(f"Task rejected: {task_id}")
                        raise TaskRejectedError(task_id)
                    return sub_results
                
                logger.debug(f"Task {task_id} processing (attempt {attempt + 1}/{max_retries})")
                time.sleep(retry_delay)
                
            except Exception as e:
                logger.warning(f"Poll error: {e}")
                time.sleep(retry_delay)
        
        logger.error(f"Task timeout after {max_retries * retry_delay}s: {task_id}")
        raise TaskTimeoutError(task_id, max_retries * retry_delay)
    
    # ==================== Text to Image ====================
    
    def text_to_image(
        self,
        prompt: str,
        port: Optional[str] = None,
        negative_prompt: str = "",
        wh_ratio: str = "16:9",
        batch_size: int = 1,
        **kwargs
    ) -> Optional[List[Dict]]:
        """
        文生图 - 支持多端口选择
        
        Args:
            prompt: 提示词
            port: 二级端口 (None=使用默认)
            negative_prompt: 负面提示词
            wh_ratio: 宽高比
            batch_size: 生成数量 (1-4)
            **kwargs: 额外参数
        """
        port_config, port_name = self._get_port_config("text_to_image", port)
        
        # 根据端点推断 module 名称
        endpoint = port_config["endpoint"]
        if "image_gen_kling" in endpoint:
            module = "image_gen_kling"
        elif "image_gen_std" in endpoint:
            module = "image_gen_std"
        else:
            module = "txt2img"
        
        display_name = port_config.get("display_name", port_name)
        
        # 构建参数
        params = {
            "batch_count": 1,
            "batch_size": batch_size,
            "guidance_scale": kwargs.get("guidance_scale", 7.5),
            "height": kwargs.get("height", 512),
            "image_guidance_scale": kwargs.get("image_guidance_scale", 1.5),
            "sample_steps": kwargs.get("sample_steps", 40),
            "sampler": kwargs.get("sampler", "Euler a"),
            "seed": kwargs.get("seed", -1),
            "strength": kwargs.get("strength", 0.8),
            "style": kwargs.get("style", "default"),
            "wh_ratio": wh_ratio,
            "width": kwargs.get("width", 512),
            "relevance": kwargs.get("relevance", []),
            "custom_params": {
                "wh_ratio": wh_ratio
            }
        }
        
        # Nano Banana 2 特殊参数
        is_nano_banana = "image_gen_std" in endpoint
        if is_nano_banana:
            params["mode"] = kwargs.get("mode", "2K")
        
        data = {
            "app": None,
            "image": None,
            "mask": None,
            "module": module,
            "negative_prompt": negative_prompt,
            "prompt": prompt,
            "params": params,
            "role": kwargs.get("role", "general"),
            "images": [],
            "magic_prompt": kwargs.get("magic_prompt", prompt if is_nano_banana else ""),
            "audios": [],
            "videos": [],
            "request_id": str(uuid.uuid4())
        }
        
        # 设置 version 参数 (Kling 需要，Nano Banana 不需要，HiDream 也需要)
        if not is_nano_banana:
            # 如果 kwargs 中提供了 version，优先使用；否则使用配置中的默认值
            data["version"] = kwargs.get("version") or port_config.get("version", "kling-image-o1")
        
        logger.info(f"Using port: {port_name} ({display_name})")
        
        # 使用默认30分钟超时配置（防止重复计费）
        return self.call_api(
            endpoint=port_config["endpoint"],
            data=data,
            result_endpoint=port_config["result_endpoint"]
        )
    
    # ==================== Image to Video ====================
    
    def image_to_video(
        self,
        prompt: str,
        image_uuid: str,
        port: Optional[str] = None,
        wh_ratio: str = "16:9",
        duration: int = 5,
        mode: str = "Slow",
        fast_mode: bool = False,
        **kwargs
    ) -> Optional[List[Dict]]:
        """
        图生视频 - 支持多端口选择
        
        ⚠️ 注意：视频生成需要 2-3 分钟，请谨慎调用
        
        Args:
            prompt: 视频动作描述
            image_uuid: 参考图片UUID
            port: 二级端口 (v3Pro/v3L/kling-video, None=使用默认)
            wh_ratio: 宽高比
            duration: 视频时长 (5或10秒)
            mode: 生成模式 (Slow/Fast)
            fast_mode: 快速模式
            **kwargs: 额外参数
        """
        port_config, port_name = self._get_port_config("image_to_video", port)
        
        display_name = port_config.get("display_name", port_name)
        
        data = {
            "image": None,
            "module": "video_diffusion_img2vid",
            "params": {
                "batch_size": 1,
                "guidance_scale": kwargs.get("guidance_scale", 7),
                "sample_steps": kwargs.get("sample_steps", 80),
                "width": kwargs.get("width", 1920),
                "height": kwargs.get("height", 1080),
                "fast_mode": fast_mode,
                "frame_num": kwargs.get("frame_num", 16),
                "seed": kwargs.get("seed", -1),
                "motion_strength": kwargs.get("motion_strength", 9),
                "max_width": kwargs.get("max_width", 1024),
                "wh_ratio": "keep",
                "cm_x": kwargs.get("cm_x", 0),
                "cm_y": kwargs.get("cm_y", 0),
                "cm_d": kwargs.get("cm_d", 0),
                "custom_params": {
                    "wh_ratio": wh_ratio
                },
                "mode": mode,
                "duration": duration,
                "x": kwargs.get("x", 0),
                "y": kwargs.get("y", 0),
                "z": kwargs.get("z", 0),
                "style": kwargs.get("style", "default")
            },
            "prompt": prompt,
            "negative_prompt": kwargs.get("negative_prompt", ""),
            "role": kwargs.get("role", "general"),
            "style": kwargs.get("style", "default"),
            "wh_ratio": wh_ratio,
            "version": port_config.get("version", "v3Pro"),
            "magic_prompt": kwargs.get("magic_prompt", ""),
            "images": [image_uuid],
            "videos": [],
            "audios": [],
            "request_id": str(uuid.uuid4())
        }
        
        logger.info(f"Using port: {port_name} ({display_name}) ⚠️ 2-3 minutes")
        
        # 使用默认30分钟超时配置（防止重复计费）
        return self.call_api(
            endpoint=port_config["endpoint"],
            data=data,
            result_endpoint=port_config["result_endpoint"]
        )
    
    # ==================== Text to Video ====================
    
    def text_to_video(
        self,
        prompt: str,
        port: Optional[str] = None,
        wh_ratio: str = "16:9",
        duration: int = 5,
        mode: str = "Slow",
        fast_mode: bool = False,
        **kwargs
    ) -> Optional[List[Dict]]:
        """
        文生视频 - 从文本描述生成视频
        
        ⚠️ 注意：视频生成需要 2-3 分钟，请谨慎调用
        
        Args:
            prompt: 视频内容描述
            port: 二级端口 (v3Pro/v3L/kling-video, None=使用默认)
            wh_ratio: 宽高比 (1:1, 4:3, 3:4, 16:9, 9:16)
            duration: 视频时长 (5或10秒)
            mode: 生成模式 (Slow/Fast)
            fast_mode: 快速模式
            **kwargs: 额外参数
            
        Returns:
            List of generated video results
        """
        port_config, port_name = self._get_port_config("text_to_video", port)
        
        display_name = port_config.get("display_name", port_name)
        
        # 根据 endpoint 确定 module
        endpoint = port_config["endpoint"]
        if "gen2vid" in endpoint:
            module = "video_diffusion_gen2vid"
        else:
            module = "video_diffusion"
        
        data = {
            "image": None,
            "module": module,
            "params": {
                "batch_size": 1,
                "guidance_scale": kwargs.get("guidance_scale", 7),
                "sample_steps": kwargs.get("sample_steps", 80),
                "width": kwargs.get("width", 1920),
                "height": kwargs.get("height", 1080),
                "fast_mode": fast_mode,
                "frame_num": kwargs.get("frame_num", 16),
                "seed": kwargs.get("seed", -1),
                "motion_strength": kwargs.get("motion_strength", 9),
                "max_width": kwargs.get("max_width", 1024),
                "wh_ratio": wh_ratio,
                "cm_x": kwargs.get("cm_x", 0),
                "cm_y": kwargs.get("cm_y", 0),
                "cm_d": kwargs.get("cm_d", 0),
                "custom_params": {},
                "mode": mode,
                "duration": duration,
                "x": kwargs.get("x", 0),
                "y": kwargs.get("y", 0),
                "z": kwargs.get("z", 0),
                "style": kwargs.get("style", "default")
            },
            "prompt": prompt,
            "negative_prompt": kwargs.get("negative_prompt", ""),
            "role": kwargs.get("role", "general"),
            "style": kwargs.get("style", "default"),
            "wh_ratio": wh_ratio,
            "version": port_config.get("version", "v3Pro"),
            "magic_prompt": kwargs.get("magic_prompt", ""),
            "images": [],
            "videos": [],
            "audios": [],
            "request_id": str(uuid.uuid4())
        }
        
        logger.info(f"Using port: {port_name} ({display_name}) ⚠️ 2-3 minutes")
        
        # 使用默认30分钟超时配置（防止重复计费）
        return self.call_api(
            endpoint=port_config["endpoint"],
            data=data,
            result_endpoint=port_config["result_endpoint"]
        )
    
    # ==================== Keyframe to Video ====================
    
    def keyframe_to_video(
        self,
        prompt: str,
        start_image_uuid: str,
        end_image_uuid: str,
        port: Optional[str] = None,
        wh_ratio: str = "keep",
        duration: int = 5,
        mode: str = "Fast",
        fast_mode: bool = True,
        **kwargs
    ) -> Optional[List[Dict]]:
        """
        视频首尾帧 - 根据首尾帧生成过渡视频
        
        使用首尾两张图片生成从首帧到尾帧的过渡视频
        
        Args:
            prompt: 视频内容描述
            start_image_uuid: 起始帧图片UUID
            end_image_uuid: 结束帧图片UUID
            port: 二级端口 (v3L/v3Pro, 默认 v3L)
            wh_ratio: 宽高比 (keep/1:1/4:3/3:4/16:9/9:16)
            duration: 视频时长 (5或10秒)
            mode: 生成模式 (Fast/Slow)
            fast_mode: 快速模式
            **kwargs: 额外参数
            
        Returns:
            List of generated video results
        """
        port_config, port_name = self._get_port_config("keyframe_to_video", port or "v3L")
        
        display_name = port_config.get("display_name", port_name)
        
        # 构建 custom_params
        custom_params = {"wh_ratio": wh_ratio}
        if wh_ratio != "keep":
            # 解析 wh_ratio 为 width:height
            try:
                w, h = wh_ratio.split(":")
                custom_params["wh_ratio"] = f"{w}:{h}"
            except:
                custom_params["wh_ratio"] = "16:9"
        
        data = {
            "image": None,
            "module": "video_diffusion_keyframes",
            "params": {
                "batch_size": 1,
                "guidance_scale": kwargs.get("guidance_scale", 7),
                "sample_steps": kwargs.get("sample_steps", 80),
                "width": kwargs.get("width", 1360),
                "height": kwargs.get("height", 768),
                "fast_mode": fast_mode,
                "frame_num": kwargs.get("frame_num", 16),
                "seed": kwargs.get("seed", -1),
                "motion_strength": kwargs.get("motion_strength", 9),
                "max_width": kwargs.get("max_width", 1024),
                "wh_ratio": wh_ratio,
                "cm_x": kwargs.get("cm_x", 0),
                "cm_y": kwargs.get("cm_y", 0),
                "cm_d": kwargs.get("cm_d", 0),
                "custom_params": custom_params,
                "mode": mode,
                "duration": duration,
                "x": kwargs.get("x", 0),
                "y": kwargs.get("y", 0),
                "z": kwargs.get("z", 0),
                "style": kwargs.get("style", "default")
            },
            "prompt": prompt,
            "negative_prompt": kwargs.get("negative_prompt", ""),
            "role": kwargs.get("role", "general"),
            "style": kwargs.get("style", "default"),
            "wh_ratio": wh_ratio if wh_ratio != "keep" else custom_params.get("wh_ratio", "16:9"),
            "version": port_config.get("version", "v3L"),
            "magic_prompt": kwargs.get("magic_prompt", ""),
            "images": [start_image_uuid, end_image_uuid],  # 首尾帧图片
            "videos": [],
            "upstream_id": end_image_uuid,  # 根据抓包，与第二张图片相同
            "audios": [],
            "request_id": str(uuid.uuid4())
        }
        
        logger.info(f"Using port: {port_name} ({display_name}) with 2 keyframes ⚠️ 2-3 minutes")
        
        # 使用默认30分钟超时配置（防止重复计费）
        return self.call_api(
            endpoint=port_config["endpoint"],
            data=data,
            result_endpoint=port_config["result_endpoint"]
        )
    
    # ==================== Template to Video ====================
    
    def template_to_video(
        self,
        image_input: str = None,
        template: str = "renovation_old_photos",
        wh_ratio: str = None,
        image_uuid: str = None,
        **kwargs
    ) -> Optional[List[Dict]]:
        """
        视频模板 - 特定类型视频特效
        
        使用预定义模板生成特效视频，支持动态加载模板配置
        
        Args:
            image_input: 输入图片路径或已上传的UUID (以 'j_' 开头的UUID)
            template: 模板名称 (renovation_old_photos, barbie, ash_out 等)
            wh_ratio: 宽高比 (16:9, 1:1, 9:16, 3:4, 4:3)，None则自动检测并裁剪
            image_uuid: 兼容参数，与 image_input 相同
            **kwargs: 额外参数
            
        Returns:
            List of generated video results
        """
        # 处理参数兼容性：支持 image_input 或 image_uuid
        if image_input is None and image_uuid is None:
            raise ValueError("必须提供 image_input 或 image_uuid 参数")
        
        # 优先使用 image_input，如果为空则使用 image_uuid
        actual_input = image_input if image_input is not None else image_uuid
        
        # 将 actual_input 赋值给 image_input 以便后续代码使用
        image_input = actual_input
        # 使用模板管理器获取配置
        from .template_manager import get_template_manager
        
        manager = get_template_manager()
        template_config = manager.get_template(template)
        
        if not template_config:
            # 回退到 api_ports.json 配置
            port_config, port_name = self._get_port_config("template_to_video", template)
            display_name = port_config.get("display_name", port_name)
            endpoint = port_config["endpoint"]
            result_endpoint = port_config["result_endpoint"]
            template_id = port_config.get("template_id")
            module = port_config.get("algo_type", "proto_transformer")
            version = port_config.get("version", "v1")
        else:
            # 使用模板管理器的配置
            display_name = template_config['name']
            endpoint = template_config['endpoint']
            result_endpoint = template_config['result_endpoint']
            template_id = template_config['template_id']
            module = template_config['module']
            version = template_config['version']
            port_name = template
        
        # 判断输入是本地图片路径还是已上传的UUID
        if image_input.startswith('j_') and len(image_input) < 50:
            # 是已上传的UUID
            input_uuid = image_input
            logger.info(f"Using existing image UUID: {input_uuid}")
        else:
            # 是本地图片路径，需要预处理和上传
            logger.info(f"Preprocessing image: {image_input}")
            input_uuid, detected_ratio = self.preprocess_image_for_template(image_input, wh_ratio)
            logger.info(f"Preprocessed with ratio: {detected_ratio}")
        
        # 检查模板是否有比例限制
        restricted_ratio = template_config.get('restricted_ratio', False) if template_config else False
        
        if restricted_ratio:
            # 强制使用 1:1 比例
            actual_wh_ratio = "1:1"
            logger.warning(f"Template '{template}' is restricted to 1:1 ratio only (API limitation)")
        else:
            # 使用指定比例或默认 1:1
            actual_wh_ratio = wh_ratio if wh_ratio else "1:1"
        
        # 如果需要不同比例，重新预处理图片
        if restricted_ratio and wh_ratio != "1:1":
            logger.info(f"Re-processing image to 1:1 ratio...")
            if not image_input.startswith('j_') or len(image_input) >= 50:
                # 是本地路径，重新处理
                input_uuid, _ = self.preprocess_image_for_template(image_input, "1:1")
            # 如果是UUID，需要重新上传（简化处理：提示用户）
        
        image_uuid = input_uuid
        
        # 构建请求数据
        try:
            data = manager.build_request_data(template, image_uuid, wh_ratio=actual_wh_ratio, **kwargs)
        except ValueError:
            # 如果模板管理器中没有，使用默认构建逻辑
            data = self._build_default_template_data(
                image_uuid, template, actual_wh_ratio, module, version, template_id, **kwargs
            )
        
        logger.info(f"Using template: {port_name} ({display_name}) with ratio {actual_wh_ratio} ⚠️ 2-3 minutes")
        
        # 使用默认30分钟超时配置（防止重复计费）
        return self.call_api(
            endpoint=endpoint,
            data=data,
            result_endpoint=result_endpoint
        )
    
    def _build_default_template_data(
        self, image_uuid: str, template: str, wh_ratio: str,
        module: str, version: str, template_id: str, **kwargs
    ) -> Dict[str, Any]:
        """构建默认的模板请求数据"""
        return {
            "module": module,
            "version": version,
            "prompt": kwargs.get("prompt", ""),
            "images": [image_uuid],
            "masks": [],
            "videos": [],
            "audios": [],
            "params": {
                "mode": kwargs.get("mode", "Fast"),
                "style": kwargs.get("style", "default"),
                "height": kwargs.get("height", -1),
                "width": kwargs.get("width", -1),
                "seed": kwargs.get("seed", -1),
                "duration": kwargs.get("duration", 5),
                "motion": kwargs.get("motion", 0),
                "x": kwargs.get("x", 0),
                "y": kwargs.get("y", 0),
                "z": kwargs.get("z", 0),
                "reserved_str": kwargs.get("reserved_str", ""),
                "batch_size": 1,
                "wh_ratio": wh_ratio,
                "custom_params": {
                    "prompts": kwargs.get("prompts", []),
                    "master_template_id": template_id
                }
            },
            "en_prompt": kwargs.get("en_prompt", ""),
            "negative_prompt": kwargs.get("negative_prompt", ""),
            "en_negative_prompt": kwargs.get("en_negative_prompt", ""),
            "magic_prompt": kwargs.get("magic_prompt", ""),
            "template_id": template_id,
            "upstream_id": kwargs.get("upstream_id", ""),
            "pipeline_id": kwargs.get("pipeline_id", ""),
            "request_id": str(uuid.uuid4())
        }
    
    def download_image(self, image_id: str, output_path: Optional[str] = None) -> str:
        """
        下载图片到本地
        
        Args:
            image_id: 图片ID (如 "p_xxxxx")
            output_path: 保存路径，默认保存到 /tmp/
            
        Returns:
            本地文件路径，如果下载失败返回空字符串
        """
        if output_path is None:
            output_path = f"/tmp/{image_id}.png"
        
        # 使用正确的 storage URL 格式
        urls_to_try = [
            f"https://storage.vivago.ai/image/{image_id}.jpg",
            f"https://storage.vivago.ai/image/{image_id}.png",
        ]
        
        for url in urls_to_try:
            try:
                resp = requests.get(url, headers=self.headers, timeout=1800, allow_redirects=True)  # 30分钟
                if resp.status_code == 200 and len(resp.content) > 1000:
                    with open(output_path, 'wb') as f:
                        f.write(resp.content)
                    logger.info(f"Image downloaded: {output_path}")
                    return output_path
            except Exception as e:
                logger.debug(f"Failed to download from {url}: {e}")
                continue
        
        return ""
    
    def download_video(self, video_id: str, output_path: Optional[str] = None) -> str:
        """
        下载视频到本地
        
        Args:
            video_id: 视频文件名 (如 "xxxxxx.mp4")
            output_path: 保存路径，默认保存到 /tmp/
            
        Returns:
            本地文件路径，如果下载失败返回空字符串
        """
        if output_path is None:
            output_path = f"/tmp/{video_id}"
        
        # 使用正确的 media URL 格式 (不需要认证)
        url = f"https://media.vivago.ai/{video_id}"
        
        try:
            # 视频下载不需要 Authorization header
            resp = requests.get(url, timeout=1800, allow_redirects=True)  # 30分钟
            if resp.status_code == 200 and len(resp.content) > 10000:  # 视频至少 10KB
                with open(output_path, 'wb') as f:
                    f.write(resp.content)
                logger.info(f"Video downloaded: {output_path}")
                return output_path
        except Exception as e:
            logger.debug(f"Failed to download video: {e}")
        
        return ""
    
    # ==================== Image to Image ====================
    
    def image_to_image(
        self,
        prompt: str,
        image_uuids: List[str],
        port: Optional[str] = None,
        wh_ratio: str = "16:9",
        strength: float = 0.8,
        relevance: Optional[List[float]] = None,
        **kwargs
    ) -> Optional[List[Dict]]:
        """
        图生图 - 支持多图输入 (Nano Banana 2 / Kling O1)
        
        支持 Nano Banana 2 和 Kling O1 模型，支持多张参考图片融合生成
        
        Args:
            prompt: 图像描述
            image_uuids: 参考图片UUID列表 (最多5张)
            port: 二级端口 (nano-banana/kling-image, 默认 nano-banana)
            wh_ratio: 宽高比 (1:1, 4:3, 3:4, 16:9, 9:16)
            strength: 变化强度 (0.0-1.0, 默认0.8)
            relevance: 每张图片的参考权重列表 (默认每张0.9)
            **kwargs: 额外参数
            
        Returns:
            List of generated image results
        """
        port_config, port_name = self._get_port_config("image_to_image", port or "nano-banana")
        
        display_name = port_config.get("display_name", port_name)
        version = port_config.get("version", "nano-banana-2")
        
        # 根据端口确定 module
        if "kling" in port_name:
            module = "image_gen_kling"
        else:
            module = "image_gen_std"
        
        # 设置默认 relevance
        if relevance is None:
            relevance = [0.9] * len(image_uuids)
        
        # 确保 relevance 长度与 image_uuids 一致
        if len(relevance) != len(image_uuids):
            logger.warning(f"relevance length ({len(relevance)}) != image count ({len(image_uuids)}), adjusting...")
            relevance = [0.9] * len(image_uuids)
        
        # 构建 custom_params
        custom_params = {"wh_ratio": wh_ratio}
        if "kling" in port_name:
            custom_params["enhance"] = "2k"
        
        data = {
            "app": None,
            "image": image_uuids,  # 多图输入数组
            "mask": kwargs.get("mask"),
            "module": module,
            "negative_prompt": kwargs.get("negative_prompt", ""),
            "prompt": prompt,
            "params": {
                "batch_count": kwargs.get("batch_count", 1),
                "batch_size": kwargs.get("batch_size", 1),
                "guidance_scale": kwargs.get("guidance_scale", 7.5),
                "height": kwargs.get("height", 512),
                "image_guidance_scale": kwargs.get("image_guidance_scale", 1.5),
                "sample_steps": kwargs.get("sample_steps", 40),
                "sampler": kwargs.get("sampler", "Euler a"),
                "seed": kwargs.get("seed", -1),
                "strength": strength,
                "style": kwargs.get("style", "default"),
                "wh_ratio": wh_ratio,
                "width": kwargs.get("width", 512),
                "relevance": relevance,  # 每张图的权重
                "custom_params": custom_params
            },
            "role": kwargs.get("role", "general"),
            "images": [],  # 根据抓包，这里为空数组
            "magic_prompt": kwargs.get("magic_prompt", ""),
            "audios": [],
            "videos": [],
            "request_id": str(uuid.uuid4())
        }
        
        # Kling 需要 version 参数，Nano Banana 不需要
        if "kling" in port_name:
            data["version"] = version
        
        # Nano Banana 2 需要 mode 参数
        if "nano" in port_name:
            data["params"]["mode"] = "2K"
        
        logger.info(f"Using port: {port_name} ({display_name}) with {len(image_uuids)} images")
        
        # 使用默认30分钟超时配置（防止重复计费）
        return self.call_api(
            endpoint=port_config["endpoint"],
            data=data,
            result_endpoint=port_config["result_endpoint"]
        )
    
    def get_image_result(self, image_id: str) -> dict:
        """
        获取图片结果信息
        
        Returns:
            包含图片信息的字典
        """
        return {
            "image_id": image_id,
            "vivago_url": f"https://vivago.ai/history/image",
            "direct_url": f"https://storage.vivago.ai/image/{image_id}.jpg",
            "note": "图片可直接通过 direct_url 下载"
        }
    
    def get_video_result(self, video_id: str) -> dict:
        """
        获取视频结果信息
        
        Returns:
            包含视频信息的字典
        """
        return {
            "video_id": video_id,
            "vivago_url": f"https://vivago.ai/history/video",
            "direct_url": f"https://media.vivago.ai/{video_id}.mp4",
            "note": "视频可直接通过 direct_url 下载"
        }


def create_client(
    [REDACTED_SECRET] = None,
    ports_config_path: Optional[str] = None
) -> VivagoClient:
    """
    Create Vivago client from environment or parameters.
    
    Environment variables:
        HIDREAM_AUTHORIZATION: API token (required, fallback to HIDREAM_TOKEN)
    """
    token = token or os.environ.get("HIDREAM_AUTHORIZATION") or os.environ.get("HIDREAM_TOKEN")
    
    if not token:
        raise MissingCredentialError(
            "API token required. Set HIDREAM_AUTHORIZATION env var or pass token parameter."
        )
    
    return VivagoClient(token, ports_config_path)


if __name__ == "__main__":
    # 示例：查看可用端口
    logging.basicConfig(level=logging.INFO)
    
    try:
        client = create_client()
        
        print("\n=== 可用一级功能 ===")
        for cat_id, info in client.list_categories().items():
            print(f"{cat_id}: {info['name']} ({info['name_en']}) - {info['status']}")
        
        print("\n=== 文生图可用端口 ===")
        for port_id, info in client.list_ports("text_to_image").items():
            print(f"  {port_id}: {info['name']} - {'✅' if info['tested'] else '⏳'}")
        
        print("\n=== 图生视频可用端口 ===")
        for port_id, info in client.list_ports("image_to_video").items():
            print(f"  {port_id}: {info['name']} - {'✅' if info['tested'] else '⏳'}")
            
    except MissingCredentialError:
        print("请设置 HIDREAM_TOKEN 环境变量以运行示例")


path: scripts/config_loader.py
content: #!/usr/bin/env python3
"""
配置加载器 - 支持分体式配置文件
自动检测并使用 config/ 目录或回退到 api_ports.json
"""
import json
import os
from typing import Dict, Any, Optional


class ConfigLoader:
    """配置加载器 - 支持模块化配置"""
    
    def __init__(self, config_dir: Optional[str] = None):
        """
        初始化配置加载器
        
        Args:
            config_dir: 配置目录路径，默认使用 scripts/config/
        """
        if config_dir is None:
            base_dir = os.path.dirname(os.path.abspath(__file__))
            config_dir = os.path.join(base_dir, 'config')
        
        self.config_dir = config_dir
        self._config: Optional[Dict[str, Any]] = None
    
    def load(self) -> Dict[str, Any]:
        """
        加载配置
        
        优先尝试加载分体式配置(config/)，如果不存在则回退到api_ports.json
        """
        if self._config is not None:
            return self._config
        
        # 尝试分体式配置
        if os.path.exists(self.config_dir):
            config = self._load_split_config()
            if config:
                self._config = config
                return config
        
        # 回退到单一文件
        self._config = self._load_legacy_config()
        return self._config
    
    def _load_split_config(self) -> Optional[Dict[str, Any]]:
        """加载分体式配置"""
        base_file = os.path.join(self.config_dir, 'base.json')
        if not os.path.exists(base_file):
            return None
        
        # 加载基础配置
        with open(base_file, 'r', encoding='utf-8') as f:
            config = json.load(f)
        
        config['categories'] = {}
        
        # 加载各个分类配置
        category_files = [
            'text_to_image.json',
            'image_to_video.json',
            'text_to_video.json',
            'keyframe_to_video.json',
            'image_to_image.json',
            'template_to_video.json'
        ]
        
        for filename in category_files:
            filepath = os.path.join(self.config_dir, filename)
            if os.path.exists(filepath):
                with open(filepath, 'r', encoding='utf-8') as f:
                    cat_config = json.load(f)
                    if 'categories' in cat_config:
                        config['categories'].update(cat_config['categories'])
        
        return config
    
    def _load_legacy_config(self) -> Dict[str, Any]:
        """加载旧版单一配置文件"""
        base_dir = os.path.dirname(os.path.abspath(__file__))
        legacy_file = os.path.join(base_dir, 'api_ports.json')
        
        with open(legacy_file, 'r', encoding='utf-8') as f:
            return json.load(f)
    
    def reload(self) -> Dict[str, Any]:
        """重新加载配置"""
        self._config = None
        return self.load()


# 全局配置加载器实例
_config_loader: Optional[ConfigLoader] = None


def get_config_loader(config_dir: Optional[str] = None) -> ConfigLoader:
    """获取全局配置加载器实例"""
    global _config_loader
    if _config_loader is None or config_dir is not None:
        _config_loader = ConfigLoader(config_dir)
    return _config_loader


def load_ports_config(config_dir: Optional[str] = None) -> Dict[str, Any]:
    """
    加载端口配置的便捷函数
    
    Args:
        config_dir: 可选的配置目录路径
        
    Returns:
        完整的端口配置字典
    """
    loader = get_config_loader(config_dir)
    return loader.load()


path: scripts/enums.py
content: #!/usr/bin/env python3
"""
Type definitions and enums for Vivago AI Skill
"""
from enum import IntEnum
from typing import TypedDict, Optional, List, Dict, Any
from dataclasses import dataclass


class TaskStatus(IntEnum):
    """任务状态码枚举"""
    PENDING = 0      # 等待中
    COMPLETED = 1    # 已完成
    PROCESSING = 2   # 处理中
    FAILED = 3       # 失败
    REJECTED = 4     # 被拒绝（内容审核）


class AspectRatio(str):
    """宽高比常量"""
    RATIO_1_1 = "1:1"
    RATIO_4_3 = "4:3"
    RATIO_3_4 = "3:4"
    RATIO_16_9 = "16:9"
    RATIO_9_16 = "9:16"
    RATIO_KEEP = "keep"


class VideoDuration(int):
    """视频时长常量"""
    SHORT = 5    # 5秒
    LONG = 10    # 10秒


class VideoMode(str):
    """视频生成模式"""
    SLOW = "Slow"
    FAST = "Fast"


class PortCategory(str):
    """一级功能分类"""
    TEXT_TO_IMAGE = "text_to_image"
    IMAGE_TO_VIDEO = "image_to_video"
    TEXT_TO_VIDEO = "text_to_video"
    IMAGE_TO_IMAGE = "image_to_image"
    KEYFRAME_TO_VIDEO = "keyframe_to_video"
    TEMPLATE_TO_VIDEO = "template_to_video"


class PortName(str):
    """二级端口名称"""
    # 文生图
    KLING_IMAGE = "kling-image"
    HIDREAM_TXT2IMG = "hidream-txt2img"
    NANO_BANANA = "nano-banana"
    # 视频
    V3PRO = "v3Pro"
    V3L = "v3L"
    KLING_VIDEO = "kling-video"


class ModuleName(str):
    """API模块名称"""
    TXT2IMG = "txt2img"
    IMAGE_GEN_KLING = "image_gen_kling"
    IMAGE_GEN_STD = "image_gen_std"
    VIDEO_DIFFUSION = "video_diffusion"
    VIDEO_DIFFUSION_IMG2VID = "video_diffusion_img2vid"
    VIDEO_DIFFUSION_KEYFRAMES = "video_diffusion_keyframes"
    VIDEO_DIFFUSION_GEN2VID = "video_diffusion_gen2vid"
    PROTO_TRANSFORMER = "proto_transformer"


@dataclass
class GenerationResult:
    """生成结果数据类"""
    task_id: str
    status: TaskStatus
    urls: List[str]
    message: Optional[str] = None
    
    @property
    def is_success(self) -> bool:
        return self.status == TaskStatus.COMPLETED
    
    @property
    def is_failed(self) -> bool:
        return self.status in (TaskStatus.FAILED, TaskStatus.REJECTED)


class TaskResultDict(TypedDict):
    """任务结果字典类型"""
    task_status: int
    result: List[str]
    task_id: str


# JSON类型别名
JSONDict = Dict[str, Any]


path: scripts/generate_for_feishu.py
content: #!/usr/bin/env python3
"""
生成图片并通过飞书发送 - Feishu专用版本
"""
import os
import sys

# Add scripts directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))

from vivago_client import create_client


def generate_and_send(prompt: str, port: str = 'kling-image', wh_ratio: str = '16:9'):
    """
    生成图片并保存到本地，返回文件路径供飞书发送
    
    Returns:
        tuple: (success: bool, file_paths: list, message: str)
    """
    try:
        client = create_client()
        
        # 生成图片
        results = client.text_to_image(
            prompt=prompt,
            port=port,
            wh_ratio=wh_ratio,
            batch_size=1
        )
        
        if not results:
            return False, [], "生成失败"
        
        # 下载图片
        saved_files = []
        for result in results:
            image_id = result.get('image')
            if not image_id:
                continue
            
            output_path = f"/tmp/{image_id}.jpg"
            downloaded_path = client.download_image(image_id, output_path)
            
            if downloaded_path and os.path.exists(downloaded_path):
                saved_files.append(downloaded_path)
        
        if saved_files:
            return True, saved_files, f"成功生成 {len(saved_files)} 张图片"
        else:
            return False, [], "图片下载失败"
            
    except Exception as e:
        return False, [], f"错误: {str(e)}"


if __name__ == '__main__':
    # 命令行调用，输出文件路径
    if len(sys.argv) < 2:
        print("Usage: python generate_for_feishu.py <prompt> [port] [ratio]")
        sys.exit(1)
    
    prompt = sys.argv[1]
    port = sys.argv[2] if len(sys.argv) > 2 else 'kling-image'
    ratio = sys.argv[3] if len(sys.argv) > 3 else '16:9'
    
    success, files, message = generate_and_send(prompt, port, ratio)
    
    # 输出文件路径到 stdout，供外部读取
    if success and files:
        for f in files:
            print(f)
    else:
        print(f"ERROR: {message}", file=sys.stderr)
        sys.exit(1)


path: scripts/generate_video_for_feishu.py
content: #!/usr/bin/env python3
"""
生成视频 - Feishu专用版本 (发送视频URL)

注意：飞书API不支持直接发送可播放视频，只能发送链接
"""
import os
import sys

# Add scripts directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))

from vivago_client import create_client


def generate_video(prompt: str, port: str = 'v3L', wh_ratio: str = '16:9', duration: int = 5):
    """
    生成视频，返回视频URL供飞书发送
    
    Returns:
        tuple: (success: bool, video_info: dict, message: str)
    """
    try:
        client = create_client()
        
        print(f"🎬 正在生成视频...")
        print(f"   Prompt: {prompt}")
        print(f"   模型: {port}")
        print(f"   时长: {duration}秒")
        print(f"   预计需要 2-3 分钟...")
        print()
        
        # 生成视频
        results = client.text_to_video(
            prompt=prompt,
            port=port,
            wh_ratio=wh_ratio,
            duration=duration,
            mode='Fast' if port == 'v3L' else 'Slow'
        )
        
        if not results:
            return False, {}, "生成失败"
        
        # 获取视频信息
        video_info = results[0]
        video_id = video_info.get('video')
        
        if not video_id:
            return False, {}, "未获取到视频ID"
        
        # 构建视频信息
        video_data = {
            'video_id': video_id,
            'video_url': f"https://media.vivago.ai/{video_id}",
            'prompt': prompt,
            'port': port,
            'duration': duration,
            'algo_version': video_info.get('algo_version'),
            'seed': video_info.get('seed')
        }
        
        return True, video_data, "视频生成成功"
            
    except Exception as e:
        import traceback
        traceback.print_exc()
        return False, {}, f"错误: {str(e)}"


def format_feishu_message(video_data: dict) -> str:
    """
    格式化飞书消息内容
    
    由于飞书API限制，视频只能发送链接格式，不能直接发送文件
    """
    port_display = {
        'v3L': 'Vivago.ai 2.0 360p (快速)',
        'v3Pro': 'Vivago.ai 2.0 (高质量)',
        'kling-video': 'Kling video O1 (最优)'
    }
    
    return f"""🎬 视频生成完成！

📹 视频链接：{video_data['video_url']}

📋 视频信息：
• 模型：{port_display.get(video_data['port'], video_data['port'])}
• 时长：{video_data['duration']}秒
• Prompt：{video_data['prompt']}
• Video ID：{video_data['video_id']}

💡 提示：点击链接即可观看和下载视频"""


if __name__ == '__main__':
    # 命令行调用
    if len(sys.argv) < 2:
        print("Usage: python generate_video_for_feishu.py <prompt> [port] [ratio] [duration]")
        print("  port: v3L (fast), v3Pro (quality), kling-video")
        print("  duration: 5 or 10")
        print("\n注意：飞书API不支持直接发送视频文件，将返回视频链接")
        sys.exit(1)
    
    prompt = sys.argv[1]
    port = sys.argv[2] if len(sys.argv) > 2 else 'v3L'
    ratio = sys.argv[3] if len(sys.argv) > 3 else '16:9'
    duration = int(sys.argv[4]) if len(sys.argv) > 4 else 5
    
    success, video_data, message = generate_video(prompt, port, ratio, duration)
    
    if success and video_data:
        # 输出视频URL供外部读取
        print(f"VIDEO_URL:{video_data['video_url']}")
        print(f"VIDEO_ID:{video_data['video_id']}")
        print(f"PROMPT:{video_data['prompt']}")
        print(f"PORT:{video_data['port']}")
        print(f"DURATION:{video_data['duration']}")
        
        # 输出飞书消息格式
        feishu_msg = format_feishu_message(video_data)
        print(f"FEISHU_MSG:{feishu_msg}")
    else:
        print(f"ERROR:{message}", file=sys.stderr)
        sys.exit(1)


path: scripts/api_ports.json
content: {
  "version": "0.6.0",
  "last_updated": "2026-03-06",
  "base_url": "https://vivago.ai/api/gw",
  "_architecture_notes": {
    "level_1": "一级功能（大功能划分）",
    "level_2": "二级端口（具体API端点配置）",
    "naming": "display_name 与 Vivago 网站模型名称对齐",
    "endpoint_version": "相同端点 + 不同 version = 不同模型名称",
    "extensibility": "未来可扩展的一级功能包括: keyframe_to_video, template_to_video"
  },
  "categories": {
    "text_to_image": {
      "name": "文生图",
      "name_en": "Text to Image",
      "status": "active",
      "default_port": "kling-image",
      "description": "从文本描述生成图像",
      "ports": {
        "kling-image": {
          "name": "Kling Image",
          "display_name": "Kling O1",
          "status": "tested",
          "endpoint": "/v3/image/image_gen_kling/async",
          "result_endpoint": "/v3/image/txt2img/async/results",
          "version": "kling-image-o1",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 4,
          "tested": true,
          "speed": "fast",
          "quality": "good",
          "notes": "速度快，质量优秀，推荐使用"
        },
        "hidream-txt2img": {
          "name": "HiDream Txt2Img",
          "display_name": "Vivago.ai 2.0",
          "status": "tested",
          "endpoint": "/v3/image/txt2img/async",
          "result_endpoint": "/v3/image/txt2img/async/results",
          "version": "v3L",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 4,
          "tested": true,
          "speed": "medium",
          "quality": "good",
          "notes": "已测试，质量良好"
        },
        "nano-banana": {
          "name": "Nano Banana",
          "display_name": "Nano Banana 2",
          "status": "tested",
          "endpoint": "/v3/image/image_gen_std/async",
          "result_endpoint": "/v3/image/txt2img/async/results",
          "version": "nano-banana-2",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 4,
          "tested": true,
          "speed": "slow",
          "quality": "excellent",
          "notes": "已测试，效果优秀，回调使用 txt2img 路径"
        }
      }
    },
    "image_to_video": {
      "name": "图生视频",
      "name_en": "Image to Video",
      "status": "active",
      "default_port": "v3Pro",
      "description": "从静态图像生成视频",
      "warning": "生成时间较长(2-3分钟)，请谨慎调用",
      "ports": {
        "v3Pro": {
          "name": "V3 Pro",
          "display_name": "Vivago.ai 2.0",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3Pro",
          "supported_ratios": [
            "keep",
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "slow",
          "quality": "excellent",
          "notes": "高质量，生成较慢"
        },
        "v3L": {
          "name": "V3 Lite",
          "display_name": "Vivago.ai 2.0 360p",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3L",
          "supported_ratios": [
            "keep",
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "fast",
          "quality": "good",
          "notes": "360p分辨率，生成速度快"
        },
        "kling-video": {
          "name": "Kling Video",
          "display_name": "Kling video O1",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "kling-video-o1",
          "supported_ratios": [
            "keep",
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "medium",
          "quality": "excellent",
          "notes": "Kling视频模型，质量优秀"
        }
      }
    },
    "text_to_video": {
      "name": "文生视频",
      "name_en": "Text to Video",
      "status": "testing",
      "default_port": "v3L",
      "description": "从文本描述生成视频",
      "warning": "生成时间较长(2-3分钟)，请谨慎调用",
      "ports": {
        "v3L": {
          "name": "V3 Lite",
          "display_name": "Vivago.ai 2.0 360p",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3L",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "fast",
          "quality": "good",
          "notes": "360p文生视频/图生视频，速度快"
        },
        "v3Pro": {
          "name": "V3 Pro",
          "display_name": "Vivago.ai 2.0",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3Pro",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "slow",
          "quality": "excellent",
          "notes": "高质量文生视频/图生视频"
        },
        "kling-video": {
          "name": "Kling Video",
          "display_name": "Kling video O1",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_gen2vid/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "kling-video-o1",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "medium",
          "quality": "excellent",
          "notes": "文生视频/图生视频已测试，使用 gen2vid 端点"
        }
      }
    },
    "keyframe_to_video": {
      "name": "视频首尾帧",
      "name_en": "Keyframe-to-Video",
      "status": "active",
      "default_port": "v3L",
      "description": "根据首尾帧生成过渡视频",
      "ports": {
        "v3L": {
          "name": "V3 Lite Keyframe",
          "display_name": "Vivago.ai 2.0 360p",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_keyframes/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3L",
          "supported_ratios": [
            "keep",
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "fast",
          "quality": "good",
          "notes": "快速版本，360p分辨率"
        },
        "v3Pro": {
          "name": "V3 Pro Keyframe",
          "display_name": "Vivago.ai 2.0",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_keyframes/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3Pro",
          "supported_ratios": [
            "keep",
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "slow",
          "quality": "excellent",
          "notes": "高质量版本，慢但效果更好"
        }
      }
    },
    "image_to_image": {
      "name": "图生图",
      "name_en": "Image to Image",
      "status": "active",
      "default_port": "kling-image",
      "description": "图像风格转换、编辑、AI肖像、虚拟试衣等功能",
      "notes": "支持多图输入融合，可用于风格迁移、图像编辑、AI换脸、虚拟试衣等场景",
      "ports": {
        "kling-image": {
          "name": "Kling Image Img2Img",
          "display_name": "Kling O1",
          "status": "tested",
          "endpoint": "/v3/image/image_gen_kling/async",
          "result_endpoint": "/v3/image/txt2img/async/results",
          "version": "kling-image-o1",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 4,
          "max_images": 5,
          "tested": true,
          "speed": "fast",
          "quality": "excellent",
          "notes": "Kling O1 图生图，速度快，支持多图输入，enhance=2k，推荐使用"
        },
        "nano-banana": {
          "name": "Nano Banana Img2Img",
          "display_name": "Nano Banana 2",
          "status": "tested",
          "endpoint": "/v3/image/image_gen_std/async",
          "result_endpoint": "/v3/image/txt2img/async/results",
          "version": "nano-banana-2",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "max_images": 5,
          "tested": true,
          "speed": "slow",
          "quality": "excellent",
          "notes": "Nano Banana 2 图生图，质量优秀但较慢，支持多图输入"
        }
      }
    },
    "template_to_video": {
      "name": "视频模板",
      "name_en": "Template-to-Video",
      "status": "active",
      "default_port": "renovation_old_photos",
      "description": "特定类型视频特效（固定视频模板）",
      "notes": "不同template对应不同端点和参数",
      "ports": {
        "static_shot": {
          "name": "Static Shot",
          "display_name": "Static Shot",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "056c8f3b-3b9f-4b7c-bc1b-c790b1512dc8.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Static Shot"
        },
        "renovation_of_old_photos": {
          "name": "Renovation of old photos",
          "display_name": "Renovation of old photos",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "060c47d2-bb44-4a2f-85aa-00165843a07b.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Renovation of old photos"
        },
        "ash_out": {
          "name": "Ash out",
          "display_name": "Ash out",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "0c5806e2-9324-4652-8a2e-59276de345a1.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Ash out"
        },
        "mystic_traveler": {
          "name": "Mystic Traveler",
          "display_name": "Mystic Traveler",
          "status": "tested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "104e7ce0-6996-4802-9d60-ea2c20dc518d.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Template: Mystic Traveler",
          "result_type": "video"
        },
        "shake_it_down": {
          "name": "Shake It Down",
          "display_name": "Shake It Down",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "10d8747c-0afd-4af1-88fa-cc4e464b5383.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Shake It Down"
        },
        "1930s_vintage_style": {
          "name": "1930s vintage style",
          "display_name": "1930s vintage style",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "113f556f-1c3b-4493-a169-fdc6d265ff55.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: 1930s vintage style"
        },
        "barbie": {
          "name": "Barbie",
          "display_name": "Barbie",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "120e0a05-d4d5-4b04-9afd-62b2a6bd6f94.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Barbie"
        },
        "autumn_feast": {
          "name": "Autumn Feast",
          "display_name": "Autumn Feast",
          "status": "tested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "1340d4d6-1a9d-44b5-ba5f-25a451f435d5.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Template: Autumn Feast",
          "result_type": "video"
        },
        "paparazzi_rush": {
          "name": "Paparazzi Rush",
          "display_name": "Paparazzi Rush",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "14041335-fcde-4013-8e47-f9c047da700d.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Paparazzi Rush"
        },
        "kiss_kiss": {
          "name": "Kiss Kiss",
          "display_name": "Kiss Kiss",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "165f52b8-3e14-4e3e-a39d-9491f2ce24ed.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Kiss Kiss"
        },
        "hip_twist": {
          "name": "Hip Twist",
          "display_name": "Hip Twist",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "18d7276f-b4d3-4078-a7bc-f196575ab258.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Hip Twist"
        },
        "magic_reveal_ravenclaw": {
          "name": "Magic Reveal （Ravenclaw）",
          "display_name": "Magic Reveal （Ravenclaw）",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "18d7782f-bfe8-49a1-9935-4a262d621fec.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Magic Reveal （Ravenclaw）"
        },
        "animal_cooking": {
          "name": "Animal Cooking",
          "display_name": "Animal Cooking",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "205b63fc-8d5e-4fd4-a6fc-7f207b6118af.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Animal Cooking"
        },
        "girlfriends_rose": {
          "name": "Girlfriend's Rose",
          "display_name": "Girlfriend's Rose",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "22673849-17b7-4bab-82d3-d8b8fee41417.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Girlfriend's Rose"
        },
        "eye_of_the_storm": {
          "name": "Eye of the Storm",
          "display_name": "Eye of the Storm",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "24b50c1f-d050-4ad6-94dc-1c762dc34b6a.mp4",
          "supported_ratios": [
            "1:1",
            "16:9",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Eye of the Storm"
        },
        "jungle_reign": {
          "name": "Jungle Reign",
          "display_name": "Jungle Reign",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "26747906-a44c-4d82-b88e-9f96119f990a.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Jungle Reign"
        },
        "the_final_professional": {
          "name": "The Final Professional",
          "display_name": "The Final Professional",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "26f77fbb-a6c9-42a4-8669-b722b336656a.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: The Final Professional"
        },
        "moving_figure": {
          "name": "Moving Figure",
          "display_name": "Moving Figure",
          "status": "tested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "2858143d-8a81-427c-b99f-f84ac8116f80.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Template: Moving Figure",
          "result_type": "video"
        },
        "1940s_suit_portrait": {
          "name": "1940s suit portrait",
          "display_name": "1940s suit portrait",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "292bd62c-f0e6-474a-86c9-c57503d79e37.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: 1940s suit portrait"
        },
        "bikini": {
          "name": "Bikini",
          "display_name": "Bikini",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "bb679f1b-ff17-4029-bf6a-e67fc91b1ee4.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Bikini"
        },
        "2000s_y2k": {
          "name": "2000s Y2K",
          "display_name": "2000s Y2K",
          "status": "tested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "91768459-aae6-4f4b-aef9-b57b77e50596.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "⚠️ CONTENT_REJECTED: 使用成年男性/女性样本可能触发内容审核，建议更换样本重试",
          "result_type": "video"
        },
        "1960s_elegant": {
          "name": "1960s Elegant",
          "display_name": "1960s Elegant",
          "status": "tested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "30643be1-3bbe-463c-ab7e-842ac6e506da.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Template: 1960s Elegant",
          "result_type": "video"
        },
        "color_the_lines": {
          "name": "Color the Lines",
          "display_name": "Color the Lines",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "32170db2-3792-4184-95f2-4515114517dd.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Color the Lines"
        },
        "1970s_disco": {
          "name": "1970s disco",
          "display_name": "1970s disco",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "325c53f6-2ffd-4896-b232-fd2d69a7d8fd.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: 1970s disco"
        },
        "1990s_punk_rock": {
          "name": "1990s Punk Rock",
          "display_name": "1990s Punk Rock",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "379b6201-7828-423a-93b0-185432405a62.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: 1990s Punk Rock"
        },
        "victory_dance": {
          "name": "Victory Dance",
          "display_name": "Victory Dance",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_animate/async",
          "result_endpoint": "/v3/video/video_diffusion_animate/async/results",
          "algo_type": "video_diffusion_animate",
          "version": "v1",
          "template_id": "37f1f5ca-e438-490b-b5bc-bbe47d5d67d7.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Victory Dance"
        },
        "magic_reveal_gryffindor": {
          "name": "Magic Reveal （Gryffindor）",
          "display_name": "Magic Reveal （Gryffindor）",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "3c1f7cfc-df0b-4147-899c-0030f9c912bb.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Magic Reveal （Gryffindor）"
        },
        "jiggle_jiggle": {
          "name": "Jiggle Jiggle",
          "display_name": "Jiggle Jiggle",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "e942eec6-9b14-42db-815f-38723322ce98.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Jiggle Jiggle"
        },
        "shaking": {
          "name": "Shaking",
          "display_name": "Shaking",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "3f20d5d8-8332-4cfb-af0e-1aa665595807.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Shaking"
        },
        "gentle_rose": {
          "name": "Gentle Rose",
          "display_name": "Gentle Rose",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "3f3d65c4-f5b6-4723-912a-bdad07aee31a.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Gentle Rose"
        },
        "doodle_alive": {
          "name": "Doodle Alive",
          "display_name": "Doodle Alive",
          "status": "tested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "419b1355-9192-4f20-ae5f-501ccc7decf3.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Template: Doodle Alive",
          "result_type": "video"
        },
        "sexy_pants": {
          "name": "Sexy Pants",
          "display_name": "Sexy Pants",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "438ca94c-2a65-4ba8-a1c2-57008d059c16.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Sexy Pants"
        },
        "1970s": {
          "name": "1970s",
          "display_name": "1970s",
          "status": "tested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "4f12d18f-ca18-45ec-8b22-495184deed61.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Template: 1970s",
          "result_type": "video"
        },
        "gratitude_photo": {
          "name": "Gratitude Photo",
          "display_name": "Gratitude Photo",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "4f41015b-e41f-4134-954e-1034edcec928.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Gratitude Photo"
        },
        "wings_of_divinity": {
          "name": "Wings of Divinity",
          "display_name": "Wings of Divinity",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "516aeec0-9259-4638-8d47-74e85bd09c1b.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Wings of Divinity"
        },
        "ghibli": {
          "name": "Ghibli",
          "display_name": "Ghibli",
          "status": "untested",
          "endpoint": "/v3/pipeline/style_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "style_transformer",
          "version": "v1",
          "template_id": "51c6856c-60ec-4a32-8855-0a8251ededfb.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Ghibli"
        },
        "me_in_hand": {
          "name": "Me in Hand",
          "display_name": "Me in Hand",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "ef1d7794-3648-4ae6-b481-826fa5eb6422.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Me in Hand"
        },
        "frost_alert": {
          "name": "Frost Alert",
          "display_name": "Frost Alert",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "fcf1a5db-aa29-4287-8593-b8cbe3eea26a.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Frost Alert"
        },
        "music_box": {
          "name": "MUSIC BOX",
          "display_name": "MUSIC BOX",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "f9cd3729-02aa-4437-89f8-d2b6d302b3c1.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: MUSIC BOX"
        },
        "glasses_display": {
          "name": "Glasses Display",
          "display_name": "Glasses Display",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_multi_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_multi_img2vid/async/results",
          "algo_type": "video_diffusion_multi_img2vid",
          "version": "v3Pro",
          "template_id": "94e05404-b8c0-4c11-b21a-88e439e93039.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Glasses Display"
        },
        "instant_sadness": {
          "name": "Instant Sadness",
          "display_name": "Instant Sadness",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "89d96114-0edc-4b90-81e1-319571195727.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Instant Sadness"
        },
        "headphone_vibe": {
          "name": "Headphone Vibe",
          "display_name": "Headphone Vibe",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "d33314c1-2cfc-4443-9cab-bf786db7e6e2.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Headphone Vibe"
        },
        "apt": {
          "name": "APT",
          "display_name": "APT",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_animate/async",
          "result_endpoint": "/v3/video/video_diffusion_animate/async/results",
          "algo_type": "video_diffusion_animate",
          "version": "v1",
          "template_id": "78a0033b-fc02-45dd-82ee-ae97baad5da7.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: APT"
        },
        "a_family": {
          "name": "A Family",
          "display_name": "A Family",
          "status": "tested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "a58a2891-acd7-406a-9bb1-988a1763de80.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Template: A Family",
          "result_type": "video"
        },
        "turkey_chasing": {
          "name": "Turkey Chasing",
          "display_name": "Turkey Chasing",
          "status": "needs_fix",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "bfbf99a7-e76a-4c78-8f16-68c8ca4e4d4f.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "✅ Prompt已修复"
        },
        "1970s_punk_animation": {
          "name": "1970s punk animation",
          "display_name": "1970s punk animation",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "f0ed7c29-7a01-4970-9482-03006d06b5d9.mp4",
          "supported_ratios": [
            "4:3",
            "3:4",
            "9:16",
            "1:1",
            "16:9"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: 1970s punk animation"
        },
        "drawings_alive": {
          "name": "Drawings Alive",
          "display_name": "Drawings Alive",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "f7e60a3f-7ee3-429f-be46-80793e96093a.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Drawings Alive"
        },
        "face_punch": {
          "name": "Face Punch",
          "display_name": "Face Punch",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "66b24a71-1a04-4695-8368-48f86642a9c2.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Face Punch"
        },
        "bring_comics_to_life": {
          "name": "Bring Comics to Life",
          "display_name": "Bring Comics to Life",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "a855fd12-1a85-4e83-a9f7-14a599d63735.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Bring Comics to Life"
        },
        "magic_reveal_hufflepuff": {
          "name": "Magic Reveal （Hufflepuff）",
          "display_name": "Magic Reveal （Hufflepuff）",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "f59260f4-86c6-4bc0-96eb-07574adc8883.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Magic Reveal （Hufflepuff）"
        },
        "glasses": {
          "name": "Glasses",
          "display_name": "Glasses",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "e63148ec-5939-4acd-8eed-56e22c5c67c2.mp4",
          "supported_ratios": [
            "1:1",
            "16:9",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Glasses"
        },
        "angel_wings": {
          "name": "Angel Wings",
          "display_name": "Angel Wings",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "90a5574d-9121-4225-ac2c-a54c6c96ea89.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Angel Wings"
        },
        "phoenix_wings": {
          "name": "Phoenix Wings",
          "display_name": "Phoenix Wings",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "9791d0ea-2f2c-47d2-b9c9-b934bb9cfd84.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Phoenix Wings"
        },
        "ai_archaeologist": {
          "name": "AI archaeologist",
          "display_name": "AI archaeologist",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "8219a1cb-61a1-435a-a991-d92e5dbfb73c.mp4",
          "supported_ratios": [
            "4:3",
            "3:4",
            "9:16",
            "1:1",
            "16:9"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: AI archaeologist"
        },
        "romantic_kiss": {
          "name": "Romantic Kiss",
          "display_name": "Romantic Kiss",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "86dddb80-e925-42c2-8511-d8e5b163a1c7.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Romantic Kiss"
        },
        "phoenix_warrior": {
          "name": "Phoenix Warrior",
          "display_name": "Phoenix Warrior",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "80c76107-08f2-4bdd-bea8-3e5b7c168fc9.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Phoenix Warrior"
        },
        "metallic_liquid_": {
          "name": "Metallic Liquid ",
          "display_name": "Metallic Liquid ",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "e13ef1f6-a193-476c-86b9-4fdabc381f1a.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "4:3",
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Metallic Liquid "
        },
        "tracking_shot": {
          "name": "Tracking shot",
          "display_name": "Tracking shot",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "dfac4909-4d10-4449-bbde-9805dd3faa9a.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Tracking shot"
        },
        "swag_zone": {
          "name": "Swag Zone",
          "display_name": "Swag Zone",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "5d1f6f0c-817e-43b2-aa46-1a50052aaf63.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Swag Zone"
        },
        "relax": {
          "name": "Relax",
          "display_name": "Relax",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "91f2c950-30ed-4253-af54-ed5be2458397.mp4",
          "supported_ratios": [
            "1:1",
            "16:9",
            "9:16",
            "4:3",
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Relax"
        },
        "boyfriends_rose": {
          "name": "Boyfriend's Rose",
          "display_name": "Boyfriend's Rose",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "a71e77dd-e4c7-4707-bc09-a90314d59fad.mp4",
          "supported_ratios": [
            "1:1",
            "9:16",
            "3:4",
            "4:3",
            "16:9"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Boyfriend's Rose"
        },
        "gta_star": {
          "name": "GTA star",
          "display_name": "GTA star",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "ba629d2e-9719-4f95-ad20-f3c58366d182.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: GTA star"
        },
        "3d_architectural_models": {
          "name": "3D Architectural Models",
          "display_name": "3D Architectural Models",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "d9c79609-7fbf-4709-a558-c029e1380824.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: 3D Architectural Models"
        },
        "tiger_snuggle": {
          "name": "Tiger Snuggle",
          "display_name": "Tiger Snuggle",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "ab8ca58d-c131-486d-90e9-98a9c48daefd.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Tiger Snuggle"
        },
        "earth_zoom_in": {
          "name": "Earth Zoom In",
          "display_name": "Earth Zoom In",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "5421e29c-e9d7-49bd-a2ef-c7a1154a7132.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Earth Zoom In"
        },
        "graduation": {
          "name": "Graduation",
          "display_name": "Graduation",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "ed9cee6c-7252-4323-b6e1-fa680f932d54.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Graduation"
        },
        "roar_of_the_dustlands": {
          "name": "Roar of the Dustlands",
          "display_name": "Roar of the Dustlands",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "cd86b87e-9146-4df4-abf8-db6e0d7bab6d.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Roar of the Dustlands"
        },
        "starship_chef": {
          "name": "Starship Chef",
          "display_name": "Starship Chef",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "68d42973-40f8-40cc-a208-19110afc85a4.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Starship Chef"
        },
        "autumn_stroll": {
          "name": "Autumn Stroll",
          "display_name": "Autumn Stroll",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "9a0ddcb7-a263-4ede-a4ba-bae3c3bfe367.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Autumn Stroll"
        },
        "crystal_wings": {
          "name": "Crystal Wings",
          "display_name": "Crystal Wings",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "a006eb49-50c3-4bbe-b905-2ea1ec1b67d9.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Crystal Wings"
        },
        "flaming_hand": {
          "name": "Flaming Hand",
          "display_name": "Flaming Hand",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "916c4c35-e1b5-46e8-b509-c6062e8a15c4.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Flaming Hand"
        },
        "panther_queen": {
          "name": "Panther Queen",
          "display_name": "Panther Queen",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "f58233ca-ea37-4875-9947-c543633c0722.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Panther Queen"
        },
        "iron_man": {
          "name": "Iron Man",
          "display_name": "Iron Man",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "8879f1a2-ea28-4c00-93eb-d0a4f58f151b.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Iron Man"
        },
        "gun_point": {
          "name": "Gun Point",
          "display_name": "Gun Point",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "75acf6b9-e6c9-49e9-9947-4010fe1ebb09.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Gun Point"
        },
        "back_to_steampunk": {
          "name": "Back to Steampunk",
          "display_name": "Back to Steampunk",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "8eb5f661-51a9-48b4-a8a0-926885a5e859.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Back to Steampunk"
        },
        "turkey_away": {
          "name": "Turkey Away",
          "display_name": "Turkey Away",
          "status": "needs_fix",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "fdc290e5-4832-48b3-b6a0-1a84545bb849.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "✅ Prompt已修复"
        },
        "bald_me": {
          "name": "Bald Me",
          "display_name": "Bald Me",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "7fb4e9b2-96ae-4fae-9188-28509c1911ec.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Bald Me"
        },
        "1980s_cyber_anime_": {
          "name": "1980s Cyber anime ",
          "display_name": "1980s Cyber anime ",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "bb5435ac-4963-4c97-96bf-2d6d0a1b84fc.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: 1980s Cyber anime "
        },
        "dance": {
          "name": "Dance",
          "display_name": "Dance",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "fbc9e607-c891-4d25-b4d9-c2405712f123.mp4",
          "supported_ratios": [
            "1:1",
            "16:9",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Dance"
        },
        "fairy": {
          "name": "Fairy",
          "display_name": "Fairy",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "86378a7d-89c5-459e-90c6-3ee7b1dc9898.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Fairy"
        },
        "ring_ring": {
          "name": "Ring Ring",
          "display_name": "Ring Ring",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "b1fadca0-e88c-4609-9d7d-14bfd42eebd8.mp4",
          "supported_ratios": [
            "1:1",
            "16:9",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Ring Ring"
        },
        "anime_figure": {
          "name": "Anime Figure",
          "display_name": "Anime Figure",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "5617ad50-c185-49ce-a270-ca036956cac6.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Anime Figure"
        },
        "limbo_dance": {
          "name": "Limbo Dance",
          "display_name": "Limbo Dance",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_animate/async",
          "result_endpoint": "/v3/video/video_diffusion_animate/async/results",
          "algo_type": "video_diffusion_animate",
          "version": "v1",
          "template_id": "d8c771f0-a255-4fec-b430-31448c5c02f4.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Limbo Dance"
        },
        "glam_cat": {
          "name": "Glam Cat",
          "display_name": "Glam Cat",
          "status": "needs_fix",
          "endpoint": "/v3/pipeline/figure_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "figure_transformer",
          "version": "v1",
          "template_id": "ae5e1f9c-0411-4960-9b74-3d4251c6523b.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "✅ Prompt已修复"
        },
        "satisfying_cut": {
          "name": "Satisfying Cut",
          "display_name": "Satisfying Cut",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "ff83acfa-9270-4cc7-8fa9-f41e859c0657.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Satisfying Cut"
        },
        "orbit_shot": {
          "name": "Orbit Shot",
          "display_name": "Orbit Shot",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "bb14b1e0-c816-40da-bf36-f3bbcf25b5b3.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Orbit Shot"
        },
        "chicken_time": {
          "name": "Chicken Time",
          "display_name": "Chicken Time",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "f826a97b-52e0-4496-b7fa-42cf8b27d2a2.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Chicken Time"
        },
        "planetary_racer": {
          "name": "Planetary Racer",
          "display_name": "Planetary Racer",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "8c2f6fb0-fe0a-478e-a139-7d36e5361cee.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Planetary Racer"
        },
        "squish_it": {
          "name": "Squish it",
          "display_name": "Squish it",
          "status": "untested",
          "endpoint": "/v3/video/video_effect_squeeze/async",
          "result_endpoint": "/v3/video/video_effect_squeeze/async/results",
          "algo_type": "video_effect_squeeze",
          "version": "v1",
          "template_id": "ef42229b-77f6-4db1-bf7f-458de654cf60.mp4",
          "supported_ratios": [
            "1:1",
            "9:16",
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Squish it"
        },
        "zoom_out": {
          "name": "Zoom out",
          "display_name": "Zoom out",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "b9ed32c5-c8a6-45dc-85a6-c526da3ea44b.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Zoom out"
        },
        "ghost_rider": {
          "name": "Ghost Rider",
          "display_name": "Ghost Rider",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "c4e187ec-fcf4-41ce-b92d-416634084cc5.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Ghost Rider"
        },
        "earth_zoom_out_in": {
          "name": "Earth Zoom Out In",
          "display_name": "Earth Zoom Out In",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "a3793dda-b3f3-43cf-8639-bd5758d091be.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Earth Zoom Out In"
        },
        "thankful_vibes": {
          "name": "Thankful Vibes",
          "display_name": "Thankful Vibes",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "817d4fab-c3bb-4ab9-8e49-42600225a2e5.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Thankful Vibes"
        },
        "sunglasses_display": {
          "name": "Sunglasses Display",
          "display_name": "Sunglasses Display",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_multi_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_multi_img2vid/async/results",
          "algo_type": "video_diffusion_multi_img2vid",
          "version": "v3Pro",
          "template_id": "d9e0fd8f-1d9e-4978-8916-19e276ec0a46.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Sunglasses Display"
        },
        "cloud_person": {
          "name": "Cloud person",
          "display_name": "Cloud person",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "737648df-4e3b-413f-9856-e3c11ab3d5c6.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Cloud person"
        },
        "handheld_shot": {
          "name": "Handheld Shot",
          "display_name": "Handheld Shot",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "9b01bcb7-cc6c-419c-805c-6a16b5ece24e.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Handheld Shot"
        },
        "dream_wings": {
          "name": "Dream Wings",
          "display_name": "Dream Wings",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "a475cd91-9c4a-4075-ab79-4c6025863a2c.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Dream Wings"
        },
        "turkey_giant": {
          "name": "Turkey Giant",
          "display_name": "Turkey Giant",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "b36db7c4-91e1-4770-abde-1bdf5fa2533d.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Turkey Giant"
        },
        "ice_cream": {
          "name": "Ice cream",
          "display_name": "Ice cream",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "5c8a2443-a3be-4315-9a16-b00b394189ac.mp4",
          "supported_ratios": [
            "1:1",
            "16:9",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Ice cream"
        },
        "sexy_man": {
          "name": "Sexy man",
          "display_name": "Sexy man",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "ab114168-6bf7-45c7-b0ca-7e750b403059.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Sexy man"
        },
        "push_in": {
          "name": "Push-in",
          "display_name": "Push-in",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "ec47c10d-5192-4be1-a1ea-d5400f7d391d.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Push-in"
        },
        "little_turkey": {
          "name": "Little Turkey",
          "display_name": "Little Turkey",
          "status": "needs_fix",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "9b2c5c17-7abe-4ffe-b66a-f402e7e7c7a6.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "✅ Prompt已修复"
        },
        "cat_woman": {
          "name": "Cat Woman",
          "display_name": "Cat Woman",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "88f3be0a-c048-4b8c-9d4c-b8667318dee0.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Cat Woman"
        },
        "cyber_cooker": {
          "name": "Cyber Cooker",
          "display_name": "Cyber Cooker",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "aef7b5d0-b454-42e5-928f-105b351df088.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Cyber Cooker"
        },
        "joker": {
          "name": "Joker",
          "display_name": "Joker",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "9897fe70-9603-41eb-b349-d93f11cb2d46.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Joker"
        },
        "flash_flood": {
          "name": "Flash Flood",
          "display_name": "Flash Flood",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "f6182198-b91d-4f45-afdf-dde82a86eb4d.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Flash Flood"
        },
        "ghibli2": {
          "name": "Ghibli2",
          "display_name": "Ghibli2",
          "status": "untested",
          "endpoint": "/v3/pipeline/style_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "style_transformer",
          "version": "v1",
          "template_id": "520fc40e-d569-4e0e-af1b-29c2371d50f2.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Ghibli2"
        },
        "snow_white": {
          "name": "Snow White",
          "display_name": "Snow White",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "55b914c6-63b2-4dc0-82a2-95c83e39c30f.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Snow White"
        },
        "animal_and_turkey": {
          "name": "Animal and Turkey",
          "display_name": "Animal and Turkey",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "ed5e0932-f5a7-4942-bc4f-79ece99e2d3c.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Animal and Turkey"
        },
        "boom_hair": {
          "name": "Boom Hair",
          "display_name": "Boom Hair",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "8a15d34f-2bfb-4164-ab28-6a32327f970c.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Boom Hair"
        },
        "curl_pop": {
          "name": "Curl Pop",
          "display_name": "Curl Pop",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "69c57e2f-7db3-4967-ad32-56ea398dd465.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Curl Pop"
        },
        "earth_zoom_out": {
          "name": "Earth Zoom Out",
          "display_name": "Earth Zoom Out",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "d433b908-aa91-47c6-a8de-0b40b70718ba.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Earth Zoom Out"
        },
        "urban_night_rider": {
          "name": "Urban Night Rider",
          "display_name": "Urban Night Rider",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "8f2818c2-e8f8-487c-b02b-5be3ff202fb9.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Urban Night Rider"
        },
        "paparazzi": {
          "name": "Paparazzi",
          "display_name": "Paparazzi",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "a8390351-5180-4a9a-9247-b81f6fb9f9f4.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Paparazzi"
        },
        "mermaid": {
          "name": "Mermaid",
          "display_name": "Mermaid",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_template/async",
          "result_endpoint": "/v3/video/video_diffusion_template/async/results",
          "algo_type": "video_diffusion_template",
          "version": "v3Pro",
          "template_id": "c87d4faa-abd5-41ad-89e6-73153d709bb9.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Mermaid"
        },
        "fleet_captain": {
          "name": "Fleet Captain",
          "display_name": "Fleet Captain",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "5f8f24a8-983c-48bd-85d7-fdca354dfc42.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Fleet Captain"
        },
        "fire_wings": {
          "name": "Fire Wings",
          "display_name": "Fire Wings",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "7a057976-af4e-4445-ae66-8b89afef40ae.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Fire Wings"
        },
        "1960s": {
          "name": "1960s",
          "display_name": "1960s",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "7d525864-0843-46bc-91f7-db9cbdb28622.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: 1960s"
        },
        "dadada": {
          "name": "DADADA",
          "display_name": "DADADA",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_animate/async",
          "result_endpoint": "/v3/video/video_diffusion_animate/async/results",
          "algo_type": "video_diffusion_animate",
          "version": "v1",
          "template_id": "a1b6b944-c13f-4307-8488-febd0d035c7e.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: DADADA"
        },
        "long_hair": {
          "name": "Long hair",
          "display_name": "Long hair",
          "status": "untested",
          "endpoint": "/v2/image/image_easy_edit/async",
          "result_endpoint": "/v2/image/image_easy_edit/async/results",
          "algo_type": "image_easy_edit",
          "version": "v1",
          "template_id": "752ca89f-a94e-4aa6-8fc1-f3b42ea4cecc.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Long hair"
        },
        "elevator_turkey": {
          "name": "Elevator Turkey",
          "display_name": "Elevator Turkey",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "8f4644b0-c206-49e6-bf8e-d75dd0e468bd.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Elevator Turkey"
        },
        "fall_into_feast": {
          "name": "Fall into feast",
          "display_name": "Fall into feast",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "7741b58b-ecba-40c2-a701-c208ab7f2d0e.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Fall into feast"
        },
        "food_product_display": {
          "name": "Food Product Display",
          "display_name": "Food Product Display",
          "status": "untested",
          "endpoint": "/v3/pipeline/outfit_transformer/async",
          "result_endpoint": "/v3/pipeline/outfit_transformer/async/results",
          "algo_type": "outfit_transformer",
          "version": "v1",
          "template_id": "73e8afeb-a0a7-4f6b-935b-2e3fd50ac898.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Food Product Display"
        },
        "cozy_harvest": {
          "name": "Cozy Harvest",
          "display_name": "Cozy Harvest",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "590363ef-c093-491c-bf5f-507b3a8480db.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Cozy Harvest"
        },
        "phone_bite": {
          "name": "Phone Bite",
          "display_name": "Phone Bite",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "975b4d75-14d0-4c5f-be0a-c2b9d15ded10.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Phone Bite"
        },
        "be_the_collectible": {
          "name": "Be the Collectible",
          "display_name": "Be the Collectible",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "986d3e0f-e0c2-4ac2-bbc5-0a48da71e662.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Be the Collectible"
        },
        "ice_queen": {
          "name": "Ice Queen",
          "display_name": "Ice Queen",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "f7281c26-0957-4811-9bc9-1bb8cf8cb774.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Ice Queen"
        },
        "shades_on": {
          "name": "Shades On",
          "display_name": "Shades On",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "df222792-3aee-4681-aaf0-9914b01e8c8e.mp4",
          "supported_ratios": [
            "1:1",
            "16:9",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Shades On"
        },
        "skeletal_bloom": {
          "name": "Skeletal Bloom",
          "display_name": "Skeletal Bloom",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "f968142a-3688-402e-a7ce-9332fcca5273.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Skeletal Bloom"
        },
        "magic_reveal_slytherin": {
          "name": "Magic Reveal （Slytherin）",
          "display_name": "Magic Reveal （Slytherin）",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "aa73f29a-a16b-4ac8-8b79-a8221af7e453.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Magic Reveal （Slytherin）"
        },
        "muscles": {
          "name": "Muscles",
          "display_name": "Muscles",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "d60e6872-10ce-4a3b-a0d7-9acae94d4fea.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Muscles"
        },
        "mechanical_wings": {
          "name": "Mechanical Wings",
          "display_name": "Mechanical Wings",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "d403024f-96ea-413f-8987-12692629ce37.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "3:4",
            "4:3"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Mechanical Wings"
        },
        "golden_retriever": {
          "name": "Golden Retriever",
          "display_name": "Golden Retriever",
          "status": "untested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "9e8e8035-8b00-4559-bf74-7a28049d829b.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "9:16",
            "4:3",
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Golden Retriever"
        },
        "back_to_school": {
          "name": "Back to School",
          "display_name": "Back to School",
          "status": "untested",
          "endpoint": "/v3/pipeline/avatar_transformer/async",
          "result_endpoint": "/v3/pipeline/avatar_transformer/async/results",
          "algo_type": "avatar_transformer",
          "version": "v1",
          "template_id": "6f897723-db01-4954-9966-51094340bdbf.mp4",
          "supported_ratios": [
            "3:4",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Back to School"
        },
        "christmas_baby": {
          "name": "Christmas Baby",
          "display_name": "Christmas Baby",
          "status": "untested",
          "endpoint": "/v3/pipeline/pet_dance/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "pet_dance",
          "version": "v1",
          "template_id": "2d4b1db7-b804-4b91-a668-631e69a85ffb.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Christmas Baby"
        },
        "christmas_gift": {
          "name": "Christmas Gift",
          "display_name": "Christmas Gift",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "e4846223-eb80-4289-be6a-fe2ab516da1b.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Christmas Gift"
        },
        "2026": {
          "name": "2026",
          "display_name": "2026",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "adf3f8f9-4d1f-4e95-8501-565fd9b89d14.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: 2026"
        },
        "crystal_ball": {
          "name": "Crystal Ball",
          "display_name": "Crystal Ball",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "23294ef6-3ce1-4e97-8d09-d2e418e55c03.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Crystal Ball"
        },
        "battle": {
          "name": "Battle",
          "display_name": "Battle",
          "status": "untested",
          "endpoint": "/v3/pipeline/outfit_transformer/async",
          "result_endpoint": "/v3/pipeline/outfit_transformer/async/results",
          "algo_type": "outfit_transformer",
          "version": "v1",
          "template_id": "46c702a7-e13e-408d-8876-d432ac19a320.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Battle"
        },
        "girlfriend": {
          "name": "Girlfriend",
          "display_name": "Girlfriend",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "c5ba0934-a70a-438c-97a1-fa1e1a0f92a0.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Girlfriend"
        },
        "motorcycle_boy": {
          "name": "Motorcycle Boy",
          "display_name": "Motorcycle Boy",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "2dd9a902-7386-42c9-9e00-8cc8d028cd51.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "younger_self": {
          "name": "Younger Self",
          "display_name": "Younger Self",
          "status": "untested",
          "endpoint": "/v3/pipeline/outfit_transformer/async",
          "result_endpoint": "/v3/pipeline/outfit_transformer/async/results",
          "algo_type": "outfit_transformer",
          "version": "v1",
          "template_id": "d174b522-9e81-4ee4-a3a7-cc05bbab14b8.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Younger Self"
        },
        "nine_grid_pet": {
          "name": "Nine Grid Pet",
          "display_name": "Nine Grid Pet",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "3dcf3974-560c-4f4f-90de-ef91347b9bc8.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "with_deceased": {
          "name": "With Deceased",
          "display_name": "With Deceased",
          "status": "untested",
          "endpoint": "/v3/pipeline/outfit_transformer/async",
          "result_endpoint": "/v3/pipeline/outfit_transformer/async/results",
          "algo_type": "outfit_transformer",
          "version": "v1",
          "template_id": "d1cf7866-402e-435f-a1a2-a83905fc2d85.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: With Deceased"
        },
        "dinner_party": {
          "name": "Dinner Party",
          "display_name": "Dinner Party",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "7f9238cf-7850-4d2a-b4ab-da89e39ddd85.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "cyberpunk_2026": {
          "name": "Cyberpunk 2026",
          "display_name": "Cyberpunk 2026",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "da7c178e-911f-4380-90fc-57886f1c4c21.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Cyberpunk 2026"
        },
        "hi_2026": {
          "name": "Hi 2026",
          "display_name": "Hi 2026",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "378ca9df-1f1a-4b24-a256-3aeb60dce263.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Hi 2026"
        },
        "midnight_neon": {
          "name": "Midnight Neon",
          "display_name": "Midnight Neon",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "8f9d900b-d5ce-416f-908a-4f62a4adbb22.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "garden_lover": {
          "name": "Garden Lover",
          "display_name": "Garden Lover",
          "status": "untested",
          "endpoint": "/v3/pipeline/outfit_transformer/async",
          "result_endpoint": "/v3/pipeline/outfit_transformer/async/results",
          "algo_type": "outfit_transformer",
          "version": "v1",
          "template_id": "a54cc0a3-fcfa-40e6-86ae-03099681bf01.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Garden Lover"
        },
        "goodnight_kiss": {
          "name": "Goodnight Kiss",
          "display_name": "Goodnight Kiss",
          "status": "untested",
          "endpoint": "/v3/pipeline/outfit_transformer/async",
          "result_endpoint": "/v3/pipeline/outfit_transformer/async/results",
          "algo_type": "outfit_transformer",
          "version": "v1",
          "template_id": "83e7da92-ee22-4981-8922-f8b05c136163.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Goodnight Kiss"
        },
        "christmas_bear": {
          "name": "Christmas Bear",
          "display_name": "Christmas Bear",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "0290743c-cc30-447e-89ee-b5e3675bef26.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Christmas Bear"
        },
        "domineering_ceo": {
          "name": "Domineering CEO",
          "display_name": "Domineering CEO",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "e338c0f8-5469-4653-9681-f489ae0be89c.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "christmas_card": {
          "name": "Christmas Card",
          "display_name": "Christmas Card",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "3f9f3543-23dd-4414-ae6b-7d97ed9abec4.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Christmas Card"
        },
        "fireworks": {
          "name": "Fireworks",
          "display_name": "Fireworks",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "2424df7d-7c75-4981-96cc-2f5360e7c2d9.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Fireworks"
        },
        "photo_restore": {
          "name": "Photo Restore",
          "display_name": "Photo Restore",
          "status": "untested",
          "endpoint": "/v3/pipeline/figure_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "figure_transformer",
          "version": "v1",
          "template_id": "43535e4c-1569-4b7b-870d-6c236e7bcbb5.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Photo Restore"
        },
        "white_sweater": {
          "name": "White Sweater",
          "display_name": "White Sweater",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "586810a5-5954-4b0a-ab1f-ee74e8cd0706.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "elegant_gentle": {
          "name": "Elegant Gentle",
          "display_name": "Elegant Gentle",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "5818f6e2-3afe-40ee-8a14-ee01a123df2a.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "anime_kiss": {
          "name": "Anime Kiss",
          "display_name": "Anime Kiss",
          "status": "untested",
          "endpoint": "/v3/pipeline/outfit_transformer/async",
          "result_endpoint": "/v3/pipeline/outfit_transformer/async/results",
          "algo_type": "outfit_transformer",
          "version": "v1",
          "template_id": "7c5526e4-ac34-4005-9886-ca5b4fcfa0ca.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Anime Kiss"
        },
        "costume_change": {
          "name": "Costume Change",
          "display_name": "Costume Change",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "7f8b2675-649d-4f7f-b408-ff16cdb8174b.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "lens_heartbeat": {
          "name": "Lens Heartbeat",
          "display_name": "Lens Heartbeat",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "573a83fd-803b-4f17-8027-196ff1931f7b.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "santa_claus": {
          "name": "Santa Claus",
          "display_name": "Santa Claus",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "c9b2f812-8bdb-44c4-a100-7c334f914a5e.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Santa Claus"
        },
        "snowfield": {
          "name": "Snowfield",
          "display_name": "Snowfield",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "be68c43b-bd73-4f94-8a31-a8bb16250042.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Snowfield"
        },
        "darkroom_flash": {
          "name": "Darkroom Flash",
          "display_name": "Darkroom Flash",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "e6a1bf0b-ba57-4241-b9d5-abaeffed802f.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "noble_person": {
          "name": "Noble Person",
          "display_name": "Noble Person",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "b35a1c15-844f-442f-ae61-cbb33bfa272e.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "hold_deceased": {
          "name": "Hold Deceased",
          "display_name": "Hold Deceased",
          "status": "untested",
          "endpoint": "/v3/pipeline/outfit_transformer/async",
          "result_endpoint": "/v3/pipeline/outfit_transformer/async/results",
          "algo_type": "outfit_transformer",
          "version": "v1",
          "template_id": "07644730-b4bd-4bef-bb08-07235434e218.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Hold Deceased"
        },
        "boyfriend": {
          "name": "Boyfriend",
          "display_name": "Boyfriend",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "8091e2bf-0eb7-4b75-a06a-1fd123d53c15.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Boyfriend"
        },
        "heavenly_hug": {
          "name": "Heavenly Hug",
          "display_name": "Heavenly Hug",
          "status": "needs_fix",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "82482327-dddd-4133-a105-b7aefc877c98.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "✅ Prompt已修复"
        },
        "christmas_girl": {
          "name": "Christmas Girl",
          "display_name": "Christmas Girl",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "f9dc1c67-8bbf-4f36-be0a-2842c93201dd.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Christmas Girl"
        },
        "fighting_giant": {
          "name": "Fighting Giant",
          "display_name": "Fighting Giant",
          "status": "needs_fix",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion_img2vid/async/results",
          "algo_type": "video_diffusion_img2vid",
          "version": "v3L",
          "template_id": "23524908-5565-461b-a489-fd3f959d8a0e.mp4",
          "supported_ratios": [
            "16:9",
            "1:1",
            "3:4",
            "4:3",
            "9:16"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "✅ Prompt已修复"
        },
        "night_chat": {
          "name": "Night Chat",
          "display_name": "Night Chat",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "f25e0b60-f03a-468f-9d26-8e5412f2d952.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "beach_dance": {
          "name": "Beach Dance",
          "display_name": "Beach Dance",
          "status": "untested",
          "endpoint": "/v3/pipeline/pet_dance/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "pet_dance",
          "version": "v1",
          "template_id": "89a76e78-a3ff-4497-a52c-7158254de350.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Beach Dance"
        },
        "polaroid": {
          "name": "Polaroid",
          "display_name": "Polaroid",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "46a6d384-5863-4480-8880-e337b3993a41.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "christmas_pet": {
          "name": "Christmas Pet",
          "display_name": "Christmas Pet",
          "status": "untested",
          "endpoint": "/v3/pipeline/pet_dance/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "pet_dance",
          "version": "v1",
          "template_id": "1381d9e1-34cf-4cae-8517-98e782c148c7.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Christmas Pet"
        },
        "pet_sled": {
          "name": "Pet Sled",
          "display_name": "Pet Sled",
          "status": "untested",
          "endpoint": "/v3/pipeline/proto_transformer/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "proto_transformer",
          "version": "v1",
          "template_id": "efc34290-26aa-4b64-a579-72033e15ad2b.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Pet Sled"
        },
        "kiss_hand": {
          "name": "Kiss Hand",
          "display_name": "Kiss Hand",
          "status": "needs_fix",
          "endpoint": "/v3/pipeline/outfit_transformer/async",
          "result_endpoint": "/v3/pipeline/outfit_transformer/async/results",
          "algo_type": "outfit_transformer",
          "version": "v1",
          "template_id": "e2830f9d-01b7-4ed5-9451-ab26121002c4.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "✅ Prompt已修复"
        },
        "black_rose": {
          "name": "Black Rose",
          "display_name": "Black Rose",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "8636f6fa-b970-48e1-9746-917eb0b52029.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "couple_kissing": {
          "name": "Couple Kissing",
          "display_name": "Couple Kissing",
          "status": "untested",
          "endpoint": "/v3/pipeline/outfit_transformer/async",
          "result_endpoint": "/v3/pipeline/outfit_transformer/async/results",
          "algo_type": "outfit_transformer",
          "version": "v1",
          "template_id": "d00361dc-d729-4389-92e5-5a0ad24dcbb3.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Couple Kissing"
        },
        "advanced_image": {
          "name": "Advanced Image",
          "display_name": "Advanced Image",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "ae0d7579-3520-4d25-8fb1-75a5810bd45e.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "gentle_pet": {
          "name": "Gentle Pet",
          "display_name": "Gentle Pet",
          "status": "untested",
          "endpoint": "/v3/pipeline/pet_dance/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "algo_type": "pet_dance",
          "version": "v1",
          "template_id": "a7d9ee6a-d198-480c-a3c9-b68ad002c390.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": false,
          "notes": "Template: Gentle Pet"
        },
        "cool_boss": {
          "name": "Cool Boss",
          "display_name": "Cool Boss",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "dfd628a4-d1cb-4fba-b876-4d9c26e0fd63.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        },
        "three_frames": {
          "name": "Three Frames",
          "display_name": "Three Frames",
          "status": "tested",
          "endpoint": "/v3/image/image_edit_gen/async",
          "result_endpoint": "/v3/image/image_edit_gen/async/results",
          "algo_type": "image_edit_gen",
          "version": "v1",
          "template_id": "5b483264-29a5-44f4-9e81-0e19f45d01eb.mp4",
          "supported_ratios": [
            "3:4"
          ],
          "max_batch_size": 1,
          "tested": true,
          "notes": "Tested: Returns image (static effect)",
          "result_type": "image"
        }
      }
    }
  },
  "_future_features": {
    "planned": []
  }
}

path: scripts/logging_config.py
content: #!/usr/bin/env python3
"""
统一日志配置

Usage:
    from logging_config import get_logger
    logger = get_logger(__name__)
"""
import logging
import sys
from typing import Optional


DEFAULT_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"


def configure_logging(
    level: int = logging.INFO,
    format_str: str = DEFAULT_FORMAT,
    date_format: str = DEFAULT_DATE_FORMAT,
    handler: Optional[logging.Handler] = None
) -> None:
    """
    配置根日志记录器
    
    Args:
        level: 日志级别，默认 INFO
        format_str: 日志格式
        date_format: 日期格式
        handler: 自定义handler，默认使用StreamHandler
    """
    if handler is None:
        handler = logging.StreamHandler(sys.stdout)
    
    formatter = logging.Formatter(format_str, date_format)
    handler.setFormatter(formatter)
    
    # 配置根记录器
    root_logger = logging.getLogger()
    root_logger.setLevel(level)
    root_logger.addHandler(handler)
    
    # 设置第三方库的日志级别
    logging.getLogger('urllib3').setLevel(logging.WARNING)
    logging.getLogger('botocore').setLevel(logging.WARNING)


def get_logger(name: str) -> logging.Logger:
    """获取命名日志记录器"""
    return logging.getLogger(name)


# 默认配置（在导入时自动应用）
configure_logging()


path: scripts/txt2img.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Text to Image Generator
Generate images from text prompts using Vivago AI.
"""

import argparse
import json
import logging
import os
import sys
import time
from typing import Optional

# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

try:
    from scripts.vivago_client import create_client
    from scripts.exceptions import MissingCredentialError
except ImportError:
    print("Error: Failed to import required modules. Please ensure you are running from the project root.")
    sys.exit(1)

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    datefmt='%H:%M:%S'
)
logger = logging.getLogger(__name__)

def setup_args():
    parser = argparse.ArgumentParser(
        description='Generate images from text using Vivago AI'
    )
    
    parser.add_argument(
        '--prompt', '-p',
        required=True,
        help='Text description of desired image'
    )
    
    parser.add_argument(
        '--negative-prompt', '-np',
        default='',
        help='What to avoid in generation'
    )
    
    parser.add_argument(
        '--wh-ratio', '-r',
        default='16:9',
        choices=['1:1', '4:3', '3:4', '16:9', '9:16'],
        help='Aspect ratio (default: 16:9)'
    )
    
    parser.add_argument(
        '--batch-size', '-b',
        type=int,
        default=1,
        help='Number of images to generate (1-4, default: 1)'
    )
    
    parser.add_argument(
        '--version', '-v',
        default='v3L',
        help='Model version (default: v3L)'
    )

    parser.add_argument(
        '--port',
        default='hidream-txt2img',
        help='Specific port to use (default: hidream-txt2img)'
    )
    
    parser.add_argument(
        '--output', '-o',
        default='results.json',
        help='Output file for results (default: results.json)'
    )
    
    parser.add_argument(
        '--token',
        default=os.environ.get('HIDREAM_AUTHORIZATION') or os.environ.get('HIDREAM_TOKEN'),
        help='API token (or set HIDREAM_AUTHORIZATION env var)'
    )
    
    # Advanced parameters
    parser.add_argument(
        '--guidance-scale',
        type=float,
        default=7.5,
        help='Guidance scale (default: 7.5)'
    )
    
    parser.add_argument(
        '--sample-steps',
        type=int,
        default=40,
        help='Sampling steps (default: 40)'
    )
    
    parser.add_argument(
        '--seed',
        type=int,
        default=-1,
        help='Random seed (-1 for random, default: -1)'
    )
    
    parser.add_argument(
        '--enhance',
        default='1k',
        choices=['1k', '2k', '4k'],
        help='Enhancement level (default: 1k)'
    )
    
    return parser.parse_args()

def main():
    args = setup_args()
    
    # Validate batch size
    if not 1 <= args.batch_size <= 4:
        logger.error("Batch size must be between 1 and 4")
        sys.exit(1)
    
    # Create client
    try:
        client = create_client([REDACTED_SECRET])
    except MissingCredentialError:
        print("\nError: API Token not found.")
        print("Please set HIDREAM_AUTHORIZATION environment variable or use --token argument.")
        print("\nTo set it for this session:")
        print('export HIDREAM_AUTHORIZATION="your_token_here"')
        sys.exit(1)
    except Exception as e:
        logger.error(f"Failed to create client: {e}")
        sys.exit(1)
    
    # Generate images
    print(f"\n🎨 Generating {args.batch_size} image(s)...")
    print(f"   Prompt: {args.prompt}")
    print(f"   Ratio: {args.wh_ratio}")
    print(f"   Model: {args.version}")
    if args.port:
        print(f"   Port: {args.port}")
    
    start_time = time.time()
    try:
        results = client.text_to_image(
            prompt=args.prompt,
            negative_prompt=args.negative_prompt,
            wh_ratio=args.wh_ratio,
            batch_size=args.batch_size,
            version=args.version,
            port=args.port,
            guidance_scale=args.guidance_scale,
            sample_steps=args.sample_steps,
            seed=args.seed,
            enhance=args.enhance
        )
    except KeyboardInterrupt:
        print("\n\n⚠️  Operation cancelled by user.")
        sys.exit(1)
    except Exception as e:
        print(f"\n❌ Generation failed: {e}")
        sys.exit(1)
        
    duration = time.time() - start_time
    
    if not results:
        print("\n❌ Generation failed: No results returned.")
        sys.exit(1)
    
    # Save results
    output_data = {
        'prompt': args.prompt,
        'parameters': {
            'wh_ratio': args.wh_ratio,
            'batch_size': args.batch_size,
            'version': args.version,
            'guidance_scale': args.guidance_scale,
            'sample_steps': args.sample_steps,
            'seed': args.seed,
            'enhance': args.enhance
        },
        'results': results
    }
    
    try:
        with open(args.output, 'w') as f:
            json.dump(output_data, f, indent=2)
        print(f"\n💾 Results saved to: {args.output}")
    except IOError as e:
        logger.error(f"Failed to save results: {e}")
    
    # Print summary
    print(f"\n✨ Done in {duration:.1f}s")
    
    for i, result in enumerate(results):
        if not isinstance(result, dict):
            print(f"[{i+1}] Invalid result format")
            continue
            
        status = result.get('task_status')
        status_map = {1: '✅ Completed', 3: '❌ Failed', 4: '🚫 Rejected'}
        status_text = status_map.get(status, f'❓ Unknown ({status})')
        
        print(f"\nImage {i+1}: {status_text}")
        if status == 1:
            image_id = result.get('image', '')
            if image_id and image_id.startswith('p_'):
                url = f"https://storage.vivago.ai/image/{image_id}.jpg"
                print(f"   ID:  {image_id}")
                print(f"   URL: {url}")
            elif image_id:
                print(f"   URL: {image_id}")


if __name__ == '__main__':
    main()


path: scripts/img2video.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Image to Video Generator
Generate videos from images using Vivago AI.
"""

import argparse
import json
import logging
import os
import sys

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from scripts.vivago_client import create_client

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)


def main():
    parser = argparse.ArgumentParser(
        description='Generate videos from images using Vivago AI'
    )
    
    parser.add_argument(
        '--prompt', '-p',
        required=True,
        help='Text description of video motion'
    )
    
    parser.add_argument(
        '--image', '-i',
        required=True,
        help='Path to source image or image_uuid'
    )
    
    parser.add_argument(
        '--wh-ratio', '-r',
        default='16:9',
        choices=['1:1', '4:3', '3:4', '16:9', '9:16'],
        help='Aspect ratio (default: 16:9)'
    )
    
    parser.add_argument(
        '--version', '-v',
        default='v3Pro',
        choices=['v3Pro', 'v3L', 'kling-video-o1'],
        help='Model version (default: v3Pro)'
    )
    
    parser.add_argument(
        '--duration', '-d',
        type=int,
        default=5,
        choices=[5, 10],
        help='Video duration in seconds (default: 5)'
    )
    
    parser.add_argument(
        '--mode', '-m',
        default='Slow',
        choices=['Slow', 'Fast'],
        help='Generation mode (default: Slow)'
    )
    
    parser.add_argument(
        '--fast-mode',
        action='store_true',
        help='Enable fast mode'
    )
    
    parser.add_argument(
        '--motion-strength',
        type=int,
        default=9,
        help='Motion strength (default: 9)'
    )
    
    parser.add_argument(
        '--negative-prompt', '-np',
        default='',
        help='What to avoid in generation'
    )
    
    parser.add_argument(
        '--output', '-o',
        default='video_result.json',
        help='Output file for results (default: video_result.json)'
    )
    
    parser.add_argument(
        '--token',
        default=os.environ.get('HIDREAM_TOKEN'),
        help='API token (or set HIDREAM_TOKEN env var)'
    )
    
    parser.add_argument(
        '--storage-ak',
        default=os.environ.get('STORAGE_AK'),
        help='[Deprecated] Storage access key - no longer required'
    )
    
    parser.add_argument(
        '--storage-sk',
        default=os.environ.get('STORAGE_SK'),
        help='[Deprecated] Storage secret key - no longer required'
    )
    
    args = parser.parse_args()
    
    # Create client
    try:
        client = create_client([REDACTED_SECRET])
    except ValueError as e:
        logger.error(f"Failed to create client: {e}")
        sys.exit(1)
    
    # Handle image input
    image_uuid = args.image
    if os.path.exists(args.image):
        logger.info(f"Uploading image: {args.image}")
        try:
            image_uuid = client.upload_image(args.image)
            logger.info(f"Image uploaded: {image_uuid}")
        except Exception as e:
            logger.error(f"Failed to upload image: {e}")
            sys.exit(1)
    else:
        logger.info(f"Using existing image_uuid: {image_uuid}")
    
    # Generate video
    logger.info(f"Generating video from image: {image_uuid}")
    logger.info(f"Prompt: {args.prompt}")
    logger.info(f"Version: {args.version}, Duration: {args.duration}s, Mode: {args.mode}")
    
    results = client.image_to_video(
        prompt=args.prompt,
        image_uuid=image_uuid,
        wh_ratio=args.wh_ratio,
        version=args.version,
        duration=args.duration,
        mode=args.mode,
        fast_mode=args.fast_mode,
        motion_strength=args.motion_strength,
        negative_prompt=args.negative_prompt
    )
    
    if not results:
        logger.error("Video generation failed")
        sys.exit(1)
    
    # Save results
    output_data = {
        'prompt': args.prompt,
        'image_uuid': image_uuid,
        'parameters': {
            'wh_ratio': args.wh_ratio,
            'version': args.version,
            'duration': args.duration,
            'mode': args.mode,
            'fast_mode': args.fast_mode,
            'motion_strength': args.motion_strength
        },
        'results': results
    }
    
    with open(args.output, 'w') as f:
        json.dump(output_data, f, indent=2)
    
    logger.info(f"Results saved to: {args.output}")
    
    # Print video URLs
    for i, result in enumerate(results):
        status = result.get('task_status')
        status_text = {1: '✓ Completed', 3: '✗ Failed', 4: '⊘ Rejected'}.get(status, '? Unknown')
        video_url = result.get('video') or result.get('image') or 'N/A'
        
        print(f"\n[{i+1}] {status_text}")
        print(f"    URL: {video_url}")
        
        if result.get('task_completion'):
            print(f"    Progress: {result['task_completion']*100:.0f}%")


if __name__ == '__main__':
    main()


path: scripts/__init__.py
content: """
Vivago AI Skill - Python Package

Usage:
    from scripts import VivagoClient
    from scripts.enums import TaskStatus
    from scripts.exceptions import TaskFailedError
"""

from .vivago_client import VivagoClient, create_client
from .template_manager import TemplateManager
from .config_loader import load_ports_config
from .enums import TaskStatus, AspectRatio, PortCategory, PortName, ModuleName
from .exceptions import (
    VivagoError, VivagoAPIError, InvalidPortError,
    TaskFailedError, TaskRejectedError, TaskTimeoutError,
    ImageUploadError, MissingCredentialError
)

__version__ = "0.3.0"
__all__ = [
    "VivagoClient",
    "create_client",
    "TemplateManager",
    "load_ports_config",
    "TaskStatus",
    "AspectRatio",
    "PortCategory",
    "PortName",
    "ModuleName",
    "VivagoError",
    "VivagoAPIError",
    "InvalidPortError",
    "TaskFailedError",
    "TaskRejectedError",
    "TaskTimeoutError",
    "ImageUploadError",
    "MissingCredentialError",
]


path: scripts/template_manager.py
content: #!/usr/bin/env python3
"""
模板管理器 - 动态加载和管理视频模板
支持从JSON文件动态加载模板配置
"""
import json
import os
from typing import Dict, Any, Optional

class TemplateManager:
    """
    视频模板管理器
    
    支持动态加载模板配置，便于后续扩展更多模板
    """
    
    def __init__(self, templates_file: Optional[str] = None):
        """
        初始化模板管理器
        
        Args:
            templates_file: 模板配置文件路径，默认使用内置路径
        """
        if templates_file is None:
            # 默认路径
            base_dir = os.path.dirname(os.path.abspath(__file__))
            templates_file = os.path.join(base_dir, 'templates_data.json')
        
        self.templates_file = templates_file
        self.templates: Dict[str, Dict[str, Any]] = {}
        self._load_templates()
    
    def _load_templates(self):
        """从文件加载模板数据"""
        if not os.path.exists(self.templates_file):
            print(f"警告: 模板文件不存在 {self.templates_file}")
            return
        
        try:
            with open(self.templates_file, 'r', encoding='utf-8') as f:
                data = json.load(f)
            
            # 解析模板数据
            for item in data:
                master = item.get('master_template', {})
                gen_params = item.get('gen_params', {})
                
                template_id = self._generate_template_id(master.get('name', ''))
                
                # 解析嵌套的params结构
                params = gen_params.get('params', {})
                inner_params = params.get('params', {})  # 内层params包含custom_params
                
                # 判断是否为受限类型（只支持1:1）
                algo_type = gen_params.get('algo_type', '')
                restricted_ratio = algo_type == 'video_diffusion_img2vid'
                
                self.templates[template_id] = {
                    'name': master.get('name'),
                    'uuid': master.get('uuid'),
                    'algo_type': algo_type,
                    'endpoint': gen_params.get('generate_path', '').replace('/api/gw', ''),
                    'result_endpoint': self._get_result_endpoint(gen_params),
                    'module': params.get('module'),
                    'version': params.get('version'),
                    'prompt': params.get('prompt', ''),
                    'template_id': params.get('template_id'),
                    'custom_params': inner_params.get('custom_params', {}),
                    'params': params,
                    'inputs': gen_params.get('inputs', []),
                    # 保存失效标记
                    'deprecated': gen_params.get('deprecated', False),
                    'deprecated_reason': gen_params.get('deprecated_reason', ''),
                    # 标记受限比例
                    'restricted_ratio': restricted_ratio,
                    'supported_ratios': ['1:1'] if restricted_ratio else ['16:9', '1:1', '9:16', '4:3', '3:4'],
                }
            
            print(f"已加载 {len(self.templates)} 个模板")
            
        except Exception as e:
            print(f"加载模板失败: {e}")
    
    def _generate_template_id(self, name: str) -> str:
        """从模板名称生成ID"""
        return (name.lower()
                .replace(' ', '_')
                .replace("'", "")
                .replace('-', '_')
                .replace('（', '')
                .replace('）', '')
                .replace('(', '')
                .replace(')', ''))
    
    def _get_result_endpoint(self, gen_params: Dict) -> str:
        """确定回调路径"""
        algo_type = gen_params.get('algo_type', '')
        result_path = gen_params.get('result_path', '')
        
        # 对于 video_diffusion_img2vid 类型的模板，使用对应的回调路径
        if algo_type == 'video_diffusion_img2vid':
            return '/v3/video/video_diffusion_img2vid/async/results'
        
        # 对于其他模板类型使用 video_diffusion 回调路径
        # 因为 pipeline 回调路径经常返回 404
        if algo_type in ['proto_transformer', 'avatar_transformer', 'style_transformer']:
            return '/v3/video/video_diffusion/async/results'
        
        # 其他使用自己的回调路径
        return result_path.replace('/api/gw', '') if result_path else '/v3/video/video_diffusion/async/results'
    
    def get_template(self, template_id: str) -> Optional[Dict[str, Any]]:
        """
        获取模板配置
        
        Args:
            template_id: 模板ID
            
        Returns:
            模板配置字典，如果模板已失效会添加警告信息
        """
        template = self.templates.get(template_id)
        if template:
            # 检查模板是否已失效
            if template.get('deprecated'):
                print(f"⚠️  警告: 模板 '{template_id}' 已失效")
                print(f"   原因: {template.get('deprecated_reason', 'API端点不再支持')}")
                print(f"   请使用其他替代模板")
        return template
    
    def list_templates(self) -> Dict[str, str]:
        """
        列出所有可用模板
        
        Returns:
            {template_id: template_name}
        """
        return {k: v['name'] for k, v in self.templates.items()}
    
    def get_template_api_config(self, template_id: str) -> Dict[str, Any]:
        """
        获取模板的API配置（用于api_ports.json）
        
        Args:
            template_id: 模板ID
            
        Returns:
            API配置字典
        """
        template = self.get_template(template_id)
        if not template:
            return {}
        
        # 获取支持的宽高比
        supported_ratios = ['16:9', '1:1', '9:16', '3:4', '4:3']
        for input_item in template.get('inputs', []):
            if input_item.get('key') == 'wh_ratio' and 'value_list' in input_item:
                supported_ratios = input_item['value_list']
                break
        
        return {
            'name': template['name'],
            'display_name': template['name'],
            'status': 'untested',
            'endpoint': template['endpoint'],
            'result_endpoint': template['result_endpoint'],
            'algo_type': template['algo_type'],
            'template_id': template['template_id'],
            'supported_ratios': supported_ratios,
            'max_batch_size': 1,
            'tested': False,
            'notes': f'Template: {template["name"]}'
        }
    
    def build_request_data(self, template_id: str, image_uuid: str, **kwargs) -> Dict[str, Any]:
        """
        构建模板请求的完整数据
        
        Args:
            template_id: 模板ID
            image_uuid: 输入图片UUID
            **kwargs: 其他参数
            
        Returns:
            请求数据字典
        """
        template = self.get_template(template_id)
        if not template:
            raise ValueError(f"未知模板: {template_id}")
        
        # 获取模板的基础参数
        base_params = template.get('params', {})
        custom_params = template.get('custom_params', {})
        
        # 构建custom_params，保留原有的所有参数
        final_custom_params = dict(custom_params)  # 复制所有原始custom_params
        final_custom_params['master_template_id'] = custom_params.get('master_template_id', template['template_id'])
        
        # 关键: custom_params 中也需要 wh_ratio (抓包发现的差异)
        wh_ratio = kwargs.get('wh_ratio', '1:1')
        final_custom_params['wh_ratio'] = wh_ratio
        
        # 添加用户传入的额外custom_params
        if 'custom_params' in kwargs:
            final_custom_params.update(kwargs['custom_params'])
        
        # 构建请求数据
        data = {
            'module': template['module'],
            'version': template['version'],
            'prompt': kwargs.get('prompt', base_params.get('prompt', '')),
            'images': [image_uuid],
            'masks': [],
            'videos': [],
            'audios': [],
            'params': {
                'mode': kwargs.get('mode', base_params.get('params', {}).get('mode', 'Fast')),
                'style': kwargs.get('style', 'default'),
                'height': kwargs.get('height', -1),
                'width': kwargs.get('width', -1),
                'seed': kwargs.get('seed', -1),
                'duration': kwargs.get('duration', base_params.get('params', {}).get('duration', 5)),
                'motion': kwargs.get('motion', 0),
                'x': kwargs.get('x', 0),
                'y': kwargs.get('y', 0),
                'z': kwargs.get('z', 0),
                'reserved_str': kwargs.get('reserved_str', ''),
                'batch_size': 1,
                'wh_ratio': kwargs.get('wh_ratio', '1:1'),
                'custom_params': final_custom_params
            },
            'en_prompt': kwargs.get('en_prompt', ''),
            'negative_prompt': kwargs.get('negative_prompt', ''),
            'en_negative_prompt': kwargs.get('en_negative_prompt', ''),
            'magic_prompt': kwargs.get('magic_prompt', ''),
            'template_id': template['template_id'],
            'upstream_id': kwargs.get('upstream_id', ''),
            'pipeline_id': kwargs.get('pipeline_id', ''),
            'create_type': 'effect_video_generate',
            'cost_number': kwargs.get('cost_number', 30),
            'request_id': kwargs.get('request_id', ''),
            # 新增 create_submit 字段（网页端必需）
            'create_submit': {
                'create_type': 'effect_video_generate',
                'cost_number': kwargs.get('cost_number', 30),
                'create_number': 1,
                'model_provider': 'vivago',
                'model_name': f"{template['module']}-{template['version']}",
                'effect_type': 'video',
                'effect_id': template['template_id'],
                'effect_name': template['name'],
                'upload_type': 'assets',
                'resource_type': 'image'
            }
        }
        
        return data
    
    def export_to_api_ports(self) -> Dict[str, Any]:
        """
        导出所有模板到api_ports.json格式
        
        Returns:
            端口配置字典
        """
        ports = {}
        for template_id in self.templates:
            config = self.get_template_api_config(template_id)
            if config:
                ports[template_id] = config
        return ports


# 全局模板管理器实例
_template_manager = None

def get_template_manager(templates_file: Optional[str] = None) -> TemplateManager:
    """
    获取全局模板管理器实例（单例模式）
    
    Args:
        templates_file: 模板配置文件路径
        
    Returns:
        TemplateManager实例
    """
    global _template_manager
    if _template_manager is None:
        _template_manager = TemplateManager(templates_file)
    return _template_manager


if __name__ == '__main__':
    # 测试
    manager = get_template_manager()
    
    print("\n可用模板:")
    for tid, name in manager.list_templates().items():
        print(f"  - {tid}: {name}")
    
    print("\n测试获取模板配置:")
    config = manager.get_template('renovation_old_photos')
    if config:
        print(f"  模板: {config['name']}")
        print(f"  端点: {config['endpoint']}")
        print(f"  algo_type: {config['algo_type']}")


path: scripts/generate_image.py
content: #!/usr/bin/env python3
"""
生成图片并保存到本地 - 自动发送结果版本
"""
import os
import sys
import argparse

# Add scripts directory to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'scripts'))

from vivago_client import create_client


def main():
    parser = argparse.ArgumentParser(description='Generate image using Vivago AI')
    parser.add_argument('prompt', help='Image description')
    parser.add_argument('--port', default='kling-image', help='Model port (kling-image, hidream-txt2img, nano-banana)')
    parser.add_argument('--ratio', default='16:9', help='Aspect ratio (1:1, 4:3, 3:4, 16:9, 9:16)')
    parser.add_argument('--batch-size', type=int, default=1, help='Number of images to generate (1-4)')
    
    args = parser.parse_args()
    
    print(f"🎨 正在生成图片...")
    print(f"   Prompt: {args.prompt}")
    print(f"   模型: {args.port}")
    print(f"   比例: {args.ratio}")
    print()
    
    try:
        client = create_client()
        
        # 生成图片
        results = client.text_to_image(
            prompt=args.prompt,
            port=args.port,
            wh_ratio=args.ratio,
            batch_size=args.batch_size
        )
        
        if not results:
            print("❌ 生成失败")
            return 1
        
        # 下载并保存每张图片
        saved_files = []
        for i, result in enumerate(results):
            image_id = result.get('image')
            if not image_id:
                continue
            
            print(f"📥 正在下载图片 {i+1}/{len(results)}...")
            
            # 尝试下载
            output_path = f"/tmp/{image_id}.jpg"
            downloaded_path = client.download_image(image_id, output_path)
            
            if downloaded_path and os.path.exists(downloaded_path):
                file_size = os.path.getsize(downloaded_path)
                print(f"   ✅ 已保存: {downloaded_path} ({file_size/1024:.1f} KB)")
                saved_files.append(downloaded_path)
            else:
                print(f"   ❌ 下载失败")
        
        print()
        print("=" * 60)
        
        if saved_files:
            print(f"✅ 成功生成并下载 {len(saved_files)} 张图片")
            # 输出文件路径供外部工具读取
            with open('/tmp/vivago_output_files.txt', 'w') as f:
                for path in saved_files:
                    f.write(path + '\n')
            return 0
        else:
            print("❌ 图片下载失败")
            return 1
        
    except Exception as e:
        print(f"❌ 错误: {e}")
        import traceback
        traceback.print_exc()
        return 1


if __name__ == '__main__':
    sys.exit(main())


path: scripts/txt2vid.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Text to Video Generator
Generate videos from text prompts using Vivago AI.
"""

import argparse
import json
import logging
import os
import sys
import time
from typing import Optional

# Add parent directory to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

try:
    from scripts.vivago_client import create_client
    from scripts.exceptions import MissingCredentialError
except ImportError:
    print("Error: Failed to import required modules. Please ensure you are running from the project root.")
    sys.exit(1)

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s',
    datefmt='%H:%M:%S'
)
logger = logging.getLogger(__name__)

def setup_args():
    parser = argparse.ArgumentParser(
        description='Generate videos from text using Vivago AI'
    )
    
    parser.add_argument(
        '--prompt', '-p',
        required=True,
        help='Text description of desired video'
    )
    
    parser.add_argument(
        '--negative-prompt', '-np',
        default='',
        help='What to avoid in generation'
    )
    
    parser.add_argument(
        '--wh-ratio', '-r',
        default='16:9',
        choices=['1:1', '4:3', '3:4', '16:9', '9:16'],
        help='Aspect ratio (default: 16:9)'
    )
    
    parser.add_argument(
        '--duration', '-d',
        type=int,
        default=5,
        choices=[5, 10],
        help='Video duration in seconds (default: 5)'
    )
    
    parser.add_argument(
        '--port',
        default='v3Pro',
        help='Specific port to use (v3Pro, v3L, kling-video)'
    )
    
    parser.add_argument(
        '--output', '-o',
        default='video_results.json',
        help='Output file for results (default: video_results.json)'
    )
    
    parser.add_argument(
        '--token',
        default=os.environ.get('HIDREAM_AUTHORIZATION') or os.environ.get('HIDREAM_TOKEN'),
        help='API token (or set HIDREAM_AUTHORIZATION env var)'
    )
    
    return parser.parse_args()

def main():
    args = setup_args()
    
    # Create client
    try:
        client = create_client([REDACTED_SECRET])
    except MissingCredentialError:
        print("\nError: API Token not found.")
        print("Please set HIDREAM_AUTHORIZATION environment variable or use --token argument.")
        sys.exit(1)
    except Exception as e:
        logger.error(f"Failed to create client: {e}")
        sys.exit(1)
    
    # Generate video
    print(f"\n🎥 Generating video...")
    print(f"   Prompt: {args.prompt}")
    print(f"   Ratio: {args.wh_ratio}")
    print(f"   Port: {args.port}")
    print(f"   Duration: {args.duration}s")
    
    start_time = time.time()
    try:
        results = client.text_to_video(
            prompt=args.prompt,
            negative_prompt=args.negative_prompt,
            wh_ratio=args.wh_ratio,
            duration=args.duration,
            port=args.port
        )
    except KeyboardInterrupt:
        print("\n\n⚠️  Operation cancelled by user.")
        sys.exit(1)
    except Exception as e:
        print(f"\n❌ Generation failed: {e}")
        sys.exit(1)
        
    duration = time.time() - start_time
    
    if not results:
        print("\n❌ Generation failed: No results returned.")
        sys.exit(1)
    
    # Save results
    output_data = {
        'prompt': args.prompt,
        'parameters': {
            'wh_ratio': args.wh_ratio,
            'duration': args.duration,
            'port': args.port
        },
        'results': results
    }
    
    try:
        with open(args.output, 'w') as f:
            json.dump(output_data, f, indent=2)
        print(f"\n💾 Results saved to: {args.output}")
    except IOError as e:
        logger.error(f"Failed to save results: {e}")
    
    # Print summary
    print(f"\n✨ Done in {duration:.1f}s")
    
    for i, result in enumerate(results):
        status = result.get('task_status')
        status_map = {1: '✅ Completed', 3: '❌ Failed', 4: '🚫 Rejected'}
        status_text = status_map.get(status, f'❓ Unknown ({status})')
        
        print(f"\nVideo {i+1}: {status_text}")
        if status == 1:
            video_id = result.get('video', '')
            if video_id:
                # Remove 'v_' prefix if present, but don't append .mp4 if it already has it
                clean_id = video_id[2:] if video_id.startswith('v_') else video_id
                url = f"https://media.vivago.ai/{clean_id}"
                if not url.endswith('.mp4'):
                    url += '.mp4'
                print(f"   ID:  {video_id}")
                print(f"   URL: {url}")

if __name__ == '__main__':
    main()


path: scripts/exceptions.py
content: #!/usr/bin/env python3
"""
Vivago AI Skill 异常类定义
"""


class VivagoError(Exception):
    """Vivago AI Skill 基础异常"""
    pass


class VivagoAPIError(VivagoError):
    """API调用异常"""
    def __init__(self, message: str, code: int = None, response: dict = None):
        super().__init__(message)
        self.code = code
        self.response = response


class MissingCredentialError(VivagoError):
    """缺少认证信息"""
    pass


class InvalidPortError(VivagoError):
    """无效端口配置"""
    def __init__(self, port: str, category: str = None, available: list = None):
        message = f"Invalid port: {port}"
        if category:
            message = f"Invalid port '{port}' for category '{category}'"
        if available:
            message += f". Available: {', '.join(available)}"
        super().__init__(message)
        self.port = port
        self.category = category
        self.available = available


class TaskError(VivagoError):
    """任务执行异常基类"""
    def __init__(self, task_id: str, message: str):
        super().__init__(f"Task {task_id}: {message}")
        self.task_id = task_id


class TaskFailedError(TaskError):
    """任务失败"""
    def __init__(self, task_id: str, reason: str = None):
        message = reason or "Task failed"
        super().__init__(task_id, message)
        self.reason = reason


class TaskRejectedError(TaskError):
    """任务被拒绝（内容审核）"""
    def __init__(self, task_id: str, reason: str = "Content rejected"):
        super().__init__(task_id, f"Rejected - {reason}")
        self.reason = reason


class TaskTimeoutError(TaskError):
    """任务超时"""
    def __init__(self, task_id: str, timeout_seconds: int):
        super().__init__(task_id, f"Timeout after {timeout_seconds}s")
        self.timeout_seconds = timeout_seconds


class ImageUploadError(VivagoError):
    """图片上传失败"""
    def __init__(self, path: str, reason: str = None):
        message = f"Failed to upload image: {path}"
        if reason:
            message += f" ({reason})"
        super().__init__(message)
        self.path = path
        self.reason = reason


class TemplateNotFoundError(VivagoError):
    """模板未找到"""
    def __init__(self, template_id: str, available: list = None):
        message = f"Template not found: {template_id}"
        if available:
            suggestions = [t for t in available if template_id.lower() in t.lower()][:5]
            if suggestions:
                message += f". Did you mean: {', '.join(suggestions)}?"
        super().__init__(message)
        self.template_id = template_id


path: scripts/export_templates.py
content: #!/usr/bin/env python3
"""
将 template_list_full.json 解析并导出为 templates_data.json
用于 template_manager.py 动态加载
"""
import json
import re
import os

def parse_and_export():
    """解析模板文件并导出"""
    
    input_file = 'template_list_full.json'
    output_file = 'templates_data.json'
    
    if not os.path.exists(input_file):
        print(f"错误: 文件不存在 {input_file}")
        return
    
    # 读取文件内容
    with open(input_file, 'r', encoding='utf-8') as f:
        content = f.read()
    
    # 尝试解析JSON
    try:
        data = json.loads(content)
    except json.JSONDecodeError:
        print("JSON格式错误，尝试修复...")
        # 尝试修复常见的JSON格式问题
        content = re.sub(r'},\s*},', '}, },', content)
        content = re.sub(r'},\s*}\s*,\s*\]', '} } ]', content)
        try:
            data = json.loads(content)
        except json.JSONDecodeError as e:
            print(f"无法修复JSON: {e}")
            print("尝试手动提取模板数据...")
            # 手动提取
            data = extract_templates_manually(content)
    
    # 验证数据
    if not isinstance(data, list):
        print(f"错误: 数据格式不正确，期望列表但得到 {type(data)}")
        return
    
    print(f"成功解析 {len(data)} 个模板")
    
    # 简化和标准化数据
    simplified = []
    for item in data:
        master = item.get('master_template', {})
        gen_params = item.get('gen_params', {})
        
        simplified_item = {
            'master_template': {
                'uuid': master.get('uuid'),
                'name': master.get('name')
            },
            'gen_params': {
                'algo_type': gen_params.get('algo_type'),
                'generate_path': gen_params.get('generate_path'),
                'result_path': gen_params.get('result_path'),
                'component_type': gen_params.get('component_type', 'video'),
                'inputs': gen_params.get('inputs', []),
                'params': gen_params.get('params', {}),
                'params_version': gen_params.get('params_version', 'v3')
            }
        }
        simplified.append(simplified_item)
    
    # 保存到输出文件
    with open(output_file, 'w', encoding='utf-8') as f:
        json.dump(simplified, f, indent=2, ensure_ascii=False)
    
    print(f"已导出到 {output_file}")
    
    # 显示统计信息
    algo_types = {}
    for item in simplified:
        algo = item['gen_params']['algo_type']
        name = item['master_template']['name']
        if algo not in algo_types:
            algo_types[algo] = []
        algo_types[algo].append(name)
    
    print("\n按 algo_type 分类:")
    for algo, names in sorted(algo_types.items()):
        print(f"  {algo}: {len(names)} 个")
        for name in names:
            print(f"    - {name}")

def extract_templates_manually(content: str) -> list:
    """手动从文本中提取模板数据"""
    templates = []
    
    # 使用正则表达式提取关键信息
    pattern = r'"master_template":\s*\{[^}]*"uuid":\s*"([^"]+)"[^}]*"name":\s*"([^"]+)"[^}]*\}'
    matches = re.findall(pattern, content, re.DOTALL)
    
    for uuid, name in matches:
        # 查找对应的 gen_params
        # 简化处理，只提取基本信息
        templates.append({
            'master_template': {'uuid': uuid, 'name': name},
            'gen_params': {
                'algo_type': 'unknown',
                'generate_path': '',
                'result_path': '',
                'params': {}
            }
        })
    
    return templates

if __name__ == '__main__':
    os.chdir('/root/.openclaw/skills/vivago-ai/scripts')
    parse_and_export()


path: scripts/parse_templates.py
content: #!/usr/bin/env python3
"""
解析 template_list 并添加到 api_ports.json
"""
import json
import re

def parse_template_list():
    # 读取文件内容
    with open('template_list_full.json', 'r') as f:
        content = f.read()
    
    # 修复格式问题：移除对象之间的多余逗号
    content = re.sub(r'},\s*},', '}, },', content)
    content = re.sub(r'},\s*}\s*,\s*\]', '} } ]', content)
    
    # 尝试解析
    try:
        data = json.loads(content)
    except json.JSONDecodeError as e:
        print(f"JSON 解析错误: {e}")
        print("尝试逐个解析对象...")
        
        # 手动提取每个模板对象
        pattern = r'"master_template":\s*\{[^}]+"uuid":\s*"([^"]+)",\s*"name":\s*"([^"]+)"[^}]*\}[^}]*"gen_params":\s*\{[^}]*"algo_type":\s*"([^"]+)"[^}]*"generate_path":\s*"([^"]+)"[^}]*"result_path":\s*"([^"]+)"'
        
        matches = re.findall(pattern, content, re.DOTALL)
        print(f"找到 {len(matches)} 个模板")
        
        templates = []
        for match in matches:
            uuid, name, algo_type, gen_path, res_path = match
            templates.append({
                'uuid': uuid,
                'name': name,
                'algo_type': algo_type,
                'generate_path': gen_path,
                'result_path': res_path
            })
        
        return templates
    
    # 如果成功解析
    templates = []
    for item in data:
        master = item.get('master_template', {})
        gen_params = item.get('gen_params', {})
        
        templates.append({
            'uuid': master.get('uuid'),
            'name': master.get('name'),
            'algo_type': gen_params.get('algo_type'),
            'generate_path': gen_params.get('generate_path'),
            'result_path': gen_params.get('result_path'),
            'module': gen_params.get('params', {}).get('module'),
            'version': gen_params.get('params', {}).get('version'),
            'prompt': gen_params.get('params', {}).get('prompt', '')
        })
    
    return templates

def main():
    templates = parse_template_list()
    print(f"共解析 {len(templates)} 个模板")
    
    # 按 algo_type 分类
    by_algo = {}
    for t in templates:
        algo = t.get('algo_type', 'unknown')
        if algo not in by_algo:
            by_algo[algo] = []
        by_algo[algo].append(t)
    
    print("\n按 algo_type 分类:")
    for algo, items in by_algo.items():
        print(f"\n{algo}: {len(items)} 个")
        for item in items[:3]:  # 只显示前3个
            print(f"  - {item['name']} ({item['uuid'][:8]}...)")
        if len(items) > 3:
            print(f"  ... 还有 {len(items)-3} 个")

if __name__ == '__main__':
    main()


path: scripts/config/image_to_video.json
content: {
  "categories": {
    "image_to_video": {
      "name": "图生视频",
      "name_en": "Image to Video",
      "status": "active",
      "default_port": "v3Pro",
      "description": "从静态图像生成视频",
      "warning": "生成时间较长(2-3分钟)，请谨慎调用",
      "ports": {
        "v3Pro": {
          "name": "V3 Pro",
          "display_name": "Vivago.ai 2.0",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3Pro",
          "supported_ratios": [
            "keep",
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "slow",
          "quality": "excellent",
          "notes": "高质量，生成较慢"
        },
        "v3L": {
          "name": "V3 Lite",
          "display_name": "Vivago.ai 2.0 360p",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3L",
          "supported_ratios": [
            "keep",
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "fast",
          "quality": "good",
          "notes": "360p分辨率，生成速度快"
        },
        "kling-video": {
          "name": "Kling Video",
          "display_name": "Kling video O1",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_img2vid/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "kling-video-o1",
          "supported_ratios": [
            "keep",
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "medium",
          "quality": "excellent",
          "notes": "Kling视频模型，质量优秀"
        }
      }
    }
  }
}

path: scripts/config/image_to_image.json
content: {
  "categories": {
    "image_to_image": {
      "name": "图生图",
      "name_en": "Image to Image",
      "status": "active",
      "default_port": "kling-image",
      "description": "图像风格转换、编辑、AI肖像、虚拟试衣等功能",
      "notes": "支持多图输入融合，可用于风格迁移、图像编辑、AI换脸、虚拟试衣等场景",
      "ports": {
        "kling-image": {
          "name": "Kling Image Img2Img",
          "display_name": "Kling O1",
          "status": "tested",
          "endpoint": "/v3/image/image_gen_kling/async",
          "result_endpoint": "/v3/image/txt2img/async/results",
          "version": "kling-image-o1",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 4,
          "max_images": 5,
          "tested": true,
          "speed": "fast",
          "quality": "excellent",
          "notes": "Kling O1 图生图，速度快，支持多图输入，enhance=2k，推荐使用"
        },
        "nano-banana": {
          "name": "Nano Banana Img2Img",
          "display_name": "Nano Banana 2",
          "status": "tested",
          "endpoint": "/v3/image/image_gen_std/async",
          "result_endpoint": "/v3/image/txt2img/async/results",
          "version": "nano-banana-2",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "max_images": 5,
          "tested": true,
          "speed": "slow",
          "quality": "excellent",
          "notes": "Nano Banana 2 图生图，质量优秀但较慢，支持多图输入"
        }
      }
    }
  }
}

path: scripts/config/text_to_video.json
content: {
  "categories": {
    "text_to_video": {
      "name": "文生视频",
      "name_en": "Text to Video",
      "status": "testing",
      "default_port": "v3L",
      "description": "从文本描述生成视频",
      "warning": "生成时间较长(2-3分钟)，请谨慎调用",
      "ports": {
        "v3L": {
          "name": "V3 Lite",
          "display_name": "Vivago.ai 2.0 360p",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3L",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "fast",
          "quality": "good",
          "notes": "360p文生视频/图生视频，速度快"
        },
        "v3Pro": {
          "name": "V3 Pro",
          "display_name": "Vivago.ai 2.0",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3Pro",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "slow",
          "quality": "excellent",
          "notes": "高质量文生视频/图生视频"
        },
        "kling-video": {
          "name": "Kling Video",
          "display_name": "Kling video O1",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_gen2vid/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "kling-video-o1",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "medium",
          "quality": "excellent",
          "notes": "文生视频/图生视频已测试，使用 gen2vid 端点"
        }
      }
    }
  }
}

path: scripts/config/keyframe_to_video.json
content: {
  "categories": {
    "keyframe_to_video": {
      "name": "视频首尾帧",
      "name_en": "Keyframe-to-Video",
      "status": "active",
      "default_port": "v3L",
      "description": "根据首尾帧生成过渡视频",
      "ports": {
        "v3L": {
          "name": "V3 Lite Keyframe",
          "display_name": "Vivago.ai 2.0 360p",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_keyframes/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3L",
          "supported_ratios": [
            "keep",
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "fast",
          "quality": "good",
          "notes": "快速版本，360p分辨率"
        },
        "v3Pro": {
          "name": "V3 Pro Keyframe",
          "display_name": "Vivago.ai 2.0",
          "status": "tested",
          "endpoint": "/v3/video/video_diffusion_keyframes/async",
          "result_endpoint": "/v3/video/video_diffusion/async/results",
          "version": "v3Pro",
          "supported_ratios": [
            "keep",
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 1,
          "supported_durations": [
            5,
            10
          ],
          "tested": true,
          "speed": "slow",
          "quality": "excellent",
          "notes": "高质量版本，慢但效果更好"
        }
      }
    }
  }
}

path: scripts/config/base.json
content: {
  "version": "0.6.0",
  "last_updated": "2026-03-06",
  "base_url": "https://vivago.ai/api/gw",
  "_architecture_notes": {
    "level_1": "一级功能（大功能划分）",
    "level_2": "二级端口（具体API端点配置）",
    "naming": "display_name 与 Vivago 网站模型名称对齐",
    "endpoint_version": "相同端点 + 不同 version = 不同模型名称",
    "extensibility": "未来可扩展的一级功能包括: keyframe_to_video, template_to_video"
  }
}

path: scripts/config/text_to_image.json
content: {
  "categories": {
    "text_to_image": {
      "name": "文生图",
      "name_en": "Text to Image",
      "status": "active",
      "default_port": "kling-image",
      "description": "从文本描述生成图像",
      "ports": {
        "kling-image": {
          "name": "Kling Image",
          "display_name": "Kling O1",
          "status": "tested",
          "endpoint": "/v3/image/image_gen_kling/async",
          "result_endpoint": "/v3/image/txt2img/async/results",
          "version": "kling-image-o1",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 4,
          "tested": true,
          "speed": "fast",
          "quality": "good",
          "notes": "速度快，质量优秀，推荐使用"
        },
        "hidream-txt2img": {
          "name": "HiDream Txt2Img",
          "display_name": "Vivago.ai 2.0",
          "status": "tested",
          "endpoint": "/v3/image/txt2img/async",
          "result_endpoint": "/v3/image/txt2img/async/results",
          "version": "v3L",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 4,
          "tested": true,
          "speed": "medium",
          "quality": "good",
          "notes": "已测试，质量良好"
        },
        "nano-banana": {
          "name": "Nano Banana",
          "display_name": "Nano Banana 2",
          "status": "tested",
          "endpoint": "/v3/image/image_gen_std/async",
          "result_endpoint": "/v3/image/txt2img/async/results",
          "version": "nano-banana-2",
          "supported_ratios": [
            "1:1",
            "4:3",
            "3:4",
            "16:9",
            "9:16"
          ],
          "max_batch_size": 4,
          "tested": true,
          "speed": "slow",
          "quality": "excellent",
          "notes": "已测试，效果优秀，回调使用 txt2img 路径"
        }
      }
    }
  }
}

Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)