Cut.ProDocs
Guías

Transcribiendo audio y vídeo

Del habla al SRT, sin recortar ni renderizar nada

La transcripción es independiente del resto de la API: no necesita envío, ni edit, ni render. Mandas un archivo de audio o de vídeo y recibes el texto con tiempos, en JSON o como subtítulo listo.

El audio suelto vale. Un podcast en .mp3 no tiene que convertirse en vídeo primero.

Envía el archivo

POST /transcriptions/upload devuelve un media_id y una URL firmada. El file_size es obligatorio: es lo que decide si el archivo cabe en tu plan y en el espacio que te queda.

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"
  }'
Respuesta
{
  "media_id": "7412908365112823",
  "kind": "audio",
  "upload_url": "https://storage.cut.pro/...",
  "expires_in": 3600
}

Después haz el PUT de los bytes en la upload_url, con el mismo Content-Type que declaraste. No mandes tu clave en ese PUT: la URL ya está firmada.

curl -X PUT "UPLOAD_URL" \
  -H "Content-Type: audio/mpeg" \
  --data-binary @episodio-42.mp3

Empieza el trabajo

POST /transcriptions cobra por minuto de audio, con un extra cuando pides identificar quién habla.

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": "es"
  }'
Respuesta 202
{
  "id": "7412908372001144",
  "status": "processing",
  "credits_charged": 62
}

En audios cortos la detección automática de idioma confunde portugués con español. Si ya sabes el idioma, manda source_language.

Una respuesta 200 en vez de 202 significa que no hizo falta ejecutar nada: o ese mismo audio ya estaba transcrito (el texto se comparte por media, así que repetir es caché y credits_charged llega en 0), o no habla nadie, y entonces el status es no_speech y no hay cobro.

Haz el seguimiento

curl https://api.cut.pro/api/v1/transcriptions/7412908372001144 \
  -H "X-Api-Key: $CUTPRO_API_KEY"

El status pasa de processing a ready, no_speech o failed. Consulta cada 5 o 10 segundos.

Recoge el texto

Dos salidas, del mismo trabajo.

JSON con tiempos, para indexar, buscar o dibujar tú mismo:

curl https://api.cut.pro/api/v1/transcriptions/7412908372001144/transcript \
  -H "X-Api-Key: $CUTPRO_API_KEY"
Respuesta
{
  "language": "es",
  "speaker_labels": true,
  "cues": [
    { "start": 12.4, "end": 15.9, "text": "Empezamos mal, y tardamos dos años en darnos cuenta.", "speaker": "A" },
    { "start": 16.1, "end": 18.7, "text": "¿Mal en qué sentido?", "speaker": "B" }
  ]
}

Un archivo de subtítulos listo, en srt (por defecto), vtt o txt. El cuerpo es el archivo, no JSON:

curl "https://api.cut.pro/api/v1/transcriptions/7412908372001144/download?format=srt" \
  -H "X-Api-Key: $CUTPRO_API_KEY" \
  -o episodio-42.srt

Mientras el trabajo no termina, las dos rutas responden 409 TRANSCRIPTION_NOT_READY. No es un id equivocado: solo hay que seguir consultando.

Quién habla

Con speaker_labels: true, cada tramo llega con una etiqueta (A, B, ...) y el archivo de subtítulos escribe el nombre antes de la frase. Pasa speakers=false en la descarga si quieres el mismo texto sin las etiquetas.

La columna de quién habla no se puede añadir después. Si un audio se transcribió sin speaker_labels y cambiaste de idea, llama otra vez a POST /transcriptions con el mismo media_id y speaker_labels: true: reprocesa y vuelve a cobrar.

El flujo entero

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: "es" }),
});

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(" "));

Borrando

DELETE /transcriptions/{transcriptionId} saca el trabajo de los listados. El archivo subido sigue en tu biblioteca, así que el mismo media_id se puede transcribir otra vez, y esa repetición es caché, no un segundo cobro.

En esta página