Skill: benhuebner01/claw-video-generator (version 1.0.0)

--- name: json2video-pinterest description: Generate Pinterest-optimized vertical videos using JSON2Video API. Supports AI-generated or URL-based images, AI-generated or provided voiceovers, optional subtitles, and zoom effects. Use when creating video content for Pinterest affiliate marketing, creating vertical social media videos, automating video production with JSON2Video API, or generating videos with voiceovers and subtitles. --- # JSON2Video Pinterest Skill Generate vertical videos (1080x1920) optimized for Pinterest using the JSON2Video API. ## Prerequisites 1. **JSON2Video API Key**: Sign up at https://json2video.com/get-api-key/ 2. **Set Environment Variable**: ```bash export JSON2VIDEO_API_KEY="your_api_key_here" ``` ## Quick Start Create a video using a JSON configuration file: ```bash python3 scripts/generate_video.py --config my-video.json --wait ``` ## Configuration Format The video is defined as an array of scenes. Each scene contains: | Property | Type | Description | |----------|------|-------------| | `image` | object | Image configuration (AI-generated or URL) | | `voice` | object | Voice configuration (generated TTS or URL) | | `text_overlay` | string | Optional text displayed on scene | | `subtitles` | boolean | Enable/disable subtitles | | `zoom_effect` | boolean | Add Ken Burns zoom effect | | `duration` | number | Override scene duration (seconds) | ### Image Configuration **AI-Generated Image:** ```json { "image": { "source": "ai", "ai_provider": "flux-schnell", "ai_prompt": "A minimalist workspace with laptop..." } } ``` **Available AI Providers:** - `flux-pro` - Highest quality, realistic images - `flux-schnell` - Fast generation, good quality - `freepik-classic` - Digital artwork style **URL-Based Image:** ```json { "image": { "source": "https://example.com/image.jpg" } } ``` ### Voice Configuration **AI-Generated Voice (TTS):** ```json { "voice": { "source": "generated", "text": "Your voiceover text here", "voice_id": "en-US-EmmaMultilingualNeural", "model": "azure" } } ``` **Provided Audio File:** ```json { "voice": { "source": "https://example.com/voiceover.mp3" } } ``` **Note on Scene Duration**: The voiceover determines scene length automatically. Each scene's duration matches its audio length. For provided audio files, ensure they match your intended scene timing. ### Complete Example ```json { "resolution": "instagram-story", "quality": "high", "cache": true, "scenes": [ { "image": { "source": "ai", "ai_provider": "flux-schnell", "ai_prompt": "Affiliate marketing workspace with laptop and coffee" }, "voice": { "source": "generated", "text": "Here's how to make money with affiliate marketing", "voice_id": "en-US-Neural2-F" }, "text_overlay": "Affiliate Marketing 101", "subtitles": true, "zoom_effect": true } ] } ``` ## Advanced: Split Long Voiceover into Scenes For long scripts, split into multiple scenes with shorter voice segments: ```json { "scenes": [ { "image": { "source": "ai", "ai_prompt": "Hook image" }, "voice": { "source": "generated", "text": "Attention-grabbing hook..." }, "zoom_effect": true }, { "image": { "source": "ai", "ai_prompt": "Step 1 image" }, "voice": { "source": "generated", "text": "Step one is to..." }, "zoom_effect": true }, { "image": { "source": "ai", "ai_prompt": "CTA image" }, "voice": { "source": "generated", "text": "Click the link in bio..." }, "zoom_effect": false } ] } ``` ## Command Reference **Create video from config:** ```bash python3 scripts/generate_video.py --config video.json --wait ``` **Create without waiting:** ```bash python3 scripts/generate_video.py --config video.json --no-wait ``` **Check status of existing project:** ```bash python3 scripts/generate_video.py --project-id YOUR_PROJECT_ID ``` ## Resolution Options | Resolution | Dimensions | Use Case | |------------|------------|----------| | `instagram-story` | 1080x1920 | **Pinterest/Reels/Stories** (recommended) | | `instagram-feed` | 1080x1080 | Square posts | | `full-hd` | 1920x1080 | Landscape YouTube | | `hd` | 1280x720 | Standard HD | | `custom` | Any | Custom dimensions | ## Voice Models & IDs ### Azure (Default - FREE, no credits consumed) **Voice format:** `en-US-EmmaMultilingualNeural` Common Azure voices: - `en-US-EmmaMultilingualNeural` - Female, natural (recommended) - `en-US-GuyNeural` - Male, professional - `en-US-JennyNeural` - Female, friendly - `en-GB-SoniaNeural` - British female - `en-GB-RyanNeural` - British male See [Microsoft Azure Speech Voices](https://learn.microsoft.com/en-us/azure/ai-services/speech-service/language-support?tabs=tts) for full list. ### ElevenLabs (Premium - consumes credits) **Voice names:** Natural names like `Bella`, `Antoni`, `Nova`, `Shimmer` Available voices: Daniel, Serena, Antoni, Bella, Nova, Shimmer, and more. See [ElevenLabs Voice Library](https://elevenlabs.io/app/voice-library) for full list. ## Security - **API Key is NEVER stored in skill files** - API key must be set as environment variable `JSON2VIDEO_API_KEY` - Script validates key exists before any API calls ## Example Files - `scripts/example-config.json` - Basic example with one scene - `scripts/example-advanced.json` - Multi-scene affiliate marketing video ## Advanced Usage See [ADVANCED.md](ADVANCED.md) for: - Multi-scene video architecture patterns - Image source strategies (AI vs URL vs hybrid) - Voiceover patterns and best practices - Subtitle styling options - Pinterest-specific content tips - Batch processing workflows - Credit consumption optimization ## Troubleshooting **Error: "JSON2VIDEO_API_KEY environment variable not set"** → Run: `export JSON2VIDEO_API_KEY="your_key"` **Error: "Render failed"** → Check: Image URLs are publicly accessible → Check: AI prompts don't violate content policies → Check: Audio files are valid MP3/WAV **Video takes too long** → Enable `cache: true` in config → Use `flux-schnell` instead of `flux-pro` for faster generation → Pre-generate AI images and use URLs instead ## Python API Usage For programmatic use in other scripts: ```python from scripts.generate_video import create_pinterest_video scenes = [ { "image": {"source": "ai", "ai_prompt": "..."}, "voice": {"source": "generated", "text": "..."}, "subtitles": True, "zoom_effect": True } ] video_url = create_pinterest_video(scenes, wait=True) print(f"Video ready: {video_url}") ```

