Guía de integración de Omnimed AI
Integrá el dictado de informes de Omnimed AI en tu RIS, HIS o visor.
1. Widget
Agregá un tag <script> a tu página. Se inyecta un botón flotante de grabación; el informe generado puede autoinsertarse en el editor activo.
Usá esta opción si: querés la integración más rápida y te sirve la interfaz de grabación por defecto.
2. API directa
Enviá un archivo de audio por POST a /api/omni-ai y manejá la respuesta vos mismo.
Usá esta opción si: necesitás control total de la interfaz de grabación o integrás desde un cliente nativo o de escritorio.
URL base: https://api.omniai.com.ar
v1: autenticación solo por medical_center_id. Para integraciones nuevas usá la v2, con Partner Credential y Widget con token.
Descripción general
Omni-AI convierte un dictado médico (audio) en un informe estructurado basado en plantilla, en una sola llamada. Hay dos formas de integrarse:
- Widget embebible — agregá un tag
<script>a tu página. El médico graba desde un botón flotante, el informe se devuelve y puede autoinsertarse en tu editor. - API directa — llamá a
POST https://api.omniai.com.ar/api/omni-aicon el archivo de audio y manejá la respuesta vos mismo.
Ambas vías usan el mismo endpoint omni-ai por detrás. Empezá por el Widget salvo que necesites control total de la interfaz de grabación.
Autenticación
Cada partner de integración recibe un medical_center_id (provisto por Omni). El ID del centro identifica las plantillas, el idioma y el estilo de informe usados para generar el resultado. Hoy no se requiere bearer token.
Escribí a [email protected] para solicitar un ID de centro.
Widget
Embeber
<script src="https://api.omniai.com.ar/static/widget/v1/omni-widget.min.js"
data-center-id="YOUR_CENTER_ID"
data-username="DOCTOR_USERNAME"
async></script>Se inyecta un botón de grabación flotante abajo a la derecha. El médico lo toca, dicta, frena, y el widget muestra el informe generado.
data-username es opcional. Es el usuario del médico en el centro (el mismo login que usa en la app de escritorio) y decide contra qué plantillas se matchea el dictado:
data-username | Plantillas que se matchean |
|---|---|
| ausente | Solo las plantillas generales del centro |
| un usuario del centro | Las generales más las asignadas a ese médico (directamente o por grupo) |
| un usuario que el centro no conoce | Solo las generales |
Autoinsertar el informe
El widget emite un evento omniai:report cuando el informe está listo, y de nuevo cada vez que el médico hace clic en "Insertar". event.detail trae html, text y el recordingId del dictado; si autoinsertás, reemplazá lo que ya insertaste para ese recordingId en lugar de agregar una segunda copia. Escuchalo y pegá el HTML (o el texto plano) en tu editor:
window.addEventListener('omniai:report', (event) => {
const { html, text } = event.detail;
// p. ej. pegar en el campo contenteditable / rich-text con foco
document.execCommand('insertHTML', false, html);
});O inicializalo manualmente con un callback:
OmniWidget.init({
centerId: 'YOUR_CENTER_ID',
username: 'DOCTOR_USERNAME', // opcional, mismas reglas que data-username
onReport: ({ html, text, recordingId }) => {
// Insertar en tu editor de informes
},
onAudio: (audio) => {
// Opcional: guardar tu propia copia de la grabación (ver abajo)
}
});Guardar una copia del audio
Si tu sistema tiene que conservar el dictado original (p. ej. retención legal en el PACS), el widget te entrega la grabación cruda en el momento en que el médico frena, antes de subirla, así tu copia existe aunque el procesamiento falle. Escuchá omniai:audio o pasá onAudio a init:
window.addEventListener('omniai:audio', (event) => {
const { blob, mimeType, kind, recordingId, parentRecordingId } = event.detail;
// p. ej. subir el blob a tu storage, con tu ID de estudio + recordingId
});| Campo | Descripción |
|---|---|
| blob | La grabación (Blob), normalmente WebM/Opus (Ogg o MP4 en navegadores sin WebM) |
| mimeType | Tipo MIME del blob |
| durationSeconds | Duración de la grabación |
| sizeBytes | Tamaño del blob |
| kind | "dictation" o "refinement" (una corrección por voz) |
| recordingId | ID único de esta grabación. El payload de omniai:report trae el recordingId del dictado, que vincula audio e informe |
| parentRecordingId | En un refinamiento, el recordingId del dictado que corrige; null en dictados |
| recordedAt | Timestamp ISO 8601 |
El blob no sale de tu página por este evento: guardarlo depende de tu código.
Ciclo de vida del widget
| Estado | Significado |
|---|---|
| idle | Listo para grabar |
| recording | Micrófono activo |
| paused | Grabación en pausa |
| processing | Audio subido, generando informe |
| result | Informe visible en el panel |
| error | Falló el procesamiento; el médico puede volver a grabar |
Requisitos del navegador
- HTTPS (requerido para acceder al micrófono)
- Permiso otorgado para
navigator.mediaDevices.getUserMedia - Duración máxima de grabación: 10 minutos (configurable)
API directa
Generar un informe
POST https://api.omniai.com.ar/api/omni-ai
Content-Type: multipart/form-dataParámetros
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| file | File | Condicional | Archivo de audio (wav, mp3, m4a, ogg, flac, webm). Requerido salvo que se envíe transcription_text |
| transcription_text | String | Condicional | Texto del dictado usado en lugar del audio (ver abajo). Requerido salvo que se envíe file |
| medical_center_id | String | Sí | Identificador del centro provisto por Omni |
| user_id | String | No | Identificador del médico/usuario para analítica |
| username | String | No | Usuario del médico en el centro. El matching usa las plantillas visibles para ese médico (generales + asignadas). Ausente o desconocido: solo generales. Se ignora si se envía template_html |
| language | String | No | Se acepta por compatibilidad y se ignora: el dictado y el informe usan el idioma del centro (medical_center_id) |
| metadata | String (JSON) | No | Metadata JSON libre, máx 10KB |
| dicom_metadata | String (JSON) | No | Estudio pedido, de la orden / headers DICOM; dispara los avisos de discrepancia (ver abajo) |
| template_html | String (HTML) | No | Plantilla preseleccionada: generar el informe contra exactamente esta plantilla (ver abajo) |
| template_name | String | No | Etiqueta de la plantilla preseleccionada; se devuelve como study_name / template_used |
| include_template | Boolean | No | Devolver el HTML de la plantilla que usó el servidor, para resaltar diferencias |
| include_rtf | Boolean | No | Devolver versión RTF de cada informe |
Nota: Sin template_html, la detección del estudio es automática a partir de la transcripción y la plantilla se matchea contra las plantillas guardadas del centro — no necesitás enviar el nombre del estudio.
Varios estudios en un dictado: reports siempre tiene una sola entrada. Si el médico dicta varios estudios, sus informes vienen concatenados en ese único report_html, study_name / template_used los listan separados por coma, y metadata.is_multi_study es true.
Enviar texto en lugar de audio (transcription_text)
Si tu sistema ya tiene el dictado como texto (tipeado, o transcripto de tu lado), envialo como transcription_text y omití el audio — se saltea el paso de transcripción y el texto entra al pipeline exactamente donde entraría la transcripción: la detección del estudio, el matching de plantillas y la generación se comportan igual. Se combina naturalmente con template_html (sin transcripción y sin matching — el camino más rápido y totalmente determinístico).
- El audio siempre gana. Si enviás ambos, se transcribe el audio y
transcription_textse ignora por completo. No hay fallback: si el audio falla al transcribirse (p. ej. micrófono muteado) la llamada da error aunque hayas enviado texto. - Un campo
fileen blanco o untranscription_texten blanco ("" o espacios) se trata como ausente. No enviar ninguno devuelve un 400. - Texto plano, máx 50KB.
- En la respuesta:
duration_secondses0.0,speaker_countes1,language_detectedrefleja tu parámetrolanguage, ymetadata.input_sourcees"text"(vs"audio"). - La facturación no cambia — un informe generado desde texto consume crédito exactamente igual que uno generado desde audio.
Enviar tu propia plantilla (template_html)
Cuando tu sistema sabe qué plantilla debe seguir el informe (por clínica, por centro derivador, por modalidad), enviala en la llamada:
- La detección del estudio y el matching de plantillas se saltean por completo — el informe sigue tu plantilla de forma determinística.
- El resultado es siempre un único informe, aunque el dictado mencione varios estudios.
- El HTML se sanitiza del lado del servidor antes de usarse; el tamaño máximo es 100KB. HTML demasiado grande, o markup que queda vacío tras la sanitización, devuelve un 400. Un campo en blanco ("" o espacios) se trata como ausente: la llamada cae al camino normal de matching.
- HTML es el formato recomendado: preserva negritas, títulos y estructura de bloques.
- En la respuesta:
template_match_confidencees1.0ymetadata.detection_sourcees"preselected_template". - Con
include_template=true, eltemplate_htmlde la respuesta es la plantilla sanitizada que el servidor usó realmente — hacé el diff contra esa, no contra tu original.
Respuesta exitosa
{
"success": true,
"message": "Successfully processed audio and generated 1 report(s)",
"data": {
"transcription": "...",
"language_detected": "spa",
"duration_seconds": 42.1,
"speaker_count": 1,
"reports": [
{
"study_name": "IRM de Rodilla",
"template_used": "IRM de Rodilla",
"template_match_confidence": 1.0,
"report_html": "<p>...</p>",
"report_rtf": null,
"transcription_segment": "...",
"template_html": null
}
],
"warnings": [],
"context_checks": {
"laterality": {"status": "ok"},
"modality": {"status": "skipped", "reason": "not_dictated"},
"region": {"status": "ok"}
},
"processing_time_ms": 7441,
"metadata": {
"study_id": 12345,
"detection_source": "preselected_template",
"input_source": "audio",
"is_multi_study": false,
"...": "..."
}
}
}Tip: Guardá data.metadata.study_id si planeás refinar este informe más adelante.
Avisos de discrepancia (lateralidad, modalidad, región)
Enviá el estudio pedido en dicom_metadata y la API lo compara con lo que dictó el médico. Una discrepancia vuelve en data.warnings; el informe se genera exactamente igual y nunca se modifica ni se bloquea por eso. Ninguno de los dos lados se asume correcto: la orden también puede estar mal.
{
"dicom_modality": "CT",
"dicom_bodypart": ["HAND"],
"dicom_laterality": "R",
"dicom_series_description": ["TC MANO DERECHA"]
}dicom_modality(requerido si se envíadicom_metadata): Modality DICOM (CT,MR,DX/CR,US,MG).dicom_bodypart: Body Part Examined DICOM, un valor (HAND,KNEE,SHOULDER,FOOT,BREAST, ...). La región se chequea en extremidades y mama; el resto todavía no.dicom_laterality:R,LoB(también se aceptaderecha/izquierda/bilateral). Si falta, el lado se lee dedicom_series_descriptioncuando lo dice en palabras (DER,IZQ,DERECHA,LEFT, ...).- El contexto libre, como la indicación clínica, va en
metadata: es contexto para el informe, no entrada de estos chequeos.
Cada aviso:
{
"code": "LATERALITY_MISMATCH",
"study_name": "custom",
"context_value": "R",
"dictated_value": "L",
"evidence": "quinto metacarpiano del pie izquierdo",
"message": "El estudio está indicado como derecho y se dictó izquierdo."
}code:LATERALITY_MISMATCH,MODALITY_MISMATCHoREGION_MISMATCH.context_value/dictated_value: lateralidadR/L; modalidadRX,TC,RM,ECO,MAMO; región una clave en español comomano,pie,rodilla,muneca.evidence: fragmento literal del dictado, cuando lo hay.message: en español, listo para mostrarle al radiólogo.
context_checks indica, por chequeo, si corrió: ok, mismatch, o skipped con un reason (no_context: no se envió; unmapped: un valor que todavía no chequeamos; not_dictated: el médico no lo dijo; bilateral_context: la orden es bilateral; llm_unavailable: el chequeo no llegó a terminar a tiempo). Un warnings vacío con chequeos skipped significa "no verificado", no "verificado OK".
Los chequeos están calibrados para evitar falsas alarmas: ante la duda no avisan. Mencionar el otro lado para comparar ("comparado con la rodilla izquierda") u otra modalidad como sugerencia ("se sugiere RM") no son discrepancias.
Ejemplos
cURL
curl -X POST https://api.omniai.com.ar/api/omni-ai \
-F "[email protected]" \
-F "medical_center_id=YOUR_CENTER_ID" \
-F "user_id=dr_martinez" \
-F "language=spa"cURL (plantilla preseleccionada)
curl -X POST https://api.omniai.com.ar/api/omni-ai \
-F "[email protected]" \
-F "medical_center_id=YOUR_CENTER_ID" \
-F "template_html=<h2>IRM de Rodilla</h2><p>...</p>" \
-F "template_name=IRM de Rodilla" \
-F "include_template=true"Python
import requests
url = "https://api.omniai.com.ar/api/omni-ai"
with open("audio.m4a", "rb") as f:
response = requests.post(
url,
files={"file": f},
data={
"medical_center_id": "YOUR_CENTER_ID",
"user_id": "dr_lopez",
"language": "spa",
},
)
response.raise_for_status()
result = response.json()
for report in result["data"]["reports"]:
print(report["study_name"], "->", report["report_html"])JavaScript (fetch)
async function processAudio(audioFile, centerId, userId) {
const form = new FormData();
form.append('file', audioFile);
form.append('medical_center_id', centerId);
form.append('user_id', userId);
form.append('language', 'spa');
const res = await fetch('https://api.omniai.com.ar/api/omni-ai', {
method: 'POST',
body: form,
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || `HTTP ${res.status}`);
}
const { data } = await res.json();
return data.reports[0].report_html;
}Refinar un informe
POST https://api.omniai.com.ar/api/omni-ai/refine
Content-Type: multipart/form-dataEl médico dicta una corrección sobre un informe ya generado ("cambiar menisco interno por menisco externo"). El servidor transcribe el audio, cambia solo el elemento apuntado — más la Conclusión del informe cuando el cambio la afecta — y devuelve el informe actualizado. El refinamiento nunca regenera el informe completo, y no se factura.
Parámetros
| Parámetro | Tipo | Requerido | Descripción |
|---|---|---|---|
| file | File | Condicional | Audio con la corrección dictada. Requerido salvo que se envíe transcription_text |
| transcription_text | String | Condicional | La corrección como texto en lugar de audio (máx 50KB, mismas reglas que en generación: el audio gana, blanco = ausente) |
| medical_center_id | String | Sí | Identificador del centro provisto por Omni |
| report_html | String (HTML) | Sí | El último informe generado, exactamente como lo devolvió la API (o como quedó tras el último refinamiento) |
| template_html | String (HTML) | No | La plantilla original del informe; se usa como referencia de estilo/formato de solo lectura (máx 100KB) |
| template_name | String | No | Nombre de la plantilla/tipo de estudio, habilita reglas por tipo de estudio |
| selected_html | String (HTML) | No | Elemento HTML exacto a refinar; omitilo para que el modelo ubique el objetivo desde el dictado |
| study_id | Integer | No | El metadata.study_id de la llamada de generación, para trazar el refinamiento |
| user_id | String | No | Identificador del médico/usuario para analítica |
| language | String | No | Se acepta por compatibilidad y se ignora: se usa el idioma del centro |
Respuesta exitosa
{
"success": true,
"message": "Successfully refined report",
"data": {
"transcription": "cambiar menisco interno por menisco externo",
"report_html": "<p>...informe completo con la corrección aplicada...</p>",
"patch": {
"refined_text": "<p>Menisco externo sin alteraciones.</p>",
"target_html": "<p>Menisco interno sin alteraciones.</p>",
"conclusion_text": null,
"conclusion_original_text": null
},
"processing_time_ms": 3120,
"metadata": {
"medical_center_id": "your-center",
"study_id": 12345,
"event_id": "678",
"model_used": "gemini-2.5-flash",
"...": "..."
}
}
}data.report_htmles el informe completo con la corrección ya aplicada — usalo directamente.data.patchdescribe las regiones exactas que cambiaron, si preferís empalmarlas en tu propio editor: reemplazá la primera ocurrencia detarget_htmlporrefined_text, y la deconclusion_original_textporconclusion_text(cuando no sea null).- Para encadenar refinamientos, enviá el
report_htmldevuelto como entrada de la siguiente llamada de refine.
Ejemplo
curl -X POST https://api.omniai.com.ar/api/omni-ai/refine \
-F "[email protected]" \
-F "medical_center_id=YOUR_CENTER_ID" \
-F "report_html=<p>...último informe generado...</p>" \
-F "template_html=<p>...plantilla original...</p>" \
-F "study_id=12345"Respuesta de error
Los errores devuelven un status no-2xx con:
{
"success": false,
"error": "Human-readable error message",
"error_code": 400
}error_code suele ser el status HTTP. Algunos errores traen un código más específico (p. ej. 1011: el centro no tiene plantillas; 1006: el modelo de informes no está disponible).
| Código | Causa | Solución |
|---|---|---|
| 400 | Falta la entrada (ni file ni transcription_text), formato de audio no soportado, metadata JSON malformada, o template_html demasiado grande | Enviá wav/mp3/m4a/ogg/flac/webm o transcription_text (máx 50KB); validá el JSON bajo 10KB; mantené template_html bajo 100KB |
| 403 | Centro deshabilitado / no provisionado | Contactá a Omni para habilitar tu medical_center_id |
| 422 | El centro no tiene plantillas cargadas (error_code 1011) | Contactá a Omni para cargar las plantillas de tu centro |
| 500 | Falló la transcripción o la generación del informe (p. ej. audio en silencio / micrófono muteado) | Revisá la grabación; reintentá con backoff; contactá a soporte si persiste |
| 503 | El modelo de generación de informes no está disponible momentáneamente (error_code 1006) | Reintentá con backoff |
Resaltar los cambios de la IA (diff en el cliente)
El Widget resalta lo que cambió la IA haciendo un diff entre la plantilla y el informe generado. Podés hacer lo mismo en tu interfaz con jsdiff (diff en npm) — sin soporte del servidor:
import { diffWords } from 'diff';
function highlightChanges(templateText, reportText) {
return diffWords(templateText, reportText)
.map((part) =>
part.added
? `<mark class="ai-change">${part.value}</mark>`
: part.removed
? ''
: part.value
)
.join('');
}- Línea base: pedí
include_template=trueen la generación y hacé el diff contra eltemplate_htmldevuelto. - Después de un refine, hacé el diff entre el
report_htmlanterior y el nuevo para resaltar solo la corrección. - Hacé el diff sobre el texto visible (p. ej. vía
DOMParser) en lugar de strings HTML crudos para resaltados más limpios.
Health check
Usá este endpoint para verificar conectividad desde tu backend:
curl https://api.omniai.com.ar/api/healthRespuesta:
{
"status": "healthy",
"services": {
"api": {
"service": "api",
"status": "healthy",
"available": true
}
}
}Soporte
Consultas y aprovisionamiento: [email protected]
