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"
}'{
"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.mp3Empieza 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"
}'{
"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"{
"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.srtMientras 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.