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

Generate json

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