# Panel Whisper EITB — control local (MAMP + MySQL)

Primera pieza del sistema de orquestación: espejo de assets de Directus,
selección de assets para entrenar, y auditoría de entrenamientos. Todavía
NO dispara nada en GCP — eso se conecta en el siguiente paso.

## Instalación (Windows 11 + MAMP)

1. Copia toda esta carpeta dentro de `C:\MAMP\htdocs\`, por ejemplo como
   `C:\MAMP\htdocs\whisper-eitb-panel\`.
2. Abre el panel de MAMP y arranca los servidores (Apache + MySQL). Anota
   el puerto de MySQL que use tu MAMP (por defecto suele ser `8889`; en
   MAMP PRO puede ser `3306`) — lo necesitas en el paso 4.
3. Crea la base de datos. Puedes usar phpMyAdmin (accesible desde el panel
   de MAMP) o la consola de MySQL:
   ```sql
   CREATE DATABASE whisper_eitb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
   ```
   Luego importa `schema.sql` contra esa base (en phpMyAdmin: pestaña
   "Importar"; por consola: `mysql -u root -p whisper_eitb < schema.sql`).
4. Copia `config.php.example` como `config.php` (mismo directorio) y
   rellena:
   - `db.port`: el puerto de MySQL de tu MAMP.
   - `directus.base_url`, `directus.token`, `directus.query_path`: tus
     credenciales y la query de Directus que ya tienes validada.
   - Deja `gcp` y `webhook` vacíos por ahora, se rellenan en el siguiente
     paso del plan.
5. Abre `http://localhost:8888/whisper-eitb-panel/` (ajusta el puerto de
   Apache si tu MAMP usa otro) — deberías ver el resumen con 0 assets.
6. Ve a "Assets" y pulsa "Sincronizar con Directus".

## Sobre la sincronización con Directus (ya ajustada a tu JSON real)

`sync_directus.php` está escrito contra la forma real de tu respuesta:
`media_type` (video/audio) para el tipo de asset, `type` (vod/...) guardado
aparte en `content_type`, `eitb_mam_id` guardado tal cual (ver más abajo),
y `available_audios` usado para saber los códigos de idioma del audio.

El punto delicado es que `audios[].media_language_id` y
`subtitles[].language` son **ids numéricos**, no códigos (`"baq"`, `"spa"`...),
y no tenemos una tabla de idiomas de Directus a mano. La solución
implementada: cuando `audios[]` y `available_audios[]` tienen la misma
longitud (el caso normal, un audio por asset), se emparejan por posición y
se aprende `id -> código` en la tabla `directus_language_map`. Esa tabla
persiste entre sincronizaciones, así que ese mismo aprendizaje se reutiliza
para resolver el idioma de los subtítulos (que comparten el mismo espacio
de ids). Probado con tu JSON de ejemplo contra MariaDB real: aprende
`1 -> baq` a partir del audio, y con eso resuelve también el subtítulo
(que solo traía `"language": 1`) sin que hiciera falta ninguna tabla de
idiomas externa. Si algún asset raro tiene más audios que códigos en
`available_audios`, esa pista se queda sin resolver por ahora y se avisa
en el flash de la sincronización — un "Sincronizar" posterior, cuando ya
se haya visto ese id en otro asset con conteos que sí cuadren, lo resuelve
solo (no hace falta ninguna acción manual).

**Descarga del subtítulo — ya resuelta.** El campo correcto es
`subtitles.file.filename_disk` (no `filename_download`), y con eso se
construye la URL directa de descarga sin pasar por la API de Directus:
`subtitle_cdn_base` (en `config.php`, hoy `https://cdnstorage.primeran.eus/directus/eitb/`)
+ `filename_disk`. `sync_directus.php` ya guarda esa URL resuelta en
`asset_media.file_url`, y `run_detail.php` la muestra como enlace directo.
Verificado con un ejemplo real: la URL construida coincide con la que
confirmaste que funciona.

**Paginación.** Con "miles de archivos", una sola llamada a Directus puede
no traer todo. `fetchAllDirectusItems()` pagina solo (`limit=200&page=N`
sobre lo que tengas en `directus.query_path`, quitando cualquier
`limit`/`page` que ya llevara) hasta que una página vuelve con menos de
200 — así que en `config.php` pon la query de **listado** (colección
`media`, sin id en la ruta, sin `limit=1`), tal cual la que nos diste:
```
/items/media?fields=id,title,type,media_type,created_on,available_audios,eitb_mam_id,series,audios.id,audios.media_language_id,subtitles.id,subtitles.language,subtitles.file.filename_disk&filter[subtitles][language][_eq]=1&filter[audios][media_language_id][_eq]=1
```
Esto ya filtra a los items que tienen audio Y subtítulo en euskera (id de
idioma 1, confirmado). El código también soporta que Directus devuelva
`data` como objeto único (si alguna vez pruebas contra `/items/media/:id`)
o como array (el caso normal de listado) — normalizado automáticamente.

Un hueco que queda abierto, no bloquea usar el panel ya mismo:
- **`series`**: en el ejemplo viene `null`. `extractSeriesName()` cubre los
  casos razonables (string suelto, u objeto con `title`/`name`), pero no
  hemos visto un ejemplo con serie real — si al sincronizar ves
  `series_name` vacío en la base para un episodio de serie, pásame un
  ejemplo de ese campo con valor y lo afino (útil más adelante para poder
  filtrar/seleccionar "toda una serie" de golpe).

## `eitb_mam_id`: la pieza para Backblaze

Cada asset trae `eitb_mam_id` (ej. `"10098477"`), que se guarda en
`assets.eitb_mam_id`. Todo apunta a que esta es la referencia real para
localizar el máster de vídeo/audio en Backblaze — a diferencia del
subtítulo (fichero propio en Directus), el audio no tiene fichero aparte:
vive como pista dentro del máster. Cuando ataquemos la parte de Backblaze,
el primer paso será confirmar cómo se traduce este `eitb_mam_id` a una ruta
o nombre de fichero real en el bucket.

## Sobre la selección de la pista de audio en euskera

Al crear un run (`train_create.php`), para cada asset se coge la primera
fila de `asset_media` marcada como `is_euskera_candidate = 1` (hoy se
calcula comparando el código de idioma ya resuelto contra `baq`, `eus`,
`eu` — ver `EUSKERA_LANGUAGE_CODES` en `lib/db.php`). Si un asset tiene más
de una pista candidata, se avisa en el log del run para que lo revises a
mano; no se intenta adivinar cuál es la "buena".

Ojo: que el *código de idioma en Directus* sea `baq` no garantiza que la
pista de audio dentro del fichero mp4/mkv real esté etiquetada igual en
sus metadatos internos (`baq` vs `eus` vs sin etiqueta legible). Eso hay
que verificarlo con `ffprobe -show_streams` contra un master real antes de
que el proceso en GCP intente extraer la pista automáticamente — tarea
pendiente del siguiente paso (la parte de GCP/Backblaze), no de este.

## Qué falta todavía (fuera del alcance de esta pieza)

- El botón "Enviar a entrenar" crea el `training_run` en estado `queued`
  pero no llama a ningún sitio — falta el endpoint disparador en GCP.
- Nada escribe en `training_run_events`/`training_runs` salvo esta propia
  web — cuando montemos el lado de GCP, ese proceso irá actualizando el
  estado real (vía llamada a un webhook de esta web, una vez esté en un
  servidor público, según lo que hablamos).
- La tabla `models` está lista pero nadie la rellena todavía.