path: ADVANCED.md
content: # JSON2Video Pinterest Skill - Advanced Usage

## Multi-Scene Video Architecture

When creating videos with voiceover, the recommended pattern is:

```
Scene 1: Hook (3-5 seconds)
  └─ Image + Short attention-grabbing voiceover

Scene 2: Value/Benefit (5-8 seconds)
  └─ Image + Explain the benefit

Scene 3: How-to/Steps (10-15 seconds)
  └─ Image + Walk through the process

Scene 4: Social Proof (5-8 seconds)
  └─ Image + Results/testimonial

Scene 5: CTA (3-5 seconds)
  └─ Image + Call-to-action
```

Total: ~30-45 seconds (optimal for Pinterest)

## Image Source Strategies

### Strategy 1: Full AI Generation (Simplest)
- Pros: Fully automated, unique images
- Cons: Consumes more credits, slower
- Best for: Testing ideas, rapid prototyping

### Strategy 2: Hybrid (Production Recommended)
- Create images with Nano Banana Pro / DALL-E
- Upload to Cloudinary
- Use URLs in JSON2Video config
- Pros: Faster renders, cheaper, reusable assets
- Cons: Requires external image generation step

### Strategy 3: Stock + AI Enhancement
- Start with stock images
- Use AI for specific elements or backgrounds
- Pros: Professional quality
- Cons: Requires design skills

## Voiceover Patterns

### Pattern A: Generated TTS (Fully Automated)
```json
{
  "voice": {
    "source": "generated",
    "text": "Your script here",
    "voice_id": "en-US-Neural2-F"
  }
}
```

### Pattern B: Pre-Recorded Segments (Highest Quality)
```json
{
  "voice": {
    "source": "https://your-cdn.com/scene-1.mp3"
  }
}
```

### Pattern C: Mixed (Best of Both)
- Use TTS for simple scenes
- Use pre-recorded for important CTAs

## Subtitle Styling

Customize subtitle appearance:

