# Entrenamiento del modelo ML local para OCR de etiquetas

Este documento describe cómo preparar datos, entrenar el artefacto local y activarlo en la API. El flujo está implementado en el código bajo `src/Module/LabelOcr/Application/Ml/` y comandos Symfony.

## Resumen del pipeline

1. **Dataset** en JSONL (una muestra por línea).
2. **Partición** train / val / test con `app:label-ocr:dataset:split`.
3. **Entrenamiento** con `app:label-ocr:model:train-local` → genera `var/ml/label_ocr_local_model.json` (o la ruta que indiques).
4. **Despliegue** vía variables de entorno (`OCR_ML_*`).
5. **Rollout**: primero **modo sombra** (solo logs), luego **ML activo** si las métricas son aceptables.

## Formato del dataset (JSONL)

Cada línea del archivo debe ser un JSON válido con esta forma mínima:

```json
{
  "rawText": "texto completo OCR como lo devuelve Vision",
  "lines": ["línea 1", "línea 2", "..."],
  "fields": {
    "recipient": "Nombre Apellido",
    "addressLine1": "123 MAIN ST",
    "city": "MIAMI",
    "state": "FL",
    "postalCode": "33166",
    "trackingNumber": "494301293540"
  }
}
```

### Campos obligatorios para el entrenador actual

| Campo en `fields` | Uso en entrenamiento |
|-------------------|----------------------|
| `recipient`       | Sí (clasificador por línea) |
| `addressLine1`    | Sí (clasificador por línea) |
| `city`, `state`, `postalCode` | No entrenan pesos propios; el extractor usa regex sobre el texto |
| `trackingNumber`  | No entrena pesos; el extractor usa patrones configurados en el artefacto |

**Recomendación:** incluye siempre `rawText` **y** `lines` coherentes (las mismas líneas que usa el normalizador). Si falta `lines`, el trainer deriva líneas desde `rawText` con `\R+`.

### Buenas prácticas de etiquetado

- **Truth limpio:** corrige a mano el nombre y la dirección como deben quedar en negocio, no copies errores del OCR si ya sabes el valor correcto.
- **Consistencia:** mismo criterio para `recipient` (ej. “Juan Pérez” vs “PEREZ, JUAN”) en todo el dataset.
- **Tracking:** normaliza como en producción (sin espacios, mayúsculas si aplica: `1Z…`, `TBA…`, dígitos puros).
- **Diversidad:** mezcla carriers (FedEx, Amazon, DHL, UPS, USPS) y calidades de foto/OCR.
- **Volumen orientativo:** pocas decenas de filas sirven para probar el pipeline; para mejora real, apunta a **cientos o miles** de ejemplos, con validación representativa.

### Archivos JSONL en el repo

- `tests/Unit/Module/LabelOcr/Application/Ml/label_ocr_dataset_sample.jsonl` — referencia de formato (y tests).
- `tests/Unit/Module/LabelOcr/Application/Ml/label_ocr_dataset_training.jsonl` — mismo esquema, pensado como entrada a `app:label-ocr:dataset:split` (añade más líneas o concatena con otro JSONL cuando tengas volumen real).

## Comandos

Desde la raíz del proyecto (`trackingpremium-api-v2`):

### 1. Partir el dataset

```bash
php bin/console app:label-ocr:dataset:split \
  /ruta/absoluta/dataset.jsonl \
  /ruta/absoluta/salida-split \
  --train-ratio=0.8 \
  --val-ratio=0.1 \
  --seed=1337
```

Genera:

- `salida-split/train.jsonl`
- `salida-split/val.jsonl`
- `salida-split/test.jsonl`

**Nota:** `train_ratio + val_ratio` debe ser **menor que 1**; el resto va a test.

### 2. Entrenar el modelo local

```bash
php bin/console app:label-ocr:model:train-local \
  /ruta/absoluta/salida-split/train.jsonl \
  /ruta/absoluta/salida-split/val.jsonl \
  /ruta/absoluta/var/ml/label_ocr_local_model.json
```

