{
  "openapi": "3.0.1",
  "info": {
    "title": "KnowledgeBuilder — repositorio de audio de reuniones",
    "description": "Repositorio de audio de reuniones: ingesta desde los dispositivos de captura,\nalmacenamiento y transcripcion con un servicio Whisper propio.\n\n## Identificadores\n\nEl `id` de una grabacion es **su nombre de fichero sin extension**, con el\nformato `<timestamp UTC>__<device_name>__<device_id>`, por ejemplo\n`2026-09-07T16-00-42Z__mau__stick-d4eb00`. Como empieza por la fecha, ordenar\npor id es ordenar cronologicamente.\n\n## Una grabacion puede tener varias transcripciones\n\nY el numero **es una identidad, no un rango**: la 1 no es mejor que la 0 por\nser posterior. Conviven dos cosas distintas:\n\n- **pasadas de Whisper**, que son *alternativas* entre si (mismo audio, otros\n  parametros);\n- **correcciones humanas**, que son una *progresion* (cada una parte de otra,\n  via `based_on`).\n\nCual vale la dice `current`, que se calcula por precedencia: una promocion\nexplicita, si no la ultima correccion humana, si no la pasada del mejor modelo\ndel ranking. `current_reason` dice cual de los tres motivos aplico, y por tanto\n**si el texto lo ha validado una persona o es solo la mejor apuesta de la\nmaquina**.\n\n**Nada se sobrescribe nunca.** Volver a transcribir añade una alternativa.\n\n## Por donde empezar\n\n`GET /api/v1/capabilities` devuelve los modelos con su velocidad y calidad\nmedidas sobre audio real de este proyecto, los valores por omision, los limites\ny la regla de precedencia completa. Leelo antes de elegir un modelo.\n\n## Autenticacion\n\n`Authorization: Bearer <token>` en todo `/api/v1`. El token del dispositivo de\ncaptura **no vale aqui**: viaja dentro del HELLO del WebSocket de ingesta y es\notro modelo de autenticacion distinto sobre el mismo dominio.",
    "version": "1.0.0"
  },
  "servers": [
    {
      "url": "https://meetingrepo.sandbox-one.com/"
    }
  ],
  "paths": {
    "/health": {
      "get": {
        "tags": [
          "KnowledgeBuilder.Api"
        ],
        "summary": "Salud del backend y del servicio Whisper. Sin autenticacion.",
        "description": "El bloque `whisper` es la respuesta de `/v1/status` del servicio: modelo cargado, fase en curso, cola y ultimo error. Si `phase` es `loading_model`, el servicio esta descargando pesos y puede tardar minutos: la primera peticion de un modelo nuevo es lenta.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnonymousTypeOfstringAndstringAndintAndintAndJsonNode"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/audios/list": {
      "get": {
        "tags": [
          "audios"
        ],
        "summary": "Lista las grabaciones, de mas reciente a mas antigua.",
        "description": "El `id` de una grabacion es su nombre de fichero sin extension: `<timestamp UTC>__<device_name>__<device_id>`. Como empieza por la fecha, ordenar por id es ordenar por fecha. Cada fila incluye `current`, la transcripcion vigente, y `current_reason`, por que lo es.",
        "parameters": [
          {
            "name": "device",
            "in": "query",
            "description": "Filtra por device_id, p.ej. stick-d4eb00.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "description": "Solo grabaciones iniciadas en o despues de esta fecha (UTC).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "description": "Solo grabaciones iniciadas en o antes de esta fecha (UTC).",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "description": "Maximo de filas, 1-1000. Por omision 100.",
            "schema": {
              "type": "integer",
              "format": "int32",
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/api/v1/audios/info/{id}": {
      "get": {
        "tags": [
          "audios"
        ],
        "summary": "Todo lo de una grabacion en una sola llamada.",
        "description": "Devuelve el `.json` de sesion completo (metadatos de captura, huecos, origenes y la procedencia de cada transcripcion), la lista de transcripciones que hay en disco, cual es la vigente y por que, y el estado del ultimo trabajo de transcripcion.\n\nEl estado del trabajo esta aqui porque encolar una transcripcion devuelve 202: este es el endpoint que se consulta para saber si ya termino. Con 90 min de audio son unos 5 minutos de espera.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/api/v1/audios/wav/{id}": {
      "get": {
        "tags": [
          "audios"
        ],
        "summary": "Descarga el WAV. Admite Range.",
        "description": "PCM 16 kHz / 16 bit / mono. Admite `Range`, que es lo que necesita el elemento `<audio>` del navegador para poder buscar dentro del fichero.\n\nUna grabacion EN CURSO no aparece por aqui: el `.wav` no existe hasta que se cierra, para no servir un fichero con la cabecera a medias.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/api/v1/transcripts/{id}": {
      "get": {
        "tags": [
          "transcripts"
        ],
        "summary": "El texto de la transcripcion VIGENTE.",
        "description": "Esto es lo que debe consumir un cliente que solo quiere \"el texto de esta reunion\". Devuelve texto plano UTF-8.\n\nLas cabeceras de respuesta `X-Transcript-Version` y `X-Transcript-Reason` dicen cual devolvio y por que, para no tener que hacer una segunda llamada. Si `X-Transcript-Reason` es `best_model`, el texto NO lo ha validado ninguna persona: es la mejor apuesta de la maquina.\n\n## Como se elige la transcripcion vigente\n\nUna grabacion puede tener VARIAS transcripciones. El numero es una IDENTIDAD, no un rango: la 1 no es mejor que la 0 por ser posterior. Puede haber varias pasadas de Whisper con parametros distintos (que son ALTERNATIVAS entre si) y correcciones manuales (que son una PROGRESION, cada una partiendo de otra via `based_on`).\n\nCual es la vigente se decide por precedencia, y gana la primera que aplique:\n\n1. `promoted` — alguien la eligio con `POST /transcripts/promote/{id}/{n}`.\n2. `manual` — la ultima correccion humana. Un humano gana siempre a la maquina.\n3. `best_model` — entre las pasadas de Whisper, la del mejor modelo segun el ranking configurado (ver `GET /api/v1/capabilities`). Empate: mayor `beam_size`, luego la mas reciente.\n4. `only_file` — hay ficheros pero el json no los describe.\n\nPor ranking y no por \"la mas reciente\" a proposito: si fuera por recencia, probar un modelo malo al final dejaria todo el sistema leyendo la peor transcripcion sin que nadie se entere.\n\nNada se sobrescribe nunca. Volver a transcribir con otros parametros añade una alternativa; no destruye la anterior.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/api/v1/transcripts/{id}/{version}": {
      "get": {
        "tags": [
          "transcripts"
        ],
        "summary": "El texto de una transcripcion concreta, por numero.",
        "description": "Para comparar alternativas: pide la 0 y la 1 y mira cual es mejor. Los numeros que existen y con que parametros salio cada uno estan en `GET /api/v1/audios/info/{id}`.\n\n## Como se elige la transcripcion vigente\n\nUna grabacion puede tener VARIAS transcripciones. El numero es una IDENTIDAD, no un rango: la 1 no es mejor que la 0 por ser posterior. Puede haber varias pasadas de Whisper con parametros distintos (que son ALTERNATIVAS entre si) y correcciones manuales (que son una PROGRESION, cada una partiendo de otra via `based_on`).\n\nCual es la vigente se decide por precedencia, y gana la primera que aplique:\n\n1. `promoted` — alguien la eligio con `POST /transcripts/promote/{id}/{n}`.\n2. `manual` — la ultima correccion humana. Un humano gana siempre a la maquina.\n3. `best_model` — entre las pasadas de Whisper, la del mejor modelo segun el ranking configurado (ver `GET /api/v1/capabilities`). Empate: mayor `beam_size`, luego la mas reciente.\n4. `only_file` — hay ficheros pero el json no los describe.\n\nPor ranking y no por \"la mas reciente\" a proposito: si fuera por recencia, probar un modelo malo al final dejaria todo el sistema leyendo la peor transcripcion sin que nadie se entere.\n\nNada se sobrescribe nunca. Volver a transcribir con otros parametros añade una alternativa; no destruye la anterior.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "version",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/api/v1/transcripts/whisper/{id}": {
      "post": {
        "tags": [
          "transcripts"
        ],
        "summary": "Encola una pasada de Whisper. Devuelve 202.",
        "description": "Crea una transcripcion NUEVA; no sobrescribe ninguna. Se consulta el progreso en `GET /api/v1/audios/info/{id}`.\n\n**409 si ya existe una pasada con los mismos `model`, `beam_size` y `vad_filter`**: daria exactamente el mismo texto. Cambia cualquiera de los tres y se permite sin mas. `force=true` la rehace igualmente. Pasar `prompt` u `hotwords` cuenta como dirigir el resultado a mano, asi que tampoco aplica el 409.\n\nUna pasada de Whisper NO se convierte en la vigente si ya hay una correccion manual o una promocion: ver la precedencia mas abajo.\n\n**Coste de cambiar de modelo**: el servicio guarda un solo modelo en memoria y lo libera a los 5 minutos. La primera peticion de un modelo que no tenga descargado tarda MINUTOS (medido: 68 s con `small`, 142 s con `medium`); ya en disco, cargarlo cuesta ~1 s. Probar cuatro modelos seguidos hace que se descargue y recargue en cada uno.\n\n## Como se elige la transcripcion vigente\n\nUna grabacion puede tener VARIAS transcripciones. El numero es una IDENTIDAD, no un rango: la 1 no es mejor que la 0 por ser posterior. Puede haber varias pasadas de Whisper con parametros distintos (que son ALTERNATIVAS entre si) y correcciones manuales (que son una PROGRESION, cada una partiendo de otra via `based_on`).\n\nCual es la vigente se decide por precedencia, y gana la primera que aplique:\n\n1. `promoted` — alguien la eligio con `POST /transcripts/promote/{id}/{n}`.\n2. `manual` — la ultima correccion humana. Un humano gana siempre a la maquina.\n3. `best_model` — entre las pasadas de Whisper, la del mejor modelo segun el ranking configurado (ver `GET /api/v1/capabilities`). Empate: mayor `beam_size`, luego la mas reciente.\n4. `only_file` — hay ficheros pero el json no los describe.\n\nPor ranking y no por \"la mas reciente\" a proposito: si fuera por recencia, probar un modelo malo al final dejaria todo el sistema leyendo la peor transcripcion sin que nadie se entere.\n\nNada se sobrescribe nunca. Volver a transcribir con otros parametros añade una alternativa; no destruye la anterior.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "model",
            "in": "query",
            "description": "base | small | medium | large-v3-turbo | large-v3. Por omision el de configuracion (large-v3-turbo). base, small y medium fallan nombres propios sobre audio de mesa.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "beam_size",
            "in": "query",
            "description": "Candidatos de beam search, 1-10. Ajuste fino: al lado del modelo es ruido. Por omision 5.",
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "vad_filter",
            "in": "query",
            "description": "Filtra silencios con Silero VAD y reduce alucinaciones en pausas largas. Por omision true.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "prompt",
            "in": "query",
            "description": "Contexto libre para orientar vocabulario y estilo, max 2000 caracteres. Pasarlo salta el guardia de duplicados.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "hotwords",
            "in": "query",
            "description": "Palabras separadas por coma, max 4000 caracteres, solo para esta pasada. Se combinan con la lista global. Pasarlo salta el guardia de duplicados.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "force",
            "in": "query",
            "description": "Rehace la pasada aunque ya exista una identica.",
            "schema": {
              "type": "boolean",
              "default": false
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/api/v1/transcripts/manual/{id}": {
      "post": {
        "tags": [
          "transcripts"
        ],
        "summary": "Guarda una correccion humana como transcripcion nueva.",
        "description": "El servidor asigna el numero siguiente y **la correccion pasa a ser la vigente automaticamente**: un humano gana a la maquina.\n\n`based_on` es de que transcripcion partes. Ponlo: es lo que permite diffear la correccion contra su origen y sacar de ahi vocabulario para las hotwords, que es la unica forma realista de que el sistema aprenda de sus errores (afinar el modelo no esta al alcance de este proyecto).\n\n## Como se elige la transcripcion vigente\n\nUna grabacion puede tener VARIAS transcripciones. El numero es una IDENTIDAD, no un rango: la 1 no es mejor que la 0 por ser posterior. Puede haber varias pasadas de Whisper con parametros distintos (que son ALTERNATIVAS entre si) y correcciones manuales (que son una PROGRESION, cada una partiendo de otra via `based_on`).\n\nCual es la vigente se decide por precedencia, y gana la primera que aplique:\n\n1. `promoted` — alguien la eligio con `POST /transcripts/promote/{id}/{n}`.\n2. `manual` — la ultima correccion humana. Un humano gana siempre a la maquina.\n3. `best_model` — entre las pasadas de Whisper, la del mejor modelo segun el ranking configurado (ver `GET /api/v1/capabilities`). Empate: mayor `beam_size`, luego la mas reciente.\n4. `only_file` — hay ficheros pero el json no los describe.\n\nPor ranking y no por \"la mas reciente\" a proposito: si fuera por recencia, probar un modelo malo al final dejaria todo el sistema leyendo la peor transcripcion sin que nadie se entere.\n\nNada se sobrescribe nunca. Volver a transcribir con otros parametros añade una alternativa; no destruye la anterior.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManualTranscript"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/api/v1/transcripts/promote/{id}/{version}": {
      "post": {
        "tags": [
          "transcripts"
        ],
        "summary": "Declara que una transcripcion concreta es la buena.",
        "description": "Fija `current` en esa transcripcion, por encima de la regla automatica. Sirve para decir \"la pasada de large-v3 es la correcta\" sin tener que copiar su texto a mano como correccion.\n\nSe puede volver a la regla automatica promocionando con `version=-1`.\n\n## Como se elige la transcripcion vigente\n\nUna grabacion puede tener VARIAS transcripciones. El numero es una IDENTIDAD, no un rango: la 1 no es mejor que la 0 por ser posterior. Puede haber varias pasadas de Whisper con parametros distintos (que son ALTERNATIVAS entre si) y correcciones manuales (que son una PROGRESION, cada una partiendo de otra via `based_on`).\n\nCual es la vigente se decide por precedencia, y gana la primera que aplique:\n\n1. `promoted` — alguien la eligio con `POST /transcripts/promote/{id}/{n}`.\n2. `manual` — la ultima correccion humana. Un humano gana siempre a la maquina.\n3. `best_model` — entre las pasadas de Whisper, la del mejor modelo segun el ranking configurado (ver `GET /api/v1/capabilities`). Empate: mayor `beam_size`, luego la mas reciente.\n4. `only_file` — hay ficheros pero el json no los describe.\n\nPor ranking y no por \"la mas reciente\" a proposito: si fuera por recencia, probar un modelo malo al final dejaria todo el sistema leyendo la peor transcripcion sin que nadie se entere.\n\nNada se sobrescribe nunca. Volver a transcribir con otros parametros añade una alternativa; no destruye la anterior.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "version",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer",
              "format": "int32"
            }
          },
          {
            "name": "by",
            "in": "query",
            "description": "Quien lo decide. Se guarda en la procedencia.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/api/v1/capabilities": {
      "get": {
        "tags": [
          "config"
        ],
        "summary": "Modelos, limites y valores por omision vigentes.",
        "description": "Para no tener que adivinar nada. Incluye los modelos con su calidad y velocidad MEDIDAS sobre audio real de este proyecto, el ranking que decide la transcripcion vigente, los defaults en uso y los limites del servicio de Whisper.",
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    },
    "/api/v1/hotwords": {
      "get": {
        "tags": [
          "config"
        ],
        "summary": "La lista de hotwords que orienta el vocabulario de Whisper.",
        "description": "Nombres propios y jerga que el modelo no puede adivinar: marcas, cocinas propias, sistemas y terminos del negocio.\n\nLa fuente de verdad es este backend (`data/hotwords.json`); el servicio de Whisper recibe una copia. Conviene que sea CORTA y especifica: las hotwords sesgan el decodificador, y una lista larga de palabras corrientes diluye el efecto y hace que el modelo \"oiga\" cosas que nadie dijo.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HotwordList"
                }
              }
            }
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      },
      "put": {
        "tags": [
          "config"
        ],
        "summary": "Reemplaza la lista de hotwords y la empuja al servicio.",
        "description": "Reemplaza la lista COMPLETA (no añade). Se normaliza: se recortan espacios, se quitan duplicados sin distinguir mayusculas y se ordena.\n\n**Afecta a todas las transcripciones futuras**, no solo a las tuyas. Es estado compartido: cambiarla sin motivo es la forma tipica de que las transcripciones empeoren sin que nadie sepa por que. Para orientar una sola pasada, usa el parametro `hotwords` de `POST /api/v1/transcripts/whisper/{id}` en vez de tocar esta lista.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/HotwordList"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "security": [
          {
            "Bearer": [ ]
          }
        ]
      }
    }
  },
  "components": {
    "schemas": {
      "AnonymousTypeOfstringAndstringAndintAndintAndJsonNode": {
        "required": [
          "status",
          "data_root",
          "audios",
          "pending_transcriptions",
          "whisper"
        ],
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "nullable": true
          },
          "data_root": {
            "type": "string",
            "nullable": true
          },
          "audios": {
            "type": "integer",
            "format": "int32"
          },
          "pending_transcriptions": {
            "type": "integer",
            "format": "int32"
          },
          "whisper": {
            "$ref": "#/components/schemas/JsonNode"
          }
        }
      },
      "HotwordList": {
        "required": [
          "hotwords"
        ],
        "type": "object",
        "properties": {
          "hotwords": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "JsonNode": {
        "nullable": true
      },
      "ManualTranscript": {
        "required": [
          "text",
          "author",
          "based_on"
        ],
        "type": "object",
        "properties": {
          "text": {
            "type": "string"
          },
          "author": {
            "type": "string",
            "nullable": true
          },
          "based_on": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          }
        }
      }
    },
    "securitySchemes": {
      "Bearer": {
        "type": "http",
        "description": "Token de la API de usuarios (Kbv:ApiToken). NO es el token del dispositivo: ese va dentro del HELLO y no vale aqui.",
        "scheme": "bearer"
      }
    }
  },
  "tags": [
    {
      "name": "KnowledgeBuilder.Api"
    },
    {
      "name": "audios"
    },
    {
      "name": "transcripts"
    },
    {
      "name": "config"
    }
  ]
}