Lewati ke konten
kenari.

Musik

Buat lagu utuh dari lirik, atau trek instrumental dari deskripsi teks. Response-nya objek JSON yang membawa audio dalam base64. Hanya model musik yang dilayani di sini. Mengirim model lain mengembalikan 400.

POST /v1/music/generations

Model musik ditandai dengan music pada field endpoints di GET /v1/models:

Terminal window
curl -s https://kenari.id/v1/models \
| jq '.data[] | select(.endpoints | index("music")) | {id, max_lyrics_chars, max_prompt_chars, max_duration_secs, pricing_lines}'

Kalau panggilan itu tidak mengembalikan apa pun, saat ini belum ada model musik yang tersedia.

Body-nya JSON.

FieldTipeWajibKeterangan
modelstringyaId model musik.
lyricsstringbersyaratKata-kata yang dinyanyikan. Wajib untuk lagu dengan vokal, yaitu saat instrumental bernilai false atau tidak diisi.
promptstringbersyaratGaya dan suasana trek. Wajib saat instrumental bernilai true. Pada lagu dengan vokal, prompt opsional dan digunakan untuk menjelaskan gaya musik.
instrumentalbooleantidaktrue membuat trek tanpa vokal. Default false. Saat true, lyrics diabaikan.
response_formatstringtidakHanya mp3 yang tersedia, dan itu default-nya. wav dan pcm mengembalikan 400.
streambooleantidakDicadangkan. true mengembalikan 400.
lyrics_optimizerbooleantidakDicadangkan. true mengembalikan 400.

Lagunya ada di data[0].b64_json, dan formatnya di data[0].format:

{
"data": [
{ "b64_json": "<base64 dari byte mp3>", "format": "mp3" }
]
}

data selalu berisi tepat satu item dan format selalu mp3. Audio bukan tautan unduhan, jadi decode base64-nya lalu tulis ke file.

Karena pembuatan lagu lambat, body bisa datang dalam dua bentuk.

  • Spasi di awal. Selama lagu dibuat, gateway menulis satu spasi setiap 20 detik untuk menjaga koneksi tetap terbuka, lalu mengirim JSON-nya. Parse seluruh body sebagai JSON, yang mengabaikan spasi di awal. Jangan perlakukan sebagai audio mentah.
  • Error yang terlambat. Kegagalan di awal tetap memakai status aslinya: 400, 402, 429 atau 503. Kegagalan setelah gateway terlanjur mengirim 200 datang sebagai HTTP 200 dengan objek error standar, misalnya {"error": {"code": "...", "message": "...", "param": null, "type": "..."}}. Selalu periksa field error sebelum mendecode audio.

lyrics adalah teks yang dinyanyikan, bukan deskripsi lagu. Mengirim “lagu pop santai tentang Jakarta” sebagai lyrics membuat model menyanyikan kalimat itu. Untuk mendeskripsikan trek, set instrumental ke true dan tulis deskripsinya di prompt.

Penanda bagian membantu model menyusun lagu:

[Verse]
Lampu kota menyala satu per satu
Aku pulang membawa cerita baru
[Chorus]
Malam ini milik kita
Sampai pagi tiba

Panjang dihitung dalam karakter Unicode, bukan byte, jadi teks beraksen tidak terpotong lebih cepat dari yang terlihat. Batasnya berbeda per model. Kalau sebuah model mencatatnya, GET /v1/models menampilkan max_lyrics_chars dan max_prompt_chars pada entrinya. Tanpa field itu, berlaku batas bawaan: 3.500 karakter untuk lyrics dan 2.000 untuk prompt. Teks yang melewati batas dijawab 400 sebelum ada biaya yang terpotong.

Panjang lagu ditentukan oleh modelnya. Tidak ada field durasi, jadi kamu tidak bisa meminta lagu yang lebih pendek atau lebih panjang. Kalau modelnya mencatatnya, max_duration_secs di GET /v1/models memberi panjang lagu maksimum dalam detik. Angka itu hanya informasi: tidak ada request yang ditolak karenanya, dan lagu sebenarnya biasanya lebih pendek.

