OttoUlbrich commited on
Commit
7e40c1d
·
1 Parent(s): d6d3678

feat: Implement API key authentication, owner bypass, and rate limiting for the Gradio interface.

Browse files
Files changed (4) hide show
  1. README.md +17 -0
  2. app.py +163 -65
  3. core.py +15 -4
  4. requirements.txt +1 -0
README.md CHANGED
@@ -56,8 +56,25 @@ Esta aplicación está lista para usar. Solo escribe tu texto en inglés y obté
56
  python app.py
57
  ```
58
 
 
59
  Visita `http://127.0.0.1:7860` en tu navegador.
60
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  ## 📁 Estructura del Proyecto
62
 
63
  - `app.py`: Interfaz de usuario con Gradio
 
56
  python app.py
57
  ```
58
 
59
+
60
  Visita `http://127.0.0.1:7860` en tu navegador.
61
 
62
+ ### ⚡️ Setup Ultrarrápido con uv
63
+
64
+ Si prefieres usar [uv](https://github.com/astral-sh/uv) para una gestión de dependencias mucho más rápida:
65
+
66
+ 1. **Instalar uv** (si no lo tienes):
67
+ ```bash
68
+ curl -LsSf https://astral.sh/uv/install.sh | sh
69
+ ```
70
+
71
+ 2. **Inicializar y ejecutar**:
72
+ ```bash
73
+ uv venv
74
+ uv pip install -r requirements.txt
75
+ uv run app.py
76
+ ```
77
+
78
  ## 📁 Estructura del Proyecto
79
 
80
  - `app.py`: Interfaz de usuario con Gradio
app.py CHANGED
@@ -1,13 +1,40 @@
1
-
2
  import gradio as gr
3
  import pandas as pd
 
 
 
 
 
4
  from core import analyze_text
5
 
6
- def interface_fn(input_text):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  """
8
  Wrapper function for Gradio interface.
9
  """
10
- result = analyze_text(input_text)
 
 
 
 
 
 
 
 
11
 
12
  # Process errors for DataFrame
13
  errors_list = result.get("errors", [])
@@ -23,75 +50,146 @@ def interface_fn(input_text):
23
  result.get("general_feedback", "")
24
  )
25
 
26
- with gr.Blocks(title="Assistant Inglés L2") as demo:
27
- gr.Markdown(
28
- """
29
- # 🇬🇧 Asistente Inteligente de Escritura Inglés (L2)
30
- Escribe en inglés y recibe correcciones, traducción y explicaciones al instante.
31
- """
32
- )
 
33
 
34
- with gr.Row():
35
- with gr.Column(scale=1):
36
- input_box = gr.Textbox(
37
- label="Tu texto en inglés",
38
- placeholder="Escribe aquí... (ej. She dont like play football)",
39
- lines=10
40
- )
41
- submit_btn = gr.Button("Analizar y Corregir", variant="primary")
42
-
43
- with gr.Column(scale=1):
44
- with gr.Row():
45
- polished_text = gr.Textbox(label="Texto Pulido", interactive=False, lines=8, scale=4)
46
- copy_polished_btn = gr.Button("📋", scale=1, size="sm")
47
- with gr.Row():
48
- spanish_translation = gr.Textbox(label="Verificación de Significado (ES)", interactive=False, lines=8, scale=4)
49
- copy_translation_btn = gr.Button("📋", scale=1, size="sm")
50
- feedback_msg = gr.Label(label="Feedback General")
 
 
 
 
 
 
51
 
52
- with gr.Row():
53
- with gr.Column():
54
- error_table = gr.Dataframe(
55
- headers=["Original", "Corrección", "Explicación", "Tipo"],
56
- label="Detalle de Errores",
57
- interactive=False,
58
- wrap=True
59
- )
60
- copy_table_btn = gr.Button("📋 Copiar Tabla", size="sm")
61
- copy_table_output = gr.Textbox(visible=False)
62
 
63
- submit_btn.click(
64
- fn=interface_fn,
65
- inputs=input_box,
66
- outputs=[polished_text, spanish_translation, error_table, feedback_msg]
67
- )
68
 
