API de Transcripción
🎙️ API de Transcripción
Sección titulada «🎙️ API de Transcripción»El flujo de transcripción tiene tres recursos:
- File (
/api/files/) — el audio que subes. Una vez subido, la transcripción comienza automáticamente. - Transcript (
/api/transcripts/) — el resultado, con metadatos (idioma, duración, número de palabras) y enlaces de descarga para SRT y DOCX. - Task (
/api/tasks/) — el trabajo en segundo plano que hizo el procesamiento (útil para reintentos).
Todos los endpoints de esta sección requieren una cookie de sesión o una clave API — consulta Autenticación.
Visión general del flujo
Sección titulada «Visión general del flujo»POST /api/files/— sube un archivo de audio dentro de un proyecto existente. La respuesta te da unfile.id. La transcripción se encola automáticamente.- Consulta
GET /api/files/{id}/hasta questatuspase atranscribed(éxito) ofailed. GET /api/transcripts/oGET /api/transcripts/{id}/— lee el registro de transcripción resultante.GET /api/transcripts/{id}/download/?type=srt|docx— obtén una URL de descarga temporal para el archivo.
POST /api/files/
Sección titulada «POST /api/files/»Sube un archivo de audio. Dispara automáticamente la transcripción cuando la suscripción del usuario tiene minutos disponibles.
Auth: clave API o sesión.
Content-Type: multipart/form-data
Campos del formulario:
| Campo | Obligatorio | Descripción |
|---|---|---|
project | sí | ID del proyecto al que pertenece el archivo. Debe ser visible para quien llama — consulta Proyectos para saber cómo obtener el ID. |
file | sí | El archivo de audio. Se admiten los formatos habituales (mp3, wav, m4a, flac, ogg, mp4, …). |
file_type | sí | Usa audio para transcripción. Otros valores (csv, xlsx, srt, vtt) se utilizan para subidas que no son de audio. |
Respuesta (201 Created):
{ "id": 4217, "project": 312, "original_filename": "interview.wav", "file_type": "audio", "status": "transcribing", "duration_seconds": 1834.2, "error_reason": "", "asr_engine": "", "uploaded_at": "2026-05-17T14:02:11Z"}Ejemplo:
curl -X POST https://app.uspeech.io/api/files/ \ -H "Authorization: Api-Key $USPEECH_KEY" \ -F "project=312" \ -F "file_type=audio" \ -F "file=@./interview.wav"import osimport requests
BASE_URL = "https://app.uspeech.io"HEADERS = {"Authorization": f"Api-Key {os.environ['USPEECH_KEY']}"}
with open("interview.wav", "rb") as fh: response = requests.post( f"{BASE_URL}/api/files/", headers=HEADERS, data={"project": 312, "file_type": "audio"}, files={"file": ("interview.wav", fh, "audio/wav")}, timeout=300, )response.raise_for_status()file_id = response.json()["id"]import { openAsBlob } from 'node:fs';
const BASE_URL = 'https://app.uspeech.io';const HEADERS = { Authorization: `Api-Key ${process.env.USPEECH_KEY}` };
const form = new FormData();form.set('project', '312');form.set('file_type', 'audio');form.set('file', await openAsBlob('./interview.wav'), 'interview.wav');
const response = await fetch(`${BASE_URL}/api/files/`, { method: 'POST', headers: HEADERS, // no fijes Content-Type — FormData define el boundary body: form,});if (!response.ok) throw new Error(await response.text());const fileId = (await response.json()).id;⚠️ Importante: un 201 solo significa que la subida fue aceptada y el trabajo encolado. La transcripción en sí se produce de forma asíncrona — consulta el patrón de polling más abajo.
GET /api/files/{id}/
Sección titulada «GET /api/files/{id}/»Lee un archivo concreto, incluyendo su status actual.
Auth: clave API o sesión.
Respuesta (200 OK): misma forma que la respuesta del POST anterior.
Referencia de estados de archivo
Sección titulada «Referencia de estados de archivo»| Estado | Significado |
|---|---|
uploaded | Archivo guardado, transcripción todavía no encolada (se usa para subidas que no son de audio). |
transcribing | Transcripción en curso. |
transcribed | Transcripción completada correctamente. Ya existe un registro Transcript. |
failed | La transcripción falló. El campo error_reason de esta misma respuesta indica el motivo (audio_too_short, hallucinated_transcript, private_transcription_unavailable, o vacío para otros errores) — consulta Códigos de estado y errores. |
analyzing, completed | Se establecen cuando un análisis posterior está corriendo o ha terminado (no aplica al flujo de solo transcripción). |
Patrón de polling
Sección titulada «Patrón de polling»file_id=4217while :; do resp=$(curl -sS "https://app.uspeech.io/api/files/${file_id}/" \ -H "Authorization: Api-Key $USPEECH_KEY") status=$(echo "$resp" | jq -r .status) echo "$(date -u +%H:%M:%S) status=$status" case "$status" in transcribed) break ;; failed) echo "falló, motivo: $(echo "$resp" | jq -r '.error_reason // "unknown"')" break ;; esac sleep 10doneimport time
while True: file_data = requests.get( f"{BASE_URL}/api/files/{file_id}/", headers=HEADERS, timeout=30 ).json() status = file_data["status"] print(f"status={status}") if status == "transcribed": break if status == "failed": raise RuntimeError(f"falló, motivo: {file_data.get('error_reason') or 'unknown'}") time.sleep(10)const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
while (true) { const fileData = await fetch(`${BASE_URL}/api/files/${fileId}/`, { headers: HEADERS, }).then((r) => r.json()); console.log(`status=${fileData.status}`); if (fileData.status === 'transcribed') break; if (fileData.status === 'failed') { throw new Error(`falló, motivo: ${fileData.error_reason || 'unknown'}`); } await sleep(10_000);}💡 Consejo: la transcripción suele tardar ~1/8 de la duración del audio. Una llamada de 10 minutos se completa normalmente en menos de dos minutos.
GET /api/transcripts/
Sección titulada «GET /api/transcripts/»Lista las transcripciones visibles para quien llama (limitadas a los proyectos a los que tiene acceso).
Auth: clave API o sesión.
Respuesta (200 OK): lista paginada. Cada elemento:
{ "id": 901, "uploaded_file": 4217, "language": "en", "duration_seconds": 1834.2, "word_count": 3120, "srt_path": "tenant/312/results/transcripts/interview.srt", "docx_path": "tenant/312/results/transcripts/interview.docx", "status": "completed", "error_reason": "", "asr_engine": "external", "created_at": "2026-05-17T14:05:33Z", "updated_at": "2026-05-17T14:08:47Z"}status aquí pasa por queued → processing → completed | failed. Cuando status es failed, error_reason contiene la causa en formato legible por máquina (audio_too_short, hallucinated_transcript, private_transcription_unavailable) o queda vacío para otros errores — consulta Códigos de estado y errores.
asr_engine registra qué servicio de reconocimiento de voz produjo la transcripción: external (el proveedor por defecto) o private (infraestructura operada por USpeech, usada cuando el proyecto requiere transcripción privada). También se devuelve en el recurso de archivo y permanece vacío hasta que la transcripción termina.
GET /api/transcripts/{id}/
Sección titulada «GET /api/transcripts/{id}/»Obtén una sola transcripción por ID. Misma estructura de campos que la respuesta de la lista.
curl https://app.uspeech.io/api/transcripts/901/ \ -H "Authorization: Api-Key $USPEECH_KEY"transcript = requests.get( f"{BASE_URL}/api/transcripts/901/", headers=HEADERS, timeout=30).json()const transcript = await fetch(`${BASE_URL}/api/transcripts/901/`, { headers: HEADERS,}).then((r) => r.json());Si la transcripción existe pero no es visible para quien llama, la respuesta es 404 Not Found.
GET /api/transcripts/{id}/download/
Sección titulada «GET /api/transcripts/{id}/download/»Devuelve una URL temporal para el artefacto de la transcripción (SRT o DOCX). La URL apunta a almacenamiento privado — úsala desde la misma máquina que recibió la respuesta.
Auth: clave API o sesión.
Parámetros de consulta:
| Parámetro | Obligatorio | Descripción |
|---|---|---|
type | no | srt (por defecto) o docx. |
Respuesta (200 OK):
{ "download_url": "https://…/interview.srt?X-Amz-Signature=…"}Si el artefacto solicitado no se ha generado (por ejemplo, la transcripción falló), la respuesta es 404 Not Found.
Ejemplo:
# Obtén la URL …url=$(curl -sS "https://app.uspeech.io/api/transcripts/901/download/?type=srt" \ -H "Authorization: Api-Key $USPEECH_KEY" | jq -r .download_url)
# … y descarga.curl -o interview.srt "$url"# Obtén la URL …url = requests.get( f"{BASE_URL}/api/transcripts/901/download/", headers=HEADERS, params={"type": "srt"}, timeout=30,).json()["download_url"]
# … y descarga. La URL viene firmada, así que no necesita cabecera de autenticación.with open("interview.srt", "wb") as fh: fh.write(requests.get(url, timeout=120).content)import { writeFile } from 'node:fs/promises';
// Obtén la URL …const { download_url: url } = await fetch( `${BASE_URL}/api/transcripts/901/download/?type=srt`, { headers: HEADERS },).then((r) => r.json());
// … y descarga. La URL viene firmada, así que no necesita cabecera de autenticación.const srt = await fetch(url).then((r) => r.arrayBuffer());await writeFile('interview.srt', Buffer.from(srt));Reintentar una transcripción fallida
Sección titulada «Reintentar una transcripción fallida»Si un archivo termina en failed, puedes reintentarlo a través de la API de Tasks.
- Encuentra la Task de transcripción correspondiente al archivo en
GET /api/tasks/?task_type=transcription(o sigue el enlace desde la aplicación web). - Llama a
POST /api/tasks/{task_id}/retry/para volver a encolarla.
No reintentes ante fallos terminales como audio_too_short — consulta Códigos de estado y errores para la lista completa de valores de error_reason. Sí conviene reintentar en el caso de private_transcription_unavailable y de fallos con error_reason vacío.
Ejemplo de principio a fin
Sección titulada «Ejemplo de principio a fin»# 1. Subirresp=$(curl -sS -X POST https://app.uspeech.io/api/files/ \ -H "Authorization: Api-Key $USPEECH_KEY" \ -F "project=312" -F "file_type=audio" \ -F "file=@./interview.wav")file_id=$(echo "$resp" | jq -r .id)
# 2. Pollingwhile :; do resp=$(curl -sS "https://app.uspeech.io/api/files/${file_id}/" \ -H "Authorization: Api-Key $USPEECH_KEY") status=$(echo "$resp" | jq -r .status) [ "$status" = "transcribed" ] && break [ "$status" = "failed" ] && { echo "transcripción fallida: $(echo "$resp" | jq -r '.error_reason // "unknown"')" exit 1 } sleep 10done
# 3. Encuentra la transcripción y descarga el SRTtranscript_id=$(curl -sS "https://app.uspeech.io/api/transcripts/?uploaded_file=${file_id}" \ -H "Authorization: Api-Key $USPEECH_KEY" | jq -r '.results[0].id')
url=$(curl -sS "https://app.uspeech.io/api/transcripts/${transcript_id}/download/?type=srt" \ -H "Authorization: Api-Key $USPEECH_KEY" | jq -r .download_url)
curl -o interview.srt "$url"import osimport time
import requests
BASE_URL = "https://app.uspeech.io"HEADERS = {"Authorization": f"Api-Key {os.environ['USPEECH_KEY']}"}
# 1. Subirwith open("interview.wav", "rb") as fh: response = requests.post( f"{BASE_URL}/api/files/", headers=HEADERS, data={"project": 312, "file_type": "audio"}, files={"file": ("interview.wav", fh, "audio/wav")}, timeout=300, )response.raise_for_status()file_id = response.json()["id"]
# 2. Pollingwhile True: file_data = requests.get( f"{BASE_URL}/api/files/{file_id}/", headers=HEADERS, timeout=30 ).json() if file_data["status"] == "transcribed": break if file_data["status"] == "failed": raise SystemExit( f"transcripción fallida: {file_data.get('error_reason') or 'unknown'}" ) time.sleep(10)
# 3. Encuentra la transcripción y descarga el SRTtranscripts = requests.get( f"{BASE_URL}/api/transcripts/", headers=HEADERS, params={"uploaded_file": file_id}, timeout=30,).json()transcript_id = transcripts["results"][0]["id"]
url = requests.get( f"{BASE_URL}/api/transcripts/{transcript_id}/download/", headers=HEADERS, params={"type": "srt"}, timeout=30,).json()["download_url"]
with open("interview.srt", "wb") as fh: fh.write(requests.get(url, timeout=120).content)import { openAsBlob } from 'node:fs';import { writeFile } from 'node:fs/promises';
const BASE_URL = 'https://app.uspeech.io';const HEADERS = { Authorization: `Api-Key ${process.env.USPEECH_KEY}` };const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// 1. Subirconst form = new FormData();form.set('project', '312');form.set('file_type', 'audio');form.set('file', await openAsBlob('./interview.wav'), 'interview.wav');
const upload = await fetch(`${BASE_URL}/api/files/`, { method: 'POST', headers: HEADERS, body: form,});if (!upload.ok) throw new Error(await upload.text());const fileId = (await upload.json()).id;
// 2. Pollingwhile (true) { const fileData = await fetch(`${BASE_URL}/api/files/${fileId}/`, { headers: HEADERS, }).then((r) => r.json()); if (fileData.status === 'transcribed') break; if (fileData.status === 'failed') { throw new Error(`transcripción fallida: ${fileData.error_reason || 'unknown'}`); } await sleep(10_000);}
// 3. Encuentra la transcripción y descarga el SRTconst transcripts = await fetch( `${BASE_URL}/api/transcripts/?uploaded_file=${fileId}`, { headers: HEADERS },).then((r) => r.json());const transcriptId = transcripts.results[0].id;
const { download_url: url } = await fetch( `${BASE_URL}/api/transcripts/${transcriptId}/download/?type=srt`, { headers: HEADERS },).then((r) => r.json());
await writeFile('interview.srt', Buffer.from(await fetch(url).then((r) => r.arrayBuffer())));