medeo-video

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.

install
source · Clone the upstream repo
git clone https://github.com/one2x-ai/medeo-video-skill
Claude Code · Install into ~/.claude/skills/
git clone --depth=1 https://github.com/one2x-ai/medeo-video-skill ~/.claude/skills/one2x-ai-medeo-video-skill-medeo-video
manifest: SKILL.md
source content

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:

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):

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_..."

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

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)

# 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

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.

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-sources
When user provides images: pass
my_uploaded_assets
Medeo decides
--recipe-id "recipe_01..."
When using a templateNone
--aspect-ratio "9:16"
When user specifies portrait/landscape
16:9
--duration-ms 30000
When user specifies durationMedeo decides
--no-render
Debug only — skip renderingAlways render

Delivery Target (
--deliver-to
)

This is critical — determines where the generated video gets sent.

Context
--deliver-to
value
Example
Feishu 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_158fd3e54407cbe170697c6c954bd4f2
Feishu private chatThe user's
open_id
(starts with
ou_
). Extract from inbound metadata
sender_id
strip the
user:
prefix
if present
ou_f7f458f4d7b4ff49ec1b8de22a1e3206
TelegramThe
chat_id
from the inbound message context
-1001234567890
DiscordThe
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: <brief>"
,
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)

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.

# 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-Specific Image Extraction Guide

PlatformHow 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
WhatsAppDownload attachment binary → save to
/tmp
--file
Generic URLAny 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

# 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:

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)
CommandWhat it doesKey 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

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.

6. Quick Commands Reference (for agent, not user-facing)

CommandDescriptionUser-visible?
recipes
List video templatesYes — "what templates are available?"
last-job
Latest job statusYes — "is my last video done?"
history
Job history (last 50)Yes — "show my video history"
config
Show current configurationNo
config-init --api-key "mk_..."
Initialize API keyOnly during setup
upload --url "URL"
Upload from public URLNo (agent internal)
upload-file --file PATH
Upload 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)

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 "<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):
      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
      .
  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

ErrorAction
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 creditsShare recharge link from error output, retry after top-up
Compose/render timeoutInform user, suggest retry. Complex scripts may take 15+ min
401/403Key may be invalid or expired, ask user to regenerate
Upload 404Some image hosts block server-side fetch; use
upload-file --url
to download first

9. Reference Docs

  • docs/recipes.md — Full recipe browsing and pagination
  • docs/assets-upload.md — All upload paths (URL, local file, IM attachments), platform-specific guides,
    upload
    vs
    upload-file
    comparison
  • docs/feishu-send.md — Sending generated video via Feishu (cover image, duration, compression)
  • 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.