69
- # Copy button functionality
70
- copy_polished_btn.click(lambda x: x, inputs=[polished_text], outputs=[polished_text], js="(x) => {navigator.clipboard.writeText(x); return x;}")
71
- copy_translation_btn.click(lambda x: x, inputs=[spanish_translation], outputs=[spanish_translation], js="(x) => {navigator.clipboard.writeText(x); return x;}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
- def format_table_for_copy(df):
74
- if df is None or len(df) == 0:
75
- return ""
76
- lines = []
77
- for _, row in df.iterrows():
78
- lines.append(f"Original: {row['Original']}\nCorrección: {row['Corrección']}\nExplicación: {row['Explicación']}\nTipo: {row['Tipo']}\n")
79
- return "\n".join(lines)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
- copy_table_btn.click(
82
- fn=format_table_for_copy,
83
- inputs=[error_table],
84
- outputs=[copy_table_output],
85
- js="(df) => {const text = df; navigator.clipboard.writeText(text); return text;}"
86
- )
87
 
88
- if __name__ == "__main__":
89
- import os
 
90
 
91
- # Verificar que la API key esté configurada
92
- api_key = os.getenv("GROQ_API_KEY")
93
- if not api_key:
94
- print("⚠️ WARNING: GROQ_API_KEY no encontrada en las variables de entorno")
95
- print("ℹ️ En Hugging Face Spaces, configúrala en Settings → Repository secrets")
 
 
96
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  demo.launch(theme=gr.themes.Soft())
 
 
1
  import gradio as gr
2
  import pandas as pd
3
+ import os
4
+ import secrets
5
+ import time
6
+ from collections import defaultdict
7
+ from dotenv import load_dotenv
8
  from core import analyze_text
9
 
10
+ # Cargar variables de entorno desde .env
11
+ load_dotenv()
12
+
13
+ # DEBUG: Verificar carga de variables
14
+ print("=" * 50)
15
+ print("DEBUG - Variables de entorno cargadas:")
16
+ print(f"GROQ_API_KEY existe: {bool(os.environ.get('GROQ_API_KEY'))}")
17
+ print(f"pass_owner value: '{os.environ.get('pass_owner')}'")
18
+ print("=" * 50)
19
+
20
+ # Rate limiting (almacenado en memoria del servidor)
21
+ login_attempts = defaultdict(list)
22
+ MAX_ATTEMPTS = 5
23
+ LOCKOUT_TIME = 300 # 5 minutos
24
+
25
+ def interface_fn(input_text, api_key):
26
  """
27
  Wrapper function for Gradio interface.
28
  """
29
+ # Si es el token del owner, usar la key del entorno
30
+ if api_key == "owner_authenticated":
31
+ actual_key = os.environ.get("GROQ_API_KEY")
32
+ if not actual_key:
33
+ return ("", "", pd.DataFrame(), "⚠️ Error de configuración del servidor")
34
+ else:
35
+ actual_key = api_key
36
+
37
+ result = analyze_text(input_text, actual_key)
38
 
39
  # Process errors for DataFrame
40
  errors_list = result.get("errors", [])
 
50
  result.get("general_feedback", "")
51
  )
52
 
53
+ def verify_key(key, request: gr.Request):
54
+ """
55
+ Verifies the input key and implements the owner bypass.
56
+ SECURE VERSION for Hugging Face Spaces.
57
+ """
58
+ # Rate limiting por IP
59
+ client_ip = request.client.host if request else "unknown"
60
+ now = time.time()
61
 
62
+ # Limpiar intentos antiguos
63
+ login_attempts[client_ip] = [t for t in login_attempts[client_ip] if now - t < LOCKOUT_TIME]
64
+
65
+ if len(login_attempts[client_ip]) >= MAX_ATTEMPTS:
66
+ return None, gr.update(visible=True), gr.update(visible=False), "⚠️ Demasiados intentos. Espera 5 minutos."
67
+
68
+ key = key.strip()
69
+
70
+ # Check for bypass con comparación de tiempo constante
71
+ if key.startswith("pass"):
72
+ pass_owner = os.environ.get("pass_owner")
73
+ if pass_owner:
74
+ expected = "pass" + pass_owner
75
+ # Usar secrets.compare_digest para evitar timing attacks
76
+ if len(key) == len(expected) and secrets.compare_digest(key, expected):
77
+ # NO retornar la API key real
78
+ # Usar un token de sesión en su lugar
79
+ session_token = "owner_authenticated"
80
+ login_attempts[client_ip] = [] # Reset intentos
81
+ return session_token, gr.update(visible=False), gr.update(visible=True), ""
82
+ else:
83
+ login_attempts[client_ip].append(now)
84
+ return None, gr.update(visible=True), gr.update(visible=False), "⚠️ Credenciales incorrectas."
85
 
