Lucas Gagneten commited on
Commit
cd12bfc
·
1 Parent(s): fe821af

dataset.zip

Browse files
Files changed (2) hide show
  1. app.py +57 -40
  2. label_editor.py +124 -76
app.py CHANGED
@@ -1,12 +1,20 @@
1
  import gradio as gr
2
- from image_loader import setup_image_components # Asumo que este módulo existe
3
  from ocr_processor import setup_tesseract, process_and_setup
4
- from label_editor import setup_label_components, update_ui, export_data, update_dataframe_and_state, display_selected_row, ALL_NER_TAGS
 
 
 
 
 
 
 
 
5
 
6
  # Configurar Tesseract al inicio
7
  setup_tesseract()
8
 
9
- # --- Función de Limpieza (Actualizada) ---
10
 
11
  def clear_ui_and_reset_states():
12
  """Limpia los componentes de la interfaz y resetea los estados."""
@@ -16,7 +24,7 @@ def clear_ui_and_reset_states():
16
  reset_image_orig_state = None
17
  reset_tokens_data_state = []
18
  reset_highlight_index_state = -1
19
- reset_image_filename_state = None # Estado del nombre de archivo único
20
 
21
  # Actualizaciones para los componentes de la interfaz
22
  image_input_update = gr.update(value=None, visible=True)
@@ -46,20 +54,17 @@ def clear_ui_and_reset_states():
46
  # --- FUNCIONES AUXILIARES DE FLUJO ---
47
 
48
  def process_image(image):
49
- """Ejecuta el OCR y el preprocesamiento inicial."""
50
  if image is None:
51
- # Añadir None para image_filename_state en el retorno de error
52
  return None, [], None, [], "Sube una imagen para comenzar...", gr.update(visible=True), gr.update(visible=False), None
53
-
54
  try:
55
- # process_and_setup ahora retorna: image_orig, tokens_data, highlighted_image, df_data, status, image_filename
56
  result = process_and_setup(image)
57
 
58
  if result[0] is None:
59
- # Añadir None para image_filename_state en el retorno de error
60
  return None, [], None, [], "Error en el procesamiento del OCR. Verifica logs.", gr.update(visible=True), gr.update(visible=False, value=None), None
61
 
62
- # Desempaquetar el resultado
63
  image_orig, tokens_data, highlighted_image, df_data, status, image_filename = result
64
 
65
  # Convertir datos para el DataFrame de Gradio (lista de listas)
@@ -81,7 +86,6 @@ def process_image(image):
81
 
82
  except Exception as e:
83
  print(f"Error en process_image: {str(e)}")
84
- # Asegurar que la función siempre retorne el número correcto de outputs
85
  return None, [], None, [], f"Error: {str(e)}", gr.update(visible=True), gr.update(visible=False, value=None), None
86
 
87
  def capture_highlight_index(evt: gr.SelectData):
@@ -96,21 +100,20 @@ def capture_highlight_index(evt: gr.SelectData):
96
  with gr.Blocks(title="Anotador NER de Facturas (LayoutXLM)") as app:
97
  gr.Markdown(
98
  """
99
- # 🧾 Anotador NER para Facturas Argentinas (LayoutXLM)
100
 
101
- **Instrucciones:**
102
- 1. **Sube** una imagen. La imagen se guarda automáticamente en `dataset/imagenes`.
103
- 2. **Haz clic en una FILA** de la tabla para **seleccionar** el token y mostrar los editores.
104
- 3. El cambio de la **Etiqueta NER** (Dropdown) y la edición del **Token** (Textbox) se aplican **automáticamente**.
105
- 4. Al exportar, la información se **agrega** a `anotacion_factura.json`.
106
  """
107
  )
108
 
109
  # Componentes de estado
110
  image_orig_state = gr.State(None)
111
- tokens_data_state = gr.State([]) # Estado de la verdad que incluye tokens y bboxes
112
- highlight_index_state = gr.State(-1) # Índice de la fila seleccionada
113
- image_filename_state = gr.State(None) # Nombre de archivo único para la exportación
114
 
115
  with gr.Row():
116
  with gr.Column(scale=1):
@@ -139,83 +142,97 @@ with gr.Blocks(title="Anotador NER de Facturas (LayoutXLM)") as app:
139
  # Columna Derecha: Edición de Etiquetas