```json
{
  "subtitles": true,
  "subtitle_style": {
    "font-family": "Montserrat",
    "font-size": "32px",
    "color": "#ffffff",
    "background-color": "rgba(0,0,0,0.7)",
    "text-align": "center",
    "font-weight": "bold"
  }
}
```

## Zoom Effect Variations

### Subtle (Recommended)
- Scale 1.05x to 1.0x over duration
- Adds motion without distraction

### Dramatic
- Scale 1.2x to 1.0x
- Good for emotional moments

### Ken Burns (Pan + Zoom)
Not directly supported, but can be simulated with multiple elements.

## Content Ideas for Pinterest Affiliate Videos

### "Listicle" Format (High Engagement)
- "5 Affiliate Networks for Beginners"
- "3 Mistakes Killing Your Commissions"
- "7 Passive Income Ideas"

### "How-To" Format (High Saves)
- "How to Set Up Your First Affiliate Link"
- "How I Made $X in My First Month"
- "How to Choose Profitable Products"

### "Myth Busting" Format (High Comments)
- "Affiliate Marketing Myths Debunked"
- "Why Most Affiliates Fail"
- "The Truth About Passive Income"

### "Before/After" Format (High Conversions)
- "My Income: Month 1 vs Month 6"
- "From $0 to $X: The Journey"

## Pinterest-Specific Tips

1. **First 3 seconds matter most** - Hook immediately
2. **Text overlays boost engagement** - Add key phrases as text
3. **Vertical format is non-negotiable** - 9:16 aspect ratio
4. **Loop consideration** - Pinterest autoplays; design for seamless loops
5. **Muted autoplay** - Ensure visuals work without sound
6. **CTA timing** - Put CTA at 80-90% of video length

## Credit Consumption Optimization

### Reduce Costs:
- Use `cache: true` (default)
- Pre-generate AI images
- Use `flux-schnell` instead of `flux-pro`
- Reuse successful templates
- Lower quality for drafts (`quality: "medium"`)

### Expected Costs:
- Simple video (3 scenes, TTS, subtitles): ~50-100 credits
- Complex video (5 scenes, AI images, TTS): ~150-250 credits

## Integration with Existing Workflow

```
1. Nano Banana Pro → Generate pin images
2. Cloudinary → Store images
3. This Skill → Create video from images + voice
4. Pinterest API → Post video pin
```

## Batch Processing

To create multiple videos:

```python
import json
from scripts.generate_video import create_pinterest_video

# Load batch config
with open('batch-videos.json') as f:
    batch = json.load(f)

for video_name, config in batch.items():
    print(f"Creating: {video_name}")
    url = create_pinterest_video(config['scenes'], wait=True)
    print(f"Done: {url}\n")
```

## Error Recovery

If a render fails:

1. Check project status: `--project-id ID`
2. If status is "error", review the message
3. Common fixes:
   - Invalid image URL → Use public HTTPS URL
   - Content policy violation → Adjust AI prompt
   - Timeout → Reduce scene count or complexity

## Future Enhancements

Potential additions to the skill:
- Direct ElevenLabs integration for premium voices
- Auto-scene-splitting based on script analysis
- A/B testing variant generation
- Analytics integration for performance tracking


