diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index a674e2b..cd81d2b 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "runway-api", - "description": "Helps users integrate Runway's public API (https://docs.dev.runwayml.com/) into their projects. Analyzes codebase compatibility, guides API key setup, and provides hands-on assistance for video generation, image generation, audio, file uploads, and direct API actions.", - "version": "1.2.0", + "description": "Video generation at scale. Generate videos, images, and audio with Runway's API — batch ad campaigns, product videos, multishot stories, and creative iteration. Supports seedance2, gen4.5, veo3, Nano, Banana Pro, and more.", + "version": "2.0.0", "author": { "name": "Runway", "email": "ops@runwayml.com" @@ -11,12 +11,13 @@ "runwayml", "video-generation", "image-generation", + "audio-generation", "ai-api", + "seedance2", "gen4", "veo3", - "characters", - "avatars", - "public-api", + "media-generation", + "batch", "runway-api" ] } diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index c074bb3..21b11f0 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,8 +1,8 @@ { "name": "runway-api", "displayName": "Runway API", - "version": "1.0.1", - "description": "Helps users integrate Runway's public API (https://docs.dev.runwayml.com/) into their projects. Analyzes codebase compatibility, guides API key setup, and provides hands-on assistance for video generation, image generation, audio, file uploads, and direct API actions.", + "version": "2.0.0", + "description": "Video generation at scale. Generate videos, images, and audio with Runway's API — batch ad campaigns, product videos, multishot stories, and creative iteration. Supports seedance2, gen4.5, veo3, Nano, Banana Pro, and more.", "author": { "name": "Runway", "email": "ops@runwayml.com" @@ -18,18 +18,13 @@ "image-generation", "audio-generation", "ai-api", + "seedance2", "gen4", "veo3", - "characters", - "avatars" - ], - "category": "ai-tools", - "tags": [ - "api-integration", "media-generation", - "video", - "image", - "audio" + "batch" ], + "category": "ai-tools", + "tags": ["media-generation", "video", "image", "audio", "api-integration"], "skills": "./skills/" } diff --git a/.gitignore b/.gitignore index 242b668..de3e29d 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ __pycache__/ # Dependencies node_modules/ + +# Test output files +test-* diff --git a/CHANGELOG.md b/CHANGELOG.md index deb6c73..2934994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2.0.0 + +- **Media generation:** New `rw-generate-video`, `rw-generate-image`, `rw-generate-audio` skills that run Python scripts directly via `uv run` — no SDK setup required +- **Runnable scripts:** Added `scripts/` directory with `generate_video.py`, `generate_image.py`, `generate_audio.py`, `list_models.py`, `get_task.py`, and shared `runway_helpers.py` +- **Seedance 2 support:** Added `seedance2` model across all generation scripts and skills (TTV, ITV, VTV, 36 credits/sec) +- **Plugin metadata:** Updated descriptions and keywords for both Claude and Cursor plugins + ## 1.1.0 - **Breaking change:** Every skill is now named with an `rw-` prefix (skill folder under `skills/`, `name` in each `SKILL.md`, and `+…` invocations). Examples: `setup-api-key` → `rw-setup-api-key`, `integrate-video` → `rw-integrate-video`. Update any documentation, shortcuts, or automation that referenced the previous names or paths. diff --git a/README.md b/README.md index 2a7e911..cacad0e 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,39 @@ # Runway API Skills -A set of skills that gives your AI coding agent the knowledge and tools to work with [Runway's public API](https://docs.dev.runwayml.com/api/) — integrate video, image, and audio generation into server-side projects, or manage Runway resources directly from the editor. +Video generation at scale. Generate videos, images, and audio with Runway's API — batch Ad campaigns, product videos, multishot stories, and creative iteration. Supports seedance2, gen4.5, veo3, Nano, Banana Pro, and more. -Works with [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://openai.com/index/codex/), and other compatible agents. +Works with [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Cursor](https://cursor.com), [Codex](https://openai.com/index/codex/), and other compatible agents. ## Two Ways to Use -### 1. Integrate into your app +### 1. Generate media at scale -Guide your agent through adding Runway capabilities to a server-side project: verify compatibility, set up credentials, write framework-specific routes, and handle edge cases like file uploads and task polling. +Tell your agent what to create and it handles the rest — calls the API, polls for completion, downloads the result. Generate one asset or orchestrate hundreds. ``` -Set up Runway video generation in my Next.js app +Generate a 15-second product video from this image using seedance2 ``` -### 2. Act on your Runway account directly - -Let your agent call the Runway API to manage resources — create avatars, update knowledge documents, trigger generations, check credit balance — without writing app code. +``` +Generate a video for each of these 5 product photos, 9:16 ratio for Instagram Reels +``` ``` -Create a new avatar called "Support Agent" with a friendly personality +Create a voiceover for this ad script, then generate a sound effect of applause ``` +### 2. Integrate into your app + +Guide your agent through adding Runway capabilities to a server-side project: verify compatibility, set up credentials, write framework-specific routes, and handle edge cases like file uploads and task polling. + ``` -List my knowledge documents for avatar abc123 +Set up Runway video generation in my Next.js app ``` ## Installation ### Claude Code (community marketplace) -Add the [Anthropic community plugins](https://github.com/anthropics/claude-plugins-community) marketplace, then install this plugin. - ```bash claude plugin marketplace add anthropics/claude-plugins-community claude plugin install runway-api-skills@claude-community @@ -50,27 +52,24 @@ Select all the skills with your keyboard (Space to select, arrow keys to navigat ## Prerequisites - A [Runway developer account](https://dev.runwayml.com/) with prepaid credits ($10 minimum) +- For generation skills: [uv](https://docs.astral.sh/uv/) (Python package runner) and `RUNWAYML_API_SECRET` env var - For integration skills: a server-side project — Node.js 18+ or Python 3.8+ with a backend framework -- For direct API actions: Node.js 20+ (zero dependencies — just the runtime script) ## Available Skills -### Getting Started +### Generation (run directly) -| Skill | Description | -| ------------------------ | ---------------------------------------------------------------------------------------------- | -| `rw-recipe-full-setup` | End-to-end setup: compatibility check → API key → SDK install → integration code → test | -| `rw-check-compatibility` | Analyze your project to verify it can safely call the Runway API server-side | -| `rw-setup-api-key` | Guide through account creation, SDK installation, and environment variable configuration | -| `rw-check-org-details` | Query your organization's rate limits, credit balance, usage tier, and daily generation counts | +Generate media assets directly — your agent runs the scripts, polls for completion, and saves the output. -### Direct API Actions +| Skill | Description | +| -------------------- | ----------------------------------------------------------------------------------- | +| `rw-generate-video` | Generate videos: text-to-video, image-to-video, video-to-video | +| `rw-generate-image` | Generate images: text-to-image with optional reference images | +| `rw-generate-audio` | Generate audio: TTS, sound effects, voice isolation, dubbing, voice conversion | -| Skill | Description | -| -------------------- | ---------------------------------------------------------------------------------- | -| `use-runway-api` | Call any public API endpoint to manage resources, trigger generations, and inspect state | +### Integration (add to your app) -### Generation (Integration) +Add Runway generation to your server-side project with framework-specific code. | Skill | Description | | -------------------- | ----------------------------------------------------------------------------------- | @@ -78,6 +77,15 @@ Select all the skills with your keyboard (Space to select, arrow keys to navigat | `rw-integrate-image` | Text-to-image generation with optional reference images via `@Tag` syntax | | `rw-integrate-audio` | Text-to-speech, sound effects, voice isolation, dubbing, and speech-to-speech | +### Getting Started + +| Skill | Description | +| ------------------------ | ---------------------------------------------------------------------------------------------- | +| `rw-recipe-full-setup` | End-to-end setup: compatibility check → API key → SDK install → integration code → test | +| `rw-check-compatibility` | Analyze your project to verify it can safely call the Runway API server-side | +| `rw-setup-api-key` | Guide through account creation, SDK installation, and environment variable configuration | +| `rw-check-org-details` | Query your organization's rate limits, credit balance, usage tier, and daily generation counts | + ### Characters (Real-Time Avatars) | Skill | Description | @@ -90,6 +98,7 @@ Select all the skills with your keyboard (Space to select, arrow keys to navigat | Skill | Description | | ------------------------ | --------------------------------------------------------------------------------------------------------------------- | +| `use-runway-api` | Call any public API endpoint to manage resources, trigger generations, and inspect state | | `rw-integrate-uploads` | Upload local files to get `runway://` URIs for use as generation inputs | | `rw-api-reference` | Complete API reference — models, endpoints, costs, rate limits, and error codes | | `rw-fetch-api-reference` | Fetch the latest API docs from [docs.dev.runwayml.com/api](https://docs.dev.runwayml.com/api/) as the source of truth | @@ -98,14 +107,14 @@ Select all the skills with your keyboard (Space to select, arrow keys to navigat ### Video -| Model | Use Case | Cost | -| ------------------------ | ----------------------------------- | ----------------- | +| Model | Use Case | Cost | +| ------------------------ | ---------------------------------------- | ----------------- | | `seedance2` | Reference image and video, long duration | 36 credits/sec | -| `gen4.5` | High quality, general purpose | 12 credits/sec | -| `gen4_turbo` | Fast, image-driven (image required) | 5 credits/sec | -| `gen4_aleph` | Video-to-video editing | 15 credits/sec | -| `veo3` | Premium quality | 40 credits/sec | -| `veo3.1` / `veo3.1_fast` | High quality / fast Google models | 10–40 credits/sec | +| `gen4.5` | High quality, general purpose | 12 credits/sec | +| `gen4_turbo` | Fast, image-driven (image required) | 5 credits/sec | +| `gen4_aleph` | Video-to-video editing | 15 credits/sec | +| `veo3` | Premium quality | 40 credits/sec | +| `veo3.1` / `veo3.1_fast` | High quality / fast Google models | 10–40 credits/sec | ### Image @@ -133,34 +142,28 @@ Select all the skills with your keyboard (Space to select, arrow keys to navigat ## Quick Start -### Integrate into a project +### Generate media directly ``` -Set up Runway video generation in my Next.js app +Generate a 10-second video of a sunset over the ocean ``` -Or go step by step: - ``` -Check if my project is compatible with the Runway API +Generate an image of a red door in a white wall ``` ``` -Help me set up my Runway API key +Create a voiceover saying "Welcome to our store" and save it as welcome.mp3 ``` -``` -Add an endpoint to generate videos from text prompts -``` - -### Direct API actions +### Integrate into a project ``` -List all my avatars +Set up Runway video generation in my Next.js app ``` ``` -Using the Runway API, generate an image of a red door in a white wall and tell me which model you used +Add an endpoint to generate videos from text prompts ``` ## Supported Frameworks diff --git a/scripts/generate_audio.py b/scripts/generate_audio.py new file mode 100644 index 0000000..8e96357 --- /dev/null +++ b/scripts/generate_audio.py @@ -0,0 +1,131 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = ["requests"] +# /// + +"""Generate audio using the Runway API (TTS, sound effects, voice isolation, dubbing).""" + +import argparse +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from runway_helpers import ( + get_api_key, + api_post, + poll_task, + download_file, + ensure_url, + output_path, +) + +AUDIO_TYPES = { + "tts": { + "endpoint": "/v1/text_to_speech", + "model": "eleven_multilingual_v2", + "description": "Text to speech", + }, + "sfx": { + "endpoint": "/v1/sound_effect", + "model": "eleven_text_to_sound_v2", + "description": "Sound effect generation", + }, + "isolate": { + "endpoint": "/v1/voice_isolation", + "model": "eleven_voice_isolation", + "description": "Isolate voice from audio", + }, + "dub": { + "endpoint": "/v1/voice_dubbing", + "model": "eleven_voice_dubbing", + "description": "Dub to another language", + }, + "sts": { + "endpoint": "/v1/speech_to_speech", + "model": "eleven_multilingual_sts_v2", + "description": "Voice conversion", + }, +} + + +def main(): + parser = argparse.ArgumentParser(description="Generate audio with the Runway API") + parser.add_argument("--filename", required=True, help="Output filename (e.g. output.mp3)") + parser.add_argument( + "--type", + required=True, + choices=list(AUDIO_TYPES.keys()), + help="Audio type: tts, sfx, isolate, dub, sts", + ) + parser.add_argument("--text", help="Text input (required for tts and sfx)") + parser.add_argument("--audio-url", help="Audio URL or local path (for isolate, dub, sts)") + parser.add_argument("--voice-id", help="Voice ID (for tts and sts)") + parser.add_argument("--target-language", help="Target language code (for dub, e.g. 'es')") + parser.add_argument("--output-dir", help="Output directory (default: cwd)") + parser.add_argument("--api-key", help="Runway API key (or set RUNWAYML_API_SECRET)") + args = parser.parse_args() + + api_key = get_api_key(args.api_key) + audio_type = AUDIO_TYPES[args.type] + endpoint = audio_type["endpoint"] + model = audio_type["model"] + + body = {"model": model} + + if args.type == "tts": + if not args.text: + print("Error: --text is required for tts.", file=sys.stderr) + sys.exit(1) + body["promptText"] = args.text + body["voice"] = {"type": "runway-preset", "presetId": args.voice_id or "Maya"} + + elif args.type == "sfx": + if not args.text: + print("Error: --text is required for sfx.", file=sys.stderr) + sys.exit(1) + body["promptText"] = args.text + + elif args.type == "isolate": + if not args.audio_url: + print("Error: --audio-url is required for isolate.", file=sys.stderr) + sys.exit(1) + body["audioUri"] = ensure_url(args.audio_url, api_key) + + elif args.type == "sts": + if not args.audio_url: + print("Error: --audio-url is required for sts.", file=sys.stderr) + sys.exit(1) + audio_uri = ensure_url(args.audio_url, api_key) + body["media"] = {"type": "audio", "uri": audio_uri} + body["voice"] = {"type": "runway-preset", "presetId": args.voice_id or "Maya"} + + elif args.type == "dub": + if not args.audio_url: + print("Error: --audio-url is required for dub.", file=sys.stderr) + sys.exit(1) + if not args.target_language: + print("Error: --target-language is required for dub.", file=sys.stderr) + sys.exit(1) + body["audioUri"] = ensure_url(args.audio_url, api_key) + body["targetLang"] = args.target_language + + print(f"Generating audio ({args.type}) with {model}...", file=sys.stderr) + task = api_post(api_key, endpoint, body) + task_id = task.get("id") + print(f"Task created: {task_id}", file=sys.stderr) + + result = poll_task(api_key, task_id) + urls = result.get("output", []) + + if not urls: + print("Error: No output URLs in result.", file=sys.stderr) + sys.exit(1) + + out = output_path(args.filename, args.output_dir) + path = download_file(urls[0], out) + print(path) + print(f"Saved: {path}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_image.py b/scripts/generate_image.py new file mode 100644 index 0000000..9ee3412 --- /dev/null +++ b/scripts/generate_image.py @@ -0,0 +1,103 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = ["requests"] +# /// + +"""Generate images using the Runway API.""" + +import argparse +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from runway_helpers import ( + get_api_key, + api_post, + poll_task, + download_file, + output_path, + IMAGE_MODELS, +) + + +def main(): + parser = argparse.ArgumentParser(description="Generate images with the Runway API") + parser.add_argument("--prompt", required=True, help="Text description of the image") + parser.add_argument( + "--filename", required=True, help="Output filename (e.g. output.png)" + ) + parser.add_argument( + "--model", + default="gemini_2.5_flash", + choices=list(IMAGE_MODELS.keys()), + help="Image model (default: gemini_2.5_flash / Nano Banana)", + ) + parser.add_argument( + "--ratio", default=None, help="Aspect ratio. gemini_2.5_flash: 1344:768, 768:1344, 1024:1024, etc. Others: 1280:720" + ) + parser.add_argument( + "--reference-images", + nargs="*", + metavar="TAG=URL", + help="Reference images as Tag=URL pairs (e.g. Style=https://...)", + ) + parser.add_argument("--output-dir", help="Output directory (default: cwd)") + parser.add_argument("--api-key", help="Runway API key (or set RUNWAYML_API_SECRET)") + args = parser.parse_args() + + api_key = get_api_key(args.api_key) + + if args.ratio: + ratio = args.ratio + elif args.model == "gemini_2.5_flash": + ratio = "1344:768" + else: + ratio = "1280:720" + + body = { + "model": args.model, + "promptText": args.prompt, + "ratio": ratio, + } + + if args.reference_images: + refs = [] + for pair in args.reference_images: + if "=" not in pair: + print( + f"Error: Reference image must be Tag=URL, got: {pair}", + file=sys.stderr, + ) + sys.exit(1) + tag, uri = pair.split("=", 1) + refs.append({"tag": tag, "uri": uri}) + body["referenceImages"] = refs + elif args.model == "gen4_image_turbo": + print("Error: gen4_image_turbo requires --reference-images.", file=sys.stderr) + sys.exit(1) + + print(f"Generating image with {args.model}...", file=sys.stderr) + task = api_post(api_key, "/v1/text_to_image", body) + task_id = task.get("id") + print(f"Task created: {task_id}", file=sys.stderr) + + result = poll_task(api_key, task_id) + urls = result.get("output", []) + + if not urls: + print("Error: No output URLs in result.", file=sys.stderr) + sys.exit(1) + + for i, url in enumerate(urls): + if len(urls) == 1: + out = output_path(args.filename, args.output_dir) + else: + base, ext = os.path.splitext(args.filename) + out = output_path(f"{base}-{i + 1}{ext}", args.output_dir) + path = download_file(url, out) + print(path) + print(f"Saved: {path}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/scripts/generate_video.py b/scripts/generate_video.py new file mode 100644 index 0000000..ee7b85f --- /dev/null +++ b/scripts/generate_video.py @@ -0,0 +1,118 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = ["requests"] +# /// + +"""Generate videos using the Runway API.""" + +import argparse +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from runway_helpers import ( + get_api_key, + api_post, + poll_task, + download_file, + ensure_url, + output_path, + VIDEO_MODELS, +) + + +def main(): + parser = argparse.ArgumentParser(description="Generate videos with the Runway API") + parser.add_argument("--prompt", required=True, help="Text description of the video") + parser.add_argument("--filename", required=True, help="Output filename (e.g. output.mp4)") + parser.add_argument( + "--model", + default="gen4.5", + choices=list(VIDEO_MODELS.keys()), + help="Video model (default: gen4.5)", + ) + parser.add_argument("--ratio", default="1280:720", help="Aspect ratio (default: 1280:720). All models use pixel-based ratios.") + parser.add_argument("--duration", type=int, default=5, help="Duration in seconds (default: 5)") + parser.add_argument("--image-url", help="Input image URL or local path for image-to-video") + parser.add_argument("--video-url", help="Input video URL or local path for video-to-video (gen4_aleph, seedance2)") + parser.add_argument("--output-dir", help="Output directory (default: cwd)") + parser.add_argument("--api-key", help="Runway API key (or set RUNWAYML_API_SECRET)") + args = parser.parse_args() + + api_key = get_api_key(args.api_key) + model_info = VIDEO_MODELS[args.model] + + valid_durations = model_info.get("durations") + duration = args.duration + if valid_durations and duration not in valid_durations: + closest = min(valid_durations, key=lambda d: abs(d - duration)) + print(f" Note: {args.model} supports durations {valid_durations}, using {closest}s instead of {duration}s.", file=sys.stderr) + duration = closest + + if args.video_url: + if "video_to_video" not in model_info["endpoints"]: + print(f"Error: {args.model} does not support video-to-video.", file=sys.stderr) + sys.exit(1) + endpoint = "/v1/video_to_video" + video_uri = ensure_url(args.video_url, api_key) + if args.model == "seedance2": + body = { + "model": args.model, + "promptVideo": video_uri, + "promptText": args.prompt, + } + else: + body = { + "model": args.model, + "videoUri": video_uri, + "promptText": args.prompt, + } + elif args.image_url: + if "image_to_video" not in model_info["endpoints"]: + print(f"Error: {args.model} does not support image-to-video.", file=sys.stderr) + sys.exit(1) + endpoint = "/v1/image_to_video" + image_uri = ensure_url(args.image_url, api_key) + body = { + "model": args.model, + "promptImage": image_uri, + "promptText": args.prompt, + "ratio": args.ratio, + } + body["duration"] = duration + else: + if "text_to_video" not in model_info["endpoints"]: + print( + f"Error: {args.model} requires an input image (--image-url). " + "It does not support text-only generation.", + file=sys.stderr, + ) + sys.exit(1) + endpoint = "/v1/text_to_video" + body = { + "model": args.model, + "promptText": args.prompt, + "ratio": args.ratio, + "duration": duration, + } + + print(f"Generating video with {args.model} ({args.duration}s, {args.ratio})...", file=sys.stderr) + task = api_post(api_key, endpoint, body) + task_id = task.get("id") + print(f"Task created: {task_id}", file=sys.stderr) + + result = poll_task(api_key, task_id) + urls = result.get("output", []) + + if not urls: + print("Error: No output URLs in result.", file=sys.stderr) + sys.exit(1) + + out = output_path(args.filename, args.output_dir) + path = download_file(urls[0], out) + print(path) + print(f"Saved: {path}", file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/scripts/get_task.py b/scripts/get_task.py new file mode 100644 index 0000000..16c7cb6 --- /dev/null +++ b/scripts/get_task.py @@ -0,0 +1,36 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = ["requests"] +# /// + +"""Check the status of a Runway API task.""" + +import argparse +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from runway_helpers import get_api_key, api_get + + +def main(): + parser = argparse.ArgumentParser(description="Check Runway task status") + parser.add_argument("--task-id", required=True, help="Task ID to check") + parser.add_argument("--api-key", help="Runway API key (or set RUNWAYML_API_SECRET)") + parser.add_argument("--wait", action="store_true", help="Poll until the task completes") + args = parser.parse_args() + + api_key = get_api_key(args.api_key) + + if args.wait: + from runway_helpers import poll_task + result = poll_task(api_key, args.task_id) + print(json.dumps(result, indent=2)) + else: + task = api_get(api_key, f"/v1/tasks/{args.task_id}") + print(json.dumps(task, indent=2)) + + +if __name__ == "__main__": + main() diff --git a/scripts/list_models.py b/scripts/list_models.py new file mode 100644 index 0000000..fc6537a --- /dev/null +++ b/scripts/list_models.py @@ -0,0 +1,55 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = ["requests"] +# /// + +"""List available Runway API models and their costs.""" + +import argparse +import json +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from runway_helpers import VIDEO_MODELS, IMAGE_MODELS, AUDIO_MODELS + + +def print_table(title, models, extra_cols=None): + print(f"\n{'=' * 60}") + print(f" {title}") + print(f"{'=' * 60}") + for name, info in models.items(): + desc = info.get("description", "") + cost = info.get("cost", "") + print(f" {name:<30} {cost:<20} {desc}") + print() + + +def main(): + parser = argparse.ArgumentParser(description="List available Runway API models") + parser.add_argument("--type", choices=["video", "image", "audio", "all"], default="all", help="Model type to list") + parser.add_argument("--json", action="store_true", help="Output as JSON") + args = parser.parse_args() + + data = {} + if args.type in ("video", "all"): + data["video"] = VIDEO_MODELS + if args.type in ("image", "all"): + data["image"] = IMAGE_MODELS + if args.type in ("audio", "all"): + data["audio"] = AUDIO_MODELS + + if args.json: + print(json.dumps(data, indent=2)) + return + + if "video" in data: + print_table("Video Models", VIDEO_MODELS) + if "image" in data: + print_table("Image Models", IMAGE_MODELS) + if "audio" in data: + print_table("Audio Models", AUDIO_MODELS) + + +if __name__ == "__main__": + main() diff --git a/scripts/runway_helpers.py b/scripts/runway_helpers.py new file mode 100644 index 0000000..06c21f2 --- /dev/null +++ b/scripts/runway_helpers.py @@ -0,0 +1,347 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = ["requests"] +# /// + +"""Shared helpers for Runway API scripts: API calls, task polling, retry, download, error handling.""" + +import json +import os +import platform +import shutil +import subprocess +import sys +import time +import mimetypes +import requests + +API_BASE = "https://api.dev.runwayml.com" +API_VERSION = "2024-11-06" + +# ── Models registry ────────────────────────────────────── + +VIDEO_MODELS = { + "seedance2": { + "endpoints": ["text_to_video", "image_to_video", "video_to_video"], + "cost": "36 credits/sec", + "description": "Reference image and video, long duration (up to 15s)", + "input": "Text, Image, and/or Video", + }, + "gen4.5": { + "endpoints": ["text_to_video", "image_to_video"], + "cost": "12 credits/sec", + "description": "High quality, general purpose", + "input": "Text and/or Image", + }, + "gen4_turbo": { + "endpoints": ["image_to_video"], + "cost": "5 credits/sec", + "description": "Fast, image-driven (image required)", + "input": "Image required", + }, + "gen4_aleph": { + "endpoints": ["video_to_video"], + "cost": "15 credits/sec", + "description": "Video editing/transformation", + "input": "Video + Text/Image", + }, + "veo3": { + "endpoints": ["text_to_video", "image_to_video"], + "cost": "40 credits/sec", + "description": "Premium quality", + "input": "Text/Image", + "durations": [8], + }, + "veo3.1": { + "endpoints": ["text_to_video", "image_to_video"], + "cost": "20-40 credits/sec", + "description": "High quality Google model", + "input": "Text/Image", + "durations": [4, 6, 8], + }, + "veo3.1_fast": { + "endpoints": ["text_to_video", "image_to_video"], + "cost": "10-15 credits/sec", + "description": "Fast Google model", + "input": "Text/Image", + "durations": [4, 6, 8], + }, +} + +IMAGE_MODELS = { + "gen4_image": { + "endpoint": "text_to_image", + "cost": "5-8 credits", + "description": "Highest quality", + }, + "gen4_image_turbo": { + "endpoint": "text_to_image", + "cost": "2 credits", + "description": "Fast and cheap", + }, + "gemini_2.5_flash": { + "endpoint": "text_to_image", + "cost": "5 credits", + "description": "Google Gemini model", + }, +} + +AUDIO_MODELS = { + "eleven_multilingual_v2": { + "endpoint": "text_to_speech", + "cost": "1 credit/50 chars", + "description": "Text to speech", + }, + "eleven_text_to_sound_v2": { + "endpoint": "sound_effect", + "cost": "1-2 credits", + "description": "Sound effect generation", + }, + "eleven_voice_isolation": { + "endpoint": "voice_isolation", + "cost": "1 credit/6 sec", + "description": "Isolate voice from audio", + }, + "eleven_voice_dubbing": { + "endpoint": "voice_dubbing", + "cost": "1 credit/2 sec", + "description": "Dub to other languages", + }, + "eleven_multilingual_sts_v2": { + "endpoint": "speech_to_speech", + "cost": "1 credit/3 sec", + "description": "Voice conversion", + }, +} + +# ── API key ────────────────────────────────────────────── + +def get_api_key(args_key=None): + key = args_key or os.environ.get("RUNWAYML_API_SECRET") + if not key: + print( + "Error: No API key. Set RUNWAYML_API_SECRET or pass --api-key.\n" + "Get your key at https://dev.runwayml.com/", + file=sys.stderr, + ) + sys.exit(1) + return key + + +def _headers(api_key): + return { + "Authorization": f"Bearer {api_key}", + "X-Runway-Version": API_VERSION, + "Content-Type": "application/json", + } + +# ── Error formatting ───────────────────────────────────── + +def format_api_error(status_code, response_text): + msg = f"API error {status_code}" + try: + data = json.loads(response_text) + error = data.get("error", data.get("message", "")) + issues = data.get("issues", []) + except (json.JSONDecodeError, TypeError): + error = response_text[:500] if response_text else "" + issues = [] + + if status_code == 400: + detail = error + if issues: + parts = [f"{i.get('path', ['?'])[-1]}: {i.get('message', '')}" for i in issues] + detail = f"{error} [{'; '.join(parts)}]" + return f"{msg}: Invalid input — {detail}" + elif status_code == 401: + return f"{msg}: Authentication failed. Check RUNWAYML_API_SECRET." + elif status_code == 429: + return f"{msg}: Rate limited. Will retry..." + elif status_code in (502, 503, 504): + return f"{msg}: Server overload. Will retry..." + return f"{msg}: {error}" + +# ── API calls with retry ───────────────────────────────── + +def api_post(api_key, endpoint, body, max_retries=3): + """POST to the Runway API with automatic retry on 429/5xx.""" + headers = _headers(api_key) + delays = [5, 15, 45] + + for attempt in range(max_retries + 1): + r = requests.post(f"{API_BASE}{endpoint}", headers=headers, json=body) + if r.ok: + return r.json() + if r.status_code in (429, 502, 503, 504) and attempt < max_retries: + delay = delays[min(attempt, len(delays) - 1)] + print( + f" {format_api_error(r.status_code, r.text)}\n" + f" Retrying in {delay}s (attempt {attempt + 1}/{max_retries})...", + file=sys.stderr, + ) + time.sleep(delay) + continue + msg = format_api_error(r.status_code, r.text) + print(f"Error: {msg}", file=sys.stderr) + sys.exit(1) + + +def api_get(api_key, path, max_retries=3): + """GET from the Runway API with automatic retry on 429/5xx.""" + headers = _headers(api_key) + delays = [5, 15, 45] + + for attempt in range(max_retries + 1): + r = requests.get(f"{API_BASE}{path}", headers=headers) + if r.ok: + return r.json() + if r.status_code in (429, 502, 503, 504) and attempt < max_retries: + delay = delays[min(attempt, len(delays) - 1)] + print(f" Retrying in {delay}s...", file=sys.stderr) + time.sleep(delay) + continue + msg = format_api_error(r.status_code, r.text) + print(f"Error: {msg}", file=sys.stderr) + sys.exit(1) + +# ── Task polling ───────────────────────────────────────── + +def poll_task(api_key, task_id, interval=5, timeout=600): + """Poll a Runway task until it reaches a terminal state.""" + start = time.time() + while time.time() - start < timeout: + task = api_get(api_key, f"/v1/tasks/{task_id}") + status = task.get("status", "") + + if status == "SUCCEEDED": + return task + if status == "FAILED": + failure = task.get("failure", "Unknown error") + failure_code = task.get("failureCode", "") + detail = f"{failure_code}: {failure}" if failure_code else str(failure) + print(f"Error: Task failed — {detail}", file=sys.stderr) + sys.exit(1) + if status == "CANCELLED": + print("Error: Task was cancelled.", file=sys.stderr) + sys.exit(1) + + elapsed = int(time.time() - start) + print(f" [{task_id[:12]}] {status} ({elapsed}s)...", file=sys.stderr) + time.sleep(interval) + + print(f"Error: Task timed out after {timeout}s.", file=sys.stderr) + sys.exit(1) + +# ── File download ──────────────────────────────────────── + +def download_file(url, filename): + """Download a URL to a local file.""" + parent = os.path.dirname(filename) + if parent: + os.makedirs(parent, exist_ok=True) + r = requests.get(url, stream=True) + r.raise_for_status() + with open(filename, "wb") as f: + for chunk in r.iter_content(chunk_size=8192): + f.write(chunk) + return os.path.abspath(filename) + +# ── Upload helper ──────────────────────────────────────── + +def upload_file(api_key, local_path): + """Upload a local file to Runway and return the runway:// URI. + + Two-step process: + 1. POST /v1/uploads with filename to get a presigned uploadUrl + fields + runwayUri + 2. POST the file to uploadUrl with the returned fields as multipart form data + """ + if not os.path.isfile(local_path): + print(f"Error: File not found: {local_path}", file=sys.stderr) + sys.exit(1) + + filename = os.path.basename(local_path) + + r = requests.post( + f"{API_BASE}/v1/uploads", + headers=_headers(api_key), + json={"filename": filename, "type": "ephemeral"}, + ) + if not r.ok: + msg = format_api_error(r.status_code, r.text) + print(f"Error creating upload: {msg}", file=sys.stderr) + sys.exit(1) + + data = r.json() + upload_url = data.get("uploadUrl") + fields = data.get("fields", {}) + runway_uri = data.get("runwayUri") + + if not upload_url or not runway_uri: + print(f"Error: Upload response missing uploadUrl or runwayUri: {json.dumps(data)}", file=sys.stderr) + sys.exit(1) + + mime_type = mimetypes.guess_type(local_path)[0] or "application/octet-stream" + with open(local_path, "rb") as f: + r2 = requests.post( + upload_url, + data=fields, + files={"file": (filename, f, mime_type)}, + ) + + if not r2.ok: + print(f"Error uploading file: {r2.status_code} {r2.text[:500]}", file=sys.stderr) + sys.exit(1) + + print(f" Uploaded: {runway_uri}", file=sys.stderr) + return runway_uri + + +def ensure_url(path_or_url, api_key): + """If input is a local file, upload it. Otherwise return as-is.""" + if path_or_url.startswith("http://") or path_or_url.startswith("https://"): + return path_or_url + if path_or_url.startswith("runway://"): + return path_or_url + return upload_file(api_key, path_or_url) + +# ── Output path helper ─────────────────────────────────── + +def output_path(filename, output_dir=None): + if output_dir: + os.makedirs(output_dir, exist_ok=True) + return os.path.join(output_dir, os.path.basename(filename)) + return filename + +# ── Cost estimation ────────────────────────────────────── + +def estimate_video_credits(model, duration): + """Rough credit estimate for a video generation.""" + cost_str = VIDEO_MODELS.get(model, {}).get("cost", "") + try: + per_sec = int(cost_str.split()[0].replace("-", "").strip("~")) + except (ValueError, IndexError): + return None + return per_sec * duration + + +def estimate_image_credits(model): + cost_str = IMAGE_MODELS.get(model, {}).get("cost", "") + try: + return int(cost_str.split()[0].replace("-", "").strip("~")) + except (ValueError, IndexError): + return None + +# ── Desktop notification ───────────────────────────────── + +def send_notification(title, message): + try: + system = platform.system() + if system == "Linux" and shutil.which("notify-send"): + subprocess.run(["notify-send", title, message], timeout=5) + elif system == "Darwin": + script = f'display notification "{message}" with title "{title}"' + subprocess.run(["osascript", "-e", script], timeout=5) + else: + print("\a", end="", file=sys.stderr) + except Exception: + pass diff --git a/skills/rw-api-reference/SKILL.md b/skills/rw-api-reference/SKILL.md index a1eb08f..f388998 100644 --- a/skills/rw-api-reference/SKILL.md +++ b/skills/rw-api-reference/SKILL.md @@ -33,12 +33,12 @@ X-Runway-Version: 2024-11-06 | `veo3.1_fast` | `POST /v1/image_to_video` or `POST /v1/text_to_video` | Text/Image | 10-15 | | `seedance2` | `POST /v1/text_to_video`, `POST /v1/image_to_video`, or `POST /v1/video_to_video` | Text, Image, and/or Video | 36 | -Video duration: **2-15 seconds** (model-dependent). Aspect ratios: `1280:720`, `720:1280`, `1104:832`, `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `21:9`, etc. +Video duration: **2-15 seconds** (model-dependent). Aspect ratios are pixel-based: `1280:720`, `720:1280`, `1104:832`, `960:960`, `832:1104`, `1584:672`, etc. **Seedance 2 specifics:** - Modes: text-to-video, image-to-video (first/last frame or image reference), video-to-video - Duration: required for TTV and ITV (in seconds) -- Aspect ratios: `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `21:9` — pixel-based ratios (e.g. `1280:720`) are **not** supported +- Aspect ratios (pixel-based): `1280:720`, `720:1280`, `960:960`, `1112:834`, `834:1112`, `1470:630`, `992:432`, `864:496`, `752:560`, `640:640`, `560:752`, `496:864` - ITV supports two mutually exclusive modes: first/last frame (`promptImage` array with `position`) or image reference (`references` array) - VTV input requirements: max 15 seconds, max 32 MB, min 720p resolution, MP4 recommended diff --git a/skills/rw-generate-audio/SKILL.md b/skills/rw-generate-audio/SKILL.md new file mode 100644 index 0000000..f52dbcc --- /dev/null +++ b/skills/rw-generate-audio/SKILL.md @@ -0,0 +1,78 @@ +--- +name: rw-generate-audio +description: "Generate audio using the Runway API via runnable scripts. Supports TTS, sound effects, voice isolation, dubbing, and voice conversion." +user-invocable: true +allowed-tools: Read, Grep, Glob, Edit, Write, Bash(uv run *), Bash(command -v uv) +--- + +# Generate Audio + +Generate audio directly using the Runway API. Supports text-to-speech, sound effects, voice isolation, dubbing, and speech-to-speech voice conversion. + +**IMPORTANT:** Run scripts from the user's working directory so output files are saved where the user expects. + +## Usage + +```bash +uv run scripts/generate_audio.py --type tts --text "Hello world" --filename "greeting.mp3" [--voice-id ID] [--api-key KEY] +``` + +## Preflight + +1. `command -v uv` must succeed +2. `RUNWAYML_API_SECRET` must be set, or pass `--api-key` + +## Audio Types + +| Type | Description | Required Args | +|------|-------------|---------------| +| `tts` | Text to speech | `--text` | +| `sfx` | Sound effect generation | `--text` | +| `isolate` | Isolate voice from audio | `--audio-url` | +| `dub` | Dub to another language | `--audio-url`, `--target-language` | +| `sts` | Voice conversion | `--audio-url` | + +## Parameters + +| Param | Description | Default | +|-------|-------------|---------| +| `--type` | Audio type (required): tts, sfx, isolate, dub, sts | -- | +| `--filename` | Output filename (required) | -- | +| `--text` | Text input (for tts and sfx) | -- | +| `--audio-url` | Audio URL or local path (for isolate, dub, sts) | -- | +| `--voice-id` | Voice preset (for tts and sts, e.g. Maya, Noah, Leslie) | Maya | +| `--target-language` | Language code (for dub, e.g. "es") | -- | +| `--output-dir` | Output directory | cwd | +| `--api-key` | Runway API key | env `RUNWAYML_API_SECRET` | + +## Examples + +**Text-to-speech:** +```bash +uv run scripts/generate_audio.py --type tts --text "Welcome to our product showcase" --filename "voiceover.mp3" +``` + +**Sound effect:** +```bash +uv run scripts/generate_audio.py --type sfx --text "Thunder rolling across a stormy sky" --filename "thunder.mp3" +``` + +**Voice isolation:** +```bash +uv run scripts/generate_audio.py --type isolate --audio-url "noisy-recording.mp3" --filename "clean-voice.mp3" +``` + +**Speech-to-speech (voice conversion):** +```bash +uv run scripts/generate_audio.py --type sts --audio-url "recording.mp3" --voice-id Noah --filename "converted.mp3" +``` + +**Dubbing:** +```bash +uv run scripts/generate_audio.py --type dub --audio-url "english-narration.mp3" --target-language es --filename "spanish-dub.mp3" +``` + +## Output + +- The script downloads the result and saves it to the specified path +- Script outputs the full path to the saved file diff --git a/skills/rw-generate-image/SKILL.md b/skills/rw-generate-image/SKILL.md new file mode 100644 index 0000000..61d08f6 --- /dev/null +++ b/skills/rw-generate-image/SKILL.md @@ -0,0 +1,83 @@ +--- +name: rw-generate-image +description: "Generate images directly using the Runway API via runnable scripts. Supports text-to-image with optional reference images." +user-invocable: true +allowed-tools: Read, Grep, Glob, Edit, Write, Bash(uv run *), Bash(command -v uv) +--- + +# Generate Image + +Generate images directly using the Runway API. This skill runs Python scripts that call the API, poll for completion, and download the result. + +**IMPORTANT:** Run scripts from the user's working directory so output files are saved where the user expects. + +## Usage + +```bash +uv run scripts/generate_image.py --prompt "your description" --filename "output.png" [--model gen4_image] [--ratio 1280:720] [--reference-images Tag=URL ...] [--api-key KEY] +``` + +## Preflight + +1. `command -v uv` must succeed +2. `RUNWAYML_API_SECRET` must be set, or pass `--api-key` + +## Available Models + +| Model | Best For | Ref Images | Cost | Speed | +|-------|----------|------------|------|-------| +| `gen4_image` | Highest quality | Optional (up to 3) | 5-8 credits | Standard | +| `gen4_image_turbo` | Fast and cheap | **Required** (1-3) | 2 credits | Fast | +| `gemini_2.5_flash` | Google Gemini | Optional (up to 3) | 5 credits | Standard | + +## Model Selection Guidance + +- "fast", "cheap", "draft" -> `gemini_2.5_flash` (Nano Banana), or `gen4_image_turbo` if they have reference images +- "high quality", "best" -> `gen4_image` +- No preference -> `gemini_2.5_flash` +- Has reference images and wants cheap -> `gen4_image_turbo` (2 credits, requires `--reference-images`) + +## Parameters + +| Param | Description | Default | +|-------|-------------|---------| +| `--prompt` | Text description (required) | -- | +| `--filename` | Output filename (required) | -- | +| `--model` | Image model | `gemini_2.5_flash` | +| `--ratio` | Aspect ratio. gemini_2.5_flash: `1344:768`, `768:1344`, `1024:1024`, etc. gen4_image: `1280:720`, `1360:768`, `1920:1080`, etc. | Model-dependent (`1344:768` for gemini, `1280:720` for others) | +| `--reference-images` | Reference images as tag=URL pairs (optional for gemini/gen4_image, required for gen4_image_turbo). Tag: lowercase, 3-16 chars, e.g. `product=URL` | -- | +| `--output-dir` | Output directory | cwd | +| `--api-key` | Runway API key | env `RUNWAYML_API_SECRET` | + +## Filename Convention + +Pattern: `yyyy-mm-dd-hh-mm-ss-name.png` + +## Examples + +**Basic image:** +```bash +uv run scripts/generate_image.py --prompt "A serene Japanese garden with cherry blossoms" --filename "2026-04-14-japanese-garden.png" +``` + +**With reference images (gen4_image):** +```bash +uv run scripts/generate_image.py --prompt "@product on a marble counter, lifestyle photo" --model gen4_image --reference-images product=https://example.com/product.jpg --filename "2026-04-14-product-lifestyle.png" +``` + +**Fast with reference images (gen4_image_turbo — requires reference images):** +```bash +uv run scripts/generate_image.py --prompt "A neon sign reading SALE in @style" --model gen4_image_turbo --reference-images style=https://example.com/style.jpg --filename "draft.png" +``` + +## Output + +- The script downloads the result and saves it to the specified path +- Script outputs the full path to the saved file +- **Do not read the image file back** -- just inform the user of the saved path + +## Common Failures + +- `Error: No API key` -> set `RUNWAYML_API_SECRET` or pass `--api-key` +- `Error: Task failed -- SAFETY.INPUT.*` -> content moderation, suggest different prompt +- `API error 429` -> rate limited, script auto-retries diff --git a/skills/rw-generate-video/SKILL.md b/skills/rw-generate-video/SKILL.md new file mode 100644 index 0000000..9998676 --- /dev/null +++ b/skills/rw-generate-video/SKILL.md @@ -0,0 +1,110 @@ +--- +name: rw-generate-video +description: "Generate videos directly using the Runway API via runnable scripts. Supports text-to-video, image-to-video, and video-to-video with seedance2, gen4.5, veo3, and more." +user-invocable: true +allowed-tools: Read, Grep, Glob, Edit, Write, Bash(uv run *), Bash(command -v uv) +--- + +# Generate Video + +Generate videos directly using the Runway API. This skill runs Python scripts that call the API, poll for completion, and download the result. + +**IMPORTANT:** Run scripts from the user's working directory so output files are saved where the user expects. + +## Usage + +```bash +uv run scripts/generate_video.py --prompt "your description" --filename "output.mp4" [--model seedance2] [--ratio 1280:720] [--duration 5] [--image-url "..."] [--api-key KEY] +``` + +## Preflight + +1. `command -v uv` must succeed. If not, tell the user to install uv: `curl -LsSf https://astral.sh/uv/install.sh | sh` +2. `RUNWAYML_API_SECRET` must be set, or the user passes `--api-key` + +## Available Models + +| Model | Best For | Input | Cost | +|-------|----------|-------|------| +| `seedance2` | Reference image and video, long duration (up to 15s) | Text, Image, and/or Video | 36 credits/sec | +| `gen4.5` | High quality, general purpose | Text and/or Image | 12 credits/sec | +| `gen4_turbo` | Fast, image-driven | Image required | 5 credits/sec | +| `gen4_aleph` | Video editing/transformation | Video + Text/Image | 15 credits/sec | +| `veo3` | Premium quality | Text/Image | 40 credits/sec | +| `veo3.1` | High quality Google model | Text/Image | 20-40 credits/sec | +| `veo3.1_fast` | Fast Google model | Text/Image | 10-15 credits/sec | + +## Model Selection Guidance + +Map user requests: +- "product ad", "e-commerce", "long video" -> `seedance2` +- "fast", "cheap", "quick" -> `veo3.1_fast` or `gen4_turbo` (if they have an image) +- "high quality", "best", "cinematic" -> `gen4.5` or `veo3` +- "edit video", "transform video" -> `gen4_aleph` or `seedance2` +- No preference -> `seedance2` + +## Parameters + +| Param | Description | Default | +|-------|-------------|---------| +| `--prompt` | Text description (required) | -- | +| `--filename` | Output filename (required) | -- | +| `--model` | Video model | `gen4.5` | +| `--ratio` | Aspect ratio (pixel-based). Common: `1280:720`, `720:1280`, `960:960`. seedance2 also supports `1112:834`, `834:1112`, `1470:630`, etc. | `1280:720` | +| `--duration` | Duration in seconds (model-dependent, seedance2 supports up to 15s) | `5` | +| `--image-url` | Image URL or local file for image-to-video | -- | +| `--video-url` | Video URL or local file for video-to-video (gen4_aleph, seedance2) | -- | +| `--output-dir` | Output directory | cwd | +| `--api-key` | Runway API key | env `RUNWAYML_API_SECRET` | + +## Filename Convention + +Generate filenames with the pattern: `yyyy-mm-dd-hh-mm-ss-name.mp4` + +Examples: +- "A cyberpunk city" -> `2026-04-14-14-23-05-cyberpunk-city.mp4` +- "Waves on a beach" -> `2026-04-14-15-30-12-beach-waves.mp4` + +## Examples + +**Text-to-video (seedance2):** +```bash +uv run scripts/generate_video.py --prompt "A serene mountain landscape at sunrise with mist" --filename "2026-04-14-mountain-sunrise.mp4" --model seedance2 --ratio 1280:720 +``` + +**Image-to-video (animate a product photo):** +```bash +uv run scripts/generate_video.py --prompt "Camera slowly zooms out, product sparkles" --image-url "product.jpg" --filename "2026-04-14-product-reveal.mp4" --model seedance2 --ratio 720:1280 +``` + +**Video-to-video (seedance2):** +```bash +uv run scripts/generate_video.py --prompt "Transform into a warm golden sunset scene" --video-url "input.mp4" --filename "2026-04-14-sunset-transform.mp4" --model seedance2 +``` + +**Fast draft:** +```bash +uv run scripts/generate_video.py --prompt "A cat playing piano" --filename "draft.mp4" --model veo3.1_fast --ratio 1280:720 --duration 4 +``` + +**Premium quality:** +```bash +uv run scripts/generate_video.py --prompt "Cinematic drone shot over Tokyo at night" --filename "tokyo.mp4" --model veo3 --ratio 1280:720 --duration 8 +``` + +## Output + +- The script downloads the result and saves it to the specified path +- Script outputs the full path to the saved file +- **Do not read the video file back** -- just inform the user of the saved path + +## Common Failures + +- `Error: No API key` -> set `RUNWAYML_API_SECRET` or pass `--api-key` +- `Error: Task failed -- SAFETY.INPUT.*` -> content moderation, suggest different prompt +- `Error: Task failed -- ASSET.INVALID` -> bad input file format, check image/video format +- `API error 429` -> rate limited, script auto-retries + +## For Batch Generation + +To generate many videos at once, run this script in a loop — the agent can orchestrate multiple calls with different prompts, images, or parameters to produce campaigns, localized variants, or creative iterations at scale. diff --git a/skills/rw-integrate-audio/SKILL.md b/skills/rw-integrate-audio/SKILL.md index dd8a625..05abe0e 100644 --- a/skills/rw-integrate-audio/SKILL.md +++ b/skills/rw-integrate-audio/SKILL.md @@ -34,8 +34,8 @@ const client = new RunwayML(); const task = await client.textToSpeech.create({ model: 'eleven_multilingual_v2', - text: 'Hello, welcome to our application!', - voiceId: 'voice_id_here' // See voice listing endpoint + promptText: 'Hello, welcome to our application!', + voice: { type: 'runway-preset', presetId: 'Maya' } }).waitForTaskOutput(); const audioUrl = task.output[0]; @@ -50,8 +50,8 @@ client = RunwayML() task = client.text_to_speech.create( model='eleven_multilingual_v2', - text='Hello, welcome to our application!', - voice_id='voice_id_here' + prompt_text='Hello, welcome to our application!', + voice={ 'type': 'runway-preset', 'presetId': 'Maya' } ).wait_for_task_output() audio_url = task.output[0] @@ -87,7 +87,7 @@ const upload = await client.uploads.createEphemeral( const task = await client.voiceIsolation.create({ model: 'eleven_voice_isolation', - audio: upload.runwayUri + audioUri: upload.runwayUri }).waitForTaskOutput(); ``` @@ -98,8 +98,8 @@ Dub audio/video into other languages. ```javascript const task = await client.voiceDubbing.create({ model: 'eleven_voice_dubbing', - audio: 'https://example.com/speech.mp3', - targetLanguage: 'es' // Spanish + audioUri: 'https://example.com/speech.mp3', + targetLang: 'es' // Spanish }).waitForTaskOutput(); ``` @@ -110,8 +110,8 @@ Convert one voice to another. ```javascript const task = await client.speechToSpeech.create({ model: 'eleven_multilingual_sts_v2', - audio: 'https://example.com/original-speech.mp3', - voiceId: 'target_voice_id' + media: { type: 'audio', uri: 'https://example.com/original-speech.mp3' }, + voice: { type: 'runway-preset', presetId: 'Noah' } }).waitForTaskOutput(); ``` @@ -133,8 +133,8 @@ app.post('/api/text-to-speech', async (req, res) => { const task = await client.textToSpeech.create({ model: 'eleven_multilingual_v2', - text, - voiceId + promptText: text, + voice: { type: 'runway-preset', presetId: voiceId || 'Maya' } }).waitForTaskOutput(); res.json({ audioUrl: task.output[0] }); diff --git a/skills/rw-integrate-video/SKILL.md b/skills/rw-integrate-video/SKILL.md index fe7cd04..25493f2 100644 --- a/skills/rw-integrate-video/SKILL.md +++ b/skills/rw-integrate-video/SKILL.md @@ -128,10 +128,8 @@ Transform an existing video with a text prompt and/or reference image. // Node.js SDK — gen4_aleph const task = await client.videoToVideo.create({ model: 'gen4_aleph', - promptVideo: 'https://example.com/source.mp4', + videoUri: 'https://example.com/source.mp4', promptText: 'Transform into an animated cartoon style', - ratio: '1280:720', - duration: 5 }).waitForTaskOutput(); ``` @@ -149,7 +147,7 @@ const task = await client.videoToVideo.create({ ### Seedance 2 -Seedance 2 supports text-to-video, image-to-video (two modes), and video-to-video. It uses aspect ratio shorthands — pixel-based ratios (e.g. `1280:720`) are **not** supported. +Seedance 2 supports text-to-video, image-to-video (two modes), and video-to-video. It uses pixel-based ratios: `1280:720`, `720:1280`, `960:960`, `1112:834`, `834:1112`, `1470:630`, `992:432`, `864:496`, `752:560`, `640:640`, `560:752`, `496:864`. #### Text-to-Video @@ -158,7 +156,7 @@ const task = await client.textToVideo.create({ model: 'seedance2', promptText: 'A calm ocean wave gently crashing on a sandy beach at sunset', duration: 5, - ratio: '16:9' + ratio: '1280:720' }).waitForTaskOutput(); ``` @@ -175,7 +173,7 @@ const task = await client.imageToVideo.create({ { uri: 'https://example.com/image2.jpg', position: 'last' } ], duration: 4, - ratio: '16:9' + ratio: '1280:720' }).waitForTaskOutput(); ``` @@ -192,7 +190,7 @@ const task = await client.imageToVideo.create({ promptImage: 'https://example.com/image.jpg', references: [{ type: 'image', uri: 'https://example.com/reference.jpg' }], duration: 4, - ratio: '16:9' + ratio: '1280:720' }).waitForTaskOutput(); ``` @@ -220,9 +218,10 @@ task = client.video_to_video.create( | `model` | string | Yes | Must be `"seedance2"` | | `promptText` | string | Yes | Text description of the desired video | | `duration` | number | Yes (TTV/ITV) | Duration in seconds | -| `ratio` | string | Yes (TTV/ITV) | `16:9`, `9:16`, `1:1`, `4:3`, `3:4`, `21:9` | +| `ratio` | string | Yes (TTV/ITV) | `1280:720`, `720:1280`, `960:960`, `1112:834`, `834:1112`, `1470:630` | | `promptImage` | string or array | Yes (ITV) | URI string or array of `{ uri, position? }` objects | -| `promptVideo` | string | Yes (VTV) | Publicly accessible URL of the input video | +| `promptVideo` | string | Yes (seedance2 VTV) | Input video URI (seedance2 only) | +| `videoUri` | string | Yes (gen4_aleph VTV) | Input video URI (gen4_aleph only) | | `references` | array | No | Image references — `[{ type: "image", uri: "..." }]` (ITV Mode 2 and VTV only) | ### Character Performance: `POST /v1/character_performance`