Spaces:
Sleeping
Sleeping
Lucas Gagneten
commited on
Commit
·
f35ce5e
1
Parent(s):
a3dc003
Gemini client
Browse files- app.py +64 -117
- gemini_ner_client.py +127 -0
- image_loader.py +9 -2
- label_editor.py +96 -50
- ocr_processor.py +12 -5
- requirements.txt +3 -0
- setup.sh +0 -5
app.py
CHANGED
|
@@ -3,12 +3,13 @@ from PIL import Image
|
|
| 3 |
import pandas as pd
|
| 4 |
|
| 5 |
# ----------------------------------------------------------------------
|
| 6 |
-
# 💡 1. IMPORTACIONES
|
| 7 |
# ----------------------------------------------------------------------
|
| 8 |
|
| 9 |
# ocr_processor: Funciones de OCR (Doctr) y guardado de imagen
|
| 10 |
from ocr_processor import process_and_setup
|
| 11 |
|
|
|
|
| 12 |
from label_editor import (
|
| 13 |
setup_label_components,
|
| 14 |
update_ui,
|
|
@@ -16,26 +17,28 @@ from label_editor import (
|
|
| 16 |
export_and_zip_dataset,
|
| 17 |
update_dataframe_and_state,
|
| 18 |
display_selected_row,
|
| 19 |
-
# ALL_NER_TAGS ya no es necesario si solo se usa en label_editor.py
|
| 20 |
)
|
| 21 |
|
| 22 |
-
#
|
| 23 |
-
from
|
| 24 |
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
# --- Función de Limpieza
|
| 27 |
|
| 28 |
-
def clear_ui_and_reset_states(tb_new_token_text, btn_add_new_token, state_new_bbox):
|
| 29 |
-
"""Limpia los componentes de la interfaz y resetea los estados."""
|
| 30 |
print("Reiniciando la interfaz y los estados...")
|
| 31 |
|
| 32 |
# Valores de reseteo para los estados de Gradio
|
| 33 |
-
reset_image_orig_state = None
|
| 34 |
-
reset_tokens_data_state = []
|
| 35 |
-
reset_highlight_index_state = -1
|
| 36 |
reset_image_filename_state = None
|
| 37 |
|
| 38 |
# Actualizaciones para los componentes de la interfaz
|
|
|
|
| 39 |
image_input_update = gr.update(value=None, visible=True)
|
| 40 |
image_output_update = gr.update(value=None, visible=False)
|
| 41 |
df_update = gr.update(value=[])
|
|
@@ -44,7 +47,7 @@ def clear_ui_and_reset_states(tb_new_token_text, btn_add_new_token, state_new_bb
|
|
| 44 |
tb_update = gr.update(value="", visible=False)
|
| 45 |
dd_update = gr.update(value="O", visible=False)
|
| 46 |
|
| 47 |
-
#
|
| 48 |
tb_new_token_update = gr.update(value="", visible=False)
|
| 49 |
btn_add_token_update = gr.update(visible=False)
|
| 50 |
reset_new_bbox_state = None
|
|
@@ -52,34 +55,42 @@ def clear_ui_and_reset_states(tb_new_token_text, btn_add_new_token, state_new_bb
|
|
| 52 |
status_update = "Sube una imagen para comenzar..."
|
| 53 |
|
| 54 |
return (
|
| 55 |
-
reset_image_orig_state,
|
| 56 |
-
reset_tokens_data_state,
|
| 57 |
-
reset_highlight_index_state,
|
| 58 |
-
reset_image_filename_state,
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
| 68 |
)
|
| 69 |
|
| 70 |
|
| 71 |
-
# ---
|
| 72 |
|
| 73 |
-
def process_image(image):
|
| 74 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
if image is None:
|
|
|
|
| 76 |
return None, [], None, [], "Sube una imagen para comenzar...", gr.update(visible=True), gr.update(visible=False, value=None), None
|
| 77 |
|
| 78 |
try:
|
| 79 |
-
# process_and_setup
|
| 80 |
-
|
|
|
|
| 81 |
|
| 82 |
if result[0] is None:
|
|
|
|
| 83 |
return None, [], None, [], "Error en el procesamiento del OCR. Verifica logs.", gr.update(visible=True), gr.update(visible=False, value=None), None
|
| 84 |
|
| 85 |
image_orig, tokens_data, highlighted_image, df_data, status, image_filename = result
|
|
@@ -90,6 +101,7 @@ def process_image(image):
|
|
| 90 |
for t, n in zip(df_data['token'], df_data['ner_tag']):
|
| 91 |
df_rows.append([t, n])
|
| 92 |
|
|
|
|
| 93 |
return (
|
| 94 |
image_orig,
|
| 95 |
tokens_data,
|
|
@@ -103,36 +115,15 @@ def process_image(image):
|
|
| 103 |
|
| 104 |
except Exception as e:
|
| 105 |
print(f"Error en process_image: {str(e)}")
|
| 106 |
-
# Nota:
|
| 107 |
return None, [], None, [], f"Error: {str(e)}", gr.update(visible=True), gr.update(visible=False, value=None), None
|
| 108 |
-
|
| 109 |
def capture_highlight_index(evt: gr.SelectData):
|
| 110 |
"""Captura el índice de fila (0-index) seleccionado en el DataFrame."""
|
| 111 |
if evt and evt.index is not None and evt.index[0] is not None:
|
| 112 |
return evt.index[0]
|
| 113 |
return gr.State(-1)
|
| 114 |
|
| 115 |
-
def setup_image_components():
|
| 116 |
-
"""Define los componentes de carga y visualización de imagen."""
|
| 117 |
-
image_input_file = gr.Image(
|
| 118 |
-
type="pil",
|
| 119 |
-
label="1. Cargar Imagen de Factura",
|
| 120 |
-
sources=["upload"],
|
| 121 |
-
height=300,
|
| 122 |
-
interactive=True,
|
| 123 |
-
visible=True
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
image_output_display = gr.Image(
|
| 127 |
-
type="pil",
|
| 128 |
-
label="Factura con Bounding Box Resaltado (Haga clic y arrastre para añadir BBox)",
|
| 129 |
-
interactive=True,
|
| 130 |
-
height=800,
|
| 131 |
-
visible=False
|
| 132 |
-
)
|
| 133 |
-
|
| 134 |
-
return image_input_file, image_output_display
|
| 135 |
-
|
| 136 |
# --- INTERFAZ GRADIO (GR.BLOCKS) ---
|
| 137 |
|
| 138 |
with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
@@ -140,7 +131,7 @@ with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
| 140 |
"""
|
| 141 |
# 🧾 Anotador NER para Facturas (LayoutXLM)
|
| 142 |
|
| 143 |
-
**Instrucciones:** 1. **Sube** una imagen. La imagen se guarda automáticamente en `dataset/imagenes`.
|
| 144 |
2. **Edita** los tokens o etiquetas. Los cambios se aplican automáticamente.
|
| 145 |
3. Haz clic en **'Guardar Anotación Actual (JSON)'** para confirmar los datos de la factura actual en `dataset/anotacion_factura.json`.
|
| 146 |
4. Haz clic en **'Descargar Dataset Completo (.zip)'** para obtener todas las imágenes y el JSON consolidado.
|
|
@@ -152,14 +143,17 @@ with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
| 152 |
tokens_data_state = gr.State([])
|
| 153 |
highlight_index_state = gr.State(-1)
|
| 154 |
image_filename_state = gr.State(None)
|
| 155 |
-
|
| 156 |
-
# 💡 Estado para el BBox Manual
|
| 157 |
-
STATE_NEW_BBOX = gr.State(value=None)
|
| 158 |
|
| 159 |
with gr.Row():
|
| 160 |
with gr.Column(scale=1):
|
| 161 |
# Columna Izquierda: Carga y Visualización
|
| 162 |
-
image_input_file, image_output_display
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
status_output = gr.Markdown("Sube una imagen para comenzar...")
|
| 165 |
btn_clear = gr.Button("🗑️ Quitar Imagen / Nuevo Documento", visible=True)
|
|
@@ -168,11 +162,11 @@ with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
| 168 |
# Columna Derecha: Edición de Etiquetas
|
| 169 |
gr.Markdown("### 2. Edición de Etiquetas NER")
|
| 170 |
|
| 171 |
-
# 💡
|
| 172 |
(
|
| 173 |
df_label_input, tb_token_editor, dd_tag_selector,
|
| 174 |
btn_save_annotation, btn_export, file_output,
|
| 175 |
-
tb_new_token_text, btn_add_new_token, temp_state_dummy
|
| 176 |
) = setup_label_components()
|
| 177 |
|
| 178 |
# Dataframe
|
|
@@ -182,16 +176,15 @@ with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
| 182 |
with gr.Row(visible=True) as editor_row:
|
| 183 |
with gr.Column(scale=2):
|
| 184 |
tb_token_editor
|
| 185 |
-
#
|
| 186 |
tb_new_token_text
|
| 187 |
with gr.Column(scale=1):
|
| 188 |
dd_tag_selector
|
| 189 |
|
| 190 |
-
# Contenedor para los botones de Guardar/Descargar
|
| 191 |
with gr.Row(visible=True):
|
| 192 |
btn_save_annotation
|
| 193 |
btn_export
|
| 194 |
-
# 💡 NUEVO BOTÓN: Agregar token manualmente
|
| 195 |
btn_add_new_token
|
| 196 |
|
| 197 |
file_output
|
|
@@ -199,10 +192,10 @@ with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
| 199 |
|
| 200 |
# --- CONEXIONES DE EVENTOS ---
|
| 201 |
|
| 202 |
-
# CONEXIÓN 1: EJECUTAR OCR
|
| 203 |
image_input_file.change(
|
| 204 |
fn=process_image,
|
| 205 |
-
inputs=[image_input_file],
|
| 206 |
outputs=[
|
| 207 |
image_orig_state, tokens_data_state, image_output_display, df_label_input, status_output,
|
| 208 |
image_input_file, image_output_display, image_filename_state
|
|
@@ -210,7 +203,7 @@ with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
| 210 |
api_name=False
|
| 211 |
)
|
| 212 |
|
| 213 |
-
# CONEXIÓN 2: Selección de FILA
|
| 214 |
df_label_input.select(
|
| 215 |
fn=capture_highlight_index,
|
| 216 |
inputs=None,
|
|
@@ -227,8 +220,7 @@ with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
| 227 |
api_name=False
|
| 228 |
)
|
| 229 |
|
| 230 |
-
# CONEXIÓN 3: Edición de Tag o Token (
|
| 231 |
-
# ... (Lógica de dd_tag_selector.change y tb_token_editor.blur/submit se mantiene) ...
|
| 232 |
dd_tag_selector.change(
|
| 233 |
fn=lambda t, d, i, new_tag_val: update_dataframe_and_state(t, d, new_tag_val, None, i, 'tag'),
|
| 234 |
inputs=[tokens_data_state, df_label_input, highlight_index_state, dd_tag_selector],
|
|
@@ -252,52 +244,8 @@ with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
| 252 |
outputs=[tokens_data_state, image_output_display],
|
| 253 |
api_name=False
|
| 254 |
)
|
| 255 |
-
|
| 256 |
-
# ----------------------------------------------------------------------
|
| 257 |
-
# 💡 CONEXIONES DE ADICIÓN MANUAL DE BBOX
|
| 258 |
-
# ----------------------------------------------------------------------
|
| 259 |
-
|
| 260 |
-
# 1. Capturar la selección en la imagen (Arrastrar el ratón sobre image_output_display)
|
| 261 |
-
image_output_display.select(
|
| 262 |
-
fn=add_new_bbox_mode,
|
| 263 |
-
inputs=[tokens_data_state, image_orig_state],
|
| 264 |
-
outputs=[
|
| 265 |
-
tb_token_editor, # 1. Oculta editor existente (si estaba visible)
|
| 266 |
-
dd_tag_selector, # 2. Muestra selector de tag (si estaba oculto)
|
| 267 |
-
tb_new_token_text, # 3. Muestra campo de texto para nuevo token 👈 ESTO LOS HACE VISIBLES
|
| 268 |
-
btn_add_new_token, # 4. Muestra botón de adición 👈 ESTO LOS HACE VISIBLES
|
| 269 |
-
STATE_NEW_BBOX # 5. Guarda coordenadas
|
| 270 |
-
]
|
| 271 |
-
)
|
| 272 |
-
|
| 273 |
-
# 2. Conectar el botón para agregar el nuevo token
|
| 274 |
-
btn_add_new_token.click(
|
| 275 |
-
fn=append_new_token,
|
| 276 |
-
inputs=[
|
| 277 |
-
tokens_data_state,
|
| 278 |
-
image_orig_state,
|
| 279 |
-
STATE_NEW_BBOX,
|
| 280 |
-
tb_new_token_text,
|
| 281 |
-
dd_tag_selector
|
| 282 |
-
],
|
| 283 |
-
outputs=[
|
| 284 |
-
tokens_data_state, # 1. Actualiza el estado de tokens
|
| 285 |
-
df_label_input, # 2. Actualiza la tabla UI
|
| 286 |
-
tb_token_editor, # 3. Oculta tb_token_editor
|
| 287 |
-
tb_new_token_text, # 4. Oculta y limpia tb_new_token_text
|
| 288 |
-
btn_add_new_token, # 5. Oculta btn_add_new_token
|
| 289 |
-
STATE_NEW_BBOX # 6. Limpia estado BBox
|
| 290 |
-
]
|
| 291 |
-
).then(
|
| 292 |
-
# Sincronizar la imagen y el estado después de agregar el token
|
| 293 |
-
fn=update_ui,
|
| 294 |
-
inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
|
| 295 |
-
outputs=[tokens_data_state, image_output_display]
|
| 296 |
-
)
|
| 297 |
-
|
| 298 |
-
# ----------------------------------------------------------------------
|
| 299 |
|
| 300 |
-
# CONEXIÓN 4: Guardar y Exportar
|
| 301 |
btn_save_annotation.click(
|
| 302 |
fn=save_current_annotation_to_json,
|
| 303 |
inputs=[image_orig_state, tokens_data_state, image_filename_state],
|
|
@@ -305,7 +253,6 @@ with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
| 305 |
api_name=False
|
| 306 |
)
|
| 307 |
|
| 308 |
-
# CONEXIÓN 4: Exportar y Comprimir (ZIP)
|
| 309 |
btn_export.click(
|
| 310 |
fn=export_and_zip_dataset,
|
| 311 |
inputs=[image_orig_state, tokens_data_state, image_filename_state],
|
|
@@ -313,15 +260,15 @@ with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
| 313 |
api_name=False
|
| 314 |
)
|
| 315 |
|
| 316 |
-
# CONEXIÓN 5: Limpiar y Reiniciar
|
| 317 |
btn_clear.click(
|
| 318 |
fn=clear_ui_and_reset_states,
|
| 319 |
-
inputs=[tb_new_token_text, btn_add_new_token, STATE_NEW_BBOX], # 💡
|
| 320 |
outputs=[
|
| 321 |
image_orig_state, tokens_data_state, highlight_index_state,
|
| 322 |
-
image_filename_state, image_input_file, image_output_display,
|
| 323 |
df_label_input, tb_token_editor, dd_tag_selector,
|
| 324 |
-
tb_new_token_text, btn_add_new_token, STATE_NEW_BBOX,
|
| 325 |
status_output
|
| 326 |
],
|
| 327 |
api_name=False
|
|
|
|
| 3 |
import pandas as pd
|
| 4 |
|
| 5 |
# ----------------------------------------------------------------------
|
| 6 |
+
# 💡 1. IMPORTACIONES
|
| 7 |
# ----------------------------------------------------------------------
|
| 8 |
|
| 9 |
# ocr_processor: Funciones de OCR (Doctr) y guardado de imagen
|
| 10 |
from ocr_processor import process_and_setup
|
| 11 |
|
| 12 |
+
# label_editor: Funciones de setup, edición y persistencia (JSON/ZIP)
|
| 13 |
from label_editor import (
|
| 14 |
setup_label_components,
|
| 15 |
update_ui,
|
|
|
|
| 17 |
export_and_zip_dataset,
|
| 18 |
update_dataframe_and_state,
|
| 19 |
display_selected_row,
|
|
|
|
| 20 |
)
|
| 21 |
|
| 22 |
+
# image_loader: Define la UI para la carga de imagen y la API Key
|
| 23 |
+
from image_loader import setup_image_components
|
| 24 |
|
| 25 |
+
# bbox_adder: Funciones para añadir manualmente tokens no detectados por OCR
|
| 26 |
+
from bbox_adder import add_new_bbox_mode, append_new_token
|
| 27 |
|
| 28 |
+
# --- Función de Limpieza/Reset ---
|
| 29 |
|
| 30 |
+
def clear_ui_and_reset_states(api_key_input, tb_new_token_text, btn_add_new_token, state_new_bbox):
|
| 31 |
+
"""Limpia los componentes de la interfaz y resetea los estados a su valor inicial."""
|
| 32 |
print("Reiniciando la interfaz y los estados...")
|
| 33 |
|
| 34 |
# Valores de reseteo para los estados de Gradio
|
| 35 |
+
reset_image_orig_state = None
|
| 36 |
+
reset_tokens_data_state = []
|
| 37 |
+
reset_highlight_index_state = -1
|
| 38 |
reset_image_filename_state = None
|
| 39 |
|
| 40 |
# Actualizaciones para los componentes de la interfaz
|
| 41 |
+
api_key_update = gr.update(value="", visible=True)
|
| 42 |
image_input_update = gr.update(value=None, visible=True)
|
| 43 |
image_output_update = gr.update(value=None, visible=False)
|
| 44 |
df_update = gr.update(value=[])
|
|
|
|
| 47 |
tb_update = gr.update(value="", visible=False)
|
| 48 |
dd_update = gr.update(value="O", visible=False)
|
| 49 |
|
| 50 |
+
# Componentes de Adición (ocultar y resetear)
|
| 51 |
tb_new_token_update = gr.update(value="", visible=False)
|
| 52 |
btn_add_token_update = gr.update(visible=False)
|
| 53 |
reset_new_bbox_state = None
|
|
|
|
| 55 |
status_update = "Sube una imagen para comenzar..."
|
| 56 |
|
| 57 |
return (
|
| 58 |
+
reset_image_orig_state, # 0. image_orig_state
|
| 59 |
+
reset_tokens_data_state, # 1. tokens_data_state
|
| 60 |
+
reset_highlight_index_state, # 2. highlight_index_state
|
| 61 |
+
reset_image_filename_state, # 3. image_filename_state
|
| 62 |
+
api_key_update, # 4. api_key_input 💡
|
| 63 |
+
image_input_update, # 5. image_input_file
|
| 64 |
+
image_output_update, # 6. image_output_display
|
| 65 |
+
df_update, # 7. df_label_input
|
| 66 |
+
tb_update, # 8. tb_token_editor
|
| 67 |
+
dd_update, # 9. dd_tag_selector
|
| 68 |
+
tb_new_token_update, # 10. tb_new_token_text
|
| 69 |
+
btn_add_token_update, # 11. btn_add_new_token
|
| 70 |
+
reset_new_bbox_state, # 12. state_new_bbox
|
| 71 |
+
status_update # 13. status_output
|
| 72 |
)
|
| 73 |
|
| 74 |
|
| 75 |
+
# --- FUNCIÓN AUXILIAR DE FLUJO: OCR y Gemini (MODIFICADA) ---
|
| 76 |
|
| 77 |
+
def process_image(image, api_key: str):
|
| 78 |
+
"""
|
| 79 |
+
Ejecuta el OCR, la inferencia de Gemini (si hay API Key) y el preprocesamiento
|
| 80 |
+
inicial, guardando la imagen.
|
| 81 |
+
"""
|
| 82 |
+
# 💡 La función ahora requiere api_key
|
| 83 |
if image is None:
|
| 84 |
+
# Nota: 8 outputs para la CONEXIÓN 1
|
| 85 |
return None, [], None, [], "Sube una imagen para comenzar...", gr.update(visible=True), gr.update(visible=False, value=None), None
|
| 86 |
|
| 87 |
try:
|
| 88 |
+
# process_and_setup requiere image y api_key (la lógica de consulta condicional está dentro de ocr_processor)
|
| 89 |
+
# Retorna: image_orig, tokens_data, highlighted_image, df_data, status, image_filename
|
| 90 |
+
result = process_and_setup(image, api_key)
|
| 91 |
|
| 92 |
if result[0] is None:
|
| 93 |
+
# Nota: 8 outputs para la CONEXIÓN 1
|
| 94 |
return None, [], None, [], "Error en el procesamiento del OCR. Verifica logs.", gr.update(visible=True), gr.update(visible=False, value=None), None
|
| 95 |
|
| 96 |
image_orig, tokens_data, highlighted_image, df_data, status, image_filename = result
|
|
|
|
| 101 |
for t, n in zip(df_data['token'], df_data['ner_tag']):
|
| 102 |
df_rows.append([t, n])
|
| 103 |
|
| 104 |
+
# Nota: 8 outputs para la CONEXIÓN 1
|
| 105 |
return (
|
| 106 |
image_orig,
|
| 107 |
tokens_data,
|
|
|
|
| 115 |
|
| 116 |
except Exception as e:
|
| 117 |
print(f"Error en process_image: {str(e)}")
|
| 118 |
+
# Nota: 8 outputs para la CONEXIÓN 1
|
| 119 |
return None, [], None, [], f"Error: {str(e)}", gr.update(visible=True), gr.update(visible=False, value=None), None
|
| 120 |
+
|
| 121 |
def capture_highlight_index(evt: gr.SelectData):
|
| 122 |
"""Captura el índice de fila (0-index) seleccionado en el DataFrame."""
|
| 123 |
if evt and evt.index is not None and evt.index[0] is not None:
|
| 124 |
return evt.index[0]
|
| 125 |
return gr.State(-1)
|
| 126 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
# --- INTERFAZ GRADIO (GR.BLOCKS) ---
|
| 128 |
|
| 129 |
with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
|
|
|
|
| 131 |
"""
|
| 132 |
# 🧾 Anotador NER para Facturas (LayoutXLM)
|
| 133 |
|
| 134 |
+
**Instrucciones:** 1. **Sube** una imagen (y opcionalmente la Clave API de Gemini para asistencia de etiquetado). La imagen se guarda automáticamente en `dataset/imagenes`.
|
| 135 |
2. **Edita** los tokens o etiquetas. Los cambios se aplican automáticamente.
|
| 136 |
3. Haz clic en **'Guardar Anotación Actual (JSON)'** para confirmar los datos de la factura actual en `dataset/anotacion_factura.json`.
|
| 137 |
4. Haz clic en **'Descargar Dataset Completo (.zip)'** para obtener todas las imágenes y el JSON consolidado.
|
|
|
|
| 143 |
tokens_data_state = gr.State([])
|
| 144 |
highlight_index_state = gr.State(-1)
|
| 145 |
image_filename_state = gr.State(None)
|
| 146 |
+
STATE_NEW_BBOX = gr.State(value=None) # Estado para BBox Manual
|
|
|
|
|
|
|
| 147 |
|
| 148 |
with gr.Row():
|
| 149 |
with gr.Column(scale=1):
|
| 150 |
# Columna Izquierda: Carga y Visualización
|
| 151 |
+
# 💡 setup_image_components retorna: api_key_input, image_input_file, image_output_display
|
| 152 |
+
api_key_input, image_input_file, image_output_display = setup_image_components()
|
| 153 |
+
|
| 154 |
+
# Se muestran explícitamente los componentes de entrada
|
| 155 |
+
api_key_input
|
| 156 |
+
image_input_file
|
| 157 |
|
| 158 |
status_output = gr.Markdown("Sube una imagen para comenzar...")
|
| 159 |
btn_clear = gr.Button("🗑️ Quitar Imagen / Nuevo Documento", visible=True)
|
|
|
|
| 162 |
# Columna Derecha: Edición de Etiquetas
|
| 163 |
gr.Markdown("### 2. Edición de Etiquetas NER")
|
| 164 |
|
| 165 |
+
# 💡 setup_label_components ya retorna los componentes de adición manual
|
| 166 |
(
|
| 167 |
df_label_input, tb_token_editor, dd_tag_selector,
|
| 168 |
btn_save_annotation, btn_export, file_output,
|
| 169 |
+
tb_new_token_text, btn_add_new_token, temp_state_dummy
|
| 170 |
) = setup_label_components()
|
| 171 |
|
| 172 |
# Dataframe
|
|
|
|
| 176 |
with gr.Row(visible=True) as editor_row:
|
| 177 |
with gr.Column(scale=2):
|
| 178 |
tb_token_editor
|
| 179 |
+
# Campo de texto para el BBox manual (se muestra condicionalmente)
|
| 180 |
tb_new_token_text
|
| 181 |
with gr.Column(scale=1):
|
| 182 |
dd_tag_selector
|
| 183 |
|
| 184 |
+
# Contenedor para los botones de Guardar/Descargar/Agregar
|
| 185 |
with gr.Row(visible=True):
|
| 186 |
btn_save_annotation
|
| 187 |
btn_export
|
|
|
|
| 188 |
btn_add_new_token
|
| 189 |
|
| 190 |
file_output
|
|
|
|
| 192 |
|
| 193 |
# --- CONEXIONES DE EVENTOS ---
|
| 194 |
|
| 195 |
+
# CONEXIÓN 1: EJECUTAR OCR y Gemini (MODIFICADA)
|
| 196 |
image_input_file.change(
|
| 197 |
fn=process_image,
|
| 198 |
+
inputs=[image_input_file, api_key_input], # 💡 AÑADIR api_key_input
|
| 199 |
outputs=[
|
| 200 |
image_orig_state, tokens_data_state, image_output_display, df_label_input, status_output,
|
| 201 |
image_input_file, image_output_display, image_filename_state
|
|
|
|
| 203 |
api_name=False
|
| 204 |
)
|
| 205 |
|
| 206 |
+
# CONEXIÓN 2: Selección de FILA
|
| 207 |
df_label_input.select(
|
| 208 |
fn=capture_highlight_index,
|
| 209 |
inputs=None,
|
|
|
|
| 220 |
api_name=False
|
| 221 |
)
|
| 222 |
|
| 223 |
+
# CONEXIÓN 3: Edición de Tag o Token (Actualiza estado y UI)
|
|
|
|
| 224 |
dd_tag_selector.change(
|
| 225 |
fn=lambda t, d, i, new_tag_val: update_dataframe_and_state(t, d, new_tag_val, None, i, 'tag'),
|
| 226 |
inputs=[tokens_data_state, df_label_input, highlight_index_state, dd_tag_selector],
|
|
|
|
| 244 |
outputs=[tokens_data_state, image_output_display],
|
| 245 |
api_name=False
|
| 246 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
|
| 248 |
+
# CONEXIÓN 4: Guardar y Exportar
|
| 249 |
btn_save_annotation.click(
|
| 250 |
fn=save_current_annotation_to_json,
|
| 251 |
inputs=[image_orig_state, tokens_data_state, image_filename_state],
|
|
|
|
| 253 |
api_name=False
|
| 254 |
)
|
| 255 |
|
|
|
|
| 256 |
btn_export.click(
|
| 257 |
fn=export_and_zip_dataset,
|
| 258 |
inputs=[image_orig_state, tokens_data_state, image_filename_state],
|
|
|
|
| 260 |
api_name=False
|
| 261 |
)
|
| 262 |
|
| 263 |
+
# CONEXIÓN 5: Limpiar y Reiniciar (MODIFICADA)
|
| 264 |
btn_clear.click(
|
| 265 |
fn=clear_ui_and_reset_states,
|
| 266 |
+
inputs=[api_key_input, tb_new_token_text, btn_add_new_token, STATE_NEW_BBOX], # 💡 AÑADIR api_key_input
|
| 267 |
outputs=[
|
| 268 |
image_orig_state, tokens_data_state, highlight_index_state,
|
| 269 |
+
image_filename_state, api_key_input, image_input_file, image_output_display, # 💡 AÑADIR api_key_input
|
| 270 |
df_label_input, tb_token_editor, dd_tag_selector,
|
| 271 |
+
tb_new_token_text, btn_add_new_token, STATE_NEW_BBOX,
|
| 272 |
status_output
|
| 273 |
],
|
| 274 |
api_name=False
|
gemini_ner_client.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# gemini_ner_client.py
|
| 2 |
+
|
| 3 |
+
from google import genai
|
| 4 |
+
from google.genai import types
|
| 5 |
+
import json
|
| 6 |
+
from error_handler import ErrorHandler
|
| 7 |
+
from ner_tags import BASE_TAGS, ALL_NER_TAGS
|
| 8 |
+
|
| 9 |
+
# --- Función para generar las instrucciones del prompt ---
|
| 10 |
+
def _generate_prompt_instructions(tokens_data: list):
|
| 11 |
+
"""
|
| 12 |
+
Genera el texto de entrada para el modelo Gemini, incluyendo los tokens y
|
| 13 |
+
sus coordenadas normalizadas.
|
| 14 |
+
"""
|
| 15 |
+
token_entries = []
|
| 16 |
+
for i, item in enumerate(tokens_data):
|
| 17 |
+
# Formato: [ID] token (x_min, y_min, x_max, y_max)
|
| 18 |
+
bbox_str = ', '.join(map(str, item['bbox_norm']))
|
| 19 |
+
token_entries.append(f"[{i}] {item['token']} ({bbox_str})")
|
| 20 |
+
|
| 21 |
+
# Listar las entidades base que el modelo debe identificar
|
| 22 |
+
entity_list = ', '.join(BASE_TAGS)
|
| 23 |
+
|
| 24 |
+
# 💡 RESTRINGIR LAS ETIQUETAS EXPLÍCITAMENTE
|
| 25 |
+
valid_tags_str = ", ".join(ALL_NER_TAGS)
|
| 26 |
+
|
| 27 |
+
system_prompt = (
|
| 28 |
+
"Eres un experto en reconocimiento de entidades nombradas (NER) para facturas. "
|
| 29 |
+
"Tu tarea es asignar etiquetas BIO a una lista de tokens de OCR. "
|
| 30 |
+
"El formato de salida DEBE ser un único objeto JSON con una clave 'annotations', "
|
| 31 |
+
"cuyo valor es una lista de diccionarios, donde cada diccionario tiene las claves 'id' y 'ner_tag'."
|
| 32 |
+
f"Las ÚNICAS etiquetas VÁLIDAS que puedes usar son: {valid_tags_str}. " # 💡 RESTRICCIÓN CLAVE EN EL PROMPT
|
| 33 |
+
"Si un token no es una entidad relevante, DEBES usar la etiqueta 'O'."
|
| 34 |
+
"El ID de cada token en el JSON debe ser el índice numérico que aparece entre corchetes ([ID])."
|
| 35 |
+
)
|
| 36 |
+
|
| 37 |
+
user_prompt = (
|
| 38 |
+
"Identifica y etiqueta las entidades BIO para los siguientes tokens. "
|
| 39 |
+
"NO cambies el ID. Incluye *todos* los IDs en la respuesta JSON. "
|
| 40 |
+
"Lista de Tokens: \n" + '\n'.join(token_entries)
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
return system_prompt, user_prompt
|
| 44 |
+
|
| 45 |
+
# --- Función Principal de Inferencia con Gemini ---
|
| 46 |
+
def get_gemini_ner_tags(api_key: str, tokens_data: list) -> list:
|
| 47 |
+
"""
|
| 48 |
+
Consulta la API de Gemini para obtener etiquetas NER para los tokens de OCR.
|
| 49 |
+
Retorna la lista de tokens_data actualizada con las nuevas etiquetas.
|
| 50 |
+
"""
|
| 51 |
+
# 💡 CONDICIÓN CLAVE: Solo se ejecuta si hay api_key
|
| 52 |
+
if not api_key:
|
| 53 |
+
print("Saltando NER asistido: No se proporcionó Clave API de Gemini.")
|
| 54 |
+
return tokens_data
|
| 55 |
+
|
| 56 |
+
# Solo proceder si hay tokens
|
| 57 |
+
if not tokens_data:
|
| 58 |
+
return tokens_data
|
| 59 |
+
|
| 60 |
+
try:
|
| 61 |
+
print("Iniciando NER asistido por Gemini...")
|
| 62 |
+
|
| 63 |
+
# 1. Inicializar el cliente
|
| 64 |
+
client = genai.Client(api_key=api_key)
|
| 65 |
+
# Usamos un modelo que soporta respuesta JSON
|
| 66 |
+
model_name = 'gemini-2.5-flash'
|
| 67 |
+
|
| 68 |
+
system_prompt, user_prompt = _generate_prompt_instructions(tokens_data)
|
| 69 |
+
|
| 70 |
+
# 2. Configurar el formato de respuesta JSON forzado
|
| 71 |
+
config = types.GenerateContentConfig(
|
| 72 |
+
system_instruction=system_prompt,
|
| 73 |
+
response_mime_type="application/json",
|
| 74 |
+
response_schema=types.Schema(
|
| 75 |
+
type=types.Type.OBJECT,
|
| 76 |
+
properties={
|
| 77 |
+
"annotations": types.Schema(
|
| 78 |
+
type=types.Type.ARRAY,
|
| 79 |
+
description="Lista de anotaciones de tokens.",
|
| 80 |
+
items=types.Schema(
|
| 81 |
+
type=types.Type.OBJECT,
|
| 82 |
+
properties={
|
| 83 |
+
"id": types.Schema(type=types.Type.INTEGER, description="El índice del token original."),
|
| 84 |
+
"ner_tag": types.Schema(type=types.Type.STRING, description="La etiqueta BIO asignada."),
|
| 85 |
+
},
|
| 86 |
+
required=["id", "ner_tag"]
|
| 87 |
+
)
|
| 88 |
+
)
|
| 89 |
+
},
|
| 90 |
+
required=["annotations"]
|
| 91 |
+
)
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
# 3. Llamar a la API
|
| 95 |
+
response = client.models.generate_content(
|
| 96 |
+
model=model_name,
|
| 97 |
+
contents=user_prompt,
|
| 98 |
+
config=config,
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# 4. Procesar la respuesta
|
| 102 |
+
response_json = json.loads(response.text)
|
| 103 |
+
new_annotations = response_json.get('annotations', [])
|
| 104 |
+
|
| 105 |
+
# 5. Aplicar las nuevas etiquetas
|
| 106 |
+
updated_tokens_data = tokens_data.copy()
|
| 107 |
+
|
| 108 |
+
for ann in new_annotations:
|
| 109 |
+
token_id = ann.get('id')
|
| 110 |
+
ner_tag = ann.get('ner_tag')
|
| 111 |
+
|
| 112 |
+
if (token_id is not None and ner_tag is not None and
|
| 113 |
+
0 <= token_id < len(updated_tokens_data)):
|
| 114 |
+
|
| 115 |
+
# 💡 VALIDACIÓN DEL TAG GENERADO: Si el tag no existe, usa 'O' por defecto
|
| 116 |
+
if ner_tag not in ALL_NER_TAGS:
|
| 117 |
+
print(f"Advertencia: Tag '{ner_tag}' no válido generado por Gemini. Usando 'O'.")
|
| 118 |
+
ner_tag = 'O'
|
| 119 |
+
|
| 120 |
+
updated_tokens_data[token_id]['ner_tag'] = ner_tag
|
| 121 |
+
|
| 122 |
+
return updated_tokens_data
|
| 123 |
+
|
| 124 |
+
except Exception as e:
|
| 125 |
+
# Muestra el error de Gradio y retorna los datos originales
|
| 126 |
+
ErrorHandler.handle_ocr_error(f"Fallo en la inferencia de Gemini: {e}")
|
| 127 |
+
return tokens_data
|
image_loader.py
CHANGED
|
@@ -4,6 +4,14 @@ import gradio as gr
|
|
| 4 |
def setup_image_components():
|
| 5 |
"""Define y retorna los componentes de imagen de Gradio."""
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
image_input = gr.Image(
|
| 8 |
type="pil",
|
| 9 |
label="1. Cargar Imagen de Factura",
|
|
@@ -20,5 +28,4 @@ def setup_image_components():
|
|
| 20 |
visible=False # Ajustado para reflejar el estado inicial de app.py
|
| 21 |
)
|
| 22 |
|
| 23 |
-
|
| 24 |
-
return image_input, highlighted_image_output
|
|
|
|
| 4 |
def setup_image_components():
|
| 5 |
"""Define y retorna los componentes de imagen de Gradio."""
|
| 6 |
|
| 7 |
+
api_key_input = gr.Textbox(
|
| 8 |
+
label="Clave API de Gemini (Opcional)",
|
| 9 |
+
type="password",
|
| 10 |
+
value="",
|
| 11 |
+
interactive=True,
|
| 12 |
+
visible=False
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
image_input = gr.Image(
|
| 16 |
type="pil",
|
| 17 |
label="1. Cargar Imagen de Factura",
|
|
|
|
| 28 |
visible=False # Ajustado para reflejar el estado inicial de app.py
|
| 29 |
)
|
| 30 |
|
| 31 |
+
return api_key_input, image_input, highlighted_image_output
|
|
|
label_editor.py
CHANGED
|
@@ -3,9 +3,10 @@ import json
|
|
| 3 |
import pandas as pd
|
| 4 |
import os
|
| 5 |
import zipfile
|
| 6 |
-
|
| 7 |
-
from
|
| 8 |
-
from
|
|
|
|
| 9 |
|
| 10 |
# --- Configuración de Directorios ---
|
| 11 |
DATASET_BASE_DIR = "dataset"
|
|
@@ -13,12 +14,57 @@ JSON_FILENAME = "anotacion_factura.json"
|
|
| 13 |
TEMP_ZIP_FILENAME = "dataset.zip"
|
| 14 |
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
# --- Funciones de Configuración y UI ---
|
|
|
|
| 17 |
|
| 18 |
def setup_label_components():
|
| 19 |
"""
|
| 20 |
-
Configura y retorna los componentes de edición de etiquetas
|
| 21 |
-
el nuevo botón 'Guardar Anotación'.
|
| 22 |
"""
|
| 23 |
|
| 24 |
# 1. Dataframe NO INTERACTIVO (Solo para visualización y selección de fila)
|
|
@@ -27,7 +73,7 @@ def setup_label_components():
|
|
| 27 |
col_count=(2, "fixed"),
|
| 28 |
datatype=["str", "str"],
|
| 29 |
label="Tabla de Tokens y Etiquetas (Haga clic en la FILA para seleccionar y editar abajo)",
|
| 30 |
-
interactive=False,
|
| 31 |
wrap=True,
|
| 32 |
value=[]
|
| 33 |
)
|
|
@@ -40,7 +86,8 @@ def setup_label_components():
|
|
| 40 |
)
|
| 41 |
|
| 42 |
dd_tag_selector = gr.Dropdown(
|
| 43 |
-
|
|
|
|
| 44 |
label="Etiqueta NER Seleccionada",
|
| 45 |
value="O",
|
| 46 |
interactive=True,
|
|
@@ -48,8 +95,6 @@ def setup_label_components():
|
|
| 48 |
)
|
| 49 |
|
| 50 |
# 3. Botones y Salida
|
| 51 |
-
|
| 52 |
-
# Botón para guardar solo la factura actual en el JSON
|
| 53 |
tb_new_token_text = gr.Textbox(
|
| 54 |
label="Nuevo Texto a Agregar",
|
| 55 |
interactive=True,
|
|
@@ -64,53 +109,57 @@ def setup_label_components():
|
|
| 64 |
)
|
| 65 |
|
| 66 |
btn_save_annotation = gr.Button("3. Guardar Anotación Actual (JSON)", variant="primary")
|
| 67 |
-
|
| 68 |
-
# Botón de Descargar ZIP (ahora es el paso 4)
|
| 69 |
btn_export = gr.Button("4. Descargar Dataset Completo (.zip)", variant="secondary")
|
| 70 |
file_output = gr.File(label="Archivo ZIP del Dataset (Imágenes + Anotaciones)")
|
| 71 |
|
| 72 |
# 5. Nuevo Estado Temporal
|
| 73 |
state_new_bbox = gr.State(value=None)
|
| 74 |
|
| 75 |
-
# Retornar el nuevo componente
|
| 76 |
return (
|
| 77 |
df_label_input, tb_token_editor, dd_tag_selector,
|
| 78 |
btn_save_annotation, btn_export, file_output,
|
| 79 |
tb_new_token_text, btn_add_new_token, state_new_bbox
|
| 80 |
)
|
| 81 |
|
| 82 |
-
#
|
|
|
|
|
|
|
| 83 |
|
| 84 |
def display_selected_row(tokens_data: list, highlight_index: int):
|
| 85 |
"""
|
| 86 |
Muestra el token y la etiqueta de la fila seleccionada en los editores externos.
|
|
|
|
| 87 |
"""
|
| 88 |
if highlight_index >= 0 and highlight_index < len(tokens_data):
|
| 89 |
token = tokens_data[highlight_index]['token']
|
| 90 |
ner_tag = tokens_data[highlight_index]['ner_tag']
|
| 91 |
|
| 92 |
-
#
|
| 93 |
-
|
| 94 |
|
|
|
|
| 95 |
return (
|
| 96 |
-
gr.update(value=token, visible=True),
|
| 97 |
-
gr.update(value=ner_tag, visible=True),
|
| 98 |
highlight_index
|
| 99 |
)
|
| 100 |
|
| 101 |
# Si no hay selección válida, oculta los componentes
|
| 102 |
-
|
| 103 |
-
return gr.update(value="", visible=False),
|
| 104 |
|
| 105 |
|
| 106 |
-
#
|
|
|
|
|
|
|
| 107 |
|
| 108 |
def update_dataframe_and_state(tokens_data: list, df_data_current, new_tag: str, new_token: str, row_index: int, update_type: str):
|
| 109 |
"""
|
| 110 |
Función unificada para actualizar la lista de tokens (estado) y el Dataframe (UI).
|
|
|
|
| 111 |
"""
|
| 112 |
|
| 113 |
-
#
|
| 114 |
if isinstance(df_data_current, pd.DataFrame):
|
| 115 |
df_list = df_data_current.values.tolist()
|
| 116 |
else:
|
|
@@ -119,37 +168,44 @@ def update_dataframe_and_state(tokens_data: list, df_data_current, new_tag: str,
|
|
| 119 |
if row_index < 0 or row_index >= len(df_list):
|
| 120 |
return tokens_data, df_list
|
| 121 |
|
| 122 |
-
if update_type == 'tag':
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
df_list[row_index][1] = new_tag
|
| 124 |
tokens_data[row_index]['ner_tag'] = new_tag
|
| 125 |
-
|
|
|
|
| 126 |
df_list[row_index][0] = new_token
|
| 127 |
tokens_data[row_index]['token'] = new_token
|
| 128 |
|
| 129 |
return tokens_data, df_list
|
| 130 |
|
| 131 |
|
| 132 |
-
#
|
|
|
|
|
|
|
| 133 |
|
| 134 |
def update_ui(image_orig, tokens_data: list, df_labels: list, highlight_index: int):
|
| 135 |
"""Actualiza la imagen resaltada basándose en el estado interno de los tokens."""
|
| 136 |
-
# Generar la imagen resaltada.
|
| 137 |
highlighted_image = draw_boxes(image_orig, tokens_data, highlight_index)
|
| 138 |
|
| 139 |
-
# Devolver el estado interno (que ya está actualizado) y la imagen
|
| 140 |
return tokens_data, highlighted_image
|
| 141 |
|
| 142 |
|
| 143 |
-
#
|
|
|
|
|
|
|
| 144 |
|
| 145 |
def save_current_annotation_to_json(image_orig, tokens_data: list, image_filename: str):
|
| 146 |
"""
|
| 147 |
-
Guarda la anotación del documento actual en el archivo JSON
|
| 148 |
-
Retorna mensajes de estado a Gradio.
|
| 149 |
"""
|
|
|
|
| 150 |
if not tokens_data or not image_filename:
|
| 151 |
gr.Warning("Error: No hay tokens o la imagen no fue procesada.")
|
| 152 |
-
# Retorna el path (vacío) y el mensaje de estado (que se mostrará en status_output)
|
| 153 |
return None, "Guardado fallido: No hay datos de imagen o tokens."
|
| 154 |
|
| 155 |
# 1. Asegurarse de que la carpeta 'dataset' exista
|
|
@@ -160,10 +216,12 @@ def save_current_annotation_to_json(image_orig, tokens_data: list, image_filenam
|
|
| 160 |
W, H = image_orig.size
|
| 161 |
new_annotations = []
|
| 162 |
for item in tokens_data:
|
|
|
|
|
|
|
| 163 |
new_annotations.append({
|
| 164 |
'token': item['token'],
|
| 165 |
'bbox_normalized': [int(b) for b in item['bbox_norm']],
|
| 166 |
-
'ner_tag':
|
| 167 |
})
|
| 168 |
|
| 169 |
new_document_entry = {
|
|
@@ -183,20 +241,17 @@ def save_current_annotation_to_json(image_orig, tokens_data: list, image_filenam
|
|
| 183 |
if isinstance(data, list):
|
| 184 |
existing_document_list = data
|
| 185 |
except Exception:
|
| 186 |
-
# Si falla la lectura, comenzar con una lista vacía
|
| 187 |
pass
|
| 188 |
|
| 189 |
# 4. Consolidar: Agregar o Sobrescribir el documento actual
|
| 190 |
is_new = True
|
| 191 |
for i, doc in enumerate(existing_document_list):
|
| 192 |
if doc.get('image', {}).get('name') == image_filename:
|
| 193 |
-
# Documento ya existe (lo editamos), lo sobrescribimos con la versión editada
|
| 194 |
existing_document_list[i] = new_document_entry
|
| 195 |
is_new = False
|
| 196 |
break
|
| 197 |
|
| 198 |
if is_new:
|
| 199 |
-
# Es un documento nuevo, lo añadimos al final
|
| 200 |
existing_document_list.append(new_document_entry)
|
| 201 |
|
| 202 |
# 5. Escribir la lista completa
|
|
@@ -216,19 +271,19 @@ def save_current_annotation_to_json(image_orig, tokens_data: list, image_filenam
|
|
| 216 |
return None, f"Error en guardado: {error_msg}"
|
| 217 |
|
| 218 |
|
| 219 |
-
#
|
|
|
|
|
|
|
| 220 |
|
| 221 |
def export_and_zip_dataset(image_orig, tokens_data: list, image_filename: str):
|
| 222 |
"""
|
| 223 |
-
|
| 224 |
-
2. Comprime toda la carpeta 'dataset/' en un archivo ZIP.
|
| 225 |
"""
|
| 226 |
|
| 227 |
-
# Paso 1: Asegurar que la anotación actual se guarde
|
| 228 |
-
# Utilizamos None para evitar que los mensajes de save_current_annotation_to_json sobrescriban el status_output antes del ZIP
|
| 229 |
save_current_annotation_to_json(image_orig, tokens_data, image_filename)
|
| 230 |
|
| 231 |
-
# Paso 2: Obtener el total de documentos para el mensaje
|
| 232 |
json_path = os.path.join(DATASET_BASE_DIR, JSON_FILENAME)
|
| 233 |
total_docs = 0
|
| 234 |
if os.path.exists(json_path):
|
|
@@ -238,33 +293,24 @@ def export_and_zip_dataset(image_orig, tokens_data: list, image_filename: str):
|
|
| 238 |
if isinstance(data, list):
|
| 239 |
total_docs = len(data)
|
| 240 |
except Exception:
|
| 241 |
-
pass
|
| 242 |
|
| 243 |
if total_docs == 0:
|
| 244 |
gr.Warning("Error: No hay datos guardados para generar el ZIP.")
|
| 245 |
return None, "Error: No se puede generar el ZIP. El archivo JSON está vacío o no existe."
|
| 246 |
|
| 247 |
# Paso 3: Crear el archivo ZIP
|
| 248 |
-
# Esto garantiza que el archivo se crea temporalmente y se limpia después.
|
| 249 |
zip_path = "/tmp/dataset.zip"
|
| 250 |
|
| 251 |
try:
|
| 252 |
-
# Asegurarse de eliminar cualquier ZIP previo que pudiera estar en /tmp
|
| 253 |
if os.path.exists(zip_path):
|
| 254 |
os.remove(zip_path)
|
| 255 |
|
| 256 |
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
| 257 |
-
|
| 258 |
-
# Recorrer todos los archivos y carpetas dentro de DATASET_BASE_DIR
|
| 259 |
for root, dirs, files in os.walk(DATASET_BASE_DIR):
|
| 260 |
for file in files:
|
| 261 |
file_path = os.path.join(root, file)
|
| 262 |
-
# La ruta que aparecerá dentro del ZIP (relativa a la carpeta 'dataset')
|
| 263 |
arcname = os.path.relpath(file_path, DATASET_BASE_DIR)
|
| 264 |
-
|
| 265 |
-
# ❌ NO EXCLUIMOS NADA (el ZIP ya no se crea dentro del propio dataset)
|
| 266 |
-
# Eliminamos el if file != TEMP_ZIP_FILENAME:
|
| 267 |
-
|
| 268 |
zipf.write(file_path, arcname)
|
| 269 |
|
| 270 |
gr.Info(f"✅ Dataset listo para descargar. Contiene {total_docs} documentos.")
|
|
|
|
| 3 |
import pandas as pd
|
| 4 |
import os
|
| 5 |
import zipfile
|
| 6 |
+
# Asume que estos módulos existen en tu proyecto
|
| 7 |
+
from error_handler import ErrorHandler
|
| 8 |
+
from ner_tags import ALL_NER_TAGS
|
| 9 |
+
from ocr_processor import draw_boxes
|
| 10 |
|
| 11 |
# --- Configuración de Directorios ---
|
| 12 |
DATASET_BASE_DIR = "dataset"
|
|
|
|
| 14 |
TEMP_ZIP_FILENAME = "dataset.zip"
|
| 15 |
|
| 16 |
|
| 17 |
+
# ----------------------------------------------------------------------
|
| 18 |
+
# 💡 FUNCIÓN AUXILIAR: Filtrado de Tags Utilizados
|
| 19 |
+
# ----------------------------------------------------------------------
|
| 20 |
+
|
| 21 |
+
def _get_available_tags(tokens_data: list, current_index: int) -> list:
|
| 22 |
+
"""
|
| 23 |
+
Determina qué etiquetas 'B-' ya están en uso por otros tokens.
|
| 24 |
+
|
| 25 |
+
Retorna la lista de ALL_NER_TAGS filtrada, excluyendo las opciones 'B-'
|
| 26 |
+
que ya aparecen en tokens_data para otros índices (principio de no repetición).
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
used_b_tags = set()
|
| 30 |
+
|
| 31 |
+
# 1. Identificar qué etiquetas 'B-' ya están en uso en el documento
|
| 32 |
+
for i, item in enumerate(tokens_data):
|
| 33 |
+
# Excluimos el token actual para permitir la re-edición del tag existente
|
| 34 |
+
if i == current_index:
|
| 35 |
+
continue
|
| 36 |
+
|
| 37 |
+
tag = item['ner_tag']
|
| 38 |
+
# Si el tag es un tag 'B-', lo añadimos al conjunto de tags usados
|
| 39 |
+
if tag.startswith('B-'):
|
| 40 |
+
used_b_tags.add(tag)
|
| 41 |
+
|
| 42 |
+
# 2. Filtrar ALL_NER_TAGS
|
| 43 |
+
filtered_tags = []
|
| 44 |
+
current_tag = tokens_data[current_index]['ner_tag'] if 0 <= current_index < len(tokens_data) else None
|
| 45 |
+
|
| 46 |
+
for tag in ALL_NER_TAGS:
|
| 47 |
+
# La etiqueta 'O' y las etiquetas 'I-' siempre están disponibles.
|
| 48 |
+
if tag == 'O' or tag.startswith('I-'):
|
| 49 |
+
filtered_tags.append(tag)
|
| 50 |
+
# Una etiqueta 'B-' solo se añade si NO está en el conjunto de tags ya usados
|
| 51 |
+
elif tag.startswith('B-') and tag not in used_b_tags:
|
| 52 |
+
filtered_tags.append(tag)
|
| 53 |
+
|
| 54 |
+
# 3. Asegurar que el tag actual se pueda seleccionar (si ya estaba aplicado y fue filtrado)
|
| 55 |
+
if current_tag and current_tag.startswith('B-') and current_tag not in filtered_tags:
|
| 56 |
+
filtered_tags.append(current_tag)
|
| 57 |
+
filtered_tags.sort() # Opcional: Reordenar si se añade al final
|
| 58 |
+
|
| 59 |
+
return filtered_tags
|
| 60 |
+
|
| 61 |
+
# ----------------------------------------------------------------------
|
| 62 |
# --- Funciones de Configuración y UI ---
|
| 63 |
+
# ----------------------------------------------------------------------
|
| 64 |
|
| 65 |
def setup_label_components():
|
| 66 |
"""
|
| 67 |
+
Configura y retorna los componentes de edición de etiquetas.
|
|
|
|
| 68 |
"""
|
| 69 |
|
| 70 |
# 1. Dataframe NO INTERACTIVO (Solo para visualización y selección de fila)
|
|
|
|
| 73 |
col_count=(2, "fixed"),
|
| 74 |
datatype=["str", "str"],
|
| 75 |
label="Tabla de Tokens y Etiquetas (Haga clic en la FILA para seleccionar y editar abajo)",
|
| 76 |
+
interactive=False,
|
| 77 |
wrap=True,
|
| 78 |
value=[]
|
| 79 |
)
|
|
|
|
| 86 |
)
|
| 87 |
|
| 88 |
dd_tag_selector = gr.Dropdown(
|
| 89 |
+
# Inicialmente usa la lista completa; las opciones se actualizarán dinámicamente
|
| 90 |
+
choices=ALL_NER_TAGS,
|
| 91 |
label="Etiqueta NER Seleccionada",
|
| 92 |
value="O",
|
| 93 |
interactive=True,
|
|
|
|
| 95 |
)
|
| 96 |
|
| 97 |
# 3. Botones y Salida
|
|
|
|
|
|
|
| 98 |
tb_new_token_text = gr.Textbox(
|
| 99 |
label="Nuevo Texto a Agregar",
|
| 100 |
interactive=True,
|
|
|
|
| 109 |
)
|
| 110 |
|
| 111 |
btn_save_annotation = gr.Button("3. Guardar Anotación Actual (JSON)", variant="primary")
|
|
|
|
|
|
|
| 112 |
btn_export = gr.Button("4. Descargar Dataset Completo (.zip)", variant="secondary")
|
| 113 |
file_output = gr.File(label="Archivo ZIP del Dataset (Imágenes + Anotaciones)")
|
| 114 |
|
| 115 |
# 5. Nuevo Estado Temporal
|
| 116 |
state_new_bbox = gr.State(value=None)
|
| 117 |
|
|
|
|
| 118 |
return (
|
| 119 |
df_label_input, tb_token_editor, dd_tag_selector,
|
| 120 |
btn_save_annotation, btn_export, file_output,
|
| 121 |
tb_new_token_text, btn_add_new_token, state_new_bbox
|
| 122 |
)
|
| 123 |
|
| 124 |
+
# ----------------------------------------------------------------------
|
| 125 |
+
# --- FUNCIÓN: Obtener la fila seleccionada y mostrar editores (MODIFICADA) ---
|
| 126 |
+
# ----------------------------------------------------------------------
|
| 127 |
|
| 128 |
def display_selected_row(tokens_data: list, highlight_index: int):
|
| 129 |
"""
|
| 130 |
Muestra el token y la etiqueta de la fila seleccionada en los editores externos.
|
| 131 |
+
ACTUALIZA las opciones del Dropdown para filtrar las etiquetas 'B-' ya utilizadas.
|
| 132 |
"""
|
| 133 |
if highlight_index >= 0 and highlight_index < len(tokens_data):
|
| 134 |
token = tokens_data[highlight_index]['token']
|
| 135 |
ner_tag = tokens_data[highlight_index]['ner_tag']
|
| 136 |
|
| 137 |
+
# 💡 Lógica de Filtrado: Obtener solo los tags disponibles
|
| 138 |
+
available_tags = _get_available_tags(tokens_data, highlight_index)
|
| 139 |
|
| 140 |
+
# Muestra los componentes, actualizando las opciones del Dropdown
|
| 141 |
return (
|
| 142 |
+
gr.update(value=token, visible=True), # tb_token_editor
|
| 143 |
+
gr.update(value=ner_tag, choices=available_tags, visible=True), # dd_tag_selector
|
| 144 |
highlight_index
|
| 145 |
)
|
| 146 |
|
| 147 |
# Si no hay selección válida, oculta los componentes
|
| 148 |
+
# Y restablece las opciones del Dropdown a la lista completa
|
| 149 |
+
return gr.update(value="", visible=False), gr.update(value="O", choices=ALL_NER_TAGS, visible=False), -1
|
| 150 |
|
| 151 |
|
| 152 |
+
# ----------------------------------------------------------------------
|
| 153 |
+
# --- FUNCIÓN: Actualizar el Dataframe y el estado de los tokens (VALIDACIÓN) ---
|
| 154 |
+
# ----------------------------------------------------------------------
|
| 155 |
|
| 156 |
def update_dataframe_and_state(tokens_data: list, df_data_current, new_tag: str, new_token: str, row_index: int, update_type: str):
|
| 157 |
"""
|
| 158 |
Función unificada para actualizar la lista de tokens (estado) y el Dataframe (UI).
|
| 159 |
+
Incluye validación para forzar la etiqueta a 'O' si no es válida.
|
| 160 |
"""
|
| 161 |
|
| 162 |
+
# Asegurar que se trabaja con una lista de listas
|
| 163 |
if isinstance(df_data_current, pd.DataFrame):
|
| 164 |
df_list = df_data_current.values.tolist()
|
| 165 |
else:
|
|
|
|
| 168 |
if row_index < 0 or row_index >= len(df_list):
|
| 169 |
return tokens_data, df_list
|
| 170 |
|
| 171 |
+
if update_type == 'tag' and new_tag is not None:
|
| 172 |
+
# 💡 VALIDACIÓN CRÍTICA: Forzar el tag a 'O' si no está en la lista maestra
|
| 173 |
+
if new_tag not in ALL_NER_TAGS:
|
| 174 |
+
print(f"ADVERTENCIA: Tag no válido detectado ('{new_tag}') en índice {row_index}. Forzando a 'O'.")
|
| 175 |
+
new_tag = 'O'
|
| 176 |
+
|
| 177 |
df_list[row_index][1] = new_tag
|
| 178 |
tokens_data[row_index]['ner_tag'] = new_tag
|
| 179 |
+
|
| 180 |
+
elif update_type == 'token' and new_token is not None:
|
| 181 |
df_list[row_index][0] = new_token
|
| 182 |
tokens_data[row_index]['token'] = new_token
|
| 183 |
|
| 184 |
return tokens_data, df_list
|
| 185 |
|
| 186 |
|
| 187 |
+
# ----------------------------------------------------------------------
|
| 188 |
+
# --- Función de Sincronización de UI/Estados (Sin Cambios) ---
|
| 189 |
+
# ----------------------------------------------------------------------
|
| 190 |
|
| 191 |
def update_ui(image_orig, tokens_data: list, df_labels: list, highlight_index: int):
|
| 192 |
"""Actualiza la imagen resaltada basándose en el estado interno de los tokens."""
|
|
|
|
| 193 |
highlighted_image = draw_boxes(image_orig, tokens_data, highlight_index)
|
| 194 |
|
|
|
|
| 195 |
return tokens_data, highlighted_image
|
| 196 |
|
| 197 |
|
| 198 |
+
# ----------------------------------------------------------------------
|
| 199 |
+
# --- FUNCIÓN: Guardar Anotación Actual (JSON) (Mínima Modificación) ---
|
| 200 |
+
# ----------------------------------------------------------------------
|
| 201 |
|
| 202 |
def save_current_annotation_to_json(image_orig, tokens_data: list, image_filename: str):
|
| 203 |
"""
|
| 204 |
+
Guarda la anotación del documento actual en el archivo JSON.
|
|
|
|
| 205 |
"""
|
| 206 |
+
|
| 207 |
if not tokens_data or not image_filename:
|
| 208 |
gr.Warning("Error: No hay tokens o la imagen no fue procesada.")
|
|
|
|
| 209 |
return None, "Guardado fallido: No hay datos de imagen o tokens."
|
| 210 |
|
| 211 |
# 1. Asegurarse de que la carpeta 'dataset' exista
|
|
|
|
| 216 |
W, H = image_orig.size
|
| 217 |
new_annotations = []
|
| 218 |
for item in tokens_data:
|
| 219 |
+
# Asegurar que el tag que se guarda es válido
|
| 220 |
+
tag_to_save = item['ner_tag'] if item['ner_tag'] in ALL_NER_TAGS else 'O'
|
| 221 |
new_annotations.append({
|
| 222 |
'token': item['token'],
|
| 223 |
'bbox_normalized': [int(b) for b in item['bbox_norm']],
|
| 224 |
+
'ner_tag': tag_to_save
|
| 225 |
})
|
| 226 |
|
| 227 |
new_document_entry = {
|
|
|
|
| 241 |
if isinstance(data, list):
|
| 242 |
existing_document_list = data
|
| 243 |
except Exception:
|
|
|
|
| 244 |
pass
|
| 245 |
|
| 246 |
# 4. Consolidar: Agregar o Sobrescribir el documento actual
|
| 247 |
is_new = True
|
| 248 |
for i, doc in enumerate(existing_document_list):
|
| 249 |
if doc.get('image', {}).get('name') == image_filename:
|
|
|
|
| 250 |
existing_document_list[i] = new_document_entry
|
| 251 |
is_new = False
|
| 252 |
break
|
| 253 |
|
| 254 |
if is_new:
|
|
|
|
| 255 |
existing_document_list.append(new_document_entry)
|
| 256 |
|
| 257 |
# 5. Escribir la lista completa
|
|
|
|
| 271 |
return None, f"Error en guardado: {error_msg}"
|
| 272 |
|
| 273 |
|
| 274 |
+
# ----------------------------------------------------------------------
|
| 275 |
+
# --- FUNCIÓN PRINCIPAL DE EXPORTACIÓN: ZIP (Sin Cambios) ---
|
| 276 |
+
# ----------------------------------------------------------------------
|
| 277 |
|
| 278 |
def export_and_zip_dataset(image_orig, tokens_data: list, image_filename: str):
|
| 279 |
"""
|
| 280 |
+
Asegura que la anotación actual se guarde y luego comprime toda la carpeta 'dataset/' en un archivo ZIP.
|
|
|
|
| 281 |
"""
|
| 282 |
|
| 283 |
+
# Paso 1: Asegurar que la anotación actual se guarde
|
|
|
|
| 284 |
save_current_annotation_to_json(image_orig, tokens_data, image_filename)
|
| 285 |
|
| 286 |
+
# Paso 2: Obtener el total de documentos para el mensaje
|
| 287 |
json_path = os.path.join(DATASET_BASE_DIR, JSON_FILENAME)
|
| 288 |
total_docs = 0
|
| 289 |
if os.path.exists(json_path):
|
|
|
|
| 293 |
if isinstance(data, list):
|
| 294 |
total_docs = len(data)
|
| 295 |
except Exception:
|
| 296 |
+
pass
|
| 297 |
|
| 298 |
if total_docs == 0:
|
| 299 |
gr.Warning("Error: No hay datos guardados para generar el ZIP.")
|
| 300 |
return None, "Error: No se puede generar el ZIP. El archivo JSON está vacío o no existe."
|
| 301 |
|
| 302 |
# Paso 3: Crear el archivo ZIP
|
|
|
|
| 303 |
zip_path = "/tmp/dataset.zip"
|
| 304 |
|
| 305 |
try:
|
|
|
|
| 306 |
if os.path.exists(zip_path):
|
| 307 |
os.remove(zip_path)
|
| 308 |
|
| 309 |
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
|
|
|
|
|
| 310 |
for root, dirs, files in os.walk(DATASET_BASE_DIR):
|
| 311 |
for file in files:
|
| 312 |
file_path = os.path.join(root, file)
|
|
|
|
| 313 |
arcname = os.path.relpath(file_path, DATASET_BASE_DIR)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
zipf.write(file_path, arcname)
|
| 315 |
|
| 316 |
gr.Info(f"✅ Dataset listo para descargar. Contiene {total_docs} documentos.")
|
ocr_processor.py
CHANGED
|
@@ -3,6 +3,7 @@ import uuid
|
|
| 3 |
from PIL import Image, ImageDraw
|
| 4 |
import numpy as np
|
| 5 |
import cv2
|
|
|
|
| 6 |
|
| 7 |
# Módulos de Doctr
|
| 8 |
from doctr.models import ocr_predictor
|
|
@@ -191,9 +192,9 @@ def save_image_to_dataset(image: Image.Image) -> str:
|
|
| 191 |
|
| 192 |
# --- Función de Flujo Principal ---
|
| 193 |
|
| 194 |
-
def process_and_setup(image_file):
|
| 195 |
"""
|
| 196 |
-
Función inicial: OCR con Doctr
|
| 197 |
configuración del estado y guardar la imagen.
|
| 198 |
"""
|
| 199 |
if image_file is None:
|
|
@@ -201,13 +202,19 @@ def process_and_setup(image_file):
|
|
| 201 |
return None, [], None, empty_df, None, None
|
| 202 |
|
| 203 |
# 💡 Llama a la función de OCR basada en Doctr
|
| 204 |
-
# image_orig es la imagen SIN preprocesar
|
| 205 |
image_orig, tokens_data, _ = get_ocr_data_doctr(image_file)
|
| 206 |
|
| 207 |
if image_orig is None:
|
| 208 |
empty_df = {'token': [], 'ner_tag': []}
|
| 209 |
return None, [], None, empty_df, "Error fatal al procesar el OCR con Doctr. Revise el log.", None
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
# --- Guardar la Imagen Original ---
|
| 212 |
image_filename = save_image_to_dataset(image_orig)
|
| 213 |
|
|
@@ -225,7 +232,7 @@ def process_and_setup(image_file):
|
|
| 225 |
|
| 226 |
# La imagen inicial no tiene resaltado
|
| 227 |
highlighted_image = image_orig.copy()
|
| 228 |
-
msg = f"OCR de Doctr completado. Tokens detectados: {len(tokens_data)}"
|
| 229 |
print(msg)
|
| 230 |
|
| 231 |
return image_orig, tokens_data, highlighted_image, df_data, msg, image_filename
|
|
|
|
| 3 |
from PIL import Image, ImageDraw
|
| 4 |
import numpy as np
|
| 5 |
import cv2
|
| 6 |
+
from gemini_ner_client import get_gemini_ner_tags
|
| 7 |
|
| 8 |
# Módulos de Doctr
|
| 9 |
from doctr.models import ocr_predictor
|
|
|
|
| 192 |
|
| 193 |
# --- Función de Flujo Principal ---
|
| 194 |
|
| 195 |
+
def process_and_setup(image_file, api_key: str): # 💡 ACEPTA LA LLAVE API
|
| 196 |
"""
|
| 197 |
+
Función inicial: OCR con Doctr, **NER asistido por Gemini**,
|
| 198 |
configuración del estado y guardar la imagen.
|
| 199 |
"""
|
| 200 |
if image_file is None:
|
|
|
|
| 202 |
return None, [], None, empty_df, None, None
|
| 203 |
|
| 204 |
# 💡 Llama a la función de OCR basada en Doctr
|
|
|
|
| 205 |
image_orig, tokens_data, _ = get_ocr_data_doctr(image_file)
|
| 206 |
|
| 207 |
if image_orig is None:
|
| 208 |
empty_df = {'token': [], 'ner_tag': []}
|
| 209 |
return None, [], None, empty_df, "Error fatal al procesar el OCR con Doctr. Revise el log.", None
|
| 210 |
+
|
| 211 |
+
# 💡 PASO NUEVO: NER Asistido por Gemini (CONDICIONAL)
|
| 212 |
+
msg_ner_assist = ""
|
| 213 |
+
if api_key:
|
| 214 |
+
# Llama a la función que SÓLO se ejecuta si api_key no es None/vacío
|
| 215 |
+
tokens_data = get_gemini_ner_tags(api_key, tokens_data)
|
| 216 |
+
msg_ner_assist = " (NER asistido)"
|
| 217 |
+
|
| 218 |
# --- Guardar la Imagen Original ---
|
| 219 |
image_filename = save_image_to_dataset(image_orig)
|
| 220 |
|
|
|
|
| 232 |
|
| 233 |
# La imagen inicial no tiene resaltado
|
| 234 |
highlighted_image = image_orig.copy()
|
| 235 |
+
msg = f"OCR de Doctr completado. Tokens detectados: {len(tokens_data)}.{msg_ner_assist}"
|
| 236 |
print(msg)
|
| 237 |
|
| 238 |
return image_orig, tokens_data, highlighted_image, df_data, msg, image_filename
|
requirements.txt
CHANGED
|
@@ -10,6 +10,9 @@ python-doctr==0.7.0
|
|
| 10 |
# Necesario para manejar arrays de imagen (np.array) y funcionalidades de CV
|
| 11 |
opencv-python
|
| 12 |
|
|
|
|
|
|
|
|
|
|
| 13 |
# --- Utilidades ---
|
| 14 |
pillow==10.1.0 # Manipulación de imágenes (PIL)
|
| 15 |
pandas # Manejo de datos (DataFrames de OCR/Tokens)
|
|
|
|
| 10 |
# Necesario para manejar arrays de imagen (np.array) y funcionalidades de CV
|
| 11 |
opencv-python
|
| 12 |
|
| 13 |
+
# --- Requerimientos de IA Generativa (Gemini) ---
|
| 14 |
+
google-genai # Librería oficial para interactuar con la API de Gemini
|
| 15 |
+
|
| 16 |
# --- Utilidades ---
|
| 17 |
pillow==10.1.0 # Manipulación de imágenes (PIL)
|
| 18 |
pandas # Manejo de datos (DataFrames de OCR/Tokens)
|
setup.sh
DELETED
|
@@ -1,5 +0,0 @@
|
|
| 1 |
-
#!/bin/bash
|
| 2 |
-
# Instalar Tesseract OCR y el paquete de idioma español
|
| 3 |
-
apt-get update
|
| 4 |
-
apt-get install -y tesseract-ocr
|
| 5 |
-
apt-get install -y tesseract-ocr-spa
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|