path: scripts/example-advanced.json
content: {
  "_comment": "Advanced Example: Multiple scenes with split voiceover",
  "resolution": "instagram-story",
  "quality": "high",
  "cache": true,
  "scenes": [
    {
      "image": {
        "source": "ai",
        "ai_provider": "flux-schnell",
        "ai_prompt": "Elegant affiliate marketing concept, golden coins flowing into a laptop screen, dark blue background, professional business style"
      },
      "voice": {
        "source": "generated",
        "text": "Are you tired of affiliate strategies that don't work? Let me show you the proven system that generates passive income.",
        "voice_id": "en-US-EmmaMultilingualNeural",
        "model": "azure"
      },
      "text_overlay": "Stop Struggling",
      "subtitles": true,
      "zoom_effect": true
    },
    {
      "image": {
        "source": "ai",
        "ai_provider": "flux-schnell",
        "ai_prompt": "Clean desk setup with notebook showing checklist, coffee cup, morning light, productivity aesthetic"
      },
      "voice": {
        "source": "generated",
        "text": "First, you need to pick a niche you're passionate about. Don't chase trends, chase your expertise.",
        "voice_id": "en-US-EmmaMultilingualNeural",
        "model": "azure"
      },
      "text_overlay": "Step 1: Pick Your Niche",
      "subtitles": true,
      "zoom_effect": true
    },
    {
      "image": {
        "source": "ai",
        "ai_provider": "flux-schnell",
        "ai_prompt": "Multiple social media icons floating around a smartphone, Instagram Pinterest TikTok logos, colorful gradient background"
      },
      "voice": {
        "source": "generated",
        "text": "Next, focus on one platform and master it. Pinterest is perfect for beginners because it's a search engine, not just social media.",
        "voice_id": "en-US-EmmaMultilingualNeural",
        "model": "azure"
      },
      "text_overlay": "Step 2: Choose Pinterest",
      "subtitles": true,
      "zoom_effect": true
    },
    {
      "image": {
        "source": "ai",
        "ai_provider": "flux-schnell",
        "ai_prompt": "Growth chart showing upward trend, green arrow pointing up, professional business infographic style, clean white background"
      },
      "voice": {
        "source": "generated",
        "text": "Finally, be consistent. Post daily for 90 days and watch your affiliate income compound.",
        "voice_id": "en-US-EmmaMultilingualNeural",
        "model": "azure"
      },
      "text_overlay": "Step 3: Stay Consistent",
      "subtitles": true,
      "zoom_effect": true
    },
    {
      "image": {
        "source": "ai",
        "ai_provider": "flux-schnell",
        "ai_prompt": "Call to action button glowing, click here style, attention grabbing red button, modern web design"
      },
      "voice": {
        "source": "generated",
        "text": "Ready to start? Click the link in bio and grab your free affiliate starter kit today.",
        "voice_id": "en-US-EmmaMultilingualNeural",
        "model": "azure"
      },
      "text_overlay": "Link in Bio!",
      "subtitles": true,
      "zoom_effect": false
    }
  ]
}


path: scripts/example-config.json
content: {
  "_comment": "JSON2Video Pinterest Video Configuration Example",
  "resolution": "instagram-story",
  "quality": "high",
  "cache": true,
  "scenes": [
    {
      "_comment": "Scene 1: AI-generated image with generated voiceover",
      "image": {
        "source": "ai",
        "ai_provider": "flux-schnell",
        "ai_prompt": "A minimalist workspace with a laptop showing affiliate marketing dashboard, soft lighting, modern aesthetic, vertical composition"
      },
      "voice": {
        "source": "generated",
        "text": "Want to make money with affiliate marketing? Here are five strategies that actually work in 2025.",
        "voice_id": "en-US-EmmaMultilingualNeural",
        "model": "azure"
      },
      "text_overlay": "5 Affiliate Strategies",
      "subtitles": true,
      "zoom_effect": true
    },
    {
      "_comment": "Scene 2: URL-based image with provided audio file",
      "image": {
        "source": "https://example.com/my-image.jpg"
      },
      "voice": {
        "source": "https://example.com/audio-scene2.mp3"
      },
      "text_overlay": "Step 1: Choose Your Niche",
      "subtitles": true,
      "zoom_effect": false
    },
    {
      "_comment": "Scene 3: AI image with generated voice, no text overlay",
      "image": {
        "source": "ai",
        "ai_provider": "flux-pro",
        "ai_prompt": "Person holding smartphone showing Pinterest app, bright colors, lifestyle photography style"
      },
      "voice": {
        "source": "generated",
        "text": "Pinterest is the hidden gem for affiliate marketers. Here's why you should start today.",
        "voice_id": "en-US-EmmaMultilingualNeural",
        "model": "azure"
      },
      "subtitles": true,
      "zoom_effect": true
    }
  ]
}


path: scripts/generate_video.py
content: #!/usr/bin/env python3
"""
JSON2Video Pinterest Video Generator
Creates vertical videos optimized for Pinterest from images and voiceover.
"""

import argparse
import json
import os
import sys
import time
import requests
from typing import Optional, List, Dict, Any
from pathlib import Path

# API Configuration
API_BASE = "https://api.json2video.com/v2"


def get_api_key() -> str:
    """Get API key from environment variable."""
    [REDACTED_SECRET]"JSON2VIDEO_API_KEY")
    if not api_key:
        raise ValueError(
            "JSON2VIDEO_API_KEY environment variable not set. "
            "Set it with: export JSON2VIDEO_API_KEY='your_key_here'"
        )
    return api_key


