Transcrevendo áudio e vídeo
Da fala ao SRT, sem clipar nem renderizar nada
A transcrição é independente do resto da API: não precisa de submissão, de edit nem de render. Você envia um arquivo de áudio ou de vídeo e recebe o texto com tempos, em JSON ou como legenda pronta.
Áudio puro é aceito. Um podcast em .mp3 não precisa virar vídeo primeiro.
Envie o arquivo
POST /transcriptions/upload devolve um media_id e uma URL assinada. O file_size é obrigatório: é ele que decide se o arquivo cabe no seu plano e no espaço que sobra.
curl -X POST https://api.cut.pro/api/v1/transcriptions/upload \
-H "X-Api-Key: $CUTPRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"file_name": "episodio-42.mp3",
"file_size": 48210332,
"content_type": "audio/mpeg"
}'{
"media_id": "7412908365112823",
"kind": "audio",
"upload_url": "https://storage.cut.pro/...",
"expires_in": 3600
}Depois faça o PUT dos bytes na upload_url, com o mesmo Content-Type que você declarou. Não mande a sua chave nesse PUT: a URL já é assinada.
curl -X PUT "UPLOAD_URL" \
-H "Content-Type: audio/mpeg" \
--data-binary @episodio-42.mp3Comece o trabalho
POST /transcriptions cobra por minuto de áudio, com acréscimo quando você pede identificação de quem fala.
curl -X POST https://api.cut.pro/api/v1/transcriptions \
-H "X-Api-Key: $CUTPRO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"media_id": "7412908365112823",
"file_name": "episodio-42.mp3",
"file_size": 48210332,
"speaker_labels": true,
"source_language": "pt"
}'{
"id": "7412908372001144",
"status": "processing",
"credits_charged": 62
}Em áudio curto a detecção automática de idioma confunde português com espanhol. Se você já sabe o idioma, mande source_language.
Resposta 200 em vez de 202 significa que nada precisou rodar: ou esse mesmo áudio já tinha sido transcrito (o texto é compartilhado por mídia, então repetir é cache e credits_charged vem 0), ou não há ninguém falando, e aí o status é no_speech e não há cobrança.
Acompanhe
curl https://api.cut.pro/api/v1/transcriptions/7412908372001144 \
-H "X-Api-Key: $CUTPRO_API_KEY"O status sai de processing para ready, no_speech ou failed. Consulte a cada 5 ou 10 segundos.
Pegue o texto
Duas saídas, a partir do mesmo trabalho.
JSON com tempos, para indexar, buscar ou desenhar você mesmo:
curl https://api.cut.pro/api/v1/transcriptions/7412908372001144/transcript \
-H "X-Api-Key: $CUTPRO_API_KEY"{
"language": "pt",
"speaker_labels": true,
"cues": [
{ "start": 12.4, "end": 15.9, "text": "A gente começou errado, e demorou dois anos pra perceber.", "speaker": "A" },
{ "start": 16.1, "end": 18.7, "text": "Errado como?", "speaker": "B" }
]
}Arquivo de legenda pronto, em srt (padrão), vtt ou txt. O corpo é o arquivo, não JSON:
curl "https://api.cut.pro/api/v1/transcriptions/7412908372001144/download?format=srt" \
-H "X-Api-Key: $CUTPRO_API_KEY" \
-o episodio-42.srtEnquanto o trabalho não terminou, as duas rotas respondem 409 TRANSCRIPTION_NOT_READY. Não é erro de id: é só continuar consultando.
Quem está falando
Com speaker_labels: true, cada trecho vem com um rótulo (A, B, ...) e o arquivo de legenda traz o nome antes da fala. Passe speakers=false no download se quiser o mesmo texto sem os rótulos.
A coluna de quem fala não dá para acrescentar depois. Se um áudio foi transcrito sem speaker_labels e você mudou de ideia, chame POST /transcriptions de novo com o mesmo media_id e speaker_labels: true: ele reprocessa e cobra de novo.
Fluxo inteiro
import { readFile, stat } from "node:fs/promises";
const API = "https://api.cut.pro/api/v1";
const HEADERS = { "X-Api-Key": process.env.CUTPRO_API_KEY, "Content-Type": "application/json" };
const FILE = "episodio-42.mp3";
const TYPE = "audio/mpeg";
async function call(path, init) {
const response = await fetch(`${API}${path}`, { ...init, headers: HEADERS });
const body = await response.json();
if (!response.ok) throw new Error(`${response.status} ${body.code}`);
return body;
}
const size = (await stat(FILE)).size;
const upload = await call("/transcriptions/upload", {
method: "POST",
body: JSON.stringify({ file_name: FILE, file_size: size, content_type: TYPE }),
});
await fetch(upload.upload_url, { method: "PUT", headers: { "Content-Type": TYPE }, body: await readFile(FILE) });
let job = await call("/transcriptions", {
method: "POST",
body: JSON.stringify({ media_id: upload.media_id, file_name: FILE, file_size: size, source_language: "pt" }),
});
while (job.status === "processing") {
await new Promise((resolve) => setTimeout(resolve, 10_000));
job = await call(`/transcriptions/${job.id}`);
}
if (job.status !== "ready") throw new Error(job.status);
const { cues } = await call(`/transcriptions/${job.id}/transcript`);
console.log(cues.map((cue) => cue.text).join(" "));Apagando
DELETE /transcriptions/{transcriptionId} tira o trabalho das listagens. A mídia enviada continua na sua biblioteca, então o mesmo media_id pode ser transcrito de novo, e essa repetição é cache, não uma segunda cobrança.