# Transcripción de audio en tickets

Convierte en texto el audio que graba el usuario, en el momento (hasta 10 MB) o en segundo plano, para dictar tickets por voz. Permiso support:tickets:ai.

Con estos endpoints conviertes en texto el audio que graba el usuario,
para que pueda dictar la descripción de su ticket o para alimentar las
[sugerencias con IA](https://os.utilia.ai/docs/developers/api/ticket-ai-suggestions).
Hay dos modos: **en el momento** (archivos de hasta 10 MB) y **en
segundo plano** (archivos más grandes).

Todas las peticiones llevan la cabecera `X-Api-Key`. Igual que las
sugerencias, exigen el aplicativo **Tickets de soporte** con tickets
propios (`support.own_tickets`) en el plan y el permiso
`support:tickets:ai` en la aplicación, que solo concede un Super
Administrador.

## Resumen de endpoints

| Método | Ruta | Descripción |
| - | - | - |
| `POST` | `/api/external/v1/tickets/ai-transcribe` | Transcribir en el momento (hasta 10 MB) |
| `POST` | `/api/external/v1/tickets/ai-transcribe-async` | Transcribir en segundo plano (archivos grandes) |
| `GET` | `/api/external/v1/tickets/ai-transcribe/:jobId/stream` | Resultado de la transcripción por SSE |
| `GET` | `/api/external/v1/tickets/ai-transcribe/:jobId` | Estado del trabajo de transcripción |

## Qué endpoint usar

**Dos formas de transcribir**

### En el momento (Hasta 10 MB)

`POST /ai-transcribe` responde con el texto en la misma llamada. Basta para casi todas las notas de voz grabadas en un navegador o en el móvil.

### En segundo plano (Más de 10 MB)

`POST /ai-transcribe-async` devuelve un `jobId`. El texto llega después por SSE (`GET .../:jobId/stream`) o consultando el estado (`GET .../:jobId`).

## Transcribir en el momento

**`POST /api/external/v1/tickets/ai-transcribe`**

Transcribe el audio y devuelve el texto en la misma respuesta. Archivos de hasta 10 MB. Permiso: support:tickets:ai.

### Cabeceras

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `X-Api-Key` | `string` | Sí | Clave de API externa (formato: utl_xxx) |
| `Content-Type` | `string` | Sí | application/json |
| `x-request-id` | `string` | No | Identificador único para seguir la petición |

### Cuerpo de la petición

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `audioBase64` | `string` | Sí | Audio codificado en base64 |
| `audioFilename` | `string` | Sí | Nombre del archivo con extensión, que indica el formato. Por ejemplo, grabacion.webm o nota-voz.mp3 |

### Respuesta correcta (201 Created)

| Nombre | Tipo | Descripción |
| - | - | - |
| `transcription` | `string` | Texto transcrito del audio |

El código es `201`, no `200`. Si tu cliente comprueba el estado, acepta cualquier `2xx`.

### Errores

| Código HTTP | Código de error | Descripción |
| - | - | - |
| 400 | `AI_AUDIO_TOO_LARGE` | El audio supera los 10 MB. Usa el modo en segundo plano |
| 400 | `VALIDATION_ERROR` | Faltan campos obligatorios o el formato del audio no es válido |
| 401 | -- | Falta la clave de API o no es válida |

### Ejemplos

#### curl

```bash
# Codificar el audio en base64 y enviarlo
AUDIO_BASE64=$(base64 -i grabacion.webm)

curl -X POST 'https://os.utilia.ai/api/external/v1/tickets/ai-transcribe' \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: utl_tu_clave_de_api' \
  -H 'x-request-id: req-transcribe-001' \
  -d "{
    \"audioBase64\": \"${AUDIO_BASE64}\",
    \"audioFilename\": \"grabacion.webm\"
  }"
```

#### fetch

```javascript
// Suponiendo que tiene un Blob de audio (por ejemplo, de MediaRecorder)
const audioBlob = /* ... su blob de audio ... */;

// Convertir a base64
const arrayBuffer = await audioBlob.arrayBuffer();
const base64 = btoa(
  new Uint8Array(arrayBuffer).reduce(
    (data, byte) => data + String.fromCharCode(byte),
    ''
  )
);

const response = await fetch(
  'https://os.utilia.ai/api/external/v1/tickets/ai-transcribe',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': 'utl_tu_clave_de_api',
      'x-request-id': 'req-transcribe-001',
    },
    body: JSON.stringify({
      audioBase64: base64,
      audioFilename: 'grabacion.webm',
    }),
  }
);

const { transcription } = await response.json();
console.log('Texto transcrito:', transcription);
```

#### axios

```javascript
import axios from 'axios';

// Suponiendo que tiene un Blob de audio
const audioBlob = /* ... su blob de audio ... */;

const arrayBuffer = await audioBlob.arrayBuffer();
const base64 = btoa(
  new Uint8Array(arrayBuffer).reduce(
    (data, byte) => data + String.fromCharCode(byte),
    ''
  )
);

const { data } = await axios.post(
  'https://os.utilia.ai/api/external/v1/tickets/ai-transcribe',
  {
    audioBase64: base64,
    audioFilename: 'grabacion.webm',
  },
  {
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': 'utl_tu_clave_de_api',
      'x-request-id': 'req-transcribe-001',
    },
  }
);

console.log('Texto transcrito:', data.transcription);
```

#### Python

```python
import base64
import requests

# Leer y codificar el archivo de audio
with open("grabacion.webm", "rb") as f:
    audio_base64 = base64.b64encode(f.read()).decode("utf-8")

response = requests.post(
    "https://os.utilia.ai/api/external/v1/tickets/ai-transcribe",
    headers={
        "Content-Type": "application/json",
        "X-Api-Key": "utl_tu_clave_de_api",
        "x-request-id": "req-transcribe-001",
    },
    json={
        "audioBase64": audio_base64,
        "audioFilename": "grabacion.webm",
    },
    timeout=60,
)

result = response.json()
print(f"Texto transcrito: {result['transcription']}")
```

Este modo espera a que termine la transcripción antes de responder: con archivos cerca de los 10 MB puede tardar varios segundos, así que muestra un indicador de carga en tu interfaz.

## Transcribir en segundo plano

**`POST /api/external/v1/tickets/ai-transcribe-async`**

Empieza una transcripción en segundo plano, para audios de cualquier tamaño. Permiso: support:tickets:ai.

### Cabeceras

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `X-Api-Key` | `string` | Sí | Clave de API externa (formato: utl_xxx) |
| `Content-Type` | `string` | Sí | application/json |
| `x-request-id` | `string` | No | Identificador único para seguir la petición |

### Cuerpo de la petición

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `audioBase64` | `string` | Sí | Audio codificado en base64 |
| `audioFilename` | `string` | Sí | Nombre del archivo con extensión (por ejemplo, reunion-larga.mp3) |

### Respuesta correcta (201 Created)

| Nombre | Tipo | Descripción |
| - | - | - |
| `jobId` | `string` | Identificador del trabajo de transcripción. Úsalo para consultar el resultado |

Aunque el trabajo queda en cola, el código es `201`, no `202`. Acepta cualquier `2xx` y quédate con el `jobId` del cuerpo.

### Ejemplos

#### curl

```bash
AUDIO_BASE64=$(base64 -i reunion-larga.mp3)

curl -X POST 'https://os.utilia.ai/api/external/v1/tickets/ai-transcribe-async' \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: utl_tu_clave_de_api' \
  -H 'x-request-id: req-transcribe-async-001' \
  -d "{
    \"audioBase64\": \"${AUDIO_BASE64}\",
    \"audioFilename\": \"reunion-larga.mp3\"
  }"
```

#### fetch

```javascript
const audioBlob = /* ... su blob de audio grande ... */;

const arrayBuffer = await audioBlob.arrayBuffer();
const base64 = btoa(
  new Uint8Array(arrayBuffer).reduce(
    (data, byte) => data + String.fromCharCode(byte),
    ''
  )
);

const response = await fetch(
  'https://os.utilia.ai/api/external/v1/tickets/ai-transcribe-async',
  {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': 'utl_tu_clave_de_api',
      'x-request-id': 'req-transcribe-async-001',
    },
    body: JSON.stringify({
      audioBase64: base64,
      audioFilename: 'reunion-larga.mp3',
    }),
  }
);

const { jobId } = await response.json();
console.log(`Job de transcripción creado: ${jobId}`);
// Ahora conecte al stream SSE o haga polling
```

#### axios

```javascript
import axios from 'axios';

const audioBlob = /* ... su blob de audio grande ... */;

const arrayBuffer = await audioBlob.arrayBuffer();
const base64 = btoa(
  new Uint8Array(arrayBuffer).reduce(
    (data, byte) => data + String.fromCharCode(byte),
    ''
  )
);

const { data } = await axios.post(
  'https://os.utilia.ai/api/external/v1/tickets/ai-transcribe-async',
  {
    audioBase64: base64,
    audioFilename: 'reunion-larga.mp3',
  },
  {
    headers: {
      'Content-Type': 'application/json',
      'X-Api-Key': 'utl_tu_clave_de_api',
      'x-request-id': 'req-transcribe-async-001',
    },
  }
);

console.log(`Job de transcripción creado: ${data.jobId}`);
```

#### Python

```python
import base64
import requests

with open("reunion-larga.mp3", "rb") as f:
    audio_base64 = base64.b64encode(f.read()).decode("utf-8")

response = requests.post(
    "https://os.utilia.ai/api/external/v1/tickets/ai-transcribe-async",
    headers={
        "Content-Type": "application/json",
        "X-Api-Key": "utl_tu_clave_de_api",
        "x-request-id": "req-transcribe-async-001",
    },
    json={
        "audioBase64": audio_base64,
        "audioFilename": "reunion-larga.mp3",
    },
    timeout=60,
)

result = response.json()
print(f"Job de transcripción creado: {result['jobId']}")
```

## Recibir el resultado por SSE

**`GET /api/external/v1/tickets/ai-transcribe/:jobId/stream`**

Conexión SSE que envía el progreso y el texto de una transcripción en segundo plano. Permiso: support:tickets:ai.

Parámetro de ruta: `jobId`.

### Parámetros de ruta

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `jobId` | `string` | Sí | Identificador del trabajo que devolvió POST /ai-transcribe-async |

### Cabeceras

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `X-Api-Key` | `string` | No | Clave de API externa (formato: utl_xxx). Obligatoria salvo que abras la conexión con un pase en el parámetro `?streamToken=` (route ai-transcribe, mismo jobId) o, mientras se admita, con la clave en `?apiKey=`. Una clave en la dirección queda en registros y servidores intermedios: usa el pase o la cabecera. |
| `Accept` | `string` | No | Recomendado: text/event-stream. Imprescindible si te autenticas con `?streamToken=` o `?apiKey=` en la dirección. |

### Formato de la respuesta

El servidor responde con `Content-Type: text/event-stream` y emite bloques separados por `\n\n`. **Todos los bloques se emiten como un único tipo de mensaje SSE (sin línea `event:`).** El tipo lógico viene dentro del JSON, en el campo `event`:

```text
data: {"event":"progress","progress":30}

data: {"event":"completed","result":{"transcription":"Hola, quiero reportar un problema con la exportación de informes..."}}

```

**Formas que no funcionan** (porque enlazan por el tipo SSE, que aquí siempre es `message`):

```javascript
es.addEventListener('completed', ...); // NUNCA se dispara
```

```python
if event.event == 'completed':  # NUNCA se cumple con sseclient
    ...
```

Lo correcto es usar el manejador genérico (`onmessage` o `event.event == 'message'`) y leer el campo `event` del JSON.

### Tipos de eventos (campo `event` dentro del JSON)

| `event` | Descripción | Resto del JSON |
| - | - | - |
| `progress` | Progreso actual | `progress: number (0-100)` |
| `completed` | La transcripción ha terminado | `result: { transcription: string }` |
| `failed` | La transcripción ha fallado | `error: string` |
| `heartbeat` | Señal para mantener viva la conexión, cada 30 segundos | (sin más campos) |

### Ejemplos

#### curl

```bash
curl -N -X GET 'https://os.utilia.ai/api/external/v1/tickets/ai-transcribe/job_tr_abc123/stream' \
  -H 'X-Api-Key: utl_tu_clave_de_api' \
  -H 'Accept: text/event-stream' \
  -H 'x-request-id: req-transcribe-stream-001'

# Salida real (sin líneas `event:`, el tipo va dentro del JSON):
# data: {"event":"progress","progress":30}
#
# data: {"event":"progress","progress":80}
#
# data: {"event":"completed","result":{"transcription":"Hola, quiero reportar un problema..."}}
```

#### fetch + ReadableStream

```javascript
// fetch con ReadableStream permite enviar el header X-Api-Key y leer los
// chunks SSE directamente. Patrón recomendado desde un backend o cuando
// no se quiere depender del EventSource nativo del navegador.

const jobId = 'job_tr_abc123';

const response = await fetch(
  `https://os.utilia.ai/api/external/v1/tickets/ai-transcribe/${jobId}/stream`,
  {
    method: 'GET',
    headers: {
      'X-Api-Key': 'utl_tu_clave_de_api',
      Accept: 'text/event-stream',
      'x-request-id': 'req-transcribe-stream-001',
    },
  },
);

