Generate 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:
bash
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):
python
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):
text
🎬 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.
bash
python3 {baseDir}/scripts/medeo_video.py spawn-task \
--message "user's video description" \
--deliver-to "oc_xxx" \
--deliver-channel "feishu"
bash
# 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"
bash
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.
FlagWhen to useDefault behavior--voice-id "voice_01..."When a specific voice is neededMedeo picks automatically--video-style-id "style_01..."When a specific visual style is neededMedeo picks automatically--asset-sourcesWhen user provides images: pass my_uploaded_assetsMedeo decides--recipe-id "recipe_01..."When using a templateNone--aspect-ratio "9:16"When user specifies portrait/landscape16:9``--duration-ms 30000When user specifies durationMedeo decides--no-renderDebug only — skip renderingAlways render
--deliver-to)#This is critical — determines where the generated video gets sent.
Context--deliver-to valueExampleFeishu group chatThe 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_158fd3e54407cbe170697c6c954bd4f2Feishu private chatThe user's open_id (starts with ou_). Extract from inbound metadata sender_id — strip the user: prefix if presentou_f7f458f4d7b4ff49ec1b8de22a1e3206TelegramThe chat_id from the inbound message context-1001234567890DiscordThe channel_id from the inbound message context1234567890123456
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.
bash
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.
bash
# 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:
bash
python3 {baseDir}/scripts/medeo_video.py spawn-task \
--message "Create a video featuring this person" \
--media-ids "media_01..."
PlatformHow to get image sourceupload-file argTelegrammessage.photo[-1].file_id``--telegram-file-idDiscordmessage.attachments[0].url (public CDN URL)--urlFeishumessage_id + image_key from message content JSON--feishu-message-id + --feishu-image-keyWhatsAppDownload attachment binary → save to /tmp``--fileGeneric URLAny direct image URL--url
Note: Discord attachment URLs are public CDN links — --url works directly. All other platforms require authentication to download.
bash
# 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:
bash
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.
text
Pipeline flow:
spawn-task (recommended, async)
└── generate (blocking, same pipeline)
├── upload (if --media-urls)
├── compose → compose-status (poll)
└── render → render-status (poll)
CommandWhat it doesKey argsgenerateBlocking full pipeline (upload→compose→render)Same as spawn-task minus deliver flagscomposeCreate project only (no render)--message, --media-ids, --recipe-id``compose-statusPoll compose task--task-id "task_01..."``renderRender existing project--project-id "project_01..."``render-statusPoll render job--job-id "render_01..."``upload-statusPoll upload job--job-id "job_01..."
All commands support --no-wait to return immediately without polling.
bash
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.
CommandDescriptionUser-visible?recipesList video templatesYes — "what templates are available?"last-jobLatest job statusYes — "is my last video done?"historyJob history (last 50)Yes — "show my video history"configShow current configurationNoconfig-init --api-key "mk_..."Initialize API keyOnly during setupupload --url "URL"Upload from public URLNo (agent internal)upload-file --file PATHUpload from local fileNo (agent internal)upload-file --url "URL"Download URL → uploadNo (agent internal)upload-file --telegram-file-id "..."Upload Telegram attachmentNo (agent internal)upload-file --feishu-image-key "..."Upload Feishu attachmentNo (agent internal)upload-status --job-id "..."Check upload job statusNo (agent internal)compose-status --task-id "..."Check compose task progressNo (agent internal)render-status --job-id "..."Check render job progressNo (agent internal)
Always async — spawn-task + sessions_spawn for generation
One call for stories — full storylines in one --message, never split
Insufficient credits — share recharge link from error output
IM image upload — Only upload images when the user explicitly asks for video generation with that image. Do NOT auto-upload every image message (user may have other skills installed). When triggered: run upload-file first → get media_id → pass to generation via --media-ids. Never ask the user for a URL if they already sent the image.
IM-native delivery — After generation, deliver the video using the IM channel's native method (not just a URL). Each channel has a dedicated delivery script:
Feishu: 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: Download video, then send via telegram_send_video.py (token from env only):
bash
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!"
Discord: Use the 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.
WhatsApp / Signal / Other: Use the message tool with media parameter, or share video_url as a link if native sending is unavailable.
Cover image URL: The generate output JSON includes 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).
Video URL: Same pattern — {ossBaseUrl}/{video_relative_path} (e.g. https://oss.prd.medeo.app/exported_video/v_xxx).
Security: Never pass bot tokens as CLI args (visible in ps). Always use env vars: TELEGRAM_BOT_TOKEN, DISCORD_BOT_TOKEN.
Timeline completion — Medeo's backend is an AI agent. Generated images/videos must be added to the Timeline to trigger task completion and rendering. Always append to your prompt: "Add the generated video/image to the Timeline."
ErrorActionsetup_required: trueGuide user to register + configure keyupload_prep_rejectedFile format/size rejected; check supported formatss3_put_failedS3 upload error; retry onceInsufficient creditsShare recharge link from error output, retry after top-upCompose/render timeoutInform user, suggest retry. Complex scripts may take 15+ min401/403Key may be invalid or expired, ask user to regenerateUpload 404Some 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.