Spaces:
Sleeping
Sleeping
Lucas Gagneten
commited on
Commit
·
fe821af
1
Parent(s):
43fda59
Generate json
Browse files- .gitignore +0 -1
- app.py +138 -102
- image_loader.py +1 -1
- label_editor.py +165 -68
- ocr_processor.py +40 -8
.gitignore
CHANGED
|
@@ -34,7 +34,6 @@ checkpoints/
|
|
| 34 |
# Archivos de anotación
|
| 35 |
annotations/
|
| 36 |
*.xml
|
| 37 |
-
*.txt
|
| 38 |
|
| 39 |
# Gradio y Hugging Face Spaces
|
| 40 |
gradio/
|
|
|
|
| 34 |
# Archivos de anotación
|
| 35 |
annotations/
|
| 36 |
*.xml
|
|
|
|
| 37 |
|
| 38 |
# Gradio y Hugging Face Spaces
|
| 39 |
gradio/
|
app.py
CHANGED
|
@@ -1,204 +1,240 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
-
from image_loader import setup_image_components
|
| 3 |
-
from ocr_processor import setup_tesseract, process_and_setup
|
| 4 |
-
from label_editor import setup_label_components, update_ui, export_data, ALL_NER_TAGS
|
| 5 |
|
| 6 |
# Configurar Tesseract al inicio
|
| 7 |
setup_tesseract()
|
| 8 |
|
| 9 |
-
# ---
|
| 10 |
|
| 11 |
-
# --- 3. INTERFAZ GRADIO (GR.BLOCKS) ---
|
| 12 |
-
|
| 13 |
-
# Función de limpieza (NUEVA FUNCIÓN)
|
| 14 |
def clear_ui_and_reset_states():
|
| 15 |
"""Limpia los componentes de la interfaz y resetea los estados."""
|
| 16 |
print("Reiniciando la interfaz y los estados...")
|
| 17 |
|
| 18 |
-
#
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
| 22 |
|
| 23 |
-
#
|
| 24 |
image_input_update = gr.update(value=None, visible=True)
|
| 25 |
image_output_update = gr.update(value=None, visible=False)
|
| 26 |
-
|
| 27 |
-
# Limpiar otros componentes
|
| 28 |
df_update = gr.update(value=[])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
status_update = "Sube una imagen para comenzar..."
|
| 30 |
|
| 31 |
return (
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
| 39 |
)
|
| 40 |
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
with gr.Blocks(title="Anotador NER de Facturas (LayoutXLM)") as app:
|
| 43 |
gr.Markdown(
|
| 44 |
"""
|
| 45 |
# 🧾 Anotador NER para Facturas Argentinas (LayoutXLM)
|
| 46 |
|
| 47 |
**Instrucciones:**
|
| 48 |
-
1. **Sube** una imagen
|
| 49 |
-
2.
|
| 50 |
-
3.
|
| 51 |
-
4. Al
|
| 52 |
"""
|
| 53 |
)
|
| 54 |
|
| 55 |
# Componentes de estado
|
| 56 |
-
image_orig_state = gr.State(None)
|
| 57 |
-
tokens_data_state = gr.State([]) #
|
| 58 |
-
highlight_index_state = gr.State(-1) #
|
|
|
|
| 59 |
|
| 60 |
with gr.Row():
|
| 61 |
with gr.Column(scale=1):
|
| 62 |
-
# Columna Izquierda: Carga y Visualización
|
| 63 |
-
|
| 64 |
-
# COMPONENTE DE ENTRADA (Visible por defecto, se oculta al subir la imagen)
|
| 65 |
image_input_file = gr.Image(
|
| 66 |
type="pil",
|
| 67 |
-
label="1. Cargar Imagen de Factura
|
| 68 |
sources=["upload"],
|
| 69 |
height=300,
|
| 70 |
interactive=True,
|
| 71 |
visible=True
|
| 72 |
)
|
| 73 |
|
| 74 |
-
# COMPONENTE DE SALIDA (Oculto por defecto, se muestra al completar el OCR)
|
| 75 |
image_output_display = gr.Image(
|
| 76 |
type="pil",
|
| 77 |
label="Factura con Bounding Box Resaltado",
|
| 78 |
interactive=False,
|
| 79 |
-
height=
|
| 80 |
visible=False
|
| 81 |
)
|
| 82 |
|
| 83 |
status_output = gr.Markdown("Sube una imagen para comenzar...")
|
| 84 |
-
|
| 85 |
-
# 🚀 BOTÓN NUEVO
|
| 86 |
btn_clear = gr.Button("🗑️ Quitar Imagen / Nuevo Documento", visible=True)
|
| 87 |
|
| 88 |
with gr.Column(scale=2):
|
| 89 |
# Columna Derecha: Edición de Etiquetas
|
| 90 |
-
gr.Markdown("### 2. Edición de Etiquetas NER
|
| 91 |
|
| 92 |
-
df_label_input, btn_export, file_output
|
|
|
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
try:
|
| 104 |
-
result = process_and_setup(image)
|
| 105 |
-
print("Procesamiento OCR completado")
|
| 106 |
-
|
| 107 |
-
if result[0] is None:
|
| 108 |
-
# Si hay error, mantener la visibilidad de carga y resetear el display
|
| 109 |
-
return None, [], None, [], "Error en el procesamiento del OCR. Verifica logs.", gr.update(visible=True), gr.update(visible=False, value=None)
|
| 110 |
-
|
| 111 |
-
image_orig, tokens_data, highlighted_image, df_data, status = result
|
| 112 |
-
print(f"Tokens detectados: {len(tokens_data) if tokens_data else 0}")
|
| 113 |
-
|
| 114 |
-
# Convertir datos para el DataFrame de Gradio (lista de listas)
|
| 115 |
-
df_rows = []
|
| 116 |
-
if df_data and isinstance(df_data, dict):
|
| 117 |
-
for t, n in zip(df_data['token'], df_data['ner_tag']):
|
| 118 |
-
df_rows.append([t, n])
|
| 119 |
-
|
| 120 |
-
# Devolvemos los estados y la actualización de visibilidad: Ocultar input, Mostrar output
|
| 121 |
-
return (
|
| 122 |
-
image_orig,
|
| 123 |
-
tokens_data,
|
| 124 |
-
highlighted_image,
|
| 125 |
-
df_rows,
|
| 126 |
-
status,
|
| 127 |
-
gr.update(visible=False), # Ocultar image_input_file
|
| 128 |
-
gr.update(visible=True) # Mostrar image_output_display
|
| 129 |
-
)
|
| 130 |
-
|
| 131 |
-
except Exception as e:
|
| 132 |
-
print(f"Error en process_image: {str(e)}")
|
| 133 |
-
# En caso de error, mantener la visibilidad de carga y resetear el display
|
| 134 |
-
return None, [], None, [], f"Error: {str(e)}", gr.update(visible=True), gr.update(visible=False, value=None)
|
| 135 |
|
| 136 |
# CONEXIÓN 1: EJECUTAR OCR AUTOMÁTICAMENTE AL CARGAR IMAGEN
|
| 137 |
image_input_file.change(
|
| 138 |
fn=process_image,
|
| 139 |
inputs=[image_input_file],
|
| 140 |
-
# image_input_file y image_output_display deben estar aquí para controlar su visibilidad
|
| 141 |
outputs=[
|
| 142 |
-
image_orig_state,
|
| 143 |
-
|
| 144 |
-
image_output_display,
|
| 145 |
-
df_label_input,
|
| 146 |
-
status_output,
|
| 147 |
-
image_input_file,
|
| 148 |
-
image_output_display
|
| 149 |
],
|
| 150 |
api_name=False
|
| 151 |
)
|
| 152 |
|
| 153 |
-
#
|
| 154 |
-
|
| 155 |
-
def capture_highlight_index(evt: gr.SelectData):
|
| 156 |
-
"""Captura el índice de la fila seleccionada si es válido."""
|
| 157 |
-
if evt and evt.index is not None and evt.index[0] is not None:
|
| 158 |
-
return evt.index[0]
|
| 159 |
-
return gr.State(-1)
|
| 160 |
-
|
| 161 |
-
# CONEXIÓN 2: Al hacer clic en una fila del Dataframe (Resaltado)
|
| 162 |
df_label_input.select(
|
| 163 |
fn=capture_highlight_index,
|
| 164 |
inputs=None,
|
| 165 |
outputs=[highlight_index_state],
|
| 166 |
queue=False
|
| 167 |
).then(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
fn=update_ui,
|
| 169 |
inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
|
| 170 |
outputs=[tokens_data_state, image_output_display],
|
| 171 |
api_name=False
|
| 172 |
)
|
| 173 |
|
| 174 |
-
# CONEXIÓN 3:
|
| 175 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
fn=update_ui,
|
| 177 |
inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
|
| 178 |
outputs=[tokens_data_state, image_output_display],
|
| 179 |
api_name=False
|
| 180 |
)
|
| 181 |
|
| 182 |
-
# CONEXIÓN
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
btn_export.click(
|
| 184 |
fn=export_data,
|
| 185 |
-
inputs=[image_orig_state, tokens_data_state],
|
| 186 |
outputs=[file_output, status_output],
|
| 187 |
api_name=False
|
| 188 |
)
|
| 189 |
|
| 190 |
-
# CONEXIÓN 5: Limpiar y Reiniciar
|
| 191 |
btn_clear.click(
|
| 192 |
fn=clear_ui_and_reset_states,
|
| 193 |
-
# No toma inputs
|
| 194 |
inputs=None,
|
| 195 |
outputs=[
|
| 196 |
image_orig_state,
|
| 197 |
tokens_data_state,
|
| 198 |
highlight_index_state,
|
|
|
|
| 199 |
image_input_file,
|
| 200 |
image_output_display,
|
| 201 |
df_label_input,
|
|
|
|
|
|
|
| 202 |
status_output
|
| 203 |
],
|
| 204 |
api_name=False
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
from image_loader import setup_image_components # Asumo que este módulo existe
|
| 3 |
+
from ocr_processor import setup_tesseract, process_and_setup
|
| 4 |
+
from label_editor import setup_label_components, update_ui, export_data, update_dataframe_and_state, display_selected_row, ALL_NER_TAGS
|
| 5 |
|
| 6 |
# Configurar Tesseract al inicio
|
| 7 |
setup_tesseract()
|
| 8 |
|
| 9 |
+
# --- Función de Limpieza (Actualizada) ---
|
| 10 |
|
|
|
|
|
|
|
|
|
|
| 11 |
def clear_ui_and_reset_states():
|
| 12 |
"""Limpia los componentes de la interfaz y resetea los estados."""
|
| 13 |
print("Reiniciando la interfaz y los estados...")
|
| 14 |
|
| 15 |
+
# Valores de reseteo para los estados de Gradio
|
| 16 |
+
reset_image_orig_state = None
|
| 17 |
+
reset_tokens_data_state = []
|
| 18 |
+
reset_highlight_index_state = -1
|
| 19 |
+
reset_image_filename_state = None # Estado del nombre de archivo único
|
| 20 |
|
| 21 |
+
# Actualizaciones para los componentes de la interfaz
|
| 22 |
image_input_update = gr.update(value=None, visible=True)
|
| 23 |
image_output_update = gr.update(value=None, visible=False)
|
|
|
|
|
|
|
| 24 |
df_update = gr.update(value=[])
|
| 25 |
+
|
| 26 |
+
# Componentes de edición (ocultar)
|
| 27 |
+
tb_update = gr.update(value="", visible=False)
|
| 28 |
+
dd_update = gr.update(value="O", visible=False)
|
| 29 |
+
|
| 30 |
status_update = "Sube una imagen para comenzar..."
|
| 31 |
|
| 32 |
return (
|
| 33 |
+
reset_image_orig_state, # image_orig_state
|
| 34 |
+
reset_tokens_data_state, # tokens_data_state
|
| 35 |
+
reset_highlight_index_state, # highlight_index_state
|
| 36 |
+
reset_image_filename_state, # image_filename_state
|
| 37 |
+
image_input_update, # image_input_file
|
| 38 |
+
image_output_update, # image_output_display
|
| 39 |
+
df_update, # df_label_input
|
| 40 |
+
tb_update, # tb_token_editor
|
| 41 |
+
dd_update, # dd_tag_selector
|
| 42 |
+
status_update # status_output
|
| 43 |
)
|
| 44 |
|
| 45 |
|
| 46 |
+
# --- FUNCIONES AUXILIARES DE FLUJO ---
|
| 47 |
+
|
| 48 |
+
def process_image(image):
|
| 49 |
+
"""Ejecuta el OCR y el preprocesamiento inicial."""
|
| 50 |
+
if image is None:
|
| 51 |
+
# Añadir None para image_filename_state en el retorno de error
|
| 52 |
+
return None, [], None, [], "Sube una imagen para comenzar...", gr.update(visible=True), gr.update(visible=False), None
|
| 53 |
+
|
| 54 |
+
try:
|
| 55 |
+
# process_and_setup ahora retorna: image_orig, tokens_data, highlighted_image, df_data, status, image_filename
|
| 56 |
+
result = process_and_setup(image)
|
| 57 |
+
|
| 58 |
+
if result[0] is None:
|
| 59 |
+
# Añadir None para image_filename_state en el retorno de error
|
| 60 |
+
return None, [], None, [], "Error en el procesamiento del OCR. Verifica logs.", gr.update(visible=True), gr.update(visible=False, value=None), None
|
| 61 |
+
|
| 62 |
+
# Desempaquetar el resultado
|
| 63 |
+
image_orig, tokens_data, highlighted_image, df_data, status, image_filename = result
|
| 64 |
+
|
| 65 |
+
# Convertir datos para el DataFrame de Gradio (lista de listas)
|
| 66 |
+
df_rows = []
|
| 67 |
+
if df_data and isinstance(df_data, dict):
|
| 68 |
+
for t, n in zip(df_data['token'], df_data['ner_tag']):
|
| 69 |
+
df_rows.append([t, n])
|
| 70 |
+
|
| 71 |
+
return (
|
| 72 |
+
image_orig,
|
| 73 |
+
tokens_data,
|
| 74 |
+
highlighted_image,
|
| 75 |
+
df_rows,
|
| 76 |
+
status,
|
| 77 |
+
gr.update(visible=False), # Ocultar image_input_file
|
| 78 |
+
gr.update(visible=True), # Mostrar image_output_display
|
| 79 |
+
image_filename # Nombre de archivo único
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
except Exception as e:
|
| 83 |
+
print(f"Error en process_image: {str(e)}")
|
| 84 |
+
# Asegurar que la función siempre retorne el número correcto de outputs
|
| 85 |
+
return None, [], None, [], f"Error: {str(e)}", gr.update(visible=True), gr.update(visible=False, value=None), None
|
| 86 |
+
|
| 87 |
+
def capture_highlight_index(evt: gr.SelectData):
|
| 88 |
+
"""Captura el índice de fila (0-index) seleccionado en el DataFrame."""
|
| 89 |
+
if evt and evt.index is not None and evt.index[0] is not None:
|
| 90 |
+
return evt.index[0]
|
| 91 |
+
return gr.State(-1)
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# --- INTERFAZ GRADIO (GR.BLOCKS) ---
|
| 95 |
+
|
| 96 |
with gr.Blocks(title="Anotador NER de Facturas (LayoutXLM)") as app:
|
| 97 |
gr.Markdown(
|
| 98 |
"""
|
| 99 |
# 🧾 Anotador NER para Facturas Argentinas (LayoutXLM)
|
| 100 |
|
| 101 |
**Instrucciones:**
|
| 102 |
+
1. **Sube** una imagen. La imagen se guarda automáticamente en `dataset/imagenes`.
|
| 103 |
+
2. **Haz clic en una FILA** de la tabla para **seleccionar** el token y mostrar los editores.
|
| 104 |
+
3. El cambio de la **Etiqueta NER** (Dropdown) y la edición del **Token** (Textbox) se aplican **automáticamente**.
|
| 105 |
+
4. Al exportar, la información se **agrega** a `anotacion_factura.json`.
|
| 106 |
"""
|
| 107 |
)
|
| 108 |
|
| 109 |
# Componentes de estado
|
| 110 |
+
image_orig_state = gr.State(None)
|
| 111 |
+
tokens_data_state = gr.State([]) # Estado de la verdad que incluye tokens y bboxes
|
| 112 |
+
highlight_index_state = gr.State(-1) # Índice de la fila seleccionada
|
| 113 |
+
image_filename_state = gr.State(None) # Nombre de archivo único para la exportación
|
| 114 |
|
| 115 |
with gr.Row():
|
| 116 |
with gr.Column(scale=1):
|
| 117 |
+
# Columna Izquierda: Carga y Visualización
|
|
|
|
|
|
|
| 118 |
image_input_file = gr.Image(
|
| 119 |
type="pil",
|
| 120 |
+
label="1. Cargar Imagen de Factura",
|
| 121 |
sources=["upload"],
|
| 122 |
height=300,
|
| 123 |
interactive=True,
|
| 124 |
visible=True
|
| 125 |
)
|
| 126 |
|
|
|
|
| 127 |
image_output_display = gr.Image(
|
| 128 |
type="pil",
|
| 129 |
label="Factura con Bounding Box Resaltado",
|
| 130 |
interactive=False,
|
| 131 |
+
height=800,
|
| 132 |
visible=False
|
| 133 |
)
|
| 134 |
|
| 135 |
status_output = gr.Markdown("Sube una imagen para comenzar...")
|
|
|
|
|
|
|
| 136 |
btn_clear = gr.Button("🗑️ Quitar Imagen / Nuevo Documento", visible=True)
|
| 137 |
|
| 138 |
with gr.Column(scale=2):
|
| 139 |
# Columna Derecha: Edición de Etiquetas
|
| 140 |
+
gr.Markdown("### 2. Edición de Etiquetas NER")
|
| 141 |
|
| 142 |
+
# CAPTURAR COMPONENTES (df_label_input, tb_token_editor, dd_tag_selector, btn_export, file_output)
|
| 143 |
+
df_label_input, tb_token_editor, dd_tag_selector, btn_export, file_output = setup_label_components()
|
| 144 |
|
| 145 |
+
# Contenedor para los editores (Token y Tag en dos columnas contiguas)
|
| 146 |
+
with gr.Row(visible=True) as editor_row:
|
| 147 |
+
with gr.Column(scale=2):
|
| 148 |
+
tb_token_editor # Textbox a la izquierda (más ancho)
|
| 149 |
+
with gr.Column(scale=1):
|
| 150 |
+
dd_tag_selector # Dropdown a la derecha (más estrecho)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
# --- CONEXIONES DE EVENTOS ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 154 |
|
| 155 |
# CONEXIÓN 1: EJECUTAR OCR AUTOMÁTICAMENTE AL CARGAR IMAGEN
|
| 156 |
image_input_file.change(
|
| 157 |
fn=process_image,
|
| 158 |
inputs=[image_input_file],
|
|
|
|
| 159 |
outputs=[
|
| 160 |
+
image_orig_state, tokens_data_state, image_output_display, df_label_input, status_output,
|
| 161 |
+
image_input_file, image_output_display, image_filename_state # Nuevo estado
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
],
|
| 163 |
api_name=False
|
| 164 |
)
|
| 165 |
|
| 166 |
+
# CONEXIÓN 2: Al hacer clic en una FILA (Selección/Resaltado)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
df_label_input.select(
|
| 168 |
fn=capture_highlight_index,
|
| 169 |
inputs=None,
|
| 170 |
outputs=[highlight_index_state],
|
| 171 |
queue=False
|
| 172 |
).then(
|
| 173 |
+
# Paso A: Mostrar el token y la etiqueta en los editores externos
|
| 174 |
+
fn=display_selected_row,
|
| 175 |
+
inputs=[tokens_data_state, highlight_index_state],
|
| 176 |
+
outputs=[tb_token_editor, dd_tag_selector, highlight_index_state],
|
| 177 |
+
).then(
|
| 178 |
+
# Paso B: Resaltar la fila en la imagen
|
| 179 |
fn=update_ui,
|
| 180 |
inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
|
| 181 |
outputs=[tokens_data_state, image_output_display],
|
| 182 |
api_name=False
|
| 183 |
)
|
| 184 |
|
| 185 |
+
# CONEXIÓN 3.1: Dropdown cambia la etiqueta NER (Actualización Automática con .change())
|
| 186 |
+
dd_tag_selector.change(
|
| 187 |
+
# Capturar el valor actual del Dropdown para pasarlo a la función de actualización
|
| 188 |
+
fn=lambda t, d, i, new_tag_val: update_dataframe_and_state(t, d, new_tag_val, None, i, 'tag'),
|
| 189 |
+
inputs=[tokens_data_state, df_label_input, highlight_index_state, dd_tag_selector],
|
| 190 |
+
outputs=[tokens_data_state, df_label_input],
|
| 191 |
+
).then(
|
| 192 |
+
# Refrescar la imagen con el resaltado actualizado
|
| 193 |
fn=update_ui,
|
| 194 |
inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
|
| 195 |
outputs=[tokens_data_state, image_output_display],
|
| 196 |
api_name=False
|
| 197 |
)
|
| 198 |
|
| 199 |
+
# CONEXIÓN 3.2: Textbox cambia el Token (Actualización Automática con .blur y .submit)
|
| 200 |
+
token_update_events = [tb_token_editor.blur, tb_token_editor.submit]
|
| 201 |
+
|
| 202 |
+
for event in token_update_events:
|
| 203 |
+
event(
|
| 204 |
+
# Capturar el valor actual del Textbox
|
| 205 |
+
fn=lambda t, d, i, new_token_val: update_dataframe_and_state(t, d, None, new_token_val, i, 'token'),
|
| 206 |
+
inputs=[tokens_data_state, df_label_input, highlight_index_state, tb_token_editor],
|
| 207 |
+
outputs=[tokens_data_state, df_label_input],
|
| 208 |
+
).then(
|
| 209 |
+
# Refrescar la imagen
|
| 210 |
+
fn=update_ui,
|
| 211 |
+
inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
|
| 212 |
+
outputs=[tokens_data_state, image_output_display],
|
| 213 |
+
api_name=False
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
# CONEXIÓN 4: Exportar (Añadiendo image_filename_state)
|
| 217 |
btn_export.click(
|
| 218 |
fn=export_data,
|
| 219 |
+
inputs=[image_orig_state, tokens_data_state, image_filename_state],
|
| 220 |
outputs=[file_output, status_output],
|
| 221 |
api_name=False
|
| 222 |
)
|
| 223 |
|
| 224 |
+
# CONEXIÓN 5: Limpiar y Reiniciar
|
| 225 |
btn_clear.click(
|
| 226 |
fn=clear_ui_and_reset_states,
|
|
|
|
| 227 |
inputs=None,
|
| 228 |
outputs=[
|
| 229 |
image_orig_state,
|
| 230 |
tokens_data_state,
|
| 231 |
highlight_index_state,
|
| 232 |
+
image_filename_state, # Nuevo output de limpieza
|
| 233 |
image_input_file,
|
| 234 |
image_output_display,
|
| 235 |
df_label_input,
|
| 236 |
+
tb_token_editor,
|
| 237 |
+
dd_tag_selector,
|
| 238 |
status_output
|
| 239 |
],
|
| 240 |
api_name=False
|
image_loader.py
CHANGED
|
@@ -15,7 +15,7 @@ def setup_image_components():
|
|
| 15 |
type="pil",
|
| 16 |
label="Factura con Bounding Box Resaltado",
|
| 17 |
interactive=False,
|
| 18 |
-
height=
|
| 19 |
)
|
| 20 |
|
| 21 |
status_output = gr.Markdown("---")
|
|
|
|
| 15 |
type="pil",
|
| 16 |
label="Factura con Bounding Box Resaltado",
|
| 17 |
interactive=False,
|
| 18 |
+
height=800
|
| 19 |
)
|
| 20 |
|
| 21 |
status_output = gr.Markdown("---")
|
label_editor.py
CHANGED
|
@@ -1,106 +1,203 @@
|
|
| 1 |
import gradio as gr
|
|
|
|
| 2 |
import json
|
|
|
|
| 3 |
from error_handler import ErrorHandler
|
| 4 |
from ner_tags import ALL_NER_TAGS
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
# --- Funciones de Configuración y UI ---
|
| 7 |
|
| 8 |
def setup_label_components():
|
| 9 |
"""
|
| 10 |
-
Configura y retorna los componentes de edición de etiquetas
|
| 11 |
-
|
|
|
|
| 12 |
"""
|
| 13 |
|
| 14 |
-
#
|
| 15 |
df_label_input = gr.Dataframe(
|
| 16 |
headers=["token", "ner_tag"],
|
| 17 |
col_count=(2, "fixed"),
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
#
|
| 21 |
-
datatype=["str", {"type": "str", "choices": ALL_NER_TAGS, "default": "O"}],
|
| 22 |
-
|
| 23 |
-
label="Tabla de Tokens y Etiquetas",
|
| 24 |
-
interactive=True,
|
| 25 |
wrap=True,
|
| 26 |
-
value=[]
|
| 27 |
)
|
| 28 |
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
file_output = gr.File(label="Archivo de Anotación JSON")
|
| 31 |
|
| 32 |
-
|
|
|
|
| 33 |
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
| 35 |
"""
|
| 36 |
-
|
| 37 |
-
y regenera la imagen resaltada.
|
| 38 |
"""
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
-
if isinstance(
|
| 45 |
-
|
| 46 |
-
new_tag = item['ner_tag']
|
| 47 |
-
|
| 48 |
-
try:
|
| 49 |
-
# La etiqueta está en el índice 1 de la fila del Dataframe
|
| 50 |
-
if len(df_labels[i]) > 1:
|
| 51 |
-
new_tag = df_labels[i][1]
|
| 52 |
-
except Exception:
|
| 53 |
-
pass
|
| 54 |
-
|
| 55 |
-
new_tokens_data.append({
|
| 56 |
-
'token': item['token'],
|
| 57 |
-
'bbox_norm': item['bbox_norm'],
|
| 58 |
-
'bbox_orig': item['bbox_orig'],
|
| 59 |
-
'ner_tag': new_tag
|
| 60 |
-
})
|
| 61 |
else:
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
# Generar la imagen resaltada
|
| 65 |
-
highlighted_image = draw_boxes(image_orig, new_tokens_data, highlight_index)
|
| 66 |
-
|
| 67 |
-
return new_tokens_data, highlighted_image
|
| 68 |
|
| 69 |
# --- Función de Exportación ---
|
| 70 |
|
| 71 |
-
def export_data(image_orig, tokens_data: list):
|
| 72 |
-
"""
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
| 75 |
return None, None
|
| 76 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
try:
|
| 78 |
-
W, H = image_orig.size
|
| 79 |
-
|
| 80 |
-
output_data = {
|
| 81 |
-
'metadata': {
|
| 82 |
-
'image_size': [W, H],
|
| 83 |
-
'format': 'Structured JSON for LayoutXLM Fine-Tuning',
|
| 84 |
-
'note': 'Contains tokens, bboxes normalized to 0-1000, and NER tags.'
|
| 85 |
-
},
|
| 86 |
-
'annotations': []
|
| 87 |
-
}
|
| 88 |
-
|
| 89 |
-
for item in tokens_data:
|
| 90 |
-
output_data['annotations'].append({
|
| 91 |
-
'token': item['token'],
|
| 92 |
-
'bbox_normalized': item['bbox_norm'],
|
| 93 |
-
'ner_tag': item['ner_tag']
|
| 94 |
-
})
|
| 95 |
-
|
| 96 |
-
temp_file = "anotacion_factura.json"
|
| 97 |
with open(temp_file, 'w', encoding='utf-8') as f:
|
| 98 |
-
|
|
|
|
| 99 |
|
| 100 |
-
gr.Info("✅ Exportación exitosa.
|
| 101 |
-
return temp_file, "Exportación
|
| 102 |
|
| 103 |
except Exception as e:
|
| 104 |
error_msg = ErrorHandler.handle_export_error(e)
|
| 105 |
-
gr.Warning(f"Error al
|
| 106 |
return None, f"Error en exportación: {error_msg}"
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
import os
|
| 3 |
import json
|
| 4 |
+
import pandas as pd
|
| 5 |
from error_handler import ErrorHandler
|
| 6 |
from ner_tags import ALL_NER_TAGS
|
| 7 |
|
| 8 |
+
# Definir la ruta base del dataset (debe coincidir con ocr_processor.py si es necesario)
|
| 9 |
+
DATASET_BASE_DIR = "dataset"
|
| 10 |
+
JSON_FILENAME = "anotacion_factura.json"
|
| 11 |
+
|
| 12 |
# --- Funciones de Configuración y UI ---
|
| 13 |
|
| 14 |
def setup_label_components():
|
| 15 |
"""
|
| 16 |
+
Configura y retorna los componentes de edición de etiquetas:
|
| 17 |
+
DataFrame (no interactivo), Textbox para Token, Dropdown para Tag,
|
| 18 |
+
botón de exportar y salida de archivo.
|
| 19 |
"""
|
| 20 |
|
| 21 |
+
# 1. Dataframe NO INTERACTIVO (Solo para visualización y selección de fila)
|
| 22 |
df_label_input = gr.Dataframe(
|
| 23 |
headers=["token", "ner_tag"],
|
| 24 |
col_count=(2, "fixed"),
|
| 25 |
+
datatype=["str", "str"],
|
| 26 |
+
label="Tabla de Tokens y Etiquetas (Haga clic en la FILA para seleccionar y editar abajo)",
|
| 27 |
+
interactive=False, # Deshabilitar la edición directa
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
wrap=True,
|
| 29 |
+
value=[]
|
| 30 |
)
|
| 31 |
|
| 32 |
+
# 2. NUEVOS COMPONENTES DE EDICIÓN EXTERNOS
|
| 33 |
+
|
| 34 |
+
# Input para editar el token
|
| 35 |
+
tb_token_editor = gr.Textbox(
|
| 36 |
+
# Eliminamos la instrucción "presionar ENTER" para reflejar el cambio automático
|
| 37 |
+
label="Token Seleccionado",
|
| 38 |
+
interactive=True,
|
| 39 |
+
visible=False # Oculto por defecto
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
# Dropdown para editar la etiqueta NER
|
| 43 |
+
dd_tag_selector = gr.Dropdown(
|
| 44 |
+
choices=ALL_NER_TAGS,
|
| 45 |
+
label="Etiqueta NER Seleccionada",
|
| 46 |
+
value="O",
|
| 47 |
+
interactive=True,
|
| 48 |
+
visible=False # Oculto por defecto
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Eliminamos btn_apply_token
|
| 52 |
+
|
| 53 |
+
# Resto de componentes
|
| 54 |
+
btn_export = gr.Button("Exportar a JSON para Fine-Tuning", variant="secondary")
|
| 55 |
file_output = gr.File(label="Archivo de Anotación JSON")
|
| 56 |
|
| 57 |
+
# Retornamos los nuevos componentes (sin btn_apply_token)
|
| 58 |
+
return df_label_input, tb_token_editor, dd_tag_selector, btn_export, file_output
|
| 59 |
|
| 60 |
+
|
| 61 |
+
# --- FUNCIÓN: Obtener la fila seleccionada y mostrar editores ---
|
| 62 |
+
|
| 63 |
+
def display_selected_row(tokens_data: list, highlight_index: int):
|
| 64 |
"""
|
| 65 |
+
Muestra el token y la etiqueta de la fila seleccionada en los editores externos.
|
|
|
|
| 66 |
"""
|
| 67 |
+
if highlight_index >= 0 and highlight_index < len(tokens_data):
|
| 68 |
+
token = tokens_data[highlight_index]['token']
|
| 69 |
+
ner_tag = tokens_data[highlight_index]['ner_tag']
|
| 70 |
+
|
| 71 |
+
# Muestra los componentes
|
| 72 |
+
visible_update = gr.update(visible=True)
|
| 73 |
+
|
| 74 |
+
# DEVOLVER LA ETIQUETA ACTUAL del token para inicializar el Dropdown correctamente
|
| 75 |
+
return (
|
| 76 |
+
gr.update(value=token, visible=True), # tb_token_editor
|
| 77 |
+
gr.update(value=ner_tag, visible=True), # dd_tag_selector
|
| 78 |
+
highlight_index
|
| 79 |
+
)
|
| 80 |
|
| 81 |
+
# Si no hay selección válida, oculta los componentes
|
| 82 |
+
hidden_update = gr.update(visible=False)
|
| 83 |
+
return gr.update(value="", visible=False), hidden_update, -1
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# --- FUNCIÓN: Actualizar el Dataframe y el estado de los tokens (Mantener) ---
|
| 87 |
+
|
| 88 |
+
def update_dataframe_and_state(tokens_data: list, df_data_current: list, new_tag: str, new_token: str, row_index: int, update_type: str):
|
| 89 |
+
"""
|
| 90 |
+
Función unificada para actualizar la lista de tokens y el Dataframe.
|
| 91 |
+
(La lógica de esta función se mantiene sin cambios)
|
| 92 |
+
"""
|
| 93 |
|
| 94 |
+
if isinstance(df_data_current, pd.DataFrame):
|
| 95 |
+
df_list = df_data_current.values.tolist()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
else:
|
| 97 |
+
df_list = df_data_current
|
| 98 |
+
|
| 99 |
+
if row_index < 0 or row_index >= len(df_list):
|
| 100 |
+
return tokens_data, df_list
|
| 101 |
+
|
| 102 |
+
if update_type == 'tag':
|
| 103 |
+
df_list[row_index][1] = new_tag
|
| 104 |
+
tokens_data[row_index]['ner_tag'] = new_tag
|
| 105 |
+
elif update_type == 'token':
|
| 106 |
+
df_list[row_index][0] = new_token
|
| 107 |
+
tokens_data[row_index]['token'] = new_token
|
| 108 |
+
|
| 109 |
+
return tokens_data, df_list
|
| 110 |
+
|
| 111 |
+
# --- FUNCIÓN: Actualizar la UI al cambiar la selección ---
|
| 112 |
+
def update_ui(image_orig, tokens_data: list, df_labels: list, highlight_index: int):
|
| 113 |
+
from ocr_processor import draw_boxes
|
| 114 |
+
highlighted_image = draw_boxes(image_orig, tokens_data, highlight_index)
|
| 115 |
+
return tokens_data, highlighted_image
|
| 116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
|
| 118 |
# --- Función de Exportación ---
|
| 119 |
|
| 120 |
+
def export_data(image_orig, tokens_data: list, image_filename: str): # <-- ACEPTAR image_filename
|
| 121 |
+
"""
|
| 122 |
+
Exporta los datos anotados a un archivo JSON estructurado.
|
| 123 |
+
Si el archivo existe, agrega las nuevas anotaciones al final.
|
| 124 |
+
"""
|
| 125 |
+
if not tokens_data or not image_filename: # Validar que el nombre del archivo exista
|
| 126 |
+
ErrorHandler.show_error("Error: No hay datos de anotación o nombre de archivo para exportar.")
|
| 127 |
return None, None
|
| 128 |
|
| 129 |
+
# 1. Asegurarse de que la carpeta 'dataset' exista
|
| 130 |
+
os.makedirs(DATASET_BASE_DIR, exist_ok=True)
|
| 131 |
+
# 2. Definir la ruta completa del archivo JSON
|
| 132 |
+
temp_file = os.path.join(DATASET_BASE_DIR, JSON_FILENAME)
|
| 133 |
+
|
| 134 |
+
new_annotations = []
|
| 135 |
+
|
| 136 |
+
# 1. Preparar las nuevas anotaciones en el formato requerido
|
| 137 |
+
for item in tokens_data:
|
| 138 |
+
new_annotations.append({
|
| 139 |
+
'token': item['token'],
|
| 140 |
+
'bbox_normalized': [int(b) for b in item['bbox_norm']],
|
| 141 |
+
'ner_tag': item['ner_tag']
|
| 142 |
+
})
|
| 143 |
+
|
| 144 |
+
# 2. Preparar el nuevo elemento a agregar al array de anotaciones (el objeto completo de la factura)
|
| 145 |
+
W, H = image_orig.size
|
| 146 |
+
|
| 147 |
+
new_document_entry = {
|
| 148 |
+
'image': {
|
| 149 |
+
'size': [W, H],
|
| 150 |
+
'name': image_filename # <-- USAR EL NOMBRE ÚNICO
|
| 151 |
+
},
|
| 152 |
+
'annotations': new_annotations
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
# 3. Leer el archivo existente y obtener los datos previos
|
| 156 |
+
# El archivo JSON principal será un ARRAY de objetos de documentos.
|
| 157 |
+
existing_document_list = []
|
| 158 |
+
total_annotations_count = 0
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
if os.path.exists(temp_file):
|
| 162 |
+
with open(temp_file, 'r', encoding='utf-8') as f:
|
| 163 |
+
data = json.load(f)
|
| 164 |
+
|
| 165 |
+
# ASUMIMOS que el archivo JSON es una lista [] de documentos
|
| 166 |
+
if isinstance(data, list):
|
| 167 |
+
existing_document_list = data
|
| 168 |
+
|
| 169 |
+
# Si el archivo está en el formato antiguo {metadata: {}, annotations: []} (solo 1 doc)
|
| 170 |
+
elif isinstance(data, dict) and 'annotations' in data:
|
| 171 |
+
# Lo convertimos a la nueva estructura de lista si solo tiene un documento
|
| 172 |
+
# Nota: Esto es peligroso, idealmente el formato de exportación debe ser consistente.
|
| 173 |
+
# Asumiremos la nueva estructura JSON será: [ {doc1}, {doc2}, ... ]
|
| 174 |
+
pass
|
| 175 |
+
|
| 176 |
+
except json.JSONDecodeError:
|
| 177 |
+
gr.Warning(f"Advertencia: El archivo {temp_file} existe pero está corrupto/vacío. Se creará uno nuevo.")
|
| 178 |
+
except Exception as e:
|
| 179 |
+
ErrorHandler.handle_export_error(e)
|
| 180 |
+
gr.Warning(f"Error al leer el archivo existente. Se agregará solo este documento.")
|
| 181 |
+
|
| 182 |
+
# 4. Consolidar los datos y contar
|
| 183 |
+
|
| 184 |
+
# 4.1. Agregar el nuevo documento a la lista
|
| 185 |
+
existing_document_list.append(new_document_entry)
|
| 186 |
+
|
| 187 |
+
# 4.2. Contar el total de tokens en todas las facturas
|
| 188 |
+
for doc in existing_document_list:
|
| 189 |
+
total_annotations_count += len(doc.get('annotations', []))
|
| 190 |
+
|
| 191 |
+
# 5. Escribir la lista completa de documentos de vuelta al archivo
|
| 192 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 193 |
with open(temp_file, 'w', encoding='utf-8') as f:
|
| 194 |
+
# Escribir la lista de documentos directamente
|
| 195 |
+
json.dump(existing_document_list, f, ensure_ascii=False, indent=4)
|
| 196 |
|
| 197 |
+
gr.Info(f"✅ Exportación exitosa. Documento '{image_filename}' agregado. Total de documentos: {len(existing_document_list)}. Tokens totales: {total_annotations_count}")
|
| 198 |
+
return temp_file, f"Exportación JSON completada. Documentos: {len(existing_document_list)}, Tokens: {total_annotations_count}"
|
| 199 |
|
| 200 |
except Exception as e:
|
| 201 |
error_msg = ErrorHandler.handle_export_error(e)
|
| 202 |
+
gr.Warning(f"Error al escribir el archivo: {error_msg}")
|
| 203 |
return None, f"Error en exportación: {error_msg}"
|
ocr_processor.py
CHANGED
|
@@ -1,14 +1,19 @@
|
|
| 1 |
import pytesseract
|
| 2 |
from PIL import Image, ImageDraw
|
| 3 |
import os
|
| 4 |
-
import cv2
|
| 5 |
-
import numpy as np
|
| 6 |
-
import pandas as pd
|
|
|
|
| 7 |
|
| 8 |
# Asumiendo que ErrorHandler es una clase o módulo que manejas aparte
|
| 9 |
# Si no lo tienes, deberás crear una implementación simple o manejar las excepciones directamente.
|
| 10 |
from error_handler import ErrorHandler
|
| 11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
# Configuración de Tesseract
|
| 13 |
TESSERACT_CONFIG = r'--oem 3 --psm 3'
|
| 14 |
# --oem 3: Usar motor LSTM (moderno)
|
|
@@ -147,24 +152,49 @@ def draw_boxes(image: Image.Image, tokens_data: list, highlight_index: int = -1)
|
|
| 147 |
return img_copy
|
| 148 |
|
| 149 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
def process_and_setup(image_file):
|
| 151 |
-
"""
|
|
|
|
|
|
|
| 152 |
if image_file is None:
|
| 153 |
empty_df = {'token': [], 'ner_tag': []}
|
| 154 |
ErrorHandler.show_error("Cargue una imagen para comenzar.")
|
| 155 |
-
return None, [], None, empty_df, None
|
| 156 |
|
| 157 |
image_orig, tokens_data, _ = get_ocr_data(image_file)
|
| 158 |
|
| 159 |
if image_orig is None:
|
| 160 |
empty_df = {'token': [], 'ner_tag': []}
|
| 161 |
-
return None, [], None, empty_df, "Error al procesar el OCR. Revise el log."
|
| 162 |
|
|
|
|
|
|
|
|
|
|
| 163 |
if not tokens_data:
|
| 164 |
empty_df = {'token': [], 'ner_tag': []}
|
| 165 |
msg = "OCR completado. No se detectaron tokens válidos."
|
| 166 |
ErrorHandler.show_error(msg)
|
| 167 |
-
return image_orig, [], None, empty_df, msg
|
| 168 |
|
| 169 |
# Crear el DataFrame inicial para la edición en Gradio
|
| 170 |
df_data = {
|
|
@@ -176,4 +206,6 @@ def process_and_setup(image_file):
|
|
| 176 |
highlighted_image = image_orig.copy()
|
| 177 |
msg = f"OCR completado. Tokens detectados: {len(tokens_data)}"
|
| 178 |
print(msg)
|
| 179 |
-
|
|
|
|
|
|
|
|
|
| 1 |
import pytesseract
|
| 2 |
from PIL import Image, ImageDraw
|
| 3 |
import os
|
| 4 |
+
import cv2
|
| 5 |
+
import numpy as np
|
| 6 |
+
import pandas as pd
|
| 7 |
+
import uuid
|
| 8 |
|
| 9 |
# Asumiendo que ErrorHandler es una clase o módulo que manejas aparte
|
| 10 |
# Si no lo tienes, deberás crear una implementación simple o manejar las excepciones directamente.
|
| 11 |
from error_handler import ErrorHandler
|
| 12 |
|
| 13 |
+
DATASET_BASE_DIR = "dataset"
|
| 14 |
+
IMAGES_DIR = os.path.join(DATASET_BASE_DIR, "imagenes")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
# Configuración de Tesseract
|
| 18 |
TESSERACT_CONFIG = r'--oem 3 --psm 3'
|
| 19 |
# --oem 3: Usar motor LSTM (moderno)
|
|
|
|
| 152 |
return img_copy
|
| 153 |
|
| 154 |
|
| 155 |
+
def save_image_to_dataset(image: Image.Image) -> str:
|
| 156 |
+
"""
|
| 157 |
+
Genera un nombre único, crea la carpeta y guarda la imagen en formato JPEG.
|
| 158 |
+
Retorna el nombre del archivo.
|
| 159 |
+
"""
|
| 160 |
+
# 1. Crear el nombre único (UUID + extensión)
|
| 161 |
+
unique_filename = f"{uuid.uuid4()}.jpeg"
|
| 162 |
+
save_path = os.path.join(IMAGES_DIR, unique_filename)
|
| 163 |
+
|
| 164 |
+
# 2. Asegurar que el directorio exista
|
| 165 |
+
os.makedirs(IMAGES_DIR, exist_ok=True)
|
| 166 |
+
|
| 167 |
+
# 3. Guardar la imagen
|
| 168 |
+
# Usamos save() en lugar de guardar en memoria
|
| 169 |
+
image.save(save_path, format="JPEG")
|
| 170 |
+
|
| 171 |
+
print(f"Imagen guardada: {save_path}")
|
| 172 |
+
return unique_filename
|
| 173 |
+
|
| 174 |
+
|
| 175 |
def process_and_setup(image_file):
|
| 176 |
+
"""
|
| 177 |
+
Función inicial: OCR, configuración del estado y ahora, guardar la imagen.
|
| 178 |
+
"""
|
| 179 |
if image_file is None:
|
| 180 |
empty_df = {'token': [], 'ner_tag': []}
|
| 181 |
ErrorHandler.show_error("Cargue una imagen para comenzar.")
|
| 182 |
+
return None, [], None, empty_df, None, None # <-- Nuevo retorno: image_filename
|
| 183 |
|
| 184 |
image_orig, tokens_data, _ = get_ocr_data(image_file)
|
| 185 |
|
| 186 |
if image_orig is None:
|
| 187 |
empty_df = {'token': [], 'ner_tag': []}
|
| 188 |
+
return None, [], None, empty_df, "Error al procesar el OCR. Revise el log.", None # <-- Nuevo retorno: image_filename
|
| 189 |
|
| 190 |
+
# --- CAMBIO CLAVE: Guardar la imagen y obtener su nombre único ---
|
| 191 |
+
image_filename = save_image_to_dataset(image_orig)
|
| 192 |
+
|
| 193 |
if not tokens_data:
|
| 194 |
empty_df = {'token': [], 'ner_tag': []}
|
| 195 |
msg = "OCR completado. No se detectaron tokens válidos."
|
| 196 |
ErrorHandler.show_error(msg)
|
| 197 |
+
return image_orig, [], None, empty_df, msg, image_filename # <-- Devolver nombre
|
| 198 |
|
| 199 |
# Crear el DataFrame inicial para la edición en Gradio
|
| 200 |
df_data = {
|
|
|
|
| 206 |
highlighted_image = image_orig.copy()
|
| 207 |
msg = f"OCR completado. Tokens detectados: {len(tokens_data)}"
|
| 208 |
print(msg)
|
| 209 |
+
|
| 210 |
+
# Devolver el nombre único de la imagen junto con los estados
|
| 211 |
+
return image_orig, tokens_data, highlighted_image, df_data, msg, image_filename
|