--- name: medeo-video version: 1.5.0 description: AI-powered video generation skill. Use when the user wants to generate videos from text descriptions, browse video recipes, upload assets, or manage video creation workflows. metadata: {"openclaw":{"emoji":"🎬","requires":{"bins":["python3"],"env":{"MEDEO_API_KEY":{"required":true,"description":"Medeo API key (starts with mk_)"}}},"tags":["video","ai-video","medeo","video-generation","rendering","recipes","media","upload"]}} --- # Medeo Video Generator Skill 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`. ## 0. Pre-Flight Check (MANDATORY β€” run before anything else) **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')" ``` - Output `ok` β†’ proceed normally - Output `missing` (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": "", "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_..."` ## 1. First-Time Setup If no API Key is configured, the script outputs `"setup_required": true`. 1. Send the user this exact link: https://medeo.app/dev/apikey (this page auto-prompts registration if not logged in, then shows the API key) 2. Once they provide the key: `python3 {baseDir}/scripts/medeo_video.py config-init --api-key "mk_..."` ## 2. Generate a Video (5-30 min, always async) Users only need to know **3 ways** to generate a video: 1. **Send text** β†’ generate video 2. **Send text + upload image** β†’ generate video using their image 3. **Send text + image URL** β†’ generate video using the URL image 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. ### Usage 1: Text only ```bash python3 {baseDir}/scripts/medeo_video.py spawn-task \ --message "user's video description" \ --deliver-to "oc_xxx" \ --deliver-channel "feishu" ``` ### Usage 2: Text + uploaded image (user sends image in chat) ```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" ``` ### Usage 3: Text + image URL ```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_assets` so Medeo uses their images instead of generating new ones. The user does not need to know this flag exists. ### Internal Parameters (agent use only β€” never expose to users) 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 | ### Delivery Target (`--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:** - Check `is_group_chat` in the inbound metadata - If `true` β†’ use `conversation_label` / `chat_id` (the `oc_` value) - If `false` β†’ use `sender_id` (the `ou_` value) Step 2: Use `sessions_spawn` with the returned args (`label: "medeo: "`, `runTimeoutSeconds: 2400`). Step 3: Tell user it's generating. Sub-agent auto-announces when done. ## 3. Upload Assets ### 3a. From URL (image already has a public URL) ```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 ``` ### 3b. From IM attachment (user sends image directly) ← NEW 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..." ``` ### Platform-Specific Image Extraction Guide | 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` | | WhatsApp | 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. ### 3c. Inline in generate pipeline ```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. ### 3d. Check Upload Status 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. ## 4. Low-Level Pipeline Commands (agent internal β€” never expose to users) > 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. ## 5. Browse Recipes ```bash python3 {baseDir}/scripts/medeo_video.py recipes # list templates python3 {baseDir}/scripts/medeo_video.py recipes --cursor # paginate ``` Use in generation: `--recipe-id "recipe_01..."`. See [docs/recipes.md](docs/recipes.md). ## 6. Quick Commands Reference (for agent, not user-facing) | 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) | ## 7. Key Rules 1. **Always async** β€” `spawn-task` + `sessions_spawn` for generation 2. **One call for stories** β€” full storylines in one `--message`, never split 3. **Insufficient credits** β€” share recharge link from error output 4. **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. 5. **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 "" --duration ` (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 "" TELEGRAM_BOT_TOKEN="$TELEGRAM_BOT_TOKEN" python3 {baseDir}/scripts/telegram_send_video.py \ --video /tmp/medeo_result.mp4 \ --to "" \ --cover-url "" \ --duration \ --caption "🎬 Video ready!" ``` - **Discord**: Use the `message` tool directly β€” download the video to `/tmp/result.mp4` via `curl -sL -o /tmp/result.mp4 ""`, then call `message(action="send", channel="discord", target="", 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`. 6. **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." ## 8. Error Handling | 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 | ## 9. Reference Docs - [docs/recipes.md](docs/recipes.md) β€” Full recipe browsing and pagination - [docs/assets-upload.md](docs/assets-upload.md) β€” All upload paths (URL, local file, IM attachments), platform-specific guides, `upload` vs `upload-file` comparison - [docs/feishu-send.md](docs/feishu-send.md) β€” Sending generated video via Feishu (cover image, duration, compression) - [docs/multi-platform.md](docs/multi-platform.md) β€” Multi-platform video delivery (Feishu, Telegram, Discord, WhatsApp) ## 10. Data Storage All data in `~/.openclaw/workspace/medeo-video/`: `config.json` (API key), `last_job.json` (latest job), `history/` (last 50 jobs). ## 11. Security Notes - **API key resolution**: env var `MEDEO_API_KEY` β†’ `config.json` β†’ built-in defaults. No legacy system-level files are read. - **Feishu delivery**: `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 delivery**: Bot token is read from `TELEGRAM_BOT_TOKEN` env var only (never CLI args). - **No secrets in skill directory**: `config.json` lives in the runtime data directory (`~/.openclaw/workspace/medeo-video/`), not in the skill source directory.