Documentation Index
Fetch the complete documentation index at: https://docs.modulate.ai/llms.txt
Use this file to discover all available pages before exploring further.
Language-specific patterns for integrating with the Velma-2 APIs. Each section covers the key patterns for that language: authentication setup, file upload, WebSocket connections, and error handling.
For full per-endpoint request and response schemas, see the API Reference tab.
cURL
cURL works for all HTTP batch endpoints. WebSocket endpoints require a dedicated tool — see WebSocket testing below.
Batch file upload with optional features
curl -X POST https://modulate-developer-apis.com/api/velma-2-stt-batch \
-H "X-API-Key: YOUR_API_KEY" \
-F "upload_file=@/path/to/recording.mp3" \
-F "speaker_diarization=true" \
-F "emotion_signal=true" \
-F "accent_signal=false" \
-F "pii_phi_tagging=false"
Replace the endpoint and fields for other batch APIs. The X-API-Key header pattern is the same for all batch endpoints.
SVD batch — minimal example
curl -X POST https://modulate-developer-apis.com/api/velma-2-synthetic-voice-detection-batch \
-H "X-API-Key: YOUR_API_KEY" \
-F "upload_file=@/path/to/audio.mp3"
Python
Setup
# Synchronous
pip install requests
# Async
pip install aiohttp
Authentication — synchronous (requests)
Pass the API key in the headers dict. Reuse the session or headers across requests.
import requests
HEADERS = {"X-API-Key": "YOUR_API_KEY"}
response = requests.post(
"https://modulate-developer-apis.com/api/velma-2-stt-batch",
headers=HEADERS,
files={"upload_file": open("recording.mp3", "rb")},
data={"speaker_diarization": "true"},
)
response.raise_for_status()
result = response.json()
Authentication — async (aiohttp)
import aiohttp
async with aiohttp.ClientSession(headers={"X-API-Key": "YOUR_API_KEY"}) as session:
# reuse session for multiple requests
pass
Batch file upload (aiohttp)
import asyncio
import aiohttp
async def transcribe(file_path: str) -> dict:
data = aiohttp.FormData()
data.add_field(
"upload_file",
open(file_path, "rb"),
filename=file_path.split("/")[-1],
content_type="application/octet-stream",
)
data.add_field("speaker_diarization", "true")
data.add_field("emotion_signal", "true")
async with aiohttp.ClientSession(headers={"X-API-Key": "YOUR_API_KEY"}) as session:
async with session.post(
"https://modulate-developer-apis.com/api/velma-2-stt-batch",
data=data,
) as resp:
resp.raise_for_status()
return await resp.json()
result = asyncio.run(transcribe("recording.mp3"))
Concurrent batch processing
Use a semaphore to respect your organization’s concurrent request limit.
import asyncio
import glob
import aiohttp
URL = "https://modulate-developer-apis.com/api/velma-2-stt-batch-english-vfast"
MAX_CONCURRENT = 5 # set to your organization's limit
semaphore = asyncio.Semaphore(MAX_CONCURRENT)
async def process_file(session: aiohttp.ClientSession, filepath: str) -> dict:
async with semaphore:
data = aiohttp.FormData()
data.add_field(
"upload_file",
open(filepath, "rb"),
filename=filepath.rsplit("/", 1)[-1],
content_type="application/octet-stream",
)
async with session.post(URL, data=data) as resp:
if resp.status != 200:
return {"file": filepath, "error": f"{resp.status}: {await resp.text()}"}
result = await resp.json()
return {"file": filepath, **result}
async def main():
files = glob.glob("audio_files/*.opus")
async with aiohttp.ClientSession(headers={"X-API-Key": "YOUR_API_KEY"}) as session:
results = await asyncio.gather(*[process_file(session, f) for f in files])
for r in results:
status = "OK" if "error" not in r else f"FAILED: {r['error']}"
print(f"{r['file']}: {status}")
asyncio.run(main())
WebSocket streaming (aiohttp)
Applies to both the STT Streaming and SVD Streaming endpoints.
import asyncio
import json
import aiohttp
API_KEY = "YOUR_API_KEY"
AUDIO_FILE = "recording.opus"
CHUNK_SIZE = 8192
async def stream_transcription():
url = (
f"wss://modulate-developer-apis.com/api/velma-2-stt-streaming"
f"?api_key={API_KEY}"
f"&speaker_diarization=true"
f"&emotion_signal=true"
)
async with aiohttp.ClientSession() as session:
async with session.ws_connect(url) as ws:
async def send_audio():
with open(AUDIO_FILE, "rb") as f:
while chunk := f.read(CHUNK_SIZE):
await ws.send_bytes(chunk)
await asyncio.sleep(CHUNK_SIZE / 4000)
await ws.send_str("")
send_task = asyncio.create_task(send_audio())
try:
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
data = json.loads(msg.data)
if data["type"] == "utterance":
u = data["utterance"]
print(f"[Speaker {u['speaker']}] {u['text']}")
elif data["type"] == "done":
print(f"Done. Duration: {data['duration_ms']}ms")
break
elif data["type"] == "error":
print(f"Error: {data['error']}")
break
elif msg.type in (
aiohttp.WSMsgType.ERROR,
aiohttp.WSMsgType.CLOSE,
aiohttp.WSMsgType.CLOSED,
):
break
finally:
if not send_task.done():
send_task.cancel()
asyncio.run(stream_transcription())
Error handling
import requests
from requests.exceptions import HTTPError
try:
response = requests.post(url, headers=headers, files=files)
response.raise_for_status()
result = response.json()
except HTTPError as e:
status = e.response.status_code
detail = e.response.json().get("detail", "unknown error")
if status == 401:
print("Invalid or missing API key")
elif status == 403:
print(f"Access denied: {detail}")
elif status == 429:
print("Rate limit exceeded — wait and retry")
elif status >= 500:
print(f"Server error ({status}): {detail} — retry after delay")
JavaScript (Node.js)
Setup
npm install ws form-data node-fetch
# or use built-in fetch (Node 18+) and the ws package for WebSocket
Batch file upload (native fetch, Node 18+)
import fs from "fs";
import path from "path";
const API_KEY = "YOUR_API_KEY";
async function transcribe(filePath) {
const formData = new FormData();
formData.append(
"upload_file",
new Blob([fs.readFileSync(filePath)]),
path.basename(filePath)
);
formData.append("speaker_diarization", "true");
formData.append("emotion_signal", "true");
const response = await fetch(
"https://modulate-developer-apis.com/api/velma-2-stt-batch",
{
method: "POST",
headers: { "X-API-Key": API_KEY },
body: formData,
}
);
if (!response.ok) {
const err = await response.json().catch(() => ({ detail: response.statusText }));
throw new Error(`${response.status}: ${err.detail}`);
}
return response.json();
}
const result = await transcribe("recording.mp3");
console.log(result.text);
for (const u of result.utterances) {
console.log(`[Speaker ${u.speaker}] (${u.language}) ${u.text}`);
}
Use the form-data package when you want to stream the file rather than reading it fully into memory.
import fs from "fs";
import path from "path";
import FormData from "form-data";
const API_KEY = "YOUR_API_KEY";
async function transcribe(filePath) {
const form = new FormData();
form.append("upload_file", fs.createReadStream(filePath), {
filename: path.basename(filePath),
});
form.append("speaker_diarization", "true");
const response = await fetch(
"https://modulate-developer-apis.com/api/velma-2-stt-batch",
{
method: "POST",
headers: {
"X-API-Key": API_KEY,
...form.getHeaders(),
},
body: form,
}
);
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
return response.json();
}
WebSocket streaming (ws package)
const WebSocket = require("ws");
const fs = require("fs");
const API_KEY = "YOUR_API_KEY";
const CHUNK_SIZE = 8192;
const url = new URL("wss://modulate-developer-apis.com/api/velma-2-stt-streaming");
url.searchParams.set("api_key", API_KEY);
url.searchParams.set("speaker_diarization", "true");
url.searchParams.set("emotion_signal", "true");
const ws = new WebSocket(url.toString());
ws.on("open", () => {
const stream = fs.createReadStream("recording.opus", { highWaterMark: CHUNK_SIZE });
stream.on("data", (chunk) => ws.send(chunk));
stream.on("end", () => ws.send(""));
});
ws.on("message", (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === "utterance") {
console.log(`[Speaker ${msg.utterance.speaker}] ${msg.utterance.text}`);
} else if (msg.type === "done") {
console.log(`Done. Duration: ${msg.duration_ms}ms`);
ws.close();
} else if (msg.type === "error") {
console.error("Error:", msg.error);
ws.close();
}
});
ws.on("close", (code, reason) => {
if (code !== 1000) console.error(`Closed unexpectedly: ${code} ${reason}`);
});
ws.on("error", (err) => console.error("WebSocket error:", err.message));
Error handling
async function safeRequest(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
let detail = response.statusText;
try {
const body = await response.json();
detail = body.detail ?? detail;
} catch {}
const err = new Error(`API error ${response.status}: ${detail}`);
err.status = response.status;
throw err;
}
return response.json();
}
try {
const result = await safeRequest(url, options);
} catch (err) {
if (err.status === 429) {
console.warn("Rate limited — wait and retry");
} else if (err.status >= 500) {
console.error("Server error — retry after delay");
} else {
throw err;
}
}
WebSocket testing
WebSocket APIs cannot be tested with cURL. Use websocat for command-line testing.
# Install
cargo install websocat
# Basic connection test (STT Streaming)
websocat "wss://modulate-developer-apis.com/api/velma-2-stt-streaming?api_key=YOUR_API_KEY&speaker_diarization=true"