86
+ # Validar formato de API key de usuario (Groq keys empiezan con gsk_)
87
+ if key and key.startswith("gsk_") and len(key) > 30:
88
+ login_attempts[client_ip] = []
89
+ return key, gr.update(visible=False), gr.update(visible=True), ""
90
+
91
+ login_attempts[client_ip].append(now)
92
+ return None, gr.update(visible=True), gr.update(visible=False), "⚠️ API Key inválida."
 
 
 
93
 
94
+ with gr.Blocks(title="Assistant Inglés L2") as demo:
95
+ # State to store the authenticated API key
96
+ api_key_state = gr.State()
 
 
97
 
98
+ # --- Login View ---
99
+ with gr.Column(visible=True) as login_view:
100
+ gr.Markdown(
101
+ """
102
+ # 🔐 Configuración
103
+ Para usar el Asistente, por favor ingresa tu **Groq API Key**.
104
+
105
+ *(Si eres el administrador, usa tu frase de paso)*
106
+ """
107
+ )
108
+ api_key_input = gr.Textbox(
109
+ label="Groq API Key (o frase de administrador)",
110
+ type="password",
111
+ placeholder="gsk_... o pass...",
112
+ lines=1
113
+ )
114
+ login_btn = gr.Button("Ingresar", variant="primary")
115
+ login_msg = gr.Markdown("")
116
 
117
+ # --- Main View ---
118
+ with gr.Column(visible=False) as main_view:
119
+ gr.Markdown(
120
+ """
121
+ # 🇬🇧 Asistente Inteligente de Escritura Inglés (L2)
122
+ Escribe en inglés y recibe correcciones, traducción y explicaciones al instante.
123
+ """
124
+ )
125
+
126
+ with gr.Row():
127
+ with gr.Column(scale=1):
128
+ input_box = gr.Textbox(
129
+ label="Tu texto en inglés",
130
+ placeholder="Escribe aquí... (ej. She dont like play football)",
131
+ lines=10
132
+ )
133
+ submit_btn = gr.Button("Analizar y Corregir", variant="primary")
134
+
135
+ with gr.Column(scale=1):
136
+ with gr.Row():
137
+ polished_text = gr.Textbox(label="Texto Pulido", interactive=False, lines=8, scale=4)
138
+ copy_polished_btn = gr.Button("📋", scale=1, size="sm")
139
+ with gr.Row():
140
+ spanish_translation = gr.Textbox(label="Verificación de Significado (ES)", interactive=False, lines=8, scale=4)
141
+ copy_translation_btn = gr.Button("📋", scale=1, size="sm")
142
+ feedback_msg = gr.Label(label="Feedback General")
143
+
144
+ with gr.Row():
145
+ with gr.Column():
146
+ error_table = gr.Dataframe(
147
+ headers=["Original", "Corrección", "Explicación", "Tipo"],
148
+ label="Detalle de Errores",
149
+ interactive=False,
150
+ wrap=True
151
+ )
152
+ copy_table_btn = gr.Button("📋 Copiar Tabla", size="sm")
153
+ copy_table_output = gr.Textbox(visible=False)
154
 
155
+ submit_btn.click(
156
+ fn=interface_fn,
157
+ inputs=[input_box, api_key_state],
158
+ outputs=[polished_text, spanish_translation, error_table, feedback_msg]
159
+ )
 
160
 
161
+ # Copy button functionality
162
+ copy_polished_btn.click(lambda x: x, inputs=[polished_text], outputs=[polished_text], js="(x) => {navigator.clipboard.writeText(x); return x;}")
163
+ copy_translation_btn.click(lambda x: x, inputs=[spanish_translation], outputs=[spanish_translation], js="(x) => {navigator.clipboard.writeText(x); return x;}")
164
 
