Lucas Gagneten commited on
Commit
f35ce5e
·
1 Parent(s): a3dc003

Gemini client

Browse files
Files changed (7) hide show
  1. app.py +64 -117
  2. gemini_ner_client.py +127 -0
  3. image_loader.py +9 -2
  4. label_editor.py +96 -50
  5. ocr_processor.py +12 -5
  6. requirements.txt +3 -0
  7. 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 ACTUALIZADAS
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
- # 💡 Importar las funciones de adición de BBox
23
- from bbox_adder import add_new_bbox_mode, append_new_token
24
 
 
 
25
 
26
- # --- Función de Limpieza (Mantener) ---
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
- # 💡 Componentes de Adición (ocultar y resetear)
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, # 0. image_orig_state
56
- reset_tokens_data_state, # 1. tokens_data_state
57
- reset_highlight_index_state, # 2. highlight_index_state
58
- reset_image_filename_state, # 3. image_filename_state
59
- image_input_update, # 4. image_input_file
60
- image_output_update, # 5. image_output_display
61
- df_update, # 6. df_label_input
62
- tb_update, # 7. tb_token_editor
63
- dd_update, # 8. dd_tag_selector
64
- tb_new_token_update, # 9. tb_new_token_text 💡
65
- btn_add_token_update, # 10. btn_add_new_token 💡
66
- reset_new_bbox_state, # 11. state_new_bbox 💡
67
- status_update # 12. status_output
 
68
  )
69
 
70
 
71
- # --- FUNCIONES AUXILIARES DE FLUJO ---
72
 
73
- def process_image(image):
74
- """Ejecuta el OCR y el preprocesamiento inicial, guardando la imagen."""
 
 
 
 
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 retorna: image_orig, tokens_data, highlighted_image, df_data, status, image_filename
80
- result = process_and_setup(image)
 
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: Los 8 outputs deben coincidir con la CONEXIÓN 1
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 = setup_image_components()
 
 
 
 
 
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
- # 💡 DESESTRUCTURACIÓN ACTUALIZADA: Capturar los 3 nuevos componentes
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 # state_new_bbox es STATE_NEW_BBOX
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
- # 💡 NUEVO COMPONENTE: Campo de texto para el BBox manual
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 (Mantener)
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 (Mantener)
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 (Mantener)
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], # 💡 Inputs de limpieza
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, # 💡 Outputs de limpieza
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
- # Solo retornamos los dos componentes de imagen que definiste en app.py
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
- from error_handler import ErrorHandler # Asume que tienes este módulo
7
- from ner_tags import ALL_NER_TAGS # Asume que tienes este módulo
8
- from ocr_processor import draw_boxes # Importación forzada para evitar errores de referencia
 
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, incluyendo
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, # Deshabilitar la edición directa
31
  wrap=True,
32
  value=[]
33
  )
@@ -40,7 +86,8 @@ def setup_label_components():
40
  )
41
 
42
  dd_tag_selector = gr.Dropdown(
43
- choices=ALL_NER_TAGS,
 
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
- # --- FUNCIÓN: Obtener la fila seleccionada y mostrar editores ---
 
 
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
- # Muestra los componentes
93
- visible_update = gr.update(visible=True)
94
 
 
95
  return (
96
- gr.update(value=token, visible=True), # tb_token_editor
97
- gr.update(value=ner_tag, visible=True), # dd_tag_selector
98
  highlight_index
99
  )
100
 
101
  # Si no hay selección válida, oculta los componentes
102
- hidden_update = gr.update(visible=False)
103
- return gr.update(value="", visible=False), hidden_update, -1
104
 
105
 
106
- # --- FUNCIÓN: Actualizar el Dataframe y el estado de los tokens ---
 
 
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
- # Manejar el caso de entrada como Pandas DataFrame (por seguridad)
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
- elif update_type == 'token':
 
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
- # --- Función de Sincronización de UI/Estados ---
 
 
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
- # --- FUNCIÓN: Guardar Anotación Actual (JSON) ---
 
 
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, sobrescribe si existe.
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': item['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
- # --- FUNCIÓN PRINCIPAL DE EXPORTACIÓN: ZIP ---
 
 
220
 
221
  def export_and_zip_dataset(image_orig, tokens_data: list, image_filename: str):
222
  """
223
- 1. Llama a save_current_annotation_to_json para asegurar que el último documento esté guardado.
224
- 2. Comprime toda la carpeta 'dataset/' en un archivo ZIP.
225
  """
226
 
227
- # Paso 1: Asegurar que la anotación actual se guarde (para incluir los últimos cambios)
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 (si el guardado fue exitoso)
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 # Si el archivo es inválido, total_docs = 0
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 (incluyendo preprocesamiento),
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