Lucas Gagneten
Gemini client
f35ce5e
import gradio as gr
from PIL import Image
import pandas as pd
# ----------------------------------------------------------------------
# 💡 1. IMPORTACIONES
# ----------------------------------------------------------------------
# ocr_processor: Funciones de OCR (Doctr) y guardado de imagen
from ocr_processor import process_and_setup
# label_editor: Funciones de setup, edición y persistencia (JSON/ZIP)
from label_editor import (
setup_label_components,
update_ui,
save_current_annotation_to_json,
export_and_zip_dataset,
update_dataframe_and_state,
display_selected_row,
)
# image_loader: Define la UI para la carga de imagen y la API Key
from image_loader import setup_image_components
# bbox_adder: Funciones para añadir manualmente tokens no detectados por OCR
from bbox_adder import add_new_bbox_mode, append_new_token
# --- Función de Limpieza/Reset ---
def clear_ui_and_reset_states(api_key_input, tb_new_token_text, btn_add_new_token, state_new_bbox):
"""Limpia los componentes de la interfaz y resetea los estados a su valor inicial."""
print("Reiniciando la interfaz y los estados...")
# Valores de reseteo para los estados de Gradio
reset_image_orig_state = None
reset_tokens_data_state = []
reset_highlight_index_state = -1
reset_image_filename_state = None
# Actualizaciones para los componentes de la interfaz
api_key_update = gr.update(value="", visible=True)
image_input_update = gr.update(value=None, visible=True)
image_output_update = gr.update(value=None, visible=False)
df_update = gr.update(value=[])
# Componentes de edición (ocultar)
tb_update = gr.update(value="", visible=False)
dd_update = gr.update(value="O", visible=False)
# Componentes de Adición (ocultar y resetear)
tb_new_token_update = gr.update(value="", visible=False)
btn_add_token_update = gr.update(visible=False)
reset_new_bbox_state = None
status_update = "Sube una imagen para comenzar..."
return (
reset_image_orig_state, # 0. image_orig_state
reset_tokens_data_state, # 1. tokens_data_state
reset_highlight_index_state, # 2. highlight_index_state
reset_image_filename_state, # 3. image_filename_state
api_key_update, # 4. api_key_input 💡
image_input_update, # 5. image_input_file
image_output_update, # 6. image_output_display
df_update, # 7. df_label_input
tb_update, # 8. tb_token_editor
dd_update, # 9. dd_tag_selector
tb_new_token_update, # 10. tb_new_token_text
btn_add_token_update, # 11. btn_add_new_token
reset_new_bbox_state, # 12. state_new_bbox
status_update # 13. status_output
)
# --- FUNCIÓN AUXILIAR DE FLUJO: OCR y Gemini (MODIFICADA) ---
def process_image(image, api_key: str):
"""
Ejecuta el OCR, la inferencia de Gemini (si hay API Key) y el preprocesamiento
inicial, guardando la imagen.
"""
# 💡 La función ahora requiere api_key
if image is None:
# Nota: 8 outputs para la CONEXIÓN 1
return None, [], None, [], "Sube una imagen para comenzar...", gr.update(visible=True), gr.update(visible=False, value=None), None
try:
# process_and_setup requiere image y api_key (la lógica de consulta condicional está dentro de ocr_processor)
# Retorna: image_orig, tokens_data, highlighted_image, df_data, status, image_filename
result = process_and_setup(image, api_key)
if result[0] is None:
# Nota: 8 outputs para la CONEXIÓN 1
return None, [], None, [], "Error en el procesamiento del OCR. Verifica logs.", gr.update(visible=True), gr.update(visible=False, value=None), None
image_orig, tokens_data, highlighted_image, df_data, status, image_filename = result
# Convertir datos para el DataFrame de Gradio (lista de listas)
df_rows = []
if df_data and isinstance(df_data, dict):
for t, n in zip(df_data['token'], df_data['ner_tag']):
df_rows.append([t, n])
# Nota: 8 outputs para la CONEXIÓN 1
return (
image_orig,
tokens_data,
highlighted_image,
df_rows,
status,
gr.update(visible=False), # Ocultar image_input_file
gr.update(visible=True), # Mostrar image_output_display
image_filename # Nombre de archivo único
)
except Exception as e:
print(f"Error en process_image: {str(e)}")
# Nota: 8 outputs para la CONEXIÓN 1
return None, [], None, [], f"Error: {str(e)}", gr.update(visible=True), gr.update(visible=False, value=None), None
def capture_highlight_index(evt: gr.SelectData):
"""Captura el índice de fila (0-index) seleccionado en el DataFrame."""
if evt and evt.index is not None and evt.index[0] is not None:
return evt.index[0]
return gr.State(-1)
# --- INTERFAZ GRADIO (GR.BLOCKS) ---
with gr.Blocks(title="Anotador NER de Facturas (Doctr/LayoutXLM)") as app:
gr.Markdown(
"""
# 🧾 Anotador NER para Facturas (LayoutXLM)
**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`.
2. **Edita** los tokens o etiquetas. Los cambios se aplican automáticamente.
3. Haz clic en **'Guardar Anotación Actual (JSON)'** para confirmar los datos de la factura actual en `dataset/anotacion_factura.json`.
4. Haz clic en **'Descargar Dataset Completo (.zip)'** para obtener todas las imágenes y el JSON consolidado.
"""
)
# --- 1. Definición de Estados Globales ---
image_orig_state = gr.State(None)
tokens_data_state = gr.State([])
highlight_index_state = gr.State(-1)
image_filename_state = gr.State(None)
STATE_NEW_BBOX = gr.State(value=None) # Estado para BBox Manual
with gr.Row():
with gr.Column(scale=1):
# Columna Izquierda: Carga y Visualización
# 💡 setup_image_components retorna: api_key_input, image_input_file, image_output_display
api_key_input, image_input_file, image_output_display = setup_image_components()
# Se muestran explícitamente los componentes de entrada
api_key_input
image_input_file
status_output = gr.Markdown("Sube una imagen para comenzar...")
btn_clear = gr.Button("🗑️ Quitar Imagen / Nuevo Documento", visible=True)
with gr.Column(scale=2):
# Columna Derecha: Edición de Etiquetas
gr.Markdown("### 2. Edición de Etiquetas NER")
# 💡 setup_label_components ya retorna los componentes de adición manual
(
df_label_input, tb_token_editor, dd_tag_selector,
btn_save_annotation, btn_export, file_output,
tb_new_token_text, btn_add_new_token, temp_state_dummy
) = setup_label_components()
# Dataframe
df_label_input
# Contenedor para los editores (Token y Tag)
with gr.Row(visible=True) as editor_row:
with gr.Column(scale=2):
tb_token_editor
# Campo de texto para el BBox manual (se muestra condicionalmente)
tb_new_token_text
with gr.Column(scale=1):
dd_tag_selector
# Contenedor para los botones de Guardar/Descargar/Agregar
with gr.Row(visible=True):
btn_save_annotation
btn_export
btn_add_new_token
file_output
# --- CONEXIONES DE EVENTOS ---
# CONEXIÓN 1: EJECUTAR OCR y Gemini (MODIFICADA)
image_input_file.change(
fn=process_image,
inputs=[image_input_file, api_key_input], # 💡 AÑADIR api_key_input
outputs=[
image_orig_state, tokens_data_state, image_output_display, df_label_input, status_output,
image_input_file, image_output_display, image_filename_state
],
api_name=False
)
# CONEXIÓN 2: Selección de FILA
df_label_input.select(
fn=capture_highlight_index,
inputs=None,
outputs=[highlight_index_state],
queue=False
).then(
fn=display_selected_row,
inputs=[tokens_data_state, highlight_index_state],
outputs=[tb_token_editor, dd_tag_selector, highlight_index_state],
).then(
fn=update_ui,
inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
outputs=[tokens_data_state, image_output_display],
api_name=False
)
# CONEXIÓN 3: Edición de Tag o Token (Actualiza estado y UI)
dd_tag_selector.change(
fn=lambda t, d, i, new_tag_val: update_dataframe_and_state(t, d, new_tag_val, None, i, 'tag'),
inputs=[tokens_data_state, df_label_input, highlight_index_state, dd_tag_selector],
outputs=[tokens_data_state, df_label_input],
).then(
fn=update_ui,
inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
outputs=[tokens_data_state, image_output_display],
api_name=False
)
token_update_events = [tb_token_editor.blur, tb_token_editor.submit]
for event in token_update_events:
event(
fn=lambda t, d, i, new_token_val: update_dataframe_and_state(t, d, None, new_token_val, i, 'token'),
inputs=[tokens_data_state, df_label_input, highlight_index_state, tb_token_editor],
outputs=[tokens_data_state, df_label_input],
).then(
fn=update_ui,
inputs=[image_orig_state, tokens_data_state, df_label_input, highlight_index_state],
outputs=[tokens_data_state, image_output_display],
api_name=False
)
# CONEXIÓN 4: Guardar y Exportar
btn_save_annotation.click(
fn=save_current_annotation_to_json,
inputs=[image_orig_state, tokens_data_state, image_filename_state],
outputs=[file_output, status_output],
api_name=False
)
btn_export.click(
fn=export_and_zip_dataset,
inputs=[image_orig_state, tokens_data_state, image_filename_state],
outputs=[file_output, status_output],
api_name=False
)
# CONEXIÓN 5: Limpiar y Reiniciar (MODIFICADA)
btn_clear.click(
fn=clear_ui_and_reset_states,
inputs=[api_key_input, tb_new_token_text, btn_add_new_token, STATE_NEW_BBOX], # 💡 AÑADIR api_key_input
outputs=[
image_orig_state, tokens_data_state, highlight_index_state,
image_filename_state, api_key_input, image_input_file, image_output_display, # 💡 AÑADIR api_key_input
df_label_input, tb_token_editor, dd_tag_selector,
tb_new_token_text, btn_add_new_token, STATE_NEW_BBOX,
status_output
],
api_name=False
)
if __name__ == "__main__":
try:
app.launch()
except Exception as e:
print(f"Error crítico durante la ejecución de la aplicación: {str(e)}")
raise