165
+ def format_table_for_copy(df):
166
+ if df is None or len(df) == 0:
167
+ return ""
168
+ lines = []
169
+ for _, row in df.iterrows():
170
+ lines.append(f"Original: {row['Original']}\nCorrección: {row['Corrección']}\nExplicación: {row['Explicación']}\nTipo: {row['Tipo']}\n")
171
+ return "\n".join(lines)
172
 
173
+ copy_table_btn.click(
174
+ fn=format_table_for_copy,
175
+ inputs=[error_table],
176
+ outputs=[copy_table_output],
177
+ js="(df) => {const text = df; navigator.clipboard.writeText(text); return text;}"
178
+ )
179
+
180
+ # --- Event Wiring ---
181
+ login_btn.click(
182
+ fn=verify_key,
183
+ inputs=[api_key_input],
184
+ outputs=[api_key_state, login_view, main_view, login_msg]
185
+ )
186
+
187
+ # Allow Enter key to submit on login
188
+ api_key_input.submit(
189
+ fn=verify_key,
190
+ inputs=[api_key_input],
191
+ outputs=[api_key_state, login_view, main_view, login_msg]
192
+ )
193
+
194
+ if __name__ == "__main__":
195
  demo.launch(theme=gr.themes.Soft())
core.py CHANGED
@@ -14,9 +14,9 @@ except ImportError:
14
  # Initialize Groq client
15
  # Security Note: API Key is loaded from environment variable GROQ_API_KEY
16
  # In Hugging Face Spaces, this will be loaded from Repository Secrets
17
- client = Groq(
18
- api_key=os.environ.get("GROQ_API_KEY"),
19
- )
20
 
21
  MODEL_NAME = "llama-3.3-70b-versatile"
22
 
@@ -45,12 +45,13 @@ If there are no errors, the "errors" list should be empty, and "corrected_text"
45
  Do not include any text outside the JSON object.
46
  """
47
 
48
- def analyze_text(input_text: str) -> dict:
49
  """
50
  Analyzes the input English text using Groq API and returns a structured response.
51
 
52
  Args:
53
  input_text (str): The English text to analyze.
 
54
 
55
  Returns:
56
  dict: The structured analysis result.
@@ -63,7 +64,17 @@ def analyze_text(input_text: str) -> dict:
63
  "general_feedback": "Por favor, ingresa un texto para analizar."
64
  }
65
 
 
 
 
 
 
 
 
 
66
  try:
 
 
67
  completion = client.chat.completions.create(
68
  model=MODEL_NAME,
69
  messages=[
 
14
  # Initialize Groq client
15
  # Security Note: API Key is loaded from environment variable GROQ_API_KEY
16
  # In Hugging Face Spaces, this will be loaded from Repository Secrets
17
+ # client = Groq(
18
+ # api_key=os.environ.get("GROQ_API_KEY"),
19
+ # )
20
 
21
  MODEL_NAME = "llama-3.3-70b-versatile"
22
 
 
45
  Do not include any text outside the JSON object.
46
  """
47
 
48
+ def analyze_text(input_text: str, api_key: str) -> dict:
49
  """
50
  Analyzes the input English text using Groq API and returns a structured response.
51
 
52
  Args:
53
  input_text (str): The English text to analyze.
54
+ api_key (str): The Groq API key to use.
55
 
56
  Returns:
57
  dict: The structured analysis result.
 
64
  "general_feedback": "Por favor, ingresa un texto para analizar."
65
  }
66
 
67
+ if not api_key:
68
+ return {
69
+ "corrected_text": input_text,
70
+ "spanish_translation": "Error de configuración",
71
+ "errors": [],
72
+ "general_feedback": "No se proporcionó una API Key válida."
73
+ }
74
+
75
  try:
76
+ client = Groq(api_key=api_key)
77
+
78
  completion = client.chat.completions.create(
79
  model=MODEL_NAME,
80
  messages=[
requirements.txt CHANGED
@@ -1,3 +1,4 @@
1
  gradio>=4.0.0
2
  groq>=0.4.0
3
  pandas>=2.0.0
 
 
1
  gradio>=4.0.0
2
  groq>=0.4.0
3
  pandas>=2.0.0
4
+ python-dotenv>=1.0.0