Sebagian besar lagu memerlukan dua sampai tiga menit, dan lagu yang lebih panjang memerlukan waktu lebih lama. Gateway menjaga koneksi tetap terbuka dan menunggu sampai 15 menit sebelum menyerah. Atur timeout klien minimal 5 menit, dan nilai longgar seperti 15 menit paling aman. Klien yang berhenti di 3 menit sesekali akan membatalkan lagu yang sebentar lagi selesai.

Baris pertama memilih satu model musik dari daftar. Isi MODEL sendiri kalau kamu sudah tahu id-nya.

Terminal window
MODEL=$(curl -s https://kenari.id/v1/models \
| jq -r '[.data[] | select(.endpoints | index("music"))][0].id')
curl https://kenari.id/v1/music/generations \
-H "Authorization: Bearer $KENARI_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"$MODEL\",
\"lyrics\": \"[Verse]\nLampu kota menyala satu per satu\n\n[Chorus]\nMalam ini milik kita\"
}" \
--max-time 900 \
-o response.json
# Berhenti jika ada objek error, lalu decode hanya string audio yang tidak kosong
jq -e 'has("error") | not' response.json > /dev/null || { jq .error response.json; exit 1; }
jq -er '.data[0].b64_json | select(type == "string" and length > 0)' response.json | base64 -d > song.mp3

Trek instrumental:

Terminal window
curl https://kenari.id/v1/music/generations \
-H "Authorization: Bearer $KENARI_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"$MODEL\",
\"instrumental\": true,
\"prompt\": \"lo-fi santai, piano lembut, hujan malam\"
}" \
--max-time 900 \
-o response.json
jq -e 'has("error") | not' response.json > /dev/null || { jq .error response.json; exit 1; }
jq -er '.data[0].b64_json | select(type == "string" and length > 0)' response.json | base64 -d > instrumental.mp3

Tidak ada method SDK OpenAI untuk musik, jadi pakai requests.

import base64
import os
import requests
headers = {"Authorization": f"Bearer {os.environ['KENARI_API_KEY']}"}
models = requests.get("https://kenari.id/v1/models", timeout=30).json()["data"]
model = next(m["id"] for m in models if "music" in m["endpoints"])
response = requests.post(
"https://kenari.id/v1/music/generations",
headers=headers,
json={
"model": model,
"lyrics": "[Verse]\nLampu kota menyala satu per satu\n\n[Chorus]\nMalam ini milik kita",
},
timeout=900,
)
body = response.json() # spasi di awal diabaikan
if "error" in body:
raise RuntimeError(body["error"]["message"])
with open("song.mp3", "wb") as f:
f.write(base64.b64decode(body["data"][0]["b64_json"]))
import fs from "node:fs";
const headers = {
Authorization: `Bearer ${process.env.KENARI_API_KEY}`,
"Content-Type": "application/json",
};
const { data: models } = await (await fetch("https://kenari.id/v1/models")).json();
const model = models.find((m) => m.endpoints.includes("music")).id;
const response = await fetch("https://kenari.id/v1/music/generations", {
method: "POST",
headers,
body: JSON.stringify({
model,
lyrics: "[Verse]\nLampu kota menyala satu per satu\n\n[Chorus]\nMalam ini milik kita",
}),
signal: AbortSignal.timeout(900_000),
});
const body = JSON.parse(await response.text()); // spasi di awal diabaikan
if (body.error) throw new Error(body.error.message);
fs.writeFileSync("song.mp3", Buffer.from(body.data[0].b64_json, "base64"));

Musik ditagih dengan harga tetap per lagu, berapa pun panjangnya. Pembuatan yang gagal tidak ditagih. Harga per lagu setiap model ada di Model & harga. Selengkapnya ada di Cara penagihan.

StatusKodeKapan
400bad_requestModel bukan model musik, field yang diwajibkan untuk jenis trekmu tidak ada, lyrics atau prompt melewati batas, atau response_format bukan mp3.
402insufficient_balanceSaldo tidak cukup untuk harga satu lagu.
429rate_limit_exceededBatas pembuatan musik bersamaan untuk akunmu tercapai. Lihat Batas laju. Tunggu sesuai header Retry-After, lalu coba lagi. Tidak ada biaya yang terpotong.
503all_providers_failed, upstream_errorTidak ada provider yang bisa membuat lagunya. Tidak ada biaya yang terpotong. Coba lagi.

Error setelah gateway mengirim 200 datang di dalam body response. Lihat Response. Kode lainnya ada di Error.