140
  gr.Markdown("### 2. Edición de Etiquetas NER")
141
 
142
- # CAPTURAR COMPONENTES (df_label_input, tb_token_editor, dd_tag_selector, btn_export, file_output)
143
- df_label_input, tb_token_editor, dd_tag_selector, btn_export, file_output = setup_label_components()
144
 
145
- # Contenedor para los editores (Token y Tag en dos columnas contiguas)
 
 
 
146
  with gr.Row(visible=True) as editor_row:
147
  with gr.Column(scale=2):
148
- tb_token_editor # Textbox a la izquierda (más ancho)
149
  with gr.Column(scale=1):
150
- dd_tag_selector # Dropdown a la derecha (más estrecho)
 
 
 
 
 
 
 
 
 
151
 
152
 
153
  # --- CONEXIONES DE EVENTOS ---
154
 
155
- # CONEXIÓN 1: EJECUTAR OCR AUTOMÁTICAMENTE AL CARGAR IMAGEN
156
  image_input_file.change(
157
  fn=process_image,
158
  inputs=[image_input_file],
159
  outputs=[
160
  image_orig_state, tokens_data_state, image_output_display, df_label_input, status_output,
161
- image_input_file, image_output_display, image_filename_state # Nuevo estado
162
  ],
163
  api_name=False
164
  )
165
 
166
- # CONEXIÓN 2: Al hacer clic en una FILA (Selección/Resaltado)
167
  df_label_input.select(
168
  fn=capture_highlight_index,
169
  inputs=None,
170
  outputs=[highlight_index_state],
171
  queue=False
172
  ).then(
173
- # Paso A: Mostrar el token y la etiqueta en los editores externos
174
  fn=display_selected_row,
175
  inputs=[tokens_data_state, highlight_index_state],
176
  outputs=[tb_token_editor, dd_tag_selector, highlight_index_state],
177
  ).then(
178
- # Paso B: Resaltar la fila en la imagen
179
  fn=update_ui,
180
  inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
181
  outputs=[tokens_data_state, image_output_display],
182
  api_name=False
183
  )
184
 
185
- # CONEXIÓN 3.1: Dropdown cambia la etiqueta NER (Actualización Automática con .change())
186
  dd_tag_selector.change(
187
- # Capturar el valor actual del Dropdown para pasarlo a la función de actualización
188
  fn=lambda t, d, i, new_tag_val: update_dataframe_and_state(t, d, new_tag_val, None, i, 'tag'),
189
  inputs=[tokens_data_state, df_label_input, highlight_index_state, dd_tag_selector],
190
  outputs=[tokens_data_state, df_label_input],
191
  ).then(
192
- # Refrescar la imagen con el resaltado actualizado
193
  fn=update_ui,
194
  inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
195
  outputs=[tokens_data_state, image_output_display],
196
  api_name=False
197
  )
198
 
199
- # CONEXIÓN 3.2: Textbox cambia el Token (Actualización Automática con .blur y .submit)
200
  token_update_events = [tb_token_editor.blur, tb_token_editor.submit]
201
 
202
  for event in token_update_events:
203
  event(
204
- # Capturar el valor actual del Textbox
205
  fn=lambda t, d, i, new_token_val: update_dataframe_and_state(t, d, None, new_token_val, i, 'token'),
206
  inputs=[tokens_data_state, df_label_input, highlight_index_state, tb_token_editor],
207
  outputs=[tokens_data_state, df_label_input],
208
  ).then(
209
- # Refrescar la imagen
210
  fn=update_ui,
211
  inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
212
  outputs=[tokens_data_state, image_output_display],
213
  api_name=False
214
  )
