Install
openclaw skills install medeo-videoAI-powered video generation skill. Use when the user wants to generate videos from text descriptions, browse video recipes, upload assets, or manage video cr...
openclaw skills install medeo-videoGenerate AI videos from text. Medeo is an AI video agent that handles full storylines, multi-scene narratives, and screenplays in a single call — shot composition, transitions, pacing, and music are all automatic.
⚠️ Do NOT split stories into multiple calls. Pass the entire screenplay in one
--message.
Before running any command, check if API key is configured:
python3 {baseDir}/scripts/medeo_video.py config 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print('ok' if d.get('apiKey') else 'missing')"
ok → proceed normallymissing (or command fails) → stop immediately, do NOT run any other commands. Send the setup message using the channel-appropriate method:Feishu — use Feishu API directly (NOT message tool — it won't render cards):
import json, urllib.request
cfg = json.loads(open("/home/ec2-user/.openclaw/openclaw.json").read())
feishu = cfg["channels"]["feishu"]["accounts"]["default"]
token = json.loads(urllib.request.urlopen(urllib.request.Request(
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal",
data=json.dumps({"app_id": feishu["appId"], "app_secret": feishu["appSecret"]}).encode(),
headers={"Content-Type": "application/json"}
)).read())["tenant_access_token"]
card = {
"config": {"wide_screen_mode": True},
"header": {"title": {"tag": "plain_text", "content": "🎬 Video Generation — API Key Required"}, "template": "blue"},
"elements": [{"tag": "div", "text": {"tag": "lark_md", "content": "You need a **Medeo API Key** to generate videos.\n\n**Steps:**\n1. Go to https://medeo.app/dev/apikey\n - No account? You'll be guided to sign up. The key appears after login.\n2. Copy the key (starts with `mk_`) and send it back to me.\n\nOnce I have it, I'll configure everything for you."}}],
}
urllib.request.urlopen(urllib.request.Request(
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id",
data=json.dumps({"receive_id": "<USER_OPEN_ID>", "msg_type": "interactive", "content": json.dumps(card)}).encode(),
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
))
Telegram / Discord / Other channels — send plain text via message tool (these channels support markdown natively):
🎬 Video Generation — API Key Required
Steps:
1. Go to https://medeo.app/dev/apikey (sign up if needed — the key appears after login)
2. Copy the key (starts with mk_) and send it back to me
Once I have it, I'll configure everything for you.
Once they provide the key: python3 {baseDir}/scripts/medeo_video.py config-init --api-key "mk_..."
If no API Key is configured, the script outputs "setup_required": true.
python3 {baseDir}/scripts/medeo_video.py config-init --api-key "mk_..."Users only need to know 3 ways to generate a video:
The agent handles everything else silently.
IMPORTANT: Before spawning the generation task, immediately reply to the user with an acknowledgment like: "🎬 Starting video generation — I'll send you the result in about 5–10 minutes." Do NOT wait in silence. The user should know their request was received.
python3 {baseDir}/scripts/medeo_video.py spawn-task \
--message "user's video description" \
--deliver-to "oc_xxx" \
--deliver-channel "feishu"
# First: upload-file to get media_id (see Section 3)
python3 {baseDir}/scripts/medeo_video.py spawn-task \
--message "user's video description" \
--media-ids "media_01..." \
--asset-sources my_uploaded_assets \
--deliver-to "oc_xxx" \
--deliver-channel "feishu"
python3 {baseDir}/scripts/medeo_video.py spawn-task \
--message "user's video description" \
--media-urls "https://example.com/photo.jpg" \
--asset-sources my_uploaded_assets \
--deliver-to "oc_xxx" \
--deliver-channel "feishu"
Agent auto-behavior: When the user provides images (Usage 2 or 3), always pass
--asset-sources my_uploaded_assetsso Medeo uses their images instead of generating new ones. The user does not need to know this flag exists.
These are handled automatically by the agent. Do NOT mention them to users or ask users to provide them.
| Flag | When to use | Default behavior |
|---|---|---|
--voice-id "voice_01..." | When a specific voice is needed | Medeo picks automatically |
--video-style-id "style_01..." | When a specific visual style is needed | Medeo picks automatically |
--asset-sources | When user provides images: pass my_uploaded_assets | Medeo decides |
--recipe-id "recipe_01..." | When using a template | None |
--aspect-ratio "9:16" | When user specifies portrait/landscape | 16:9 |
--duration-ms 30000 | When user specifies duration | Medeo decides |
--no-render | Debug only — skip rendering | Always render |
--deliver-to)This is critical — determines where the generated video gets sent.
| Context | --deliver-to value | Example |
|---|---|---|
| Feishu group chat | The group's chat_id (starts with oc_). Extract from inbound metadata conversation_label or chat_id — strip the chat: prefix if present (e.g. chat:oc_xxx → oc_xxx) | oc_158fd3e54407cbe170697c6c954bd4f2 |
| Feishu private chat | The user's open_id (starts with ou_). Extract from inbound metadata sender_id — strip the user: prefix if present | ou_f7f458f4d7b4ff49ec1b8de22a1e3206 |
| Telegram | The chat_id from the inbound message context | -1001234567890 |
| Discord | The channel_id from the inbound message context | 1234567890123456 |
How to determine group vs private on Feishu:
is_group_chat in the inbound metadatatrue → use conversation_label / chat_id (the oc_ value)false → use sender_id (the ou_ value)Step 2: Use sessions_spawn with the returned args (label: "medeo: <brief>", runTimeoutSeconds: 2400).
Step 3: Tell user it's generating. Sub-agent auto-announces when done.
python3 {baseDir}/scripts/medeo_video.py upload \
--url "https://example.com/photo.jpg" \
--project-id "project_01..." # optional: associate media with existing project
--no-wait # optional: return job_id immediately without polling
Use upload-file when the user sends an image via Telegram, Discord, Feishu, or as a local file.
This uses the direct upload API (prepare → S3 presigned PUT → register) instead of URL-based upload.
Trigger: Only when the user explicitly requests video generation AND sends an image attachment in the same message (e.g. "make a video with this photo"). Do NOT auto-upload on every image message — other skills or conversations may involve images unrelated to video generation.
# From local file (downloaded by OpenClaw from attachment)
python3 {baseDir}/scripts/medeo_video.py upload-file \
--file /tmp/user_photo.jpg
# From direct URL (Discord CDN, etc.)
python3 {baseDir}/scripts/medeo_video.py upload-file \
--url "https://cdn.discordapp.com/attachments/..."
# From Telegram (file_id from message.photo[-1].file_id)
# TELEGRAM_BOT_TOKEN must be set as env var — never pass as CLI arg (ps aux leaks it)
TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" python3 {baseDir}/scripts/medeo_video.py upload-file \
--telegram-file-id "AgACAgIAAxk..."
# From Feishu (message_id + image_key from message content)
python3 {baseDir}/scripts/medeo_video.py upload-file \
--feishu-message-id "om_xxx" \
--feishu-image-key "img_v3_xxx" \
--feishu-app-token "$FEISHU_APP_TOKEN"
Output: {"media_id": "media_01...", "filename": "photo.jpg"}
Then pass media_id to generation:
python3 {baseDir}/scripts/medeo_video.py spawn-task \
--message "Create a video featuring this person" \
--media-ids "media_01..."
| Platform | How to get image source | upload-file arg |
|---|---|---|
| Telegram | message.photo[-1].file_id | --telegram-file-id |
| Discord | message.attachments[0].url (public CDN URL) | --url |
| Feishu | message_id + image_key from message content JSON | --feishu-message-id + --feishu-image-key |
Download attachment binary → save to /tmp | --file | |
| Generic URL | Any direct image URL | --url |
Note: Discord attachment URLs are public CDN links — --url works directly. All other platforms require authentication to download.
# URL-based (existing behavior)
python3 {baseDir}/scripts/medeo_video.py spawn-task \
--message "Product showcase for this sneaker" \
--media-urls "https://example.com/front.jpg" "https://example.com/side.jpg"
Supports .jpg, .png, .webp, .mp4, .mov, .gif. Higher resolution + multiple angles = better results.
After upload or upload-file, if you need to check the upload job:
python3 {baseDir}/scripts/medeo_video.py upload-status --job-id "job_01..."
Returns media status (processing, completed, failed) and media_id once done.
These are for agent debugging or manual intervention only. Users should never see these commands.
Pipeline flow:
spawn-task (recommended, async)
└── generate (blocking, same pipeline)
├── upload (if --media-urls)
├── compose → compose-status (poll)
└── render → render-status (poll)
| Command | What it does | Key args |
|---|---|---|
generate | Blocking full pipeline (upload→compose→render) | Same as spawn-task minus deliver flags |
compose | Create project only (no render) | --message, --media-ids, --recipe-id |
compose-status | Poll compose task | --task-id "task_01..." |
render | Render existing project | --project-id "project_01..." |
render-status | Poll render job | --job-id "render_01..." |
upload-status | Poll upload job | --job-id "job_01..." |
All commands support --no-wait to return immediately without polling.
python3 {baseDir}/scripts/medeo_video.py recipes # list templates
python3 {baseDir}/scripts/medeo_video.py recipes --cursor <c> # paginate
Use in generation: --recipe-id "recipe_01...". See docs/recipes.md.
| Command | Description | User-visible? |
|---|---|---|
recipes | List video templates | Yes — "what templates are available?" |
last-job | Latest job status | Yes — "is my last video done?" |
history | Job history (last 50) | Yes — "show my video history" |
config | Show current configuration | No |
config-init --api-key "mk_..." | Initialize API key | Only during setup |
upload --url "URL" | Upload from public URL | No (agent internal) |
upload-file --file PATH | Upload from local file | No (agent internal) |
upload-file --url "URL" | Download URL → upload | No (agent internal) |
upload-file --telegram-file-id "..." | Upload Telegram attachment | No (agent internal) |
upload-file --feishu-image-key "..." | Upload Feishu attachment | No (agent internal) |
upload-status --job-id "..." | Check upload job status | No (agent internal) |
compose-status --task-id "..." | Check compose task progress | No (agent internal) |
render-status --job-id "..." | Check render job progress | No (agent internal) |
spawn-task + sessions_spawn for generation--message, never splitupload-file first → get media_id → pass to generation via --media-ids. Never ask the user for a URL if they already sent the image.python3 {baseDir}/scripts/feishu_send_video.py --video /tmp/result.mp4 --to "oc_xxx_or_ou_xxx" --cover-url "<thumbnail_url>" --duration <ms> (use oc_ chat_id for group chats, ou_ open_id for private chats; chat:oc_xxx and user:ou_xxx prefixed forms are also accepted)telegram_send_video.py (token from env only):
curl -sL -o /tmp/medeo_result.mp4 "<video_url>"
TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" python3 {baseDir}/scripts/telegram_send_video.py \
--video /tmp/medeo_result.mp4 \
--to "<chat_id>" \
--cover-url "<thumbnail_url>" \
--duration <seconds> \
--caption "🎬 Video ready!"
message tool directly — download the video to /tmp/result.mp4 via curl -sL -o /tmp/result.mp4 "<video_url>", then call message(action="send", channel="discord", target="<channel_id>", message="🎬 Video ready!", filePath="/tmp/result.mp4"). For files >25 MB, send video_url as a plain link instead.message tool with media parameter, or share video_url as a link if native sending is unavailable.thumbnail_url — the API always returns this field. Constructed as {ossBaseUrl}/{thumbnail_relative_path} (e.g. https://oss.prd.medeo.app/assets/medias/media_xxx.png).{ossBaseUrl}/{video_relative_path} (e.g. https://oss.prd.medeo.app/exported_video/v_xxx).ps). Always use env vars: TELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN.| Error | Action |
|---|---|
setup_required: true | Guide user to register + configure key |
upload_prep_rejected | File format/size rejected; check supported formats |
s3_put_failed | S3 upload error; retry once |
| Insufficient credits | Share recharge link from error output, retry after top-up |
| Compose/render timeout | Inform user, suggest retry. Complex scripts may take 15+ min |
| 401/403 | Key may be invalid or expired, ask user to regenerate |
| Upload 404 | Some image hosts block server-side fetch; use upload-file --url to download first |
upload vs upload-file comparisonAll data in ~/.openclaw/workspace/medeo-video/: config.json (API key), last_job.json (latest job), history/ (last 50 jobs).
MEDEO_API_KEY → config.json → built-in defaults. No legacy system-level files are read.feishu_send_video.py reads appId + appSecret from local ~/.openclaw/openclaw.json to call Feishu Open API. Credentials stay local and are never transmitted beyond the Feishu API.TELEGRAM_BOT_TOKEN env var only (never CLI args).config.json lives in the runtime data directory (~/.openclaw/workspace/medeo-video/), not in the skill source directory.