Generate music from a video. By default (mode=stream) returns a streaming NDJSON response with one or more parallel audio streams. Pass mode=async to instead get 202 Accepted with a task_id; poll GET /v1/tasks/:task_id until the task reaches a terminal status.
import jsonimport requestswith requests.post( "https://api.sonilo.com/v1/video-to-music", headers={"Authorization": "Bearer sk_your_api_key"}, files={"video": open("/path/to/video.mp4", "rb")}, stream=True, timeout=300,) as resp: resp.raise_for_status() for line in resp.iter_lines(decode_unicode=True): if not line: continue event = json.loads(line) print(event["type"], event)
import fs from "node:fs";const form = new FormData();const blob = await fs.openAsBlob("/path/to/video.mp4");form.append("video", blob, "video.mp4");const resp = await fetch("https://api.sonilo.com/v1/video-to-music", { method: "POST", headers: { Authorization: "Bearer sk_your_api_key" }, body: form,});if (!resp.ok) { throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);}const reader = resp.body.getReader();const decoder = new TextDecoder();let buf = "";while (true) { const { value, done } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); let nl; while ((nl = buf.indexOf("\n")) !== -1) { const line = buf.slice(0, nl); buf = buf.slice(nl + 1); if (!line) continue; console.log(JSON.parse(line)); }}
Authenticate via Bearer token. Generate keys at the API Keys page and pass them in the Authorization header on every request.
Authorization: Bearer sk_your_api_key
Body Parameters
videofilerequired
Video file upload. Provide either video or video_url, not both. Max file size: default 300MB. Max duration: 6 minutes.
video_urlstringrequired
URL to a video file. Provide either video or video_url, not both. Must be a public http:// or https:// URL; private/internal addresses are rejected.
modestring
Optional. stream (default) returns this endpoint's streaming NDJSON response, unchanged. async returns 202 Accepted with a task_id instead of streaming; poll GET /v1/tasks/:task_id for the result.
output_formatstring
Optional. m4a (default) returns AAC-encoded audio. wav returns 16-bit PCM WAV instead, transcoded server-side after generation — applies to both audio and, when isolate_vocals is used, the mux track (the isolated vocals stem is unaffected). Requires mode=async (the streaming response always returns m4a — see GET /v1/tasks/:task_id's audio[].content_type).
promptstring
Optional text prompt to guide music generation, up to 2000 characters. For best results, omit this and let the model generate based on video content.
segmentsstring
Optional JSON array of timed segment prompts; the music changes style at each boundary. Each item has start (seconds; the first must be 0, starts strictly increasing and at least 5 seconds apart, the last at least 5 seconds before the end of the video), prompt (1-200 characters), and an optional label that must be exactly one of these values — any other value is rejected with a 422 error, and it defaults to none when omitted: intro, verse, pre-chorus, chorus, bridge, break, silence, outro, none. 1-30 items.
isolate_vocalsboolean
Optional boolean, default false. When true, the video's audio is source-separated first and only the isolated vocals are used to guide generation — lets the model score against the vocal performance without the original background music interfering. Requires mode=async (the response has no room for the extra download links this adds — see GET /v1/tasks/:task_id's vocals and mux fields). No additional charge — billed the same as isolate_vocals=false.
Response
titleevent
Generated track title. Appears once near the start of the stream.
audio_chunkevent
Base64-encoded AAC/fMP4 audio fragment. Includes stream_index (0-based) and num_streams (total parallel outputs). Group chunks by stream_index and append in order.
import jsonimport requestswith requests.post( "https://api.sonilo.com/v1/video-to-music", headers={"Authorization": "Bearer sk_your_api_key"}, files={"video": open("/path/to/video.mp4", "rb")}, stream=True, timeout=300,) as resp: resp.raise_for_status() for line in resp.iter_lines(decode_unicode=True): if not line: continue event = json.loads(line) print(event["type"], event)
import fs from "node:fs";const form = new FormData();const blob = await fs.openAsBlob("/path/to/video.mp4");form.append("video", blob, "video.mp4");const resp = await fetch("https://api.sonilo.com/v1/video-to-music", { method: "POST", headers: { Authorization: "Bearer sk_your_api_key" }, body: form,});if (!resp.ok) { throw new Error(`HTTP ${resp.status}: ${await resp.text()}`);}const reader = resp.body.getReader();const decoder = new TextDecoder();let buf = "";while (true) { const { value, done } = await reader.read(); if (done) break; buf += decoder.decode(value, { stream: true }); let nl; while ((nl = buf.indexOf("\n")) !== -1) { const line = buf.slice(0, nl); buf = buf.slice(nl + 1); if (!line) continue; console.log(JSON.parse(line)); }}