if (!response.ok || !response.body) {
  throw new Error(`SSE no abierto: HTTP ${response.status}`);
}

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });

  let sep = buffer.indexOf('\n\n');
  while (sep !== -1) {
    const bloque = buffer.slice(0, sep);
    buffer = buffer.slice(sep + 2);
    sep = buffer.indexOf('\n\n');

    const dataLines = bloque
      .split('\n')
      .filter((l) => l.startsWith('data:'))
      .map((l) => l.slice(5).trimStart());
    if (dataLines.length === 0) continue;

    const data = JSON.parse(dataLines.join('\n'));
    if (data.event === 'progress') {
      console.log(`Transcribiendo: ${data.progress}%`);
    } else if (data.event === 'completed') {
      console.log('Texto:', data.result.transcription);
      reader.cancel();
      break;
    } else if (data.event === 'failed') {
      console.error('Error:', data.error);
      reader.cancel();
      break;
    }
  }
}
```

#### EventSource (navegador)

```javascript
// EventSource nativo NO permite headers, por lo que no se puede pasar
// X-Api-Key. Opciones: proxy desde tu backend, o un polyfill compatible.
// Importante: el servidor emite todos los eventos como `message`; no uses
// addEventListener('completed', ...).

import { EventSourcePolyfill } from 'event-source-polyfill';