215
-
216
- # CONEXIÓN 4: Exportar (Añadiendo image_filename_state)
 
 
 
 
 
 
 
 
217
  btn_export.click(
218
- fn=export_data,
219
  inputs=[image_orig_state, tokens_data_state, image_filename_state],
220
  outputs=[file_output, status_output],
221
  api_name=False
@@ -229,7 +246,7 @@ with gr.Blocks(title="Anotador NER de Facturas (LayoutXLM)") as app:
229
  image_orig_state,
230
  tokens_data_state,
231
  highlight_index_state,
232
- image_filename_state, # Nuevo output de limpieza
233
  image_input_file,
234
  image_output_display,
235
  df_label_input,
 
1
  import gradio as gr
2
+ from image_loader import setup_image_components # Asume que tienes este módulo
3
  from ocr_processor import setup_tesseract, process_and_setup
4
+ from label_editor import (
5
+ setup_label_components,
6
+ update_ui,
7
+ save_current_annotation_to_json, # Nueva función de guardado
8
+ export_and_zip_dataset, # Nueva función de exportación
9
+ update_dataframe_and_state,
10
+ display_selected_row,
11
+ ALL_NER_TAGS
12
+ )
13
 
14
  # Configurar Tesseract al inicio
15
  setup_tesseract()
16
 
17
+ # --- Función de Limpieza ---
18
 
19
  def clear_ui_and_reset_states():
20
  """Limpia los componentes de la interfaz y resetea los estados."""
 
24
  reset_image_orig_state = None
25
  reset_tokens_data_state = []
26
  reset_highlight_index_state = -1
27
+ reset_image_filename_state = None
28
 
29
  # Actualizaciones para los componentes de la interfaz
30
  image_input_update = gr.update(value=None, visible=True)
 
54
  # --- FUNCIONES AUXILIARES DE FLUJO ---
55
 
56
  def process_image(image):
57
+ """Ejecuta el OCR y el preprocesamiento inicial, guardando la imagen."""
58
  if image is None:
 
59
  return None, [], None, [], "Sube una imagen para comenzar...", gr.update(visible=True), gr.update(visible=False), None
60
+
61
  try:
62
+ # process_and_setup retorna: image_orig, tokens_data, highlighted_image, df_data, status, image_filename
63
  result = process_and_setup(image)
64
 
65
  if result[0] is None:
 
66
  return None, [], None, [], "Error en el procesamiento del OCR. Verifica logs.", gr.update(visible=True), gr.update(visible=False, value=None), None
67
 
 
68
  image_orig, tokens_data, highlighted_image, df_data, status, image_filename = result
69
 
70
  # Convertir datos para el DataFrame de Gradio (lista de listas)
 
86
 
87
  except Exception as e:
88
  print(f"Error en process_image: {str(e)}")
 
89
  return None, [], None, [], f"Error: {str(e)}", gr.update(visible=True), gr.update(visible=False, value=None), None
90
 
91
  def capture_highlight_index(evt: gr.SelectData):
 
100
  with gr.Blocks(title="Anotador NER de Facturas (LayoutXLM)") as app:
101
  gr.Markdown(
102
  """
103
+ # 🧾 Anotador NER para Facturas (LayoutXLM)
104
 
105
+ **Instrucciones:** 1. **Sube** una imagen. La imagen se guarda automáticamente en `dataset/imagenes`.
106
+ 2. **Edita** los tokens o etiquetas. Los cambios se aplican automáticamente.
107
+ 3. Haz clic en **'Guardar Anotación Actual (JSON)'** para confirmar los datos de la factura actual en `dataset/anotacion_factura.json`.
108
+ 4. Haz clic en **'Descargar Dataset Completo (.zip)'** para obtener todas las imágenes y el JSON consolidado.
 
109
  """
110
  )
111
 
112
  # Componentes de estado
113
  image_orig_state = gr.State(None)
114
+ tokens_data_state = gr.State([])
115
+ highlight_index_state = gr.State(-1)
116
+ image_filename_state = gr.State(None) # Nombre de archivo único
117
 
118
  with gr.Row():
119
  with gr.Column(scale=1):
 
142
  # Columna Derecha: Edición de Etiquetas
143
  gr.Markdown("### 2. Edición de Etiquetas NER")
144
 
145
+ # CAPTURAR EL NUEVO BOTÓN: btn_save_annotation
146
+ df_label_input, tb_token_editor, dd_tag_selector, btn_save_annotation, btn_export, file_output = setup_label_components()
147
 
148
+ # Dataframe
149
+ df_label_input
150
+
151
+ # Contenedor para los editores (Token y Tag)
152
  with gr.Row(visible=True) as editor_row:
153
  with gr.Column(scale=2):
154
+ tb_token_editor
155
  with gr.Column(scale=1):
156
+ dd_tag_selector
157
+
158
+ # Contenedor para los botones de Guardar/Descargar
159
+ with gr.Row(visible=True):
160
+ with gr.Column(scale=1):
161
+ btn_save_annotation # NUEVO BOTÓN: Guardar JSON
162
+ with gr.Column(scale=1):
163
+ btn_export # Botón: Descargar ZIP
164
+
165
+ file_output
166
 
167
 
168
  # --- CONEXIONES DE EVENTOS ---
169
 
170
+ # CONEXIÓN 1: EJECUTAR OCR
171
  image_input_file.change(
172
  fn=process_image,
173
  inputs=[image_input_file],
174
  outputs=[
175
  image_orig_state, tokens_data_state, image_output_display, df_label_input, status_output,
176
+ image_input_file, image_output_display, image_filename_state
177
  ],
178
  api_name=False
179
  )
180
 
181
+ # CONEXIÓN 2: Selección de FILA
182
  df_label_input.select(
183
  fn=capture_highlight_index,
184
  inputs=None,
185
  outputs=[highlight_index_state],
186
  queue=False
187
  ).then(
 
188
  fn=display_selected_row,
189
  inputs=[tokens_data_state, highlight_index_state],
190
  outputs=[tb_token_editor, dd_tag_selector, highlight_index_state],
191
  ).then(
 
192
  fn=update_ui,
193
  inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
194
  outputs=[tokens_data_state, image_output_display],
195
  api_name=False
196
  )
197
 
198
+ # CONEXIÓN 3.1: Dropdown cambia la etiqueta NER (Actualización Automática)
199
  dd_tag_selector.change(
 
200
  fn=lambda t, d, i, new_tag_val: update_dataframe_and_state(t, d, new_tag_val, None, i, 'tag'),
201
  inputs=[tokens_data_state, df_label_input, highlight_index_state, dd_tag_selector],
202
  outputs=[tokens_data_state, df_label_input],
203
  ).then(
 
204
  fn=update_ui,
205
  inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
206
  outputs=[tokens_data_state, image_output_display],
207
  api_name=False
208
  )
209
 
210
+ # CONEXIÓN 3.2: Textbox cambia el Token (Actualización Automática)
211
  token_update_events = [tb_token_editor.blur, tb_token_editor.submit]
212
 
213
  for event in token_update_events:
214
  event(
 
215
  fn=lambda t, d, i, new_token_val: update_dataframe_and_state(t, d, None, new_token_val, i, 'token'),
216
  inputs=[tokens_data_state, df_label_input, highlight_index_state, tb_token_editor],
217
  outputs=[tokens_data_state, df_label_input],
218
  ).then(
 
219
  fn=update_ui,
220
  inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
221
  outputs=[tokens_data_state, image_output_display],
222
  api_name=False
223
  )
224
+
225
+ # CONEXIÓN 3.3: Guardar Anotación Actual (JSON)
226
+ btn_save_annotation.click(
227
+ fn=save_current_annotation_to_json,
228
+ inputs=[image_orig_state, tokens_data_state, image_filename_state],
229
+ outputs=[file_output, status_output],
230
+ api_name=False
231
+ )
232
+
233
+ # CONEXIÓN 4: Exportar y Comprimir (ZIP)
234
  btn_export.click(
235
+ fn=export_and_zip_dataset,
236
  inputs=[image_orig_state, tokens_data_state, image_filename_state],
237
  outputs=[file_output, status_output],
238
  api_name=False
 
246
  image_orig_state,
247
  tokens_data_state,
248
  highlight_index_state,
249
+ image_filename_state,
250
  image_input_file,
251
  image_output_display,
252
  df_label_input,
label_editor.py CHANGED
@@ -1,21 +1,24 @@
1
  import gradio as gr
2
- import os
3
  import json
4
  import pandas as pd
5
- from error_handler import ErrorHandler
6
- from ner_tags import ALL_NER_TAGS
 
 
 
7
 
8
- # Definir la ruta base del dataset (debe coincidir con ocr_processor.py si es necesario)
9
  DATASET_BASE_DIR = "dataset"
10
  JSON_FILENAME = "anotacion_factura.json"
 
 
11
 
12
  # --- Funciones de Configuración y UI ---
13
 
14
  def setup_label_components():
15
  """
16
- Configura y retorna los componentes de edición de etiquetas:
17
- DataFrame (no interactivo), Textbox para Token, Dropdown para Tag,
18
- botón de exportar y salida de archivo.
19
  """
20
 
21
  # 1. Dataframe NO INTERACTIVO (Solo para visualización y selección de fila)
@@ -26,36 +29,35 @@ def setup_label_components():
26
  label="Tabla de Tokens y Etiquetas (Haga clic en la FILA para seleccionar y editar abajo)",
27
  interactive=False, # Deshabilitar la edición directa
28
  wrap=True,
29
- value=[]
30
  )
31
 
32
- # 2. NUEVOS COMPONENTES DE EDICIÓN EXTERNOS
33
-
34
- # Input para editar el token
35
  tb_token_editor = gr.Textbox(
36
- # Eliminamos la instrucción "presionar ENTER" para reflejar el cambio automático
37
  label="Token Seleccionado",
38
  interactive=True,
39
- visible=False # Oculto por defecto
40
  )
41
 
42
- # Dropdown para editar la etiqueta NER
43
  dd_tag_selector = gr.Dropdown(
44
  choices=ALL_NER_TAGS,
45
  label="Etiqueta NER Seleccionada",
46
  value="O",
47
  interactive=True,
48
- visible=False # Oculto por defecto
49
  )
50
 
51
- # Eliminamos btn_apply_token
52
-
53
- # Resto de componentes
54
- btn_export = gr.Button("Exportar a JSON para Fine-Tuning", variant="secondary")
55
- file_output = gr.File(label="Archivo de Anotación JSON")
56
 
57
- # Retornamos los nuevos componentes (sin btn_apply_token)
58
- return df_label_input, tb_token_editor, dd_tag_selector, btn_export, file_output
 
 
 
 
 
 
 
59
 
60
 
61
  # --- FUNCIÓN: Obtener la fila seleccionada y mostrar editores ---
@@ -71,7 +73,6 @@ def display_selected_row(tokens_data: list, highlight_index: int):
71
  # Muestra los componentes
72
  visible_update = gr.update(visible=True)
73
 
74
- # DEVOLVER LA ETIQUETA ACTUAL del token para inicializar el Dropdown correctamente
75
  return (
76
  gr.update(value=token, visible=True), # tb_token_editor
77
  gr.update(value=ner_tag, visible=True), # dd_tag_selector
@@ -83,14 +84,14 @@ def display_selected_row(tokens_data: list, highlight_index: int):
83
  return gr.update(value="", visible=False), hidden_update, -1
84
 
85
 
86
- # --- FUNCIÓN: Actualizar el Dataframe y el estado de los tokens (Mantener) ---
87
 
88
- def update_dataframe_and_state(tokens_data: list, df_data_current: list, new_tag: str, new_token: str, row_index: int, update_type: str):
89
  """
90
- Función unificada para actualizar la lista de tokens y el Dataframe.
91
- (La lógica de esta función se mantiene sin cambios)
92
  """
93
 
 
94
  if isinstance(df_data_current, pd.DataFrame):
95
  df_list = df_data_current.values.tolist()
96
  else:
@@ -108,32 +109,37 @@ def update_dataframe_and_state(tokens_data: list, df_data_current: list, new_tag
108
 
109
  return tokens_data, df_list
110
 
111
- # --- FUNCIÓN: Actualizar la UI al cambiar la selección ---
 
 
112
  def update_ui(image_orig, tokens_data: list, df_labels: list, highlight_index: int):
113
- from ocr_processor import draw_boxes
 
114
  highlighted_image = draw_boxes(image_orig, tokens_data, highlight_index)
 
 
115
  return tokens_data, highlighted_image
116
 
117
 
118
- # --- Función de Exportación ---
119
 
120
- def export_data(image_orig, tokens_data: list, image_filename: str): # <-- ACEPTAR image_filename
121
  """
122
- Exporta los datos anotados a un archivo JSON estructurado.
123
- Si el archivo existe, agrega las nuevas anotaciones al final.
124
  """
125
- if not tokens_data or not image_filename: # Validar que el nombre del archivo exista
126
- ErrorHandler.show_error("Error: No hay datos de anotación o nombre de archivo para exportar.")
127
- return None, None
 
128
 
129
  # 1. Asegurarse de que la carpeta 'dataset' exista
130
  os.makedirs(DATASET_BASE_DIR, exist_ok=True)
131
- # 2. Definir la ruta completa del archivo JSON
132
- temp_file = os.path.join(DATASET_BASE_DIR, JSON_FILENAME)
133
 
 
 
134
  new_annotations = []
135
-
136
- # 1. Preparar las nuevas anotaciones en el formato requerido
137
  for item in tokens_data:
138
  new_annotations.append({
139
  'token': item['token'],
@@ -141,63 +147,105 @@ def export_data(image_orig, tokens_data: list, image_filename: str): # <-- ACEPT
141
  'ner_tag': item['ner_tag']
142
  })
143
 
144
- # 2. Preparar el nuevo elemento a agregar al array de anotaciones (el objeto completo de la factura)
145
- W, H = image_orig.size
146
-
147
  new_document_entry = {
148
  'image': {
149
  'size': [W, H],
150
- 'name': image_filename # <-- USAR EL NOMBRE ÚNICO
151
  },
152
  'annotations': new_annotations
153
  }
154
 
155
- # 3. Leer el archivo existente y obtener los datos previos
156
- # El archivo JSON principal será un ARRAY de objetos de documentos.
157
  existing_document_list = []
158
- total_annotations_count = 0
159
-
160
  try:
161
  if os.path.exists(temp_file):
162
  with open(temp_file, 'r', encoding='utf-8') as f:
163
  data = json.load(f)
164
-
165
- # ASUMIMOS que el archivo JSON es una lista [] de documentos
166
  if isinstance(data, list):
167
  existing_document_list = data
168
-
169
- # Si el archivo está en el formato antiguo {metadata: {}, annotations: []} (solo 1 doc)
170
- elif isinstance(data, dict) and 'annotations' in data:
171
- # Lo convertimos a la nueva estructura de lista si solo tiene un documento
172
- # Nota: Esto es peligroso, idealmente el formato de exportación debe ser consistente.
173
- # Asumiremos la nueva estructura JSON será: [ {doc1}, {doc2}, ... ]
174
- pass
175
-
176
- except json.JSONDecodeError:
177
- gr.Warning(f"Advertencia: El archivo {temp_file} existe pero está corrupto/vacío. Se creará uno nuevo.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
178
  except Exception as e:
179
- ErrorHandler.handle_export_error(e)
180
- gr.Warning(f"Error al leer el archivo existente. Se agregará solo este documento.")
 
 
181
 
182
- # 4. Consolidar los datos y contar
 
 
 
 
 
 
183
 
184
- # 4.1. Agregar el nuevo documento a la lista
185
- existing_document_list.append(new_document_entry)
 
 
 
 
 
 
 
 
 
 
 
 
 
186
 
187
- # 4.2. Contar el total de tokens en todas las facturas
188
- for doc in existing_document_list:
189
- total_annotations_count += len(doc.get('annotations', []))
190
 
191
- # 5. Escribir la lista completa de documentos de vuelta al archivo
 
 
192
  try:
193
- with open(temp_file, 'w', encoding='utf-8') as f:
194
- # Escribir la lista de documentos directamente
195
- json.dump(existing_document_list, f, ensure_ascii=False, indent=4)
196
 
197
- gr.Info(f"✅ Exportación exitosa. Documento '{image_filename}' agregado. Total de documentos: {len(existing_document_list)}. Tokens totales: {total_annotations_count}")
198
- return temp_file, f"Exportación JSON completada. Documentos: {len(existing_document_list)}, Tokens: {total_annotations_count}"
 
 
 
 
 
 
 
 
 
 
 
199
 
200
  except Exception as e:
201
  error_msg = ErrorHandler.handle_export_error(e)
202
- gr.Warning(f"Error al escribir el archivo: {error_msg}")
203
- return None, f"Error en exportación: {error_msg}"
 
1
  import gradio as gr
 
2
  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"
12
  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)
 
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
  )
34
 
35
+ # 2. Componentes de Edición Externos
 
 
36
  tb_token_editor = gr.Textbox(
 
37
  label="Token Seleccionado",
38
  interactive=True,
39
+ visible=False
40
  )
41
 
 
42
  dd_tag_selector = gr.Dropdown(
43
  choices=ALL_NER_TAGS,
44
  label="Etiqueta NER Seleccionada",
45
  value="O",
46
  interactive=True,
47
+ visible=False
48
  )
49
 
50
+ # 3. Botones y Salida
 
 
 
 
51
 
52
+ # NUEVO BOTÓN: Para guardar solo la factura actual en el JSON
53
+ btn_save_annotation = gr.Button("3. Guardar Anotación Actual (JSON)", variant="primary")
54
+
55
+ # Botón de Descargar ZIP (ahora es el paso 4)
56
+ btn_export = gr.Button("4. Descargar Dataset Completo (.zip)", variant="secondary")
57
+ file_output = gr.File(label="Archivo ZIP del Dataset (Imágenes + Anotaciones)")
58
+
59
+ # Retornar el nuevo componente
60
+ return df_label_input, tb_token_editor, dd_tag_selector, btn_save_annotation, btn_export, file_output
61
 
62
 
63
  # --- FUNCIÓN: Obtener la fila seleccionada y mostrar editores ---
 
73
  # Muestra los componentes
74
  visible_update = gr.update(visible=True)
75
 
 
76
  return (
77
  gr.update(value=token, visible=True), # tb_token_editor
78
  gr.update(value=ner_tag, visible=True), # dd_tag_selector
 
84
  return gr.update(value="", visible=False), hidden_update, -1
85
 
86
 
87
+ # --- FUNCIÓN: Actualizar el Dataframe y el estado de los tokens ---
88
 
89
+ def update_dataframe_and_state(tokens_data: list, df_data_current, new_tag: str, new_token: str, row_index: int, update_type: str):
90
  """
91
+ Función unificada para actualizar la lista de tokens (estado) y el Dataframe (UI).
 
92
  """
93
 
94
+ # Manejar el caso de entrada como Pandas DataFrame (por seguridad)
95
  if isinstance(df_data_current, pd.DataFrame):
96
  df_list = df_data_current.values.tolist()
97
  else:
 
109
 
110
  return tokens_data, df_list
111
 
112
+
113
+ # --- Función de Sincronización de UI/Estados ---
114
+
115
  def update_ui(image_orig, tokens_data: list, df_labels: list, highlight_index: int):
116
+ """Actualiza la imagen resaltada basándose en el estado interno de los tokens."""
117
+ # Generar la imagen resaltada.
118
  highlighted_image = draw_boxes(image_orig, tokens_data, highlight_index)
119
+
120
+ # Devolver el estado interno (que ya está actualizado) y la imagen
121
  return tokens_data, highlighted_image
122
 
123
 
124
+ # --- FUNCIÓN: Guardar Anotación Actual (JSON) ---
125
 
126
+ def save_current_annotation_to_json(image_orig, tokens_data: list, image_filename: str):
127
  """
128
+ Guarda la anotación del documento actual en el archivo JSON, sobrescribe si existe.
129
+ Retorna mensajes de estado a Gradio.
130
  """
131
+ if not tokens_data or not image_filename:
132
+ gr.Warning("Error: No hay tokens o la imagen no fue procesada.")
133
+ # Retorna el path (vacío) y el mensaje de estado (que se mostrará en status_output)
134
+ return None, "Guardado fallido: No hay datos de imagen o tokens."
135
 
136
  # 1. Asegurarse de que la carpeta 'dataset' exista
137
  os.makedirs(DATASET_BASE_DIR, exist_ok=True)
138
+ temp_file = os.path.join(DATASET_BASE_DIR, JSON_FILENAME)
 
139
 
140
+ # 2. Preparar el nuevo elemento a agregar
141
+ W, H = image_orig.size
142
  new_annotations = []
 
 
143
  for item in tokens_data:
144
  new_annotations.append({
145
  'token': item['token'],
 
147
  'ner_tag': item['ner_tag']
148
  })
149
 
 
 
 
150
  new_document_entry = {
151
  'image': {
152
  'size': [W, H],
153
+ 'name': image_filename
154
  },
155
  'annotations': new_annotations
156
  }
157
 
158
+ # 3. Leer el archivo existente
 
159
  existing_document_list = []
 
 
160
  try:
161
  if os.path.exists(temp_file):
162
  with open(temp_file, 'r', encoding='utf-8') as f:
163
  data = json.load(f)
 
 
164
  if isinstance(data, list):
165
  existing_document_list = data
166
+ except Exception:
167
+ # Si falla la lectura, comenzar con una lista vacía
168
+ pass
169
+
170
+ # 4. Consolidar: Agregar o Sobrescribir el documento actual
171
+ is_new = True
172
+ for i, doc in enumerate(existing_document_list):
173
+ if doc.get('image', {}).get('name') == image_filename:
174
+ # Documento ya existe (lo editamos), lo sobrescribimos con la versión editada
175
+ existing_document_list[i] = new_document_entry
176
+ is_new = False
177
+ break
178
+
179
+ if is_new:
180
+ # Es un documento nuevo, lo añadimos al final
181
+ existing_document_list.append(new_document_entry)
182
+
183
+ # 5. Escribir la lista completa
184
+ try:
185
+ with open(temp_file, 'w', encoding='utf-8') as f:
186
+ json.dump(existing_document_list, f, ensure_ascii=False, indent=4)
187
+
188
+ action_message = "actualizados" if not is_new else "agregados"
189
+ total_docs = len(existing_document_list)
190
+ msg = f"Anotación '{image_filename}' {action_message} al JSON. Documentos totales: {total_docs}."
191
+ gr.Info(f"✅ {msg}")
192
+ return None, msg
193
+
194
  except Exception as e:
195
+ error_msg = ErrorHandler.handle_export_error(e)
196
+ gr.Warning(f"Error al escribir el archivo: {error_msg}")
197
+ return None, f"Error en guardado: {error_msg}"
198
+
199
 
200
+ # --- FUNCIÓN PRINCIPAL DE EXPORTACIÓN: ZIP ---
201
+
202
+ def export_and_zip_dataset(image_orig, tokens_data: list, image_filename: str):
203
+ """
204
+ 1. Llama a save_current_annotation_to_json para asegurar que el último documento esté guardado.
205
+ 2. Comprime toda la carpeta 'dataset/' en un archivo ZIP.
206
+ """
207
 
208
+ # Paso 1: Asegurar que la anotación actual se guarde (para incluir los últimos cambios)
209
+ # Utilizamos None para evitar que los mensajes de save_current_annotation_to_json sobrescriban el status_output antes del ZIP
210
+ save_current_annotation_to_json(image_orig, tokens_data, image_filename)
211
+
212
+ # Paso 2: Obtener el total de documentos para el mensaje (si el guardado fue exitoso)
213
+ json_path = os.path.join(DATASET_BASE_DIR, JSON_FILENAME)
214
+ total_docs = 0
215
+ if os.path.exists(json_path):
216
+ try:
217
+ with open(json_path, 'r', encoding='utf-8') as f:
218
+ data = json.load(f)
219
+ if isinstance(data, list):
220
+ total_docs = len(data)
221
+ except Exception:
222
+ pass # Si el archivo es inválido, total_docs = 0
223
 
224
+ if total_docs == 0:
225
+ gr.Warning("Error: No hay datos guardados para generar el ZIP.")
226
+ return None, "Error: No se puede generar el ZIP. El archivo JSON está vacío o no existe."
227
 
228
+ # Paso 3: Crear el archivo ZIP
229
+ zip_path = os.path.join(DATASET_BASE_DIR, TEMP_ZIP_FILENAME)
230
+
231
  try:
232
+ with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
 
 
233
 
234
+ # Recorrer todos los archivos y carpetas dentro de DATASET_BASE_DIR
235
+ for root, dirs, files in os.walk(DATASET_BASE_DIR):
236
+ for file in files:
237
+ file_path = os.path.join(root, file)
238
+ # La ruta que aparecerá dentro del ZIP (relativa a la carpeta 'dataset')
239
+ arcname = os.path.relpath(file_path, DATASET_BASE_DIR)
240
+
241
+ # Excluir el propio archivo ZIP si ya existía
242
+ if file != TEMP_ZIP_FILENAME:
243
+ zipf.write(file_path, arcname)
244
+
245
+ gr.Info(f"✅ Dataset listo para descargar. Contiene {total_docs} documentos.")
246
+ return zip_path, f"Dataset exportado con éxito a {TEMP_ZIP_FILENAME}. Descargue el archivo ZIP. (Total docs: {total_docs})"
247
 
248
  except Exception as e:
249
  error_msg = ErrorHandler.handle_export_error(e)
250
+ gr.Warning(f"Error al comprimir el ZIP: {error_msg}")
251
+ return None, f"Error al comprimir el dataset: {error_msg}"