Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions plugins/runway-api/LICENSE.runway-api
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 RunwayML, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
36 changes: 36 additions & 0 deletions plugins/runway-api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# runway-api

Runway API skills for Cline users building or operating media generation workflows.

## What It Does

This plugin bundles skills for two related workflows:

- Generating media directly with Runway's API, including text/image/video generation, audio generation, uploads, organization details, and general API calls.
- Integrating Runway features into server-side apps, including compatibility checks, API key setup, video/image/audio endpoints, uploads, real-time avatar characters, document knowledge, and React avatar embeds.

The plugin also includes the Python and Node helper scripts used by the direct-generation skills, plus bundled guidance for paid API calls, media uploads, outbound URL fetches, and API-key handling.

## Install

```bash
cline plugin install runway-api
```

For local development from this repository:

```bash
cline plugin install ./plugins/runway-api --cwd .
```

## Requirements

- A Runway developer account with available credits.
- `RUNWAYML_API_SECRET` in the environment or a user-created local `.env` file when making API calls.
- `uv` for the bundled Python generation helper scripts.
- Node.js 20 or newer for the general `use-runway-api` helper script.
- A server-side app when using integration skills. API keys must not be exposed in frontend code.

## Security Notes

Runway operations can spend credits and may upload or generate sensitive media. Cline should confirm paid generation, uploads, organization/account actions, external media URLs, output locations, and production code changes before acting.
Binary file added plugins/runway-api/assets/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions plugins/runway-api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { AgentPlugin } from "@cline/sdk"

const plugin: AgentPlugin = {
name: "runway-api",
manifest: {
capabilities: ["skills"],
},
}

export default plugin
19 changes: 19 additions & 0 deletions plugins/runway-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "runway-api",
"version": "0.0.0",
"private": true,
"type": "module",
"description": "Cline plugin that bundles Runway API media generation and app integration skills.",
"cline": {
"plugins": [
{
"paths": [
"./index.ts"
],
"capabilities": [
"skills"
]
}
]
}
}
130 changes: 130 additions & 0 deletions plugins/runway-api/scripts/generate_audio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# /// 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)")
args = parser.parse_args()

api_key = get_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()
103 changes: 103 additions & 0 deletions plugins/runway-api/scripts/generate_image.py
Original file line number Diff line number Diff line change
@@ -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,
ensure_url,
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)")
args = parser.parse_args()

api_key = get_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, source = pair.split("=", 1)
refs.append({"tag": tag, "uri": ensure_url(source, api_key)})
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()
Loading