diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml
new file mode 100644
index 0000000..f6b2c8b
--- /dev/null
+++ b/.gitea/workflows/ci.yaml
@@ -0,0 +1,237 @@
+name: build_docker
+run-name: ${{ gitea.event.head_commit.message }}
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ push:
+ branches:
+ - main
+
+env:
+ PADDLE_VERSION: "3.0.0"
+
+jobs:
+ essential:
+ runs-on: ubuntu-latest
+ outputs:
+ Version: 1.0.${{ gitea.run_number }}
+ repo: seryus.ddns.net
+ image_cpu: seryus.ddns.net/unir/paddle-ocr-cpu
+ image_gpu: seryus.ddns.net/unir/paddle-ocr-gpu
+ image_easyocr: seryus.ddns.net/unir/easyocr-cpu
+ image_easyocr_gpu: seryus.ddns.net/unir/easyocr-gpu
+ image_doctr: seryus.ddns.net/unir/doctr-cpu
+ image_doctr_gpu: seryus.ddns.net/unir/doctr-gpu
+ image_raytune: seryus.ddns.net/unir/raytune
+ steps:
+ - name: Output version info
+ run: |
+ echo "## Build Info" >> $GITHUB_STEP_SUMMARY
+ echo "Version: 1.0.${{ gitea.run_number }}" >> $GITHUB_STEP_SUMMARY
+ echo "Event: ${{ gitea.event_name }}" >> $GITHUB_STEP_SUMMARY
+
+ # PaddleOCR CPU image (amd64 only)
+ build_cpu:
+ runs-on: ubuntu-latest
+ needs: essential
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Gitea Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ needs.essential.outputs.repo }}
+ username: username
+ password: ${{ secrets.CI_READWRITE }}
+
+ - name: Build and push CPU image
+ uses: docker/build-push-action@v5
+ with:
+ context: src/paddle_ocr
+ file: src/paddle_ocr/Dockerfile.cpu
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ needs.essential.outputs.image_cpu }}:${{ needs.essential.outputs.Version }}
+ ${{ needs.essential.outputs.image_cpu }}:latest
+
+ # PaddleOCR GPU image (amd64 only)
+ build_gpu:
+ runs-on: ubuntu-latest
+ needs: essential
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Gitea Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ needs.essential.outputs.repo }}
+ username: username
+ password: ${{ secrets.CI_READWRITE }}
+
+ - name: Build and push GPU image
+ uses: docker/build-push-action@v5
+ with:
+ context: src/paddle_ocr
+ file: src/paddle_ocr/Dockerfile.gpu
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ needs.essential.outputs.image_gpu }}:${{ needs.essential.outputs.Version }}
+ ${{ needs.essential.outputs.image_gpu }}:latest
+
+ # EasyOCR CPU image (amd64 only)
+ build_easyocr:
+ runs-on: ubuntu-latest
+ needs: essential
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Gitea Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ needs.essential.outputs.repo }}
+ username: username
+ password: ${{ secrets.CI_READWRITE }}
+
+ - name: Build and push EasyOCR image
+ uses: docker/build-push-action@v5
+ with:
+ context: src/easyocr_service
+ file: src/easyocr_service/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ needs.essential.outputs.image_easyocr }}:${{ needs.essential.outputs.Version }}
+ ${{ needs.essential.outputs.image_easyocr }}:latest
+
+ # EasyOCR GPU image (amd64 only)
+ build_easyocr_gpu:
+ runs-on: ubuntu-latest
+ needs: essential
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Gitea Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ needs.essential.outputs.repo }}
+ username: username
+ password: ${{ secrets.CI_READWRITE }}
+
+ - name: Build and push EasyOCR GPU image
+ uses: docker/build-push-action@v5
+ with:
+ context: src/easyocr_service
+ file: src/easyocr_service/Dockerfile.gpu
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ needs.essential.outputs.image_easyocr_gpu }}:${{ needs.essential.outputs.Version }}
+ ${{ needs.essential.outputs.image_easyocr_gpu }}:latest
+
+ # DocTR CPU image (amd64 only)
+ build_doctr:
+ runs-on: ubuntu-latest
+ needs: essential
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Gitea Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ needs.essential.outputs.repo }}
+ username: username
+ password: ${{ secrets.CI_READWRITE }}
+
+ - name: Build and push DocTR image
+ uses: docker/build-push-action@v5
+ with:
+ context: src/doctr_service
+ file: src/doctr_service/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ needs.essential.outputs.image_doctr }}:${{ needs.essential.outputs.Version }}
+ ${{ needs.essential.outputs.image_doctr }}:latest
+
+ # DocTR GPU image (amd64 only)
+ build_doctr_gpu:
+ runs-on: ubuntu-latest
+ needs: essential
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Gitea Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ needs.essential.outputs.repo }}
+ username: username
+ password: ${{ secrets.CI_READWRITE }}
+
+ - name: Build and push DocTR GPU image
+ uses: docker/build-push-action@v5
+ with:
+ context: src/doctr_service
+ file: src/doctr_service/Dockerfile.gpu
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ needs.essential.outputs.image_doctr_gpu }}:${{ needs.essential.outputs.Version }}
+ ${{ needs.essential.outputs.image_doctr_gpu }}:latest
+
+ # Ray Tune OCR image (amd64 only)
+ build_raytune:
+ runs-on: ubuntu-latest
+ needs: essential
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Gitea Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ needs.essential.outputs.repo }}
+ username: username
+ password: ${{ secrets.CI_READWRITE }}
+
+ - name: Build and push Ray Tune image
+ uses: docker/build-push-action@v5
+ with:
+ context: src/raytune
+ file: src/raytune/Dockerfile
+ platforms: linux/amd64
+ push: true
+ tags: |
+ ${{ needs.essential.outputs.image_raytune }}:${{ needs.essential.outputs.Version }}
+ ${{ needs.essential.outputs.image_raytune }}:latest
diff --git a/.gitignore b/.gitignore
index 686d80f..c2762ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,9 @@ results
.DS_Store
.claude
node_modules
+src/paddle_ocr/wheels
+src/*.log
+src/output_*.ipynb
+debugset/
+
+src/dataset_hf/
diff --git a/README.md b/README.md
index ac8da34..0d4c4f7 100644
--- a/README.md
+++ b/README.md
@@ -18,11 +18,15 @@ Optimizar el rendimiento de PaddleOCR para documentos académicos en español me
## Resultados Principales
+**Tabla.** *Comparación de métricas OCR entre configuración baseline y optimizada.*
+
| Modelo | CER | Precisión Caracteres | WER | Precisión Palabras |
|--------|-----|---------------------|-----|-------------------|
| PaddleOCR (Baseline) | 7.78% | 92.22% | 14.94% | 85.06% |
| **PaddleOCR-HyperAdjust** | **1.49%** | **98.51%** | **7.62%** | **92.38%** |
+*Fuente: Elaboración propia.*
+
**Mejora obtenida:** Reducción del CER en un **80.9%**
### Configuración Óptima Encontrada
@@ -56,6 +60,8 @@ PDF (académico UNIR)
### Experimento de Optimización
+**Tabla.** *Parámetros de configuración del experimento Ray Tune.*
+
| Parámetro | Valor |
|-----------|-------|
| Número de trials | 64 |
@@ -64,6 +70,8 @@ PDF (académico UNIR)
| Trials concurrentes | 2 |
| Tiempo total | ~6 horas (CPU) |
+*Fuente: Elaboración propia.*
+
---
## Estructura del Repositorio
@@ -113,18 +121,50 @@ MastersThesis/
---
+## Rendimiento GPU
+
+Se realizó una validación adicional con aceleración GPU para evaluar la viabilidad práctica del enfoque en escenarios de producción.
+
+**Tabla.** *Comparación de rendimiento CPU vs GPU.*
+
+| Métrica | CPU | GPU (RTX 3060) | Aceleración |
+|---------|-----|----------------|-------------|
+| Tiempo/Página | 69.4s | 0.55s | **126x** |
+| Dataset completo (45 páginas) | ~52 min | ~25 seg | **126x** |
+
+*Fuente: Elaboración propia.*
+
+### Recomendación de Modelos
+
+**Tabla.** *Comparación de modelos PaddleOCR en RTX 3060.*
+
+| Modelo | VRAM | Recomendación |
+|--------|------|---------------|
+| **PP-OCRv5 Mobile** | 0.06 GB | ✓ Recomendado |
+| PP-OCRv5 Server | 5.3 GB | ✗ Causa OOM en RTX 3060 |
+
+*Fuente: Elaboración propia.*
+
+**Conclusión:** Para hardware con VRAM limitada (≤6 GB), los modelos Mobile ofrecen el mejor balance entre precisión y recursos. La aceleración GPU hace viable el procesamiento en tiempo real.
+
+---
+
## Requisitos
+**Tabla.** *Dependencias principales del proyecto y versiones utilizadas.*
+
| Componente | Versión |
|------------|---------|
-| Python | 3.11.9 |
+| Python | 3.12.3 |
| PaddlePaddle | 3.2.2 |
| PaddleOCR | 3.3.2 |
| Ray | 2.52.1 |
-| Optuna | 4.6.0 |
+| Optuna | 4.7.0 |
| jiwer | (para métricas CER/WER) |
| PyMuPDF | (para conversión PDF) |
+*Fuente: Elaboración propia.*
+
---
## Uso
@@ -155,7 +195,7 @@ python src/paddle_ocr_tuning.py \
## Fuentes de Datos
-- **Dataset**: Instrucciones para la elaboración del TFE (UNIR), 24 páginas
+- **Dataset**: 2 documentos UNIR (45 páginas total): Instrucciones TFE (24 pág.) + Plantilla TFE (21 pág.)
- **Resultados Ray Tune (PRINCIPAL)**: `src/raytune_paddle_subproc_results_20251207_192320.csv` - 64 trials de optimización con todas las métricas y configuraciones
---
@@ -234,14 +274,18 @@ python3 apply_content.py
### Archivos de Entrada y Salida
+**Tabla.** *Relación de scripts de generación con sus archivos de entrada y salida.*
+
| Script | Entrada | Salida |
|--------|---------|--------|
| `generate_mermaid_figures.py` | `docs/*.md` (bloques ```mermaid```) | `thesis_output/figures/figura_*.png`, `figures_manifest.json` |
| `apply_content.py` | `instructions/plantilla_individual.htm`, `docs/*.md`, `thesis_output/figures/*.png` | `thesis_output/plantilla_individual.htm` |
+*Fuente: Elaboración propia.*
+
### Contenido Generado Automáticamente
-- **30 tablas** con formato APA (Tabla X. *Título* + Fuente: ...)
+- **53 tablas** con formato APA (Tabla X. *Título* + Fuente: ...)
- **8 figuras** desde Mermaid (Figura X. *Título* + Fuente: Elaboración propia)
- **25 referencias** en formato APA con sangría francesa
- **Resumen/Abstract** con palabras clave
@@ -252,48 +296,70 @@ python3 apply_content.py
## Trabajo Pendiente para Completar el TFM
-### Contexto: Limitaciones de Hardware
+### Contexto: Hardware
-Este trabajo adoptó la estrategia de **optimización de hiperparámetros** en lugar de **fine-tuning** debido a:
-- **Sin GPU dedicada**: Ejecución exclusivamente en CPU
-- **Tiempo de inferencia elevado**: ~69 segundos/página en CPU
-- **Fine-tuning inviable**: Entrenar modelos de deep learning sin GPU requeriría tiempos prohibitivos
+Este trabajo adoptó la estrategia de **optimización de hiperparámetros** en lugar de **fine-tuning** debido a que el fine-tuning de modelos OCR requiere datasets etiquetados extensos y tiempos de entrenamiento prohibitivos.
+
+**Hardware utilizado:**
+- **Optimización (CPU)**: Los 64 trials de Ray Tune se ejecutaron en CPU (~69s/página)
+- **Validación (GPU)**: Se validó con RTX 3060 logrando 126x de aceleración (0.55s/página)
La optimización de hiperparámetros demostró ser una **alternativa efectiva** al fine-tuning, logrando una reducción del 80.9% en el CER sin reentrenar el modelo.
-### Tareas Completadas
-
-- [x] **Estructura docs/ según plantilla UNIR**: Todos los capítulos siguen numeración exacta (1.1, 1.2, etc.)
-- [x] **Añadir diagramas Mermaid**: 7 diagramas añadidos (pipeline OCR, arquitectura Ray Tune, gráficos de comparación)
-- [x] **Generar documento TFM unificado**: Script `apply_content.py` genera documento completo desde docs/
-- [x] **Convertir Mermaid a PNG**: Script `generate_mermaid_figures.py` genera figuras automáticamente
-
### Tareas Pendientes
-#### 1. Validación del Enfoque (Prioridad Alta)
-- [ ] **Validación cruzada en otros documentos**: Evaluar la configuración óptima en otros tipos de documentos en español (facturas, formularios, contratos) para verificar generalización
-- [ ] **Ampliar el dataset**: El dataset actual tiene solo 24 páginas. Construir un corpus más amplio y diverso (mínimo 100 páginas)
-- [ ] **Validación del ground truth**: Revisar manualmente el texto de referencia extraído automáticamente para asegurar su exactitud
-
-#### 2. Experimentación Adicional (Prioridad Media)
-- [ ] **Explorar `text_det_unclip_ratio`**: Este parámetro quedó fijado en 0.0. Incluirlo en el espacio de búsqueda podría mejorar resultados
-- [ ] **Comparativa con fine-tuning** (si se obtiene acceso a GPU): Cuantificar la brecha de rendimiento entre optimización de hiperparámetros y fine-tuning real
-- [ ] **Evaluación con GPU**: Medir tiempos de inferencia con aceleración GPU para escenarios de producción
-
-#### 3. Documentación y Presentación (Prioridad Alta)
+#### Obligatorias para Entrega
+- [ ] **Revisión final del documento**: Abrir en Word, actualizar índices (Ctrl+A → F9), ajustar figuras, guardar como .docx
- [ ] **Crear presentación**: Preparar slides para la defensa del TFM
-- [ ] **Revisión final del documento**: Verificar formato, índices y contenido en Word
-#### 4. Extensiones Futuras (Opcional)
-- [ ] **Herramienta de configuración automática**: Desarrollar una herramienta que determine automáticamente la configuración óptima para un nuevo tipo de documento
-- [ ] **Benchmark público para español**: Publicar un benchmark de OCR para documentos en español que facilite comparación de soluciones
-- [ ] **Optimización multi-objetivo**: Considerar CER, WER y tiempo de inferencia simultáneamente
+#### Opcionales (Mejoras Futuras)
+- [ ] **Validación cruzada**: Evaluar configuración en otros documentos (facturas, formularios)
+- [ ] **Explorar `text_det_unclip_ratio`**: Parámetro fijado en 0.0, podría mejorar resultados
+- [ ] **Comparativa con fine-tuning**: Cuantificar brecha vs fine-tuning real
+- [ ] **Herramienta de configuración automática**: Auto-detectar configuración óptima por documento
+- [ ] **Benchmark público para español**: Facilitar comparación de soluciones OCR
-### Recomendación de Próximos Pasos
+#### Completadas
+- [x] **Estructura docs/ según plantilla UNIR**
+- [x] **Diagramas Mermaid**: 8 figuras generadas
+- [x] **Documento TFM unificado**: Script `apply_content.py`
+- [x] **Evaluación con GPU**: RTX 3060 - 126x más rápido (0.55s/página)
-1. **Inmediato**: Abrir documento generado en Word, actualizar índices (Ctrl+A, F9), guardar como .docx
-2. **Corto plazo**: Validar en 2-3 tipos de documentos adicionales para demostrar generalización
-3. **Para la defensa**: Crear presentación con visualizaciones de resultados
+### Dataset
+
+El dataset contiene **45 páginas** de 2 documentos UNIR:
+- `src/dataset/0/`: Instrucciones TFE (24 páginas)
+- `src/dataset/1/`: Plantilla TFE (21 páginas)
+
+#### Formato Hugging Face
+
+El dataset está disponible en formato Hugging Face en `src/dataset_hf/`:
+
+```
+src/dataset_hf/
+├── README.md # Dataset card
+├── metadata.jsonl # Metadata (image_path, text, doc_id, page_num)
+└── data/ # 45 imágenes PNG
+```
+
+#### Generar/Regenerar Dataset
+
+```bash
+# Convertir de formato original a HF
+source .venv/bin/activate
+python src/dataset_formatting/convert_to_hf_dataset.py
+
+# Upload a Gitea packages (requiere GITEA_TOKEN)
+./src/dataset_formatting/upload-dataset.sh $GITEA_TOKEN
+```
+
+#### Descargar Dataset
+
+```bash
+# Desde Gitea packages
+curl -O https://seryus.ddns.net/api/packages/unir/generic/ocr-dataset-spanish/1.0.0/dataset-1.0.0.tar.gz
+tar -xzf dataset-1.0.0.tar.gz -C src/dataset_hf/
+```
---
diff --git a/apply_content.py b/apply_content.py
index 367e92c..24c79d3 100644
--- a/apply_content.py
+++ b/apply_content.py
@@ -4,9 +4,11 @@
import re
import os
from bs4 import BeautifulSoup, NavigableString
+from latex2mathml.converter import convert as latex_to_mathml
-BASE_DIR = '/Users/sergio/Desktop/MastersThesis'
-TEMPLATE = os.path.join(BASE_DIR, 'thesis_output/plantilla_individual.htm')
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+TEMPLATE_INPUT = os.path.join(BASE_DIR, 'instructions/plantilla_individual.htm')
+TEMPLATE_OUTPUT = os.path.join(BASE_DIR, 'thesis_output/plantilla_individual.htm')
DOCS_DIR = os.path.join(BASE_DIR, 'docs')
# Global counters for tables and figures
@@ -33,6 +35,32 @@ def md_to_html_para(text):
text = re.sub(r'\*([^*]+)\*', r'\1', text)
# Inline code
text = re.sub(r'`([^`]+)`', r'\1', text)
+ # Links [text](url) -> text
+ text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'\1', text)
+ return text
+
+def convert_latex_formulas(text):
+ """Convert LaTeX formulas to MathML for Word compatibility."""
+ # Block formulas $$...$$
+ def convert_block(match):
+ latex = match.group(1)
+ try:
+ mathml = latex_to_mathml(latex, display="block")
+ return f'
{mathml}
'
+ except:
+ return match.group(0) # Keep original if conversion fails
+
+ text = re.sub(r'\$\$([^$]+)\$\$', convert_block, text)
+
+ # Inline formulas $...$
+ def convert_inline(match):
+ latex = match.group(1)
+ try:
+ return latex_to_mathml(latex, display="inline")
+ except:
+ return match.group(0)
+
+ text = re.sub(r'\$([^$]+)\$', convert_inline, text)
return text
def extract_table_title(lines, current_index):
@@ -168,6 +196,7 @@ def parse_md_to_html_blocks(md_content):
# Check if previous line has table title (e.g., **Tabla 1.** *Title*)
table_title = None
+ alt_title = None # Alternative title from **bold text:** pattern
table_source = "Elaboración propia"
# Look back for table title
@@ -177,6 +206,9 @@ def parse_md_to_html_blocks(md_content):
# Extract title text
table_title = re.sub(r'\*+', '', prev_line).strip()
break
+ elif prev_line.startswith('**') and prev_line.endswith(':**'):
+ # Alternative: **Bold title:** pattern (for informal tables)
+ alt_title = re.sub(r'\*+', '', prev_line).rstrip(':').strip()
elif prev_line and not prev_line.startswith('|'):
break
@@ -197,26 +229,30 @@ def parse_md_to_html_blocks(md_content):
# Word TOC looks for text with Caption style - anchor must be outside main caption text
bookmark_id = f"_Ref_Tab{table_counter}"
if table_title:
- clean_title = table_title.replace(f"Tabla {table_counter}.", "").strip()
+ # Remove any "Tabla X." or "Tabla AX." pattern from the title
+ clean_title = re.sub(r'^Tabla\s+[A-Z]?\d+\.\s*', '', table_title).strip()
+ elif alt_title:
+ # Use alternative title from **bold text:** pattern
+ clean_title = alt_title
else:
clean_title = "Tabla de datos."
html_blocks.append(f'''Tabla {table_counter}. {clean_title}
''')
# Build table HTML with APA style (horizontal lines only, no vertical)
- table_html = ''
+ table_html = ''
for j, tline in enumerate(table_lines):
cells = [c.strip() for c in tline.split('|')[1:-1]]
table_html += ''
for cell in cells:
if j == 0:
# Header row: top and bottom border, bold text
- table_html += f'{md_to_html_para(cell)} | '
+ table_html += f'{md_to_html_para(cell)} | '
elif j == len(table_lines) - 1:
# Last row: bottom border only
- table_html += f'{md_to_html_para(cell)} | '
+ table_html += f'{md_to_html_para(cell)} | '
else:
# Middle rows: no borders
- table_html += f'{md_to_html_para(cell)} | '
+ table_html += f'{md_to_html_para(cell)} | '
table_html += '
'
table_html += '
'
html_blocks.append(table_html)
@@ -240,6 +276,7 @@ def parse_md_to_html_blocks(md_content):
if re.match(r'^[\-\*\+]\s', line):
while i < len(lines) and re.match(r'^[\-\*\+]\s', lines[i]):
item_text = lines[i][2:].strip()
+ item_text = convert_latex_formulas(item_text)
html_blocks.append(f'· {md_to_html_para(item_text)}
')
i += 1
continue
@@ -249,6 +286,7 @@ def parse_md_to_html_blocks(md_content):
num = 1
while i < len(lines) and re.match(r'^\d+\.\s', lines[i]):
item_text = re.sub(r'^\d+\.\s*', '', lines[i]).strip()
+ item_text = convert_latex_formulas(item_text)
html_blocks.append(f'{num}. {md_to_html_para(item_text)}
')
num += 1
i += 1
@@ -273,7 +311,12 @@ def parse_md_to_html_blocks(md_content):
i += 1
para_text = ' '.join(para_lines)
- html_blocks.append(f'{md_to_html_para(para_text)}
')
+ para_text = convert_latex_formulas(para_text)
+ # Check if paragraph contains MathML (already wrapped)
+ if '