def create_movie(payload: Dict[str, Any]) -> Dict[str, Any]:
    """Create a new movie render job."""
    [REDACTED_SECRET])
    url = f"{API_BASE}/movies"
    
    headers = {
        "x-api-key": api_key,
        "Content-Type": "application/json"
    }
    
    response = requests.post(url, headers=headers, json=payload)
    response.raise_for_status()
    return response.json()


def check_status(project_id: str) -> Dict[str, Any]:
    """Check the status of a movie render job."""
    [REDACTED_SECRET])
    url = f"{API_BASE}/movies?project={project_id}"
    
    headers = {"x-api-key": api_key}
    
    response = requests.get(url, headers=headers)
    response.raise_for_status()
    return response.json()


def wait_for_completion(project_id: str, poll_interval: int = 10, timeout: int = 600) -> Dict[str, Any]:
    """Poll for movie completion."""
    start_time = time.time()
    
    while time.time() - start_time < timeout:
        status = check_status(project_id)
        movie = status.get("movie", {})
        current_status = movie.get("status", "unknown")
        
        print(f"Status: {current_status}")
        
        if current_status == "done":
            print(f"✓ Video ready: {movie.get('url')}")
            return status
        elif current_status == "error":
            raise RuntimeError(f"Render failed: {movie.get('message', 'Unknown error')}")
        
        time.sleep(poll_interval)
    
    raise TimeoutError(f"Render timed out after {timeout} seconds")


def build_image_element(
    image_source: str,
    duration: float,
    width: int = 1080,
    height: int = 1920,
    zoom_effect: bool = False,
    ai_provider: Optional[str] = None,
    ai_prompt: Optional[str] = None,
    aspect_ratio: str = "vertical"
) -> Dict[str, Any]:
    """
    Build an image element.
    
    Args:
        image_source: Either "ai" for AI generation or a URL
        duration: Duration in seconds
        width: Element width
        height: Element height
        zoom_effect: Whether to add Ken Burns zoom effect
        ai_provider: AI provider (flux-pro, flux-schnell, freepik-classic)
        ai_prompt: Prompt for AI image generation
        aspect_ratio: Aspect ratio for AI generation (horizontal, vertical, squared)
    """
    element = {
        "type": "image",
        "duration": duration,
        "width": width,
        "height": height,
        "x": 0,
        "y": 0
    }
    
    # AI-generated image
    if image_source == "ai":
        if not ai_prompt:
            raise ValueError("ai_prompt required when image_source is 'ai'")
        element["provider"] = ai_provider or "flux-schnell"
        element["prompt"] = ai_prompt
        element["aspect-ratio"] = aspect_ratio
    else:
        # URL-based image
        element["src"] = image_source
    
    # Add zoom animation if requested
    if zoom_effect:
        # Start slightly zoomed in, then zoom out slowly (Ken Burns effect)
        element["animations"] = [
            {
                "type": "scale",
                "start": 1.15,
                "end": 1.0,
                "easing": "ease-out",
                "duration": duration
            }
        ]
    
    return element


def build_text_element(
    text: str,
    duration: float,
    style: Optional[Dict[str, Any]] = None,
    vertical_position: str = "bottom",
    horizontal_position: str = "center"
) -> Dict[str, Any]:
    """Build a text overlay element."""
    default_style = {
        "font-family": "Montserrat",
        "font-size": "42px",
        "color": "#ffffff",
        "text-align": "center",
        "text-shadow": "2px 2px 4px rgba(0,0,0,0.7)",
        "font-weight": "bold"
    }
    
    if style:
        default_style.update(style)
    
    return {
        "type": "text",
        "text": text,
        "duration": duration,
        "settings": default_style,
        "vertical-position": vertical_position,
        "horizontal-position": horizontal_position,
        "width": 1000,
        "height": 200,
        "x": 40,
        "y": 1600
    }


