Cut.ProDocs
Guias

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"
  }'
Resposta
{
  "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.mp3

Comece 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"
  }'
Resposta 202
{
  "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"
Resposta
{
  "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.srt

Enquanto 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.

Nesta página