
Videoagent Audio Studio
- 3.8k installs
- 762 repo stars
- Updated July 21, 2026
- pexoai/pexo-skills
videoagent-audio-studio is an agent skill for generate tts, music, sound effects, and voice clones via elevenlabs and fal.ai from one audio dispatcher skill.
About
The videoagent-audio-studio skill Generate TTS, music, sound effects, and voice clones via ElevenLabs and fal.ai from one audio dispatcher skill. Use when: User asks to generate speech, narrate text, create a voice-over, compose music, or produce a sound effect. VideoAgent Audio Studio is a smart audio dispatcher. It analyzes your request and routes it to the best available model ElevenLabs for speech and music, fal.ai for fast SFX and returns a ready-to-use audio URL. Request Type Best Model Latency --- --- --- Narrate text / Voice-over elevenlabs-tts-v3 ~3s Low-latency TTS (real-time) elevenlabs-tts-turbo <1s Background music cassetteai-music ~15s Sound effect elevenlabs-sfx ~5s Clone a voice from audio elevenlabs-voice-clone ~10s 1. Start the AudioMind server (once per session) 1. Start the AudioMind server (once per session) bash bash {baseDir}/tools/start_server.sh This starts the ElevenLabs MCP server on port 8124. The skill uses it for all audio generation. Analyze the user's request and call the appropriate tool via the MCP server: When user asks to "narrate", "read aloud", "say", or "create a voice-over":
- Simplified routing table: Removed unstable/offline models from the main reference. The skill now only surfaces models th
- Clearer use-case triggers: Added "Use when" section so the agent activates this skill at the right moment.
- Unified setup: Single ELEVENLABS_API_KEY is all you need to get started. FAL_KEY is now optional.
- Removed polling complexity: Music generation now uses cassetteai-music by default, which completes synchronously.
- Added async workflow for long-running music generation tasks.
Videoagent Audio Studio by the numbers
- 3,845 all-time installs (skills.sh)
- +9 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #124 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
videoagent-audio-studio capabilities & compatibility
- Capabilities
- simplified routing table: removed unstable/offli · clearer use case triggers: added "use when" sect · unified setup: single elevenlabs_api_key is all · removed polling complexity: music generation now · added async workflow for long running music gene
- Works with
- openai
- Use cases
- video generation · transcription
What videoagent-audio-studio says it does
This starts the ElevenLabs MCP server on port 8124. The skill uses it for all audio generation.
Analyze the user's request and call the appropriate tool via the MCP server:
When user asks to "narrate", "read aloud", "say", or "create a voice-over":
npx skills add https://github.com/pexoai/pexo-skills --skill videoagent-audio-studioAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3.8k |
|---|---|
| repo stars | ★ 762 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 21, 2026 |
| Repository | pexoai/pexo-skills ↗ |
How do I generate tts, music, sound effects, and voice clones via elevenlabs and fal.ai from one audio dispatcher skill with documented agent guidance?
Generate TTS, music, sound effects, and voice clones via ElevenLabs and fal.ai from one audio dispatcher skill.
Who is it for?
Developers who need generative media help during build work.
Skip if: Skip when the task falls outside Generative Media scope described in SKILL.md.
When should I use this skill?
Generate TTS, music, sound effects, and voice clones via ElevenLabs and fal.ai from one audio dispatcher skill.
What you get
Completed generative media workflow aligned with SKILL.md steps and validation.
- TTS audio files
- music and SFX assets
- cloned voice outputs
By the numbers
- Simplified routing table: Removed unstable/offline models from the main reference. The skill now only surfaces models th
- Clearer use-case triggers: Added "Use when" section so the agent activates this skill at the right moment.
- Unified setup: Single ELEVENLABS_API_KEY is all you need to get started. FAL_KEY is now optional.
Files
🎙️ VideoAgent Audio Studio
Use when: User asks to generate speech, narrate text, create a voice-over, compose music, or produce a sound effect.
VideoAgent Audio Studio is a smart audio dispatcher. It analyzes your request and routes it to the best available model — ElevenLabs for speech and music, fal.ai for fast SFX — and returns a ready-to-use audio URL.
---
Quick Reference
| Request Type | Best Model | Latency |
|---|---|---|
| Narrate text / Voice-over | elevenlabs-tts-v3 | ~3s |
| Low-latency TTS (real-time) | elevenlabs-tts-turbo | <1s |
| Background music | cassetteai-music | ~15s |
| Sound effect | elevenlabs-sfx | ~5s |
| Clone a voice from audio | elevenlabs-voice-clone | ~10s |
---
How to Use
1. Start the AudioMind server (once per session)
bash {baseDir}/tools/start_server.shThis starts the ElevenLabs MCP server on port 8124. The skill uses it for all audio generation.
2. Route the request
Analyze the user's request and call the appropriate tool via the MCP server:
Text-to-Speech (TTS)
When user asks to "narrate", "read aloud", "say", or "create a voice-over":
Use MCP tool: text_to_speech
text: "<the text to narrate>"
voice_id: "JBFqnCBsd6RMkjVDRZzb" # Default: "George" (professional, neutral)
model_id: "eleven_multilingual_v2" # Use "eleven_turbo_v2_5" for low latencyMusic Generation
When user asks to "compose", "create background music", or "make a soundtrack":
Use MCP tool: text_to_sound_effects (via cassetteai-music on fal.ai)
prompt: "<music description, e.g. 'upbeat lo-fi hip hop, 90 seconds'>"
duration_seconds: <duration>Sound Effect (SFX)
When user asks for a specific sound (e.g., "a door creaking", "rain on a window"):
Use MCP tool: text_to_sound_effects
text: "<sound description>"
duration_seconds: <1-22>Voice Cloning
When user provides an audio sample and wants to clone the voice:
Use MCP tool: voice_add
name: "<voice name>"
files: ["<audio_file_url>"]---
Example Conversations
User: "Voice this text for me: Welcome to our product launch"
→ Route to: text_to_speech
text: "Welcome to our product launch"
voice_id: "JBFqnCBsd6RMkjVDRZzb"
model_id: "eleven_multilingual_v2"🎙️ Voiceover done! Listen here
---
User: "Generate 60 seconds of relaxing background music for a podcast"
→ Route to: cassetteai-music (fal.ai)
prompt: "relaxing lo-fi background music for a podcast, gentle piano and soft beats, 60 seconds"
duration_seconds: 60🎵 Background music ready! Listen here
---
User: "Generate a sci-fi style door opening sound effect"
→ Route to: text_to_sound_effects
text: "a futuristic sci-fi door sliding open with a hydraulic hiss"
duration_seconds: 3---
Setup
Required
Set ELEVENLABS_API_KEY in ~/.openclaw/openclaw.json:
{
"skills": {
"entries": {
"videoagent-audio-studio": {
"enabled": true,
"env": {
"ELEVENLABS_API_KEY": "your_elevenlabs_key_here"
}
}
}
}
}Get your key at elevenlabs.io/app/settings/api-keys.
Optional (for fal.ai music & SFX models)
"FAL_KEY": "your_fal_key_here"Get your key at fal.ai/dashboard/keys.
---
Self-Hosting the Proxy
The cli.js connects to a hosted proxy by default. If you want full control — or need to serve users in regions where vercel.app is blocked — you can deploy your own instance from the proxy/ directory.
Quick Deploy (Vercel)
cd proxy
npm install
vercel --prodEnvironment Variables
Set these in your Vercel project (Dashboard → Settings → Environment Variables):
| Variable | Required For | Where to Get |
|---|---|---|
ELEVENLABS_API_KEY | TTS, SFX, Voice Clone | elevenlabs.io/app/settings/api-keys |
FAL_KEY | Music generation | fal.ai/dashboard/keys |
VALID_PRO_KEYS | (Optional) Restrict access | Comma-separated list of allowed client keys |
Point cli.js to Your Proxy
export AUDIOMIND_PROXY_URL="https://your-domain.com/api/audio"Or set it in ~/.openclaw/openclaw.json:
{
"skills": {
"entries": {
"videoagent-audio-studio": {
"env": {
"AUDIOMIND_PROXY_URL": "https://your-domain.com/api/audio"
}
}
}
}
}Custom Domain (Recommended)
If your users are in mainland China, bind a custom domain in Vercel Dashboard → Settings → Domains to avoid DNS issues with vercel.app.
---
Model Reference
| Model ID | Type | Provider | Notes |
|---|---|---|---|
eleven_multilingual_v2 | TTS | ElevenLabs | Best quality, supports 29 languages |
eleven_turbo_v2_5 | TTS | ElevenLabs | Ultra-low latency, ideal for real-time |
eleven_monolingual_v1 | TTS | ElevenLabs | English only, fastest |
cassetteai-music | Music | fal.ai | Reliable, fast music generation |
elevenlabs-sfx | SFX | ElevenLabs | High-quality sound effects (up to 22s) |
elevenlabs-voice-clone | Clone | ElevenLabs | Clone any voice from a short audio sample |
---
Changelog
v3.0.0
- Simplified routing table: Removed unstable/offline models from the main reference. The skill now only surfaces models that reliably work.
- Clearer use-case triggers: Added "Use when" section so the agent activates this skill at the right moment.
- Unified setup: Single
ELEVENLABS_API_KEYis all you need to get started.FAL_KEYis now optional. - Removed polling complexity: Music generation now uses
cassetteai-musicby default, which completes synchronously.
v2.1.0
- Added async workflow for long-running music generation tasks.
- Added
cassetteai-musicas a stable alternative for music generation.
v2.0.0
- Migrated to ElevenLabs MCP server architecture.
- Added voice cloning support.
v1.0.0
- Initial release with TTS, music, and SFX routing.
#!/usr/bin/env node
/**
* AudioMind skill CLI — call AudioMind Proxy for TTS, music, SFX.
* Usage:
* node cli.js --prompt "one minute lo-fi with piano and rain"
* node cli.js --action music --prompt "relaxing background"
* node cli.js --action tts --text "Hello world"
* node cli.js --action sfx --text "rain and thunder" --duration-seconds 10
* node cli.js --action music --prompt "..." --duration 60
*/
const fs = require("node:fs");
const path = require("node:path");
const os = require("node:os");
const PROXY_URL = process.env.AUDIOMIND_PROXY_URL || "https://audiomind-proxy.vercel.app/api/audio";
const PRO_KEY = process.env.AUDIOMIND_API_KEY || "";
function parseArgs() {
const args = process.argv.slice(2);
const out = {
action: "",
text: "",
prompt: "",
duration_seconds: null,
model: "",
fast: false,
poll_interval: 5,
max_wait_seconds: 180,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === "--action" && args[i + 1]) { out.action = args[++i].toLowerCase(); continue; }
if (args[i] === "--prompt" && args[i + 1]) { out.prompt = args[++i]; continue; }
if (args[i] === "--text" && args[i + 1]) { out.text = args[++i]; continue; }
if ((args[i] === "--duration-seconds" || args[i] === "--duration") && args[i + 1]) { out.duration_seconds = parseInt(args[++i], 10) || null; continue; }
if (args[i] === "--model" && args[i + 1]) { out.model = args[++i]; continue; }
if (args[i] === "--fast") { out.fast = true; continue; }
if (args[i] === "--poll-interval" && args[i + 1]) { out.poll_interval = Math.max(1, parseInt(args[++i], 10) || 5); continue; }
if (args[i] === "--max-wait-seconds" && args[i + 1]) { out.max_wait_seconds = Math.max(10, parseInt(args[++i], 10) || 180); continue; }
if (!out.prompt && !out.text && !args[i].startsWith("-")) out.prompt = args[i];
}
if (out.prompt && !out.text) out.text = out.prompt;
return out;
}
function inferAction(params) {
if (params.action) return params.action;
const model = String(params.model || "").toLowerCase();
if (model.includes("sfx")) return "sfx";
if (model.includes("music")) return "music";
if (model.includes("tts")) return "tts";
const combined = `${params.prompt || ""} ${params.text || ""}`.toLowerCase();
if (/\b(sfx|sound effect|whoosh|explosion|rain|thunder|bird|dog bark|foley)\b/.test(combined)) return "sfx";
if (/(sfx|sound effect|whoosh|explosion|rain|thunder|bird|dog bark|foley|doorbell|ambient)/.test(combined)) return "sfx";
if (/\b(music|track|song|melody|beat|lo-fi|soundtrack|bgm)\b/.test(combined)) return "music";
if (/(music|track|song|melody|beat|lo-?fi|soundtrack|bgm|score)/.test(combined)) return "music";
return "tts";
}
function isPendingStatus(status) {
const s = String(status || "").toLowerCase();
return s === "in_progress" || s === "processing" || s === "pending" || s === "queued";
}
function extractAudioUrl(data) {
return data?.audio_url || data?.url || data?.result?.audio_url || data?.output?.audio_url || null;
}
async function sleep(ms) {
return await new Promise((resolve) => setTimeout(resolve, ms));
}
function resolveOutputDir() {
const configured = process.env.AUDIOMIND_OUTPUT_DIR;
if (configured) return configured;
const openclawWorkspace = path.join(os.homedir(), ".openclaw", "workspace");
return path.join(openclawWorkspace, "tmp", "audiomind");
}
function persistBase64Audio(result) {
if (!result || typeof result !== "object") return result;
const b64 = result.audio_base64;
if (!b64 || typeof b64 !== "string") return result;
const format = (result.format || "mp3").toLowerCase();
const ext = format === "wav" ? "wav" : "mp3";
const dir = resolveOutputDir();
fs.mkdirSync(dir, { recursive: true });
const filename = `audiomind-${Date.now()}.${ext}`;
const filepath = path.join(dir, filename);
fs.writeFileSync(filepath, Buffer.from(b64, "base64"));
const next = { ...result };
delete next.audio_base64;
next.audio_file_path = filepath;
next.has_inline_audio_payload = true;
next.delivery_hint = "Use audio_file_path for media delivery. Never paste base64 into chat.";
return next;
}
async function pollUntilComplete(statusUrl, headers, intervalSeconds, maxWaitSeconds) {
const startedAt = Date.now();
let attempt = 0;
while (Date.now() - startedAt < maxWaitSeconds * 1000) {
attempt += 1;
await sleep(intervalSeconds * 1000);
const res = await fetch(statusUrl, { method: "GET", headers });
const data = await res.json().catch(() => ({}));
const status = data?.status;
const audioUrl = extractAudioUrl(data);
if (audioUrl || String(status).toLowerCase() === "completed" || String(status).toLowerCase() === "succeeded") {
return persistBase64Audio({ ...data, audio_url: audioUrl || data?.audio_url || null, _polled: true, _attempts: attempt });
}
if (String(status).toLowerCase() === "failed" || String(status).toLowerCase() === "error") {
return persistBase64Audio({ ...data, _polled: true, _attempts: attempt });
}
// Keep progress noise low: only occasional stderr heartbeat.
if (attempt % 3 === 0) {
process.stderr.write(`[AudioMind] still generating (attempt ${attempt}, status=${status || "pending"})\n`);
}
}
return persistBase64Audio({ status: "in_progress", status_url: statusUrl, _polled: true, _timeout: true });
}
async function main() {
const params = parseArgs();
const payload = {
action: inferAction(params),
text: params.text || params.prompt,
prompt: params.prompt || params.text,
duration_seconds: params.duration_seconds,
model: params.model || undefined,
fast: params.fast || undefined,
};
if (!payload.text && !payload.prompt) {
console.error(JSON.stringify({ error: "Missing --prompt or --text" }));
process.exit(1);
}
const headers = { "Content-Type": "application/json" };
if (PRO_KEY) headers["X-Audiomind-Key"] = PRO_KEY;
try {
const res = await fetch(PROXY_URL, {
method: "POST",
headers,
body: JSON.stringify(payload),
});
const data = persistBase64Audio(await res.json().catch(() => ({})));
if (!res.ok) {
console.error(JSON.stringify({ error: data.message || data.error || res.statusText, status: res.status, ...data }));
process.exit(1);
}
// Proactively poll long-running tasks until completion to reduce user wait anxiety.
if (data?.status_url && isPendingStatus(data?.status)) {
const polled = await pollUntilComplete(data.status_url, headers, params.poll_interval, params.max_wait_seconds);
if (polled?.audio_url || polled?.status === "completed" || polled?.status === "succeeded") {
console.log(JSON.stringify(polled));
return;
}
// If timeout/pending, return latest status to caller for follow-up.
console.log(JSON.stringify({ ...data, ...polled }));
return;
}
console.log(JSON.stringify(data));
} catch (err) {
console.error(JSON.stringify({ error: err.message, code: err.code }));
process.exit(1);
}
}
main();
.vercel
node_modules
const { fal } = require("@fal-ai/client");
const { trackGeneration, trackError, trackRateLimit } = require("../usage-store.js");
const ELEVENLABS_API_KEY = process.env.ELEVENLABS_API_KEY || "";
const FAL_KEY = process.env.FAL_KEY || "";
const VALID_PRO_KEYS = (process.env.VALID_PRO_KEYS || "").split(",").filter(Boolean);
const DEFAULT_VOICE_ID = "JBFqnCBsd6RMkjVDRZzb";
const DEFAULT_TTS_MODEL = "eleven_multilingual_v2";
fal.config({ credentials: FAL_KEY });
function json(res, status, data) {
res.setHeader("Content-Type", "application/json");
res.status(status).json(data);
}
function errorResponse(res, status, message) {
json(res, status, { error: message, status: "failed" });
}
function authenticateRequest(req) {
if (VALID_PRO_KEYS.length === 0) return true;
const key = req.headers["x-audiomind-key"];
return key && VALID_PRO_KEYS.includes(key);
}
function inferAction(body) {
if (body.action) return body.action.toLowerCase();
const model = String(body.model || "").toLowerCase();
if (model.includes("sfx")) return "sfx";
if (model.includes("music") || model.includes("cassette")) return "music";
if (model.includes("tts") || model.includes("eleven")) return "tts";
const combined = `${body.prompt || ""} ${body.text || ""}`.toLowerCase();
if (/\b(sfx|sound effect|whoosh|explosion|rain|thunder|bird|dog bark|foley)\b/.test(combined)) return "sfx";
if (/(sfx|sound effect|whoosh|explosion|rain|thunder|bird|dog bark|foley|doorbell|ambient)/.test(combined)) return "sfx";
if (/\b(music|track|song|melody|beat|lo-fi|soundtrack|bgm)\b/.test(combined)) return "music";
if (/(music|track|song|melody|beat|lo-?fi|soundtrack|bgm|score)/.test(combined)) return "music";
return "tts";
}
async function handleTTS(body) {
const text = body.text || body.prompt;
if (!text) throw new Error("Missing text for TTS");
const voiceId = body.voice_id || DEFAULT_VOICE_ID;
const modelId = body.model || DEFAULT_TTS_MODEL;
const response = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
"xi-api-key": ELEVENLABS_API_KEY,
},
body: JSON.stringify({
text,
model_id: modelId,
voice_settings: {
stability: 0.5,
similarity_boost: 0.75,
},
}),
}
);
if (!response.ok) {
const err = await response.text().catch(() => response.statusText);
throw new Error(`ElevenLabs TTS failed (${response.status}): ${err}`);
}
const arrayBuffer = await response.arrayBuffer();
const base64 = Buffer.from(arrayBuffer).toString("base64");
return {
status: "completed",
audio_base64: base64,
format: "mp3",
model: modelId,
voice_id: voiceId,
};
}
async function handleSFX(body) {
const text = body.text || body.prompt;
if (!text) throw new Error("Missing text for SFX");
const payload = { text };
if (body.duration_seconds) {
payload.duration_seconds = Math.min(Math.max(body.duration_seconds, 0.5), 30);
}
const response = await fetch(
"https://api.elevenlabs.io/v1/sound-generation",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"xi-api-key": ELEVENLABS_API_KEY,
},
body: JSON.stringify(payload),
}
);
if (!response.ok) {
const err = await response.text().catch(() => response.statusText);
throw new Error(`ElevenLabs SFX failed (${response.status}): ${err}`);
}
const arrayBuffer = await response.arrayBuffer();
const base64 = Buffer.from(arrayBuffer).toString("base64");
return {
status: "completed",
audio_base64: base64,
format: "mp3",
};
}
async function handleMusic(body) {
const prompt = body.prompt || body.text;
if (!prompt) throw new Error("Missing prompt for music generation");
const duration = body.duration_seconds || 30;
const result = await fal.subscribe("CassetteAI/music-generator", {
input: {
prompt,
duration: Math.min(Math.max(duration, 5), 180),
},
});
const audioUrl =
result?.data?.audio_file?.url ||
result?.data?.audio_url ||
result?.data?.url ||
null;
if (!audioUrl) {
throw new Error("Music generation returned no audio URL");
}
return {
status: "completed",
audio_url: audioUrl,
format: "wav",
model: "cassetteai-music",
duration,
};
}
module.exports = async function handler(req, res) {
if (req.method === "OPTIONS") {
return res.status(204).end();
}
if (req.method === "GET") {
return json(res, 200, {
service: "audiomind-proxy",
version: "1.0.0",
status: "ok",
actions: ["tts", "sfx", "music"],
});
}
if (req.method !== "POST") {
return errorResponse(res, 405, "Method not allowed");
}
if (!authenticateRequest(req)) {
trackRateLimit().catch(() => {});
return errorResponse(res, 401, "Invalid or missing API key");
}
const body = req.body;
if (!body || (!body.text && !body.prompt)) {
return errorResponse(res, 400, "Missing text or prompt in request body");
}
const action = inferAction(body);
try {
let result;
switch (action) {
case "tts":
if (!ELEVENLABS_API_KEY) {
return errorResponse(res, 503, "ElevenLabs API key not configured on server");
}
result = await handleTTS(body);
break;
case "sfx":
if (!ELEVENLABS_API_KEY) {
return errorResponse(res, 503, "ElevenLabs API key not configured on server");
}
result = await handleSFX(body);
break;
case "music":
if (!FAL_KEY) {
return errorResponse(res, 503, "fal.ai API key not configured on server");
}
result = await handleMusic(body);
break;
default:
return errorResponse(res, 400, `Unknown action: ${action}. Supported: tts, sfx, music`);
}
trackGeneration(action).catch(() => {});
return json(res, 200, result);
} catch (err) {
console.error(`[AudioMind] ${action} error:`, err.message);
trackError(action).catch(() => {});
return errorResponse(res, 500, err.message);
}
};
/**
* /api/stats — Usage statistics for audiomind-proxy.
*
* Auth: query param ?key=STATS_KEY or header X-Stats-Key
* HTML dashboard: ?ui=1 or browser Accept: text/html
*/
const { getStats } = require("../usage-store.js");
const STATS_KEY = process.env.STATS_KEY || "";
function authorized(req) {
if (!STATS_KEY) return true;
const k = req.headers["x-stats-key"] || req.query?.key || "";
return k === STATS_KEY;
}
function wantHtml(req) {
return req.query?.ui === "1" || (req.headers.accept || "").includes("text/html");
}
function renderDashboard(data, generatedAt) {
const rows = Object.entries(data.byAction)
.map(([action, s]) => `
<tr>
<td><strong>${action.toUpperCase()}</strong></td>
<td>${s.generations.toLocaleString()}</td>
<td>${s.errors.toLocaleString()}</td>
</tr>`)
.join("");
const chartBars = data.daily
.map((d) => {
const max = Math.max(...data.daily.map((x) => x.generations), 1);
const pct = Math.round((d.generations / max) * 100);
const label = d.date.slice(5);
return `<div class="bar-wrap" title="${d.date}: ${d.generations}">
<div class="bar" style="height:${pct}%"></div>
<div class="bar-label">${label}</div>
</div>`;
})
.join("");
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>AudioMind Proxy — Stats</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#0f172a;color:#e2e8f0;padding:2rem}
h1{font-size:1.5rem;font-weight:700;margin-bottom:1.5rem;color:#f8fafc}
.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin-bottom:2rem}
.card{background:#1e293b;border-radius:12px;padding:1.2rem;text-align:center}
.card .val{font-size:2rem;font-weight:700;color:#38bdf8}
.card .lbl{font-size:0.75rem;color:#94a3b8;margin-top:.3rem}
table{width:100%;border-collapse:collapse;background:#1e293b;border-radius:12px;overflow:hidden;margin-bottom:2rem}
th{background:#0f172a;padding:.7rem 1rem;text-align:left;font-size:.8rem;color:#94a3b8;text-transform:uppercase;letter-spacing:.05em}
td{padding:.7rem 1rem;border-top:1px solid #334155;font-size:.9rem}
.chart{background:#1e293b;border-radius:12px;padding:1.5rem;height:200px;display:flex;align-items:flex-end;gap:2px;overflow-x:auto}
.bar-wrap{flex:1;min-width:20px;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;height:100%}
.bar{width:100%;background:#38bdf8;border-radius:3px 3px 0 0;min-height:2px;transition:height .3s}
.bar-label{font-size:.55rem;color:#64748b;margin-top:4px;transform:rotate(-45deg);transform-origin:top left}
.ts{color:#475569;font-size:.75rem;margin-top:1.5rem}
</style>
</head>
<body>
<h1>AudioMind Proxy — Usage Dashboard</h1>
<div class="cards">
<div class="card"><div class="val">${data.total.generations.toLocaleString()}</div><div class="lbl">Total Generations</div></div>
<div class="card"><div class="val">${data.total.errors.toLocaleString()}</div><div class="lbl">Errors</div></div>
<div class="card"><div class="val">${data.total.rateLimits.toLocaleString()}</div><div class="lbl">Auth Rejections</div></div>
</div>
<table>
<thead><tr><th>Action</th><th>Generations</th><th>Errors</th></tr></thead>
<tbody>${rows}</tbody>
</table>
<div class="chart">${chartBars}</div>
<p class="ts">Generated at ${generatedAt}</p>
</body>
</html>`;
}
module.exports = async function handler(req, res) {
if (req.method === "OPTIONS") return res.status(204).end();
if (!authorized(req)) {
return res.status(401).json({ error: "Unauthorized" });
}
const data = await getStats(30);
const generatedAt = new Date().toISOString();
if (wantHtml(req)) {
res.setHeader("Content-Type", "text/html; charset=utf-8");
return res.status(200).send(renderDashboard(data, generatedAt));
}
res.setHeader("Content-Type", "application/json");
return res.status(200).json({ ...data, generated_at: generatedAt });
};
{
"name": "audiomind-proxy",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiomind-proxy",
"version": "1.0.0",
"dependencies": {
"@fal-ai/client": "^1.2.0",
"@upstash/redis": "^1.36.3"
}
},
"node_modules/@fal-ai/client": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/@fal-ai/client/-/client-1.9.4.tgz",
"integrity": "sha512-mDjF2QDq+oficSSxzmErNkseQeRXnvUBEhJy39n4PPe7jRPZeSqM2SNb27SW50rDtCtay+stwFU8zRZehlt1Qg==",
"license": "MIT",
"dependencies": {
"@msgpack/msgpack": "^3.0.0-beta2",
"eventsource-parser": "^1.1.2",
"robot3": "^0.4.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@msgpack/msgpack": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
"integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==",
"license": "ISC",
"engines": {
"node": ">= 18"
}
},
"node_modules/@upstash/redis": {
"version": "1.36.3",
"resolved": "https://registry.npmjs.org/@upstash/redis/-/redis-1.36.3.tgz",
"integrity": "sha512-wxo1ei4OHDHm4UGMgrNVz9QUEela9N/Iwi4p1JlHNSowQiPi+eljlGnfbZVkV0V4PIrjGtGFJt5GjWM5k28enA==",
"license": "MIT",
"dependencies": {
"uncrypto": "^0.1.3"
}
},
"node_modules/eventsource-parser": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz",
"integrity": "sha512-v0eOBUbiaFojBu2s2NPBfYUoRR9GjcDNvCXVaqEf5vVfpIAh9f8RCo4vXTP8c63QRKCFwoLpMpTdPwwhEKVgzA==",
"license": "MIT",
"engines": {
"node": ">=14.18"
}
},
"node_modules/robot3": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/robot3/-/robot3-0.4.1.tgz",
"integrity": "sha512-hzjy826lrxzx8eRgv80idkf8ua1JAepRc9Efdtj03N3KNJuznQCPlyCJ7gnUmDFwZCLQjxy567mQVKmdv2BsXQ==",
"license": "BSD-2-Clause"
},
"node_modules/uncrypto": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz",
"integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==",
"license": "MIT"
}
}
}
{
"name": "audiomind-proxy",
"version": "1.0.0",
"private": true,
"dependencies": {
"@fal-ai/client": "^1.2.0",
"@upstash/redis": "^1.36.3"
}
}
/**
* usage-store.js — Persistent stats tracking for audiomind-proxy.
* Backed by Upstash Redis; falls back to in-memory for local dev.
*
* Redis key schema (prefix "am"):
* am:stats:total → { generations, errors, rateLimits }
* am:stats:action:{name} → { generations, errors }
* am:stats:day:{YYYYMMDD} → { generations }
*/
const PREFIX = "am";
const DAY_TTL_S = 90 * 24 * 3600; // 90-day retention
let redis = null;
async function getRedis() {
if (redis) return redis;
const url = process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL;
const token = process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN;
if (!url || !token) return null;
try {
const { Redis } = require("@upstash/redis");
redis = new Redis({ url, token });
return redis;
} catch {
return null;
}
}
// In-memory fallback
const mem = {};
function memIncr(key, field, by = 1) {
if (!mem[key]) mem[key] = {};
mem[key][field] = (mem[key][field] || 0) + by;
}
function memGet(key) {
return mem[key] || null;
}
function datestamp() {
return new Date().toISOString().slice(0, 10).replace(/-/g, "");
}
async function hincr(r, key, field, by = 1) {
try {
return await r.hincrby(key, field, by);
} catch {
return null;
}
}
/**
* Track a successful generation.
* @param {string} action - "tts" | "sfx" | "music"
*/
async function trackGeneration(action) {
const r = await getRedis();
const day = datestamp();
const dayKey = `${PREFIX}:stats:day:${day}`;
if (r) {
await Promise.allSettled([
hincr(r, `${PREFIX}:stats:total`, "generations"),
hincr(r, `${PREFIX}:stats:action:${action}`, "generations"),
hincr(r, dayKey, "generations").then(() =>
r.expire(dayKey, DAY_TTL_S).catch(() => {})
),
]);
} else {
memIncr(`${PREFIX}:stats:total`, "generations");
memIncr(`${PREFIX}:stats:action:${action}`, "generations");
memIncr(dayKey, "generations");
}
}
/**
* Track an error during generation.
* @param {string} action - "tts" | "sfx" | "music"
*/
async function trackError(action) {
const r = await getRedis();
if (r) {
await Promise.allSettled([
hincr(r, `${PREFIX}:stats:total`, "errors"),
hincr(r, `${PREFIX}:stats:action:${action}`, "errors"),
]);
} else {
memIncr(`${PREFIX}:stats:total`, "errors");
memIncr(`${PREFIX}:stats:action:${action}`, "errors");
}
}
/**
* Track a rate-limit / auth rejection.
*/
async function trackRateLimit() {
const r = await getRedis();
if (r) {
await hincr(r, `${PREFIX}:stats:total`, "rateLimits").catch(() => {});
} else {
memIncr(`${PREFIX}:stats:total`, "rateLimits");
}
}
/**
* Retrieve aggregated statistics.
* @param {number} [days=30] - How many daily buckets to include.
*/
async function getStats(days = 30) {
const r = await getRedis();
const actions = ["tts", "sfx", "music"];
if (r) {
const [total, ...actionStats] = await Promise.all([
r.hgetall(`${PREFIX}:stats:total`),
...actions.map((a) => r.hgetall(`${PREFIX}:stats:action:${a}`)),
]);
// Daily buckets
const daily = [];
for (let i = days - 1; i >= 0; i--) {
const d = new Date();
d.setDate(d.getDate() - i);
const stamp = d.toISOString().slice(0, 10).replace(/-/g, "");
const bucket = await r.hgetall(`${PREFIX}:stats:day:${stamp}`).catch(() => null);
daily.push({
date: `${stamp.slice(0, 4)}-${stamp.slice(4, 6)}-${stamp.slice(6, 8)}`,
generations: Number(bucket?.generations || 0),
});
}
const byAction = {};
actions.forEach((a, i) => {
byAction[a] = {
generations: Number(actionStats[i]?.generations || 0),
errors: Number(actionStats[i]?.errors || 0),
};
});
return {
total: {
generations: Number(total?.generations || 0),
errors: Number(total?.errors || 0),
rateLimits: Number(total?.rateLimits || 0),
},
byAction,
daily,
};
}
// In-memory fallback
const totalMem = memGet(`${PREFIX}:stats:total`) || {};
const byAction = {};
actions.forEach((a) => {
const s = memGet(`${PREFIX}:stats:action:${a}`) || {};
byAction[a] = { generations: s.generations || 0, errors: s.errors || 0 };
});
return {
total: {
generations: totalMem.generations || 0,
errors: totalMem.errors || 0,
rateLimits: totalMem.rateLimits || 0,
},
byAction,
daily: [],
};
}
module.exports = { trackGeneration, trackError, trackRateLimit, getStats };
{
"functions": {
"api/audio.js": { "maxDuration": 120 },
"api/stats.js": { "maxDuration": 10 }
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "Access-Control-Allow-Origin", "value": "*" },
{ "key": "Access-Control-Allow-Methods", "value": "GET, POST, OPTIONS" },
{ "key": "Access-Control-Allow-Headers", "value": "Content-Type, X-Audiomind-Key, X-Stats-Key" }
]
}
]
}
#!/bin/bash
# --- Configuration ---
MAX_FREE_USES=100
COUNT_FILE="/tmp/audiomind_usage_count.txt"
# --- Check for Pro API Key ---
if [ -n "$AUDIOMIND_API_KEY" ]; then
echo "AudioMind Pro: Activated! All 24 audio tools are available."
if [ -z "$ELEVENLABS_API_KEY" ]; then
echo "Error: ELEVENLABS_API_KEY is not set for Pro mode." >&2
exit 1
fi
elevenlabs-mcp --port 8124 &
exit 0
fi
# --- Free Trial Logic ---
# Initialize count file if it doesn't exist
if [ ! -f "$COUNT_FILE" ]; then
echo 0 > "$COUNT_FILE"
fi
# Read current usage
CURRENT_USES=$(cat "$COUNT_FILE")
# Check if limit is reached
if [ "$CURRENT_USES" -ge "$MAX_FREE_USES" ]; then
echo "Error: AudioMind free trial limit of $MAX_FREE_USES uses has been reached."
echo "Please upgrade to Pro by visiting [Your Gumroad Link Here] and setting the AUDIOMIND_API_KEY." >&2
exit 1
fi
# Increment usage count for the next run
NEXT_USES=$((CURRENT_USES + 1))
echo $NEXT_USES > "$COUNT_FILE"
# Notify user about remaining uses
REMAINING=$((MAX_FREE_USES - CURRENT_USES))
echo "AudioMind: Running in Free Trial mode. $REMAINING of $MAX_FREE_USES uses remaining."
# Start the full-featured server for the trial
elevenlabs-mcp --port 8124 &
Related skills
How it compares
videoagent-audio-studio is an agent skill for generate tts, music, sound effects, and voice clones via elevenlabs and fal.ai from one audio dispatcher skill, not a generic alternative.
FAQ
Who is videoagent-audio-studio for?
Developers using Generative Media workflows with agent-guided SKILL.md steps.
When should I use videoagent-audio-studio?
Generate TTS, music, sound effects, and voice clones via ElevenLabs and fal.ai from one audio dispatcher skill.
Is videoagent-audio-studio safe to install?
Review the Security Audits panel on this page before installing in production.