def build_voice_element(
    voice_source: str,
    duration: float,
    voice_id: Optional[str] = None,
    text: Optional[str] = None,
    model: str = "azure",
    language: str = "en"
) -> Dict[str, Any]:
    """
    Build a voice element.
    
    Args:
        voice_source: "generated" for AI TTS or a URL to audio file
        duration: Duration (-1 for auto)
        voice_id: Voice ID for AI generation (Azure: "en-US-EmmaMultilingualNeural", ElevenLabs: "Bella")
        text: Text to speak (for generated voice)
        model: TTS model ("azure" - free, "elevenlabs" or "elevenlabs-flash-v2-5" - consumes credits)
        language: Language code
    """
    if voice_source == "generated":
        if not text:
            raise ValueError("text required when voice_source is 'generated'")
        return {
            "type": "voice",
            "text": text,
            "voice": voice_id or "en-US-EmmaMultilingualNeural",
            "model": model,
            "duration": -1  # Auto duration based on speech length
        }
    else:
        # External audio file
        return {
            "type": "audio",
            "src": voice_source,
            "duration": -1,
            "volume": 1.0
        }


def build_movie_subtitles(
    enabled: bool = True,
    style: Optional[Dict[str, Any]] = None,
    language: str = "en"
) -> Optional[Dict[str, Any]]:
    """
    Build a movie-level subtitle element.
    Note: Subtitles MUST be at movie level, NOT inside scenes.
    """
    if not enabled:
        return None
    
    # Subtitle settings use different property names than text elements
    default_style = {
        "font-family": "Montserrat",
        "font-size": 100,
        "style": "classic",
        "position": "bottom-center",
        "box-color": "#000000",
        "word-color": "#FFFF00",
        "line-color": "#FFFFFF",
        "outline-color": "#000000",
        "outline-width": 2
    }
    
    if style:
        default_style.update(style)
    
    return {
        "type": "subtitles",
        "language": language,
        "settings": default_style
    }


def build_scene(
    image_config: Dict[str, Any],
    voice_config: Dict[str, Any],
    text_overlay: Optional[str] = None,
    zoom_effect: bool = False,
    scene_duration: Optional[float] = None
) -> Dict[str, Any]:
    """
    Build a complete scene with image, voice, and optional text overlay.
    Note: Subtitles are handled at movie level, not per-scene.
    
    Args:
        image_config: Config for image (source, ai params, etc.)
        voice_config: Config for voice (source, text, voice_id, etc.)
        text_overlay: Optional text to display on scene
        zoom_effect: Whether to add zoom effect to image
        scene_duration: Override scene duration (auto-calculated if None)
    """
    elements = []
    
    # Build image element
    image_element = build_image_element(
        image_source=image_config.get("source", "ai"),
        duration=scene_duration or -2,  # -2 = match container
        ai_provider=image_config.get("ai_provider", "flux-schnell"),
        ai_prompt=image_config.get("ai_prompt"),
        zoom_effect=zoom_effect
    )
    elements.append(image_element)
    
    # Build voice element
    voice_element = build_voice_element(
        voice_source=voice_config.get("source", "generated"),
        duration=-1,
        voice_id=voice_config.get("voice_id"),
        text=voice_config.get("text"),
        model=voice_config.get("model", "azure"),
        language=voice_config.get("language", "en")
    )
    elements.append(voice_element)
    
    # Add text overlay if provided
    if text_overlay:
        text_element = build_text_element(
            text=text_overlay,
            duration=-2
        )
        elements.append(text_element)
    
    return {
        "elements": elements,
        "duration": scene_duration or -1  # -1 = auto from elements
    }


