Audio
Teks ke suara dan suara ke teks: dua rute, dua satuan tagih.
Ringkasan
Suara punya dua arah, dan keduanya rute terpisah. Tidak ada satu endpoint dengan parameter mode.
/v1/audio/speechbutuh API key/v1/audio/transcriptionsbutuh API keyPOST /v1/audio/speech mengubah teks menjadi audio: body-nya JSON, dan yang kamu kirim adalah teks yang akan diucapkan. POST /v1/audio/transcriptions melakukan arah sebaliknya: body-nya multipart/form-data, dan yang kamu kirim adalah berkas audionya.
Keduanya butuh API key dan keduanya sinkron: response datang sebagai satu body utuh, bukan aliran. Jadi tidak ada stream dan tidak ada frame data: di kedua rute ini.
Body teks ke suara
Bentuknya JSON biasa. Hanya dua field yang wajib, dan salah satunya menentukan biaya.
| Field | Tipe | Wajib | Artinya |
|---|---|---|---|
model | string | Wajib | Id model, persis seperti yang ada di katalog. |
input | string | Wajib | Teks yang akan diucapkan. Inilah yang ditagih, dihitung per 1.000 karakter. Lihat bagian tagihan. |
voice | string | Opsional | Nama voice. Nilai yang tersedia ditentukan modelnya. |
response_format | string | Opsional | Format audio yang diminta. Diteruskan apa adanya. |
speed | number | Opsional | Pengali kecepatan. Harus bilangan positif. |
{
"model": "<id-dari-katalog>",
"input": "Selamat pagi, ini contoh teks yang akan diucapkan.",
"voice": "<nama-voice>",
"response_format": "<format>",
"speed": 1.0
}Field di luar daftar di atas tetap diteruskan ke model, tidak dibuang. Jadi opsi spesifik yang didukung sebuah model tetap bisa kamu kirim meskipun tidak tercantum di sini.
Body suara ke teks
Rute ini hanya menerima multipart/form-data. Body JSON ditolak, sekalipun isinya lengkap.
Berkasnya adalah satu part bernama file, dan part itu wajib. Rute ini memeriksa keberadaan part tersebut sebelum meneruskan apa pun, jadi permintaan tanpa berkas dijawab 422 dengan pesan yang menyebut berkas file wajib ada. Tidak ada yang sampai ke tahap transkripsi.
Body JSON ditolak dengan cara yang sama, karena rute ini membaca part multipart dan body JSON tidak punya part apa pun untuk dibaca, termasuk file.
| Field | Tipe | Wajib | Artinya |
|---|---|---|---|
file | berkas | Wajib | Part berisi audio. Diteruskan byte-for-byte. |
model | string | Wajib | Id model, persis seperti yang ada di katalog. |
language | string | Opsional | Kode bahasa audio, kalau kamu sudah mengetahuinya. |
prompt | string | Opsional | Konteks tambahan untuk model. |
response_format | string | Opsional | Format hasil transkripsi yang diminta. |
temperature | string | Opsional | Diteruskan sebagai teks, apa adanya. |
Berkas tidak dibaca, tidak didekode, dan tidak diubah oleh MenvoraRouter. Isinya diteruskan byte-for-byte ke model, dan itu berlaku juga untuk nama berkasnya. Konsekuensinya dua: platform tidak bisa memeriksa apakah format berkasmu didukung, dan platform juga tidak bisa merusaknya di tengah jalan. Format yang diterima adalah urusan modelnya.
Satuan tagihan
Dua rute ini memakai satuan yang berbeda, dan keduanya bukan token.
| Rute | Satuan | Dihitung dari | Artinya |
|---|---|---|---|
/v1/audio/speech | thousand_chars | Panjang input | Per 1.000 karakter teks yang kamu kirim. |
/v1/audio/transcriptions | minute | Durasi audio | Per menit audio yang ditranskripsi. |
Harga satuannya ada di katalog, pada price.unit beserta price.unit_price, dan kalimat siap bacanya di price.unit_price_display. Membaca harga tanpa satuannya tidak ada gunanya: angka yang sama bisa berarti per 1.000 karakter atau per menit, tergantung modelnya.
Model yang belum punya harga satuan terbit tetap terdaftar, dengan quoted_at_submit bernilai true, unit_price bernilai null, dan tampilannya berbunyi quoted at submit. Itu bukan harga nol, dan bukan model gratis: angkanya baru ditentukan saat request dikirim.
Kapabilitas yang dibutuhkan
Kedua rute audio menuntut capabilities.audio = true pada model yang kamu pilih.
Pemeriksaan ini berjalan sebelum saldo ditahan. Request yang ditolak karena kapabilitas tidak pernah memotong saldo, tidak meninggalkan baris permintaan, dan tidak pernah dikirim ke model.
Ada dua penolakan yang berbeda, dan keduanya 422:
| Kode | Kapan muncul | Yang perlu dilakukan |
|---|---|---|
capability_mismatch | Model menyatakan audio: false. | Pakai model lain yang mendeklarasikan audio. |
capabilities_not_declared | Model belum menyatakan apa pun: capabilities bernilai null, atau objeknya ada tapi tidak memuat flag audio. | Deklarasikan capabilities.audio = true pada model itu. Tidak ada gunanya mengulang request yang sama. |
{
"error": {
"message": "Model '…' tidak dideklarasikan untuk permintaan jenis ini (butuh kapabilitas 'audio'). Pakai model lain, atau deklarasikan kapabilitas itu pada modelnya.",
"type": "invalid_request_error",
"code": "capability_mismatch"
}
}Perhatikan bahwa “tidak menyatakan” bukan “tidak bisa”. Model yang flag-nya absen dianggap belum diketahui, bukan ditolak, jadi penolakannya menyebut field yang harus diisi alih-alih menyuruhmu berganti model.
Contoh
Ganti MODEL dengan id dari katalog yang mendeklarasikan capabilities.audio = true. Model chat tidak akan lolos di sini.
export MODEL="<id-dari-katalog>"
curl "https://api.menvora.web.id/v1/audio/speech" \
-H "Authorization: Bearer sk-nusa-..." \
-H "Content-Type: application/json" \
-d "{\"model\":\"$MODEL\",\"input\":\"Selamat pagi, ini contoh teks.\"}" \
--output hasil-audioResponsnya adalah byte audio, jadi simpan ke berkas dengan --output. Tanpa itu curl mencetak byte-nya ke terminal.
export MODEL="<id-dari-katalog>"
curl "https://api.menvora.web.id/v1/audio/transcriptions" \
-H "Authorization: Bearer sk-nusa-..." \
-F "file=@rekaman.mp3" \
-F "model=$MODEL"-F membuat curl mengirim multipart/form-data sendiri, dan @ menandai bahwa nilainya adalah berkas lokal. Itu satu-satunya bentuk body yang diterima rute transkripsi.
Batasan yang perlu kamu tahu
Media bukan kemampuan semua model. Hanya model yang punya jalur media yang dilayani, dan model di luar itu dijawab 501 surface_not_served. Penolakan itu datang sebelum saldo ditahan, jadi tidak ada biaya untuk percobaan yang tidak pernah berjalan.
Ketiga penolakan media, termasuk cara membaca kapabilitas dari katalog sebelum memanggil, dibahas di Respons dan kegagalan media.
Respons audio dikirim apa adanya dengan content-type yang diberikan model. Jangan mengandalkan ekstensi berkas yang kamu minta sebagai penentu tipe; baca headernya.