const jobId = 'job_tr_abc123';

const es = new EventSourcePolyfill(
  `https://os.utilia.ai/api/external/v1/tickets/ai-transcribe/${jobId}/stream`,
  { headers: { 'X-Api-Key': 'utl_tu_clave_de_api' } },
);

es.onmessage = (msg) => {
  const data = JSON.parse(msg.data);
  switch (data.event) {
    case 'progress':
      console.log(`Transcribiendo: ${data.progress}%`);
      break;
    case 'completed':
      console.log('Texto:', data.result.transcription);
      es.close();
      break;
    case 'failed':
      console.error('Error:', data.error);
      es.close();
      break;
  }
};
```

#### Python (httpx)

```python
# NO uses sseclient con `event.event == "..."`: el servidor emite todos
# los eventos como `message`. Lee el campo `event` dentro del JSON.

import json
import httpx

JOB_ID = "job_tr_abc123"
URL = f"https://os.utilia.ai/api/external/v1/tickets/ai-transcribe/{JOB_ID}/stream"


async def transcribir_via_sse() -> str | None:
    headers = {
        "X-Api-Key": "utl_tu_clave_de_api",
        "Accept": "text/event-stream",
    }
    async with httpx.AsyncClient(timeout=None) as client:
        async with client.stream("GET", URL, headers=headers) as r:
            if r.status_code != 200:
                raise RuntimeError(f"SSE no abierto: HTTP {r.status_code}")

            buffer = ""
            async for chunk in r.aiter_text():
                buffer += chunk
                while "\n\n" in buffer:
                    bloque, buffer = buffer.split("\n\n", 1)
                    data_lines = [
                        ln[5:].lstrip() for ln in bloque.split("\n") if ln.startswith("data:")
                    ]
                    if not data_lines:
                        continue
                    data = json.loads("\n".join(data_lines))

                    if data["event"] == "progress":
                        print(f"Transcribiendo: {data.get('progress', 0)}%")
                    elif data["event"] == "completed":
                        return data["result"]["transcription"]
                    elif data["event"] == "failed":
                        raise RuntimeError(data.get("error", "Error desconocido"))
    return None