El JSON resultante incluye:

- `version`, `trainedAt`
- `lineFieldWeights` → pesos por campo (`recipient`, `addressLine1`)
- `trackingPatterns`, `cityStateZipPatterns` (configurables en código del trainer)
- `metrics` → `exact_match` por campo sobre el conjunto de evaluación usado (val si existe, si no train)

**Interpretación de métricas:** con pocos ejemplos, `exact_match` puede ser 0 o muy variable; no uses eso como señal única hasta tener volumen y split estable.

## Despliegue en la aplicación

Parámetros definidos en `config/services.yaml` (con defaults seguros):

| Variable | Default | Efecto |
|----------|---------|--------|
| `OCR_ML_ENABLED` | `0` | Si `1`, aplica fusión ML + reglas en `ProcessLabelOcrService`. |
| `OCR_ML_SHADOW_MODE` | `0` | Si `1`, ejecuta ML y registra comparación en logs **sin cambiar** la respuesta al cliente. |
| `OCR_ML_MODEL_PATH` | `%kernel.project_dir%/var/ml/label_ocr_local_model.json` | Ruta del artefacto JSON. |
| `OCR_ML_TIMEOUT_MS` | `120` | Si la inferencia supera este tiempo, se descarta la salida ML para esa petición. |

### Rollout recomendado

1. Copiar el modelo entrenado a la ruta configurada (o apuntar `OCR_ML_MODEL_PATH`).
2. Activar solo sombra: `OCR_ML_SHADOW_MODE=1`, `OCR_ML_ENABLED=0`.
3. Revisar logs con clave `label_ocr_ml_shadow_eval` (campos ML, decisiones de fusión, `diff_fields`).
4. Si la mejora es clara y sin regresiones: `OCR_ML_ENABLED=1`.

## Archivos de referencia en el repo

| Ruta | Descripción |
|------|-------------|
| `src/Module/LabelOcr/Application/Ml/LocalLabelMlExtractor.php` | Inferencia local |
| `src/Module/LabelOcr/Application/Ml/Training/LabelOcrModelTrainer.php` | Entrenamiento |
| `src/Module/LabelOcr/Application/Ml/Training/LabelOcrDatasetSplitter.php` | Split |
| `src/Module/LabelOcr/Application/Fusion/LabelFieldFusionService.php` | Fusión con parsers por reglas |
| `src/Service/ProcessLabelOcrService.php` | Orquestación OCR → parser → ML → fusión |
| `src/Command/LabelOcrSplitDatasetCommand.php` | Comando split |
| `src/Command/LabelOcrTrainModelCommand.php` | Comando train |
| `tests/Unit/Module/LabelOcr/Application/Ml/label_ocr_dataset_sample.jsonl` | Ejemplo de formato (referencia) |
| `tests/Unit/Module/LabelOcr/Application/Ml/label_ocr_dataset_training.jsonl` | Dataset base para split / train (amplíalo con datos reales) |

## Tests

```bash
./vendor/bin/phpunit tests/Unit/Module/LabelOcr/Application/Ml \
  tests/Unit/Module/LabelOcr/Application/Fusion \
  tests/Unit/Service/ProcessLabelOcrServiceTest.php
```

## Próximos pasos (cuando escales datos)

- Exportar muestras reales desde logs o base de datos (imagen → `rawText` + corrección humana).
- Particionar por **carrier** o por **fecha** para evitar fuga train→test.
- Ajustar umbrales en `LabelFieldFusionService` según tasas de `ml_fill` / `ml_override`.
- Sustituir el perceptrón lineal por un modelo más fuerte (p. ej. NER + ONNX) manteniendo la misma interfaz `LocalLabelMlExtractor` / fusión.

---

*Última actualización: alineado con el código del módulo Label OCR ML local del repositorio.*
