Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import pandas as pd | |
| import os | |
| import secrets | |
| import time | |
| from collections import defaultdict | |
| from dotenv import load_dotenv | |
| from core import analyze_text, validate_api_key, validate_glm_api_key | |
| # Cargar variables de entorno desde .env | |
| load_dotenv() | |
| # DEBUG: Verificar carga de variables | |
| print("=" * 50) | |
| print("DEBUG - Variables de entorno cargadas:") | |
| print(f"GROQ_API_KEY existe: {bool(os.environ.get('GROQ_API_KEY'))}") | |
| print(f"pass_owner value: '{os.environ.get('pass_owner')}'") | |
| print("=" * 50) | |
| # Rate limiting (almacenado en memoria del servidor) | |
| login_attempts = defaultdict(list) | |
| MAX_ATTEMPTS = 5 | |
| LOCKOUT_TIME = 300 # 5 minutos | |
| def interface_fn(input_text, api_key, provider): | |
| """ | |
| Wrapper function for Gradio interface. | |
| CRITICAL: Environment Variable Logic | |
| - If api_key == "owner_authenticated": use environment variable based on provider | |
| - Otherwise: use user-provided key | |
| """ | |
| # Si es el token del owner, usar la key del entorno según el provider | |
| if api_key == "owner_authenticated": | |
| if provider == "glm": | |
| actual_key = os.environ.get("GLM_API_KEY") | |
| else: | |
| actual_key = os.environ.get("GROQ_API_KEY") | |
| if not actual_key: | |
| return ( | |
| "", | |
| "", | |
| pd.DataFrame(), | |
| f"⚠️ Error de configuración del servidor ({provider.upper()})", | |
| ) | |
| else: | |
| actual_key = api_key | |
| result = analyze_text(input_text, actual_key, provider) | |
| # Process errors for DataFrame | |
| errors_list = result.get("errors", []) | |
| if errors_list: | |
| df_data = [ | |
| [ | |
| e.get("original"), | |
| e.get("correction"), | |
| e.get("explanation"), | |
| e.get("type"), | |
| ] | |
| for e in errors_list | |
| ] | |
| else: | |
| df_data = [] # Empty dataframe if no errors | |
| return ( | |
| result.get("corrected_text", ""), | |
| result.get("spanish_translation", ""), | |
| pd.DataFrame( | |
| df_data, columns=["Original", "Corrección", "Explicación", "Tipo"] | |
| ), | |
| result.get("general_feedback", ""), | |
| ) | |
| def verify_key(key, request: gr.Request): | |
| """ | |
| Verifies the input key and implements the owner bypass. | |
| SECURE VERSION for Hugging Face Spaces. | |
| """ | |
| # Rate limiting por IP | |
| client_ip = request.client.host if request else "unknown" | |
| now = time.time() | |
| # Limpiar intentos antiguos | |
| login_attempts[client_ip] = [ | |
| t for t in login_attempts[client_ip] if now - t < LOCKOUT_TIME | |
| ] | |
| if len(login_attempts[client_ip]) >= MAX_ATTEMPTS: | |
| return ( | |
| None, | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| "⚠️ Demasiados intentos. Espera 5 minutos.", | |
| ) | |
| key = key.strip() | |
| # Check for bypass con comparación de tiempo constante | |
| if key.startswith("pass"): | |
| pass_owner = os.environ.get("pass_owner") | |
| if pass_owner: | |
| expected = "pass" + pass_owner | |
| # Usar secrets.compare_digest para evitar timing attacks | |
| if len(key) == len(expected) and secrets.compare_digest(key, expected): | |
| # ✅ NO retornar la API key real | |
| # Usar un token de sesión en su lugar | |
| session_token = "owner_authenticated" | |
| login_attempts[client_ip] = [] # Reset intentos | |
| return ( | |
| session_token, | |
| gr.update(visible=False), | |
| gr.update(visible=True), | |
| "", | |
| ) | |
| else: | |
| login_attempts[client_ip].append(now) | |
| return ( | |
| None, | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| "⚠️ Credenciales incorrectas.", | |
| ) | |
| # Validar formato de API key de usuario (Groq keys empiezan con gsk_) | |
| if key and key.startswith("gsk_") and len(key) > 30: | |
| if validate_api_key(key): | |
| login_attempts[client_ip] = [] | |
| return key, gr.update(visible=False), gr.update(visible=True), "" | |
| else: | |
| login_attempts[client_ip].append(now) | |
| return ( | |
| None, | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| "⚠️ API Key inválida (rechazada por Groq).", | |
| ) | |
| # GLM key validation (new) | |
| if len(key) > 20: | |
| if validate_glm_api_key(key): | |
| login_attempts[client_ip] = [] | |
| return key, gr.update(visible=False), gr.update(visible=True), "" | |
| else: | |
| login_attempts[client_ip].append(now) | |
| return ( | |
| None, | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| "⚠️ API Key inválida (rechazada por GLM).", | |
| ) | |
| login_attempts[client_ip].append(now) | |
| return ( | |
| None, | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| "⚠️ API Key inválida.", | |
| ) | |
| def logout_fn(): | |
| """ | |
| Clears the API key and returns to the login screen. | |
| """ | |
| return ( | |
| None, | |
| gr.update(visible=True), | |
| gr.update(visible=False), | |
| "Sesión cerrada correctamente.", | |
| ) | |
| def update_provider(choice): | |
| """Map UI choice to internal provider value.""" | |
| return "glm" if "GLM" in choice else "groq" | |
| custom_css = """ | |
| .gradio-container { | |
| width: 95% !important; | |
| max-width: 95% !important; | |
| } | |
| #main_view_container { | |
| padding: 40px; | |
| } | |
| """ | |
| with gr.Blocks( | |
| title="GroqEnglish Assistant", | |
| theme=gr.themes.Soft(primary_hue="orange"), | |
| css=custom_css, | |
| ) as demo: | |
| # State to store the authenticated API key | |
| api_key_state = gr.State() | |
| # State to store selected provider (default: "groq") | |
| provider_state = gr.State("groq") | |
| # --- Login View --- | |
| with gr.Column(visible=True) as login_view: | |
| with gr.Row(elem_id="login_container"): | |
| with gr.Column(scale=1): | |
| pass # Spacer | |
| with gr.Column(scale=2): | |
| gr.Markdown( | |
| """ | |
| # 🚀 GroqEnglish | |
| ### Asistente Inteligente de Escritura (Inglés L2) | |
| Bienvenido. Este asistente utiliza modelos avanzados de Groq para ayudarte a pulir tu inglés. | |
| """ | |
| ) | |
| with gr.Group(): | |
| api_key_input = gr.Textbox( | |
| label="🔑 Groq API Key", | |
| type="password", | |
| placeholder="gsk_...", | |
| lines=1, | |
| info="Tu llave es necesaria para acceder al modelo.", | |
| ) | |
| login_btn = gr.Button("Ingresar", variant="primary", size="lg") | |
| gr.Markdown( | |
| """ | |
| > **¿No tienes una API Key?** | |
| > Consíguela gratis en [Groq Console](https://console.groq.com/keys). | |
| """ | |
| ) | |
| login_msg = gr.Markdown("", visible=True) | |
| with gr.Column(scale=1): | |
| pass # Spacer | |
| # --- Main View --- | |
| with gr.Column(visible=False, elem_id="main_view_container") as main_view: | |
| # Header with Title and Logout | |
| with gr.Row(variant="panel"): | |
| with gr.Column(scale=6): | |
| gr.Markdown("# 🇬🇧 GroqEnglish Studio") | |
| with gr.Column(scale=2, min_width=150): | |
| provider_radio = gr.Radio( | |
| choices=["Groq Llama 3.3", "GLM-4.7-Flash"], | |
| value="Groq Llama 3.3", | |
| label="Modelo AI", | |
| interactive=True, | |
| ) | |
| with gr.Column(scale=1, min_width=100): | |
| logout_btn = gr.Button("Cerrar Sesión", variant="secondary", size="sm") | |
| gr.Markdown( | |
| "Transforma tu inglés con correcciones gramaticales, traducciones inversas y explicaciones detalladas." | |
| ) | |
| with gr.Row(equal_height=True): | |
| # Left Column: Input | |
| with gr.Column(scale=1): | |
| with gr.Group(): | |
| gr.Markdown("### ✍️ Tu Texto") | |
| input_box = gr.Textbox( | |
| show_label=False, | |
| placeholder="Escribe aquí tu frase en inglés... (ej. I goes to the park yesterday)", | |
| lines=12, | |
| elem_id="input_text", | |
| ) | |
| submit_btn = gr.Button( | |
| "✨ Analizar y Mejorar", variant="primary", size="lg" | |
| ) | |
| # Right Column: Output | |
| with gr.Column(scale=1): | |
| with gr.Group(): | |
| gr.Markdown("### 💎 Resultado Pulido") | |
| with gr.Row(): | |
| polished_text = gr.Textbox( | |
| show_label=False, interactive=False, lines=4, scale=6 | |
| ) | |
| copy_polished_btn = gr.Button("📋", scale=1, min_width=50) | |
| with gr.Group(): | |
| gr.Markdown("### 🇪🇸 Verificación (Traducción)") | |
| with gr.Row(): | |
| spanish_translation = gr.Textbox( | |
| show_label=False, interactive=False, lines=3, scale=6 | |
| ) | |
| copy_translation_btn = gr.Button("📋", scale=1, min_width=50) | |
| with gr.Group(): | |
| feedback_msg = gr.Markdown( | |
| "### 💡 Feedback General\n*(El análisis aparecerá aquí)*" | |
| ) | |
| # Bottom Section: Detailed Errors | |
| with gr.Accordion("🔍 Detalle de Errores y Explicaciones", open=True): | |
| error_table = gr.Dataframe( | |
| headers=["Original", "Corrección", "Explicación", "Tipo"], | |
| label="Tabla de Correcciones", | |
| interactive=False, | |
| wrap=True, | |
| datatype=["str", "str", "str", "str"], | |
| ) | |
| with gr.Row(): | |
| copy_table_btn = gr.Button("📋 Copiar Tabla de Errores", size="sm") | |
| copy_table_output = gr.Textbox(visible=False) | |
| # Functionality wiring | |
| submit_btn.click( | |
| fn=interface_fn, | |
| inputs=[input_box, api_key_state, provider_state], | |
| outputs=[polished_text, spanish_translation, error_table, feedback_msg], | |
| ) | |
| # Copy functionality | |
| copy_polished_btn.click( | |
| lambda x: x, | |
| inputs=[polished_text], | |
| outputs=[polished_text], | |
| js="(x) => {navigator.clipboard.writeText(x); return x;}", | |
| ) | |
| copy_translation_btn.click( | |
| lambda x: x, | |
| inputs=[spanish_translation], | |
| outputs=[spanish_translation], | |
| js="(x) => {navigator.clipboard.writeText(x); return x;}", | |
| ) | |
| def format_table_for_copy(df): | |
| if df is None or len(df) == 0: | |
| return "" | |
| lines = [] | |
| for _, row in df.iterrows(): | |
| lines.append( | |
| f"Original: {row['Original']} | Corrección: {row['Corrección']} | Explicación: {row['Explicación']}" | |
| ) | |
| return "\n".join(lines) | |
| copy_table_btn.click( | |
| fn=format_table_for_copy, | |
| inputs=[error_table], | |
| outputs=[copy_table_output], | |
| js="(text) => {navigator.clipboard.writeText(text); return text;}", | |
| ) | |
| # --- Global Event Wiring --- | |
| # Login | |
| login_btn.click( | |
| fn=verify_key, | |
| inputs=[api_key_input], | |
| outputs=[api_key_state, login_view, main_view, login_msg], | |
| ) | |
| api_key_input.submit( | |
| fn=verify_key, | |
| inputs=[api_key_input], | |
| outputs=[api_key_state, login_view, main_view, login_msg], | |
| ) | |
| # Logout | |
| logout_btn.click( | |
| fn=logout_fn, | |
| inputs=None, | |
| outputs=[api_key_state, login_view, main_view, login_msg], | |
| ) | |
| # Provider selection | |
| provider_radio.change( | |
| fn=update_provider, inputs=[provider_radio], outputs=[provider_state] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch() | |