def create_pinterest_video(
    scenes_config: List[Dict[str, Any]],
    resolution: str = "instagram-story",  # 1080x1920 vertical
    quality: str = "high",
    cache: bool = True,
    subtitles: bool = True,
    subtitle_style: Optional[Dict[str, Any]] = None,
    wait: bool = True
) -> str:
    """
    Create a Pinterest-optimized vertical video.
    
    Args:
        scenes_config: List of scene configurations
        resolution: Video resolution preset
        quality: Rendering quality
        cache: Use caching
        subtitles: Enable movie-level subtitles
        subtitle_style: Custom subtitle styling
        wait: Wait for completion
    
    Returns:
        Project ID or video URL (if wait=True)
    """
    # Build movie payload
    movie = {
        "resolution": resolution,
        "quality": quality,
        "cache": cache,
        "scenes": [],
        "elements": []  # For movie-level elements like subtitles
    }
    
    # Build scenes
    for scene_config in scenes_config:
        scene = build_scene(
            image_config=scene_config.get("image", {"source": "ai", "ai_prompt": "Abstract background"}),
            voice_config=scene_config.get("voice", {"source": "generated", "text": "Welcome"}),
            text_overlay=scene_config.get("text_overlay"),
            zoom_effect=scene_config.get("zoom_effect", False),
            scene_duration=scene_config.get("duration")
        )
        movie["scenes"].append(scene)
    
    # Add subtitles at movie level (not per-scene)
    if subtitles:
        subtitle_element = build_movie_subtitles(
            enabled=True,
            style=subtitle_style,
            language=scenes_config[0].get("voice", {}).get("language", "en") if scenes_config else "en"
        )
        if subtitle_element:
            movie["elements"].append(subtitle_element)
    
    # Create the movie
    print("Creating video...")
    print(f"Payload preview: {json.dumps(movie, indent=2)[:500]}...")
    result = create_movie(movie)
    
    if not result.get("success"):
        raise RuntimeError(f"Failed to create movie: {result}")
    
    project_id = result.get("project")
    print(f"Project ID: {project_id}")
    
    if wait:
        final = wait_for_completion(project_id)
        return final.get("movie", {}).get("url", "")
    
    return project_id


def create_video_from_json(config_path: str, wait: bool = True) -> str:
    """Create video from a JSON configuration file."""
    with open(config_path, 'r') as f:
        config = json.load(f)
    
    # Check if any scene has subtitles disabled
    scenes = config.get("scenes", [])
    subtitles_enabled = all(scene.get("subtitles", True) for scene in scenes) if scenes else True
    
    return create_pinterest_video(
        scenes_config=scenes,
        resolution=config.get("resolution", "instagram-story"),
        quality=config.get("quality", "high"),
        cache=config.get("cache", True),
        subtitles=subtitles_enabled,
        subtitle_style=config.get("subtitle_style"),
        wait=wait
    )


def main():
    parser = argparse.ArgumentParser(description="JSON2Video Pinterest Video Generator")
    parser.add_argument("--config", "-c", help="Path to JSON config file")
    parser.add_argument("--project-id", "-p", help="Check status of existing project")
    parser.add_argument("--wait", "-w", action="store_true", default=True, help="Wait for completion")
    parser.add_argument("--no-wait", dest="wait", action="store_false", help="Don't wait for completion")
    
    args = parser.parse_args()
    
    try:
        if args.project_id:
            # Check status
            status = check_status(args.project_id)
            print(json.dumps(status, indent=2))
        elif args.config:
            # Create video from config
            result = create_video_from_json(args.config, wait=args.wait)
            print(f"Result: {result}")
        else:
            parser.print_help()
            sys.exit(1)
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()


path: scripts/test-video.json
content: {
  "_comment": "Test video for JSON2Video Pinterest Skill",
  "resolution": "instagram-story",
  "quality": "medium",
  "cache": true,
  "scenes": [
    {
      "_comment": "Scene 1: Hook with AI image and generated voice",
      "image": {
        "source": "ai",
        "ai_provider": "flux-schnell",
        "ai_prompt": "A modern minimalist desk with laptop showing charts and graphs, coffee cup, soft morning light, productivity aesthetic, clean workspace"
      },
      "voice": {
        "source": "generated",
        "text": "Want to earn passive income online? Here's the secret most affiliates don't know.",
        "voice_id": "en-US-EmmaMultilingualNeural",
        "model": "azure"
      },
      "text_overlay": "Affiliate Secrets",
      "subtitles": true,
      "zoom_effect": true
    },
    {
      "_comment": "Scene 2: Step with URL image (using a placeholder - will test error handling)",
      "image": {
        "source": "ai",
        "ai_provider": "flux-schnell",
        "ai_prompt": "Pinterest app on smartphone screen, affiliate marketing concept, modern flat design style, vibrant colors"
      },
      "voice": {
        "source": "generated",
        "text": "Pinterest is the hidden goldmine for affiliate marketers. Start creating content today.",
        "voice_id": "en-US-EmmaMultilingualNeural",
        "model": "azure"
      },
      "text_overlay": "Use Pinterest!",
      "subtitles": true,
      "zoom_effect": true
    }
  ]
}


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