```

## Consultar el estado de la transcripción

**`GET /api/external/v1/tickets/ai-transcribe/:jobId`**

Estado de una transcripción en segundo plano, como alternativa al SSE. Permiso: support:tickets:ai.

Parámetro de ruta: `jobId`.

### Parámetros de ruta

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `jobId` | `string` | Sí | Identificador del trabajo que devolvió POST /ai-transcribe-async |

### Respuesta correcta (200 OK)

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `status` | `string` | Sí | Estado del trabajo: pending, processing, completed, failed o not_found |
| `progress` | `number` | No | Porcentaje de progreso (0-100). Solo presente en estados pending y processing |
| `transcription` | `string` | No | Texto transcrito. Solo presente cuando status es completed |
| `error` | `string` | No | Mensaje de error. Solo presente cuando status es failed |

### Ejemplos

#### curl

```bash
curl -X GET 'https://os.utilia.ai/api/external/v1/tickets/ai-transcribe/job_tr_abc123' \
  -H 'X-Api-Key: utl_tu_clave_de_api' \
  -H 'x-request-id: req-transcribe-poll-001'

# Respuesta cuando está en progreso:
# {
#   "status": "processing",
#   "progress": 45
# }

# Respuesta cuando ha completado:
# {
#   "status": "completed",
#   "transcription": "Hola, quiero reportar un problema con la exportación de informes..."
# }
```

#### fetch

```javascript
const jobId = 'job_tr_abc123';

