# interface.py """ Interfaz de usuario con Gradio """ import gradio as gr from config import HUGGINGFACE_MODEL class GradioInterface: """Clase para construir y gestionar la interfaz Gradio.""" def __init__(self, batch_processor, result_navigator): """ Inicializa la interfaz. Args: batch_processor: Instancia de BatchProcessor result_navigator: Clase ResultNavigator (no instancia) """ self.batch_processor = batch_processor self.navigator = result_navigator def update_dataframe_with_edits(self, master_df, current_index, edited_table): """ Actualiza el DataFrame maestro con las ediciones del usuario. Args: master_df: DataFrame maestro current_index: Índice actual edited_table: Tabla editada Returns: pd.DataFrame: DataFrame actualizado """ if master_df is None: return master_df updated_df = self.navigator.update_df_from_table(master_df, current_index, edited_table) return updated_df def verify_and_generate_excel(self, master_df): """ Verifica el DataFrame y genera el Excel. Args: master_df: DataFrame maestro Returns: tuple: (filepath, mensaje) """ from datetime import datetime import os if master_df is None or master_df.empty: return None, "❌ No hay datos para exportar" # Generar archivo timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"facturas_lote_{timestamp}.xlsx" filepath = os.path.join(os.getcwd(), filename) master_df.to_excel(filepath, index=False, engine='openpyxl') mensaje = f"✓ Excel generado: {filename} ({len(master_df)} facturas)" return filepath, mensaje def enable_buttons(self, master_df): """Habilita botones si hay DataFrame.""" has_data = master_df is not None and not master_df.empty return ( gr.update(interactive=has_data), # prev gr.update(interactive=has_data), # save gr.update(interactive=has_data), # next gr.update(interactive=has_data) # download ) def build_interface(self): """Construye la interfaz Gradio.""" with gr.Blocks(title="NER de Facturas Argentinas por Lote") as demo: gr.Markdown( f""" # 🇦🇷 Extracción de Datos de Facturas Argentinas Carga las facutas para su procesamiento por Lote Se utiliza **LayoutLMv3** (`{HUGGINGFACE_MODEL}`) y **DocTR** forzando la **ejecución en CPU**. """ ) # ÚNICO ESTADO: El DataFrame maestro master_dataframe_state = gr.State(value=None) current_index_state = gr.State(value=0) # Sección de carga with gr.Row(): with gr.Column(scale=1): file_input = gr.Files( file_count="multiple", type="filepath", label="📂 Cargar las Facturas", interactive=True ) with gr.Column(scale=1): with gr.Row(): process_button = gr.Button( "🚀 Procesar Lote de Facturas", variant="primary", size="lg", scale=2 ) download_button = gr.Button( "📥 Descargar XLSX", variant="secondary", size="lg", scale=1, interactive=False ) status_output = gr.Textbox( label="📊 Estado", value="Carga tus facturas y haz clic en 'Procesar'", interactive=False ) gr.Markdown("---") excel_file = gr.File(label="📥 Archivo Excel Generado", visible=False) # Sección de resultados with gr.Row(): with gr.Column(scale=1): image_output = gr.Image(type="pil", label="🖼️ Factura con Entidades") with gr.Column(scale=1): filename_output = gr.Textbox(label="📄 Nombre de Archivo", interactive=False) with gr.Row(): prev_button = gr.Button("⬅️ Anterior", interactive=False, size="lg") save_button = gr.Button("💾 Guardar", variant="secondary", interactive=False, size="lg") next_button = gr.Button("Siguiente ➡️", interactive=False, size="lg") table_output = gr.Dataframe( headers=["Etiqueta", "Valor"], label="📋 Resultados de NER", interactive=True, col_count=(2, "fixed"), datatype=["str", "str"] ) # EVENTOS # Procesar lote process_button.click( fn=self.batch_processor.process_batch, inputs=[file_input], outputs=[ master_dataframe_state, # DataFrame current_index_state, # Índice inicial status_output, # Estado image_output, # Primera imagen filename_output # Primer nombre ] ).then( fn=lambda df, idx: self.navigator.get_row_as_table(df, idx), inputs=[master_dataframe_state, current_index_state], outputs=[table_output] ).then( fn=self.enable_buttons, inputs=[master_dataframe_state], outputs=[prev_button, save_button, next_button, download_button] ) # Guardar cambios save_button.click( fn=self.update_dataframe_with_edits, inputs=[master_dataframe_state, current_index_state, table_output], outputs=[master_dataframe_state] ).then( fn=lambda idx: f"✓ Cambios guardados en factura {idx + 1}", inputs=[current_index_state], outputs=[status_output] ) # Auto-guardar al editar table_output.change( fn=self.update_dataframe_with_edits, inputs=[master_dataframe_state, current_index_state, table_output], outputs=[master_dataframe_state] ) # Navegación: Anterior prev_button.click( fn=lambda df, idx: self.navigator.go_prev(df, idx, self.batch_processor), inputs=[master_dataframe_state, current_index_state], outputs=[current_index_state, image_output, table_output, status_output, filename_output] ) # Navegación: Siguiente next_button.click( fn=lambda df, idx: self.navigator.go_next(df, idx, self.batch_processor), inputs=[master_dataframe_state, current_index_state], outputs=[current_index_state, image_output, table_output, status_output, filename_output] ) # Descargar Excel download_button.click( fn=self.verify_and_generate_excel, inputs=[master_dataframe_state], outputs=[excel_file, status_output] ).then( fn=lambda: gr.update(visible=True), outputs=[excel_file] ) return demo