
Videoagent Video Studio
Generate text-to-video or image-to-video clips from an OpenClaw skill through a hosted fal.ai proxy without managing API keys locally.
Overview
videoagent-video-studio is an agent skill for the Build phase that connects text-to-video and image-to-video generation through a hosted fal.ai proxy with selectable model slugs.
Install
npx skills add https://github.com/pexoai/pexo-skills --skill videoagent-video-studioWhat is this skill?
- OpenClaw skill targeting Node 18+ with module type ESM
- POST /api/generate with mode, prompt, optional imageUrl, duration, aspectRatio, and model
- Model slugs include minimax, kling, veo, hunyuan, pixverse, grok, and seedance
- Hosted proxy pattern so agents avoid embedding FAL_KEY in the client skill package
- Server-side usage tracking with per-IP free limits and valid token lists
- Seven named model slugs: minimax, kling, veo, hunyuan, pixverse, grok, seedance
- Node engine requirement >=18
Adoption & trust: 10.1k installs on skills.sh; 732 GitHub stars; 2/3 security scanners passed (skills.sh audits).
What problem does it solve?
You want agent-driven video generation but do not want to distribute fal.ai credentials or rebuild provider-specific clients for every model.
Who is it for?
Indie builders prototyping AI video inside OpenClaw or Node agent stacks who already run or trust a small proxy service.
Skip if: Teams that need on-device rendering, strict data residency without third-party APIs, or a finished UI video editor instead of an API skill.
When should I use this skill?
OpenClaw skill for text-to-video and image-to-video — zero API keys via hosted proxy.
What do I get? / Deliverables
Your agent can call a single generate endpoint with prompt and model parameters while keys and usage limits stay on the hosted proxy.
- Video generation API requests with chosen model and aspect ratio
- Server-tracked usage and rate-limit handling for client tokens
Recommended Skills
Journey fit
Video generation is wired into the product during Build when you connect agent skills to external media APIs. Integrations subphase fits proxy endpoints, model selection, and token-gated API calls for hosted generation.
How it compares
Skill integration to a hosted video proxy, not a self-contained desktop studio or pure prompt-only markdown guide.
Common Questions / FAQ
Who is videoagent-video-studio for?
Solo builders and small teams wiring generative video into agent workflows via Node 18+ and a fal-backed proxy.
When should I use videoagent-video-studio?
During Build integrations when you need programmatic text-to-video or image-to-video for demos, ads, or content pipelines.
Is videoagent-video-studio safe to install?
It implies network calls, API secrets on the proxy, and third-party model hosts; review the Security Audits panel on this page and never commit FAL_KEY or VALID_TOKENS to a public repo.
SKILL.md
READMESKILL.md - Videoagent Video Studio
{ "name": "videoagent-video-studio", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "videoagent-video-studio", "version": "1.0.0", "engines": { "node": ">=18" } } } } { "name": "videoagent-video-studio", "version": "1.0.0", "description": "OpenClaw skill for text-to-video and image-to-video — zero API keys via hosted proxy.", "type": "module", "engines": { "node": ">=18" } } /** * videoagent-video-studio proxy — POST /api/generate * Body: { mode, prompt, imageUrl?, duration?, aspectRatio?, model? } * model: minimax | kling | veo | hunyuan | pixverse | grok | seedance */ const { fal } = require("@fal-ai/client"); const { getModel, listModels, resolveModel, FAL } = require("../models.js"); const FAL_KEY = process.env.FAL_KEY || ""; const VALID_TOKENS = (process.env.VALID_TOKENS || "").split(",").filter(Boolean); const FREE_LIMIT_PER_IP = Math.max(0, parseInt(process.env.FREE_LIMIT_PER_IP || "100", 10)); const { getIssuedToken, getTokenUsage, incrementTokenUsage, trackGeneration, trackError, trackRateLimit, } = require("../usage-store.js"); fal.config({ credentials: FAL_KEY }); function getClientIp(req) { const xff = req.headers["x-forwarded-for"]; return (typeof xff === "string" ? xff.split(",")[0] : xff?.[0])?.trim() || req.headers["x-real-ip"] || req.socket?.remoteAddress || "unknown"; } function json(res, status, data) { res.setHeader("Content-Type", "application/json"); res.status(status).json(data); } function err(res, status, message, details = null) { json(res, status, { success: false, error: message, ...(details && { details }) }); } function getBearerToken(req) { const h = req.headers.authorization; return h && h.startsWith("Bearer ") ? h.slice(7).trim() : null; } function clampDuration(d, max = 10) { const n = parseInt(d, 10); if (Number.isNaN(n) || n < 1) return 5; return Math.min(n, max); } async function runFal(modelId, mode, body) { const m = getModel(modelId); if (!m || m.provider !== FAL) throw new Error(`Unknown FAL model: ${modelId}`); const endpoint = mode === "image-to-video" ? m.i2v : m.t2v; if (!endpoint) throw new Error(`Model ${modelId} does not support ${mode}`); const normalized = { prompt: body.prompt, imageUrl: body.imageUrl || body.image_url, duration: clampDuration(body.duration), aspectRatio: body.aspectRatio || "16:9", }; const input = m.falInput(normalized, mode); const result = await fal.subscribe(endpoint, { input, logs: false }); const url = result?.data?.video?.url || result?.data?.url; if (!url) throw new Error("No video URL in response"); return { success: true, mode, model: modelId, videoUrl: url, duration: normalized.duration, aspectRatio: normalized.aspectRatio }; } function pickDefaultModel(mode) { const list = listModels(); const m = list.find(m => m.provider === FAL && (mode === "text-to-video" ? m.t2v : m.i2v)); return m ? m.id : "minimax"; } module.exports = async function handler(req, res) { if (req.method === "OPTIONS") return res.status(204).end(); // ── GET: health + model list ──────────────────────────────────────────────── if (req.method === "GET") { return json(res, 200, { service: "videoagent-video-studio-proxy", version: "2.1.0", status: "ok", modes: ["text-to-video", "image-to-video"], models: listModels(), free_limit_per_token: FREE_LIMIT_PER_IP, free_limit_reset: "daily", max_tokens_per_ip_per_day: VALID_TOKENS.length === 0 ? parseInt(process.env.MAX_TOKENS_PER_IP_PER_DAY || "3", 10) : null, }); } if (req.method !== "POST") return err(res, 405, "Method not allowed"); // ── Auth ──────────────────────────────────────────────────────────────────── const bearerToken = getBearerToken(req); if (VALID_TOKENS.length > 0) { if (!bearerTo