async function pollTranscription(jobId) {
  while (true) {
    const response = await fetch(
      `https://os.utilia.ai/api/external/v1/tickets/ai-transcribe/${jobId}`,
      {
        headers: {
          'X-Api-Key': 'utl_tu_clave_de_api',
          'x-request-id': 'req-transcribe-poll-001',
        },
      }
    );

    const job = await response.json();

    if (job.status === 'completed') {
      console.log('Texto transcrito:', job.transcription);
      return job.transcription;
    }

    if (job.status === 'failed') {
      throw new Error(`Transcripción fallida: ${job.error}`);
    }

    console.log(`Progreso: ${job.progress}%`);
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
}

const transcription = await pollTranscription(jobId);
```

#### axios

```javascript
import axios from 'axios';

const jobId = 'job_tr_abc123';

async function pollTranscription(jobId) {
  while (true) {
    const { data: job } = await axios.get(
      `https://os.utilia.ai/api/external/v1/tickets/ai-transcribe/${jobId}`,
      {
        headers: {
          'X-Api-Key': 'utl_tu_clave_de_api',
          'x-request-id': 'req-transcribe-poll-001',
        },
      }
    );

    if (job.status === 'completed') {
      console.log('Texto transcrito:', job.transcription);
      return job.transcription;
    }

    if (job.status === 'failed') {
      throw new Error(`Transcripción fallida: ${job.error}`);
    }

    console.log(`Progreso: ${job.progress}%`);
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
}

const transcription = await pollTranscription(jobId);
```

#### Python

```python
import time
import requests

job_id = "job_tr_abc123"


def poll_transcription(job_id: str) -> str:
    while True:
        response = requests.get(
            f"https://os.utilia.ai/api/external/v1/tickets/ai-transcribe/{job_id}",
            headers={
                "X-Api-Key": "utl_tu_clave_de_api",
                "x-request-id": "req-transcribe-poll-001",
            },
            timeout=30,
        )

        job = response.json()

        if job["status"] == "completed":
            print(f"Texto transcrito: {job['transcription']}")
            return job["transcription"]

        if job["status"] == "failed":
            raise Exception(f"Transcripción fallida: {job['error']}")

        print(f"Progreso: {job.get('progress', 0)}%")
        time.sleep(1)


transcription = poll_transcription(job_id)
```

## Ejemplo completo: transcripción en segundo plano con SSE

### fetch

```javascript
const API_BASE = 'https://os.utilia.ai/api/external/v1/tickets';
const headers = {
  'Content-Type': 'application/json',
  'X-Api-Key': 'utl_tu_clave_de_api',
};

// 1. Obtener el audio (ejemplo: desde un input file)
const fileInput = document.querySelector('input[type="file"]');
const audioFile = fileInput.files[0];
const arrayBuffer = await audioFile.arrayBuffer();
const base64 = btoa(
  new Uint8Array(arrayBuffer).reduce(
    (data, byte) => data + String.fromCharCode(byte),
    ''
  )
);

// 2. Decidir síncrono o asíncrono según el tamaño
const MAX_SYNC_SIZE = 10 * 1024 * 1024; // 10 MB

if (audioFile.size <= MAX_SYNC_SIZE) {
  // Transcripción síncrona
  const response = await fetch(`${API_BASE}/ai-transcribe`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      audioBase64: base64,
      audioFilename: audioFile.name,
    }),
  });
  const { transcription } = await response.json();
  console.log('Transcripción (síncrona):', transcription);
} else {
  // Transcripción asíncrona con streaming (fetch + ReadableStream)
  const enqueue = await fetch(`${API_BASE}/ai-transcribe-async`, {
    method: 'POST',
    headers,
    body: JSON.stringify({
      audioBase64: base64,
      audioFilename: audioFile.name,
    }),
  });
  const { jobId } = await enqueue.json();

  // Importante: el SSE emite TODOS los eventos como `message`. Lee el
  // campo `event` del JSON; no uses addEventListener('completed', ...).
  const stream = await fetch(`${API_BASE}/ai-transcribe/${jobId}/stream`, {
    method: 'GET',
    headers: {
      'X-Api-Key': 'utl_tu_clave_de_api',
      Accept: 'text/event-stream',
    },
  });

  const reader = stream.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  outer: while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });
    let sep = buffer.indexOf('\n\n');
    while (sep !== -1) {
      const bloque = buffer.slice(0, sep);
      buffer = buffer.slice(sep + 2);
      sep = buffer.indexOf('\n\n');
      const dataLines = bloque
        .split('\n')
        .filter((l) => l.startsWith('data:'))
        .map((l) => l.slice(5).trimStart());
      if (dataLines.length === 0) continue;
      const data = JSON.parse(dataLines.join('\n'));
      if (data.event === 'completed') {
        console.log('Transcripción (asíncrona):', data.result.transcription);
        reader.cancel();
        break outer;
      }
      if (data.event === 'failed') {
        console.error('Error:', data.error);
        reader.cancel();
        break outer;
      }
    }
  }
}
```

### Python

```python
import base64
import json
import os

import requests

API_BASE = "https://os.utilia.ai/api/external/v1/tickets"
HEADERS = {
    "Content-Type": "application/json",
    "X-Api-Key": "utl_tu_clave_de_api",
}

audio_path = "grabacion.mp3"
MAX_SYNC_SIZE = 10 * 1024 * 1024  # 10 MB

with open(audio_path, "rb") as f:
    audio_bytes = f.read()
    audio_base64 = base64.b64encode(audio_bytes).decode("utf-8")

file_size = os.path.getsize(audio_path)

if file_size <= MAX_SYNC_SIZE:
    # Transcripción síncrona
    response = requests.post(
        f"{API_BASE}/ai-transcribe",
        headers=HEADERS,
        json={
            "audioBase64": audio_base64,
            "audioFilename": os.path.basename(audio_path),
        },
        timeout=60,
    )
    print(f"Transcripción (síncrona): {response.json()['transcription']}")
else:
    # Transcripción asíncrona con streaming
    response = requests.post(
        f"{API_BASE}/ai-transcribe-async",
        headers=HEADERS,
        json={
            "audioBase64": audio_base64,
            "audioFilename": os.path.basename(audio_path),
        },
        timeout=60,
    )
    job_id = response.json()["jobId"]

    # Importante: el SSE emite TODOS los eventos como `message`. NO uses
    # sseclient con `event.event == "completed"`. Lee el campo `event`
    # dentro del JSON.
    stream_response = requests.get(
        f"{API_BASE}/ai-transcribe/{job_id}/stream",
        headers={
            "X-Api-Key": "utl_tu_clave_de_api",
            "Accept": "text/event-stream",
        },
        stream=True,
    )

    buffer = ""
    for chunk in stream_response.iter_content(chunk_size=None, decode_unicode=True):
        if not chunk:
            continue
        buffer += chunk
        while "\n\n" in buffer:
            bloque, buffer = buffer.split("\n\n", 1)
            data_lines = [
                ln[5:].lstrip() for ln in bloque.split("\n") if ln.startswith("data:")
            ]
            if not data_lines:
                continue
            data = json.loads("\n".join(data_lines))
            if data["event"] == "completed":
                print(f"Transcripción (asíncrona): {data['result']['transcription']}")
                stream_response.close()
                break
            if data["event"] == "failed":
                print(f"Error: {data['error']}")
                stream_response.close()
                break
```

La extensión de `audioFilename` es la que indica el formato del audio, así que ponla siempre y que sea la correcta. Las grabaciones de navegador (WebM) y los formatos habituales como MP4 o MP3 se admiten.

## Siguiente paso

Con el texto transcrito, pide las [sugerencias con IA](https://os.utilia.ai/docs/developers/api/ticket-ai-suggestions)
para rellenar el formulario o crea el ticket directamente con los
[endpoints de tickets](https://os.utilia.ai/docs/developers/api/tickets).
