CharlieBonito commited on
Commit
79aa6fb
·
verified ·
1 Parent(s): 737cd0b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +160 -177
app.py CHANGED
@@ -9,54 +9,70 @@ import base64
9
  from PIL import Image
10
  import io
11
 
12
- # --- CONFIGURACIÓN ---
13
  MODEL_REPO = "CharlieBonito/clarity-guard-gemma4-7b"
14
  MODEL_FILE = "Checkpoint-375-Ollama-Clean-7.5B-Q4_K_M.gguf"
15
  MMPROJ_FILE = "mmproj-Checkpoint-375-Ollama-Clean-BF16.gguf"
16
-
17
  LLAMA_SERVER = "/opt/llama-cpp/llama-server"
18
  MODEL_DIR = "/app/models"
19
- SERVER_URL = "http://127.0.0.1:8080"
20
-
21
  server_process = None
 
22
 
23
- # --- SYSTEM PROMPT ---
24
  CLARITYGUARD_PROMPT = """# CLARITYGUARD ASSISTANT — NEURO-INCLUSIVE EDITION v4.4
25
  **Language policy:** Reply in the same language the user uses.
26
- Comienza siempre con: "Got it.", "Sure!", "Hi there!" o "Understood."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  ---
28
- **Version:** ClarityGuard v4.4"""
 
 
 
29
 
30
 
31
- # --- DESCARGA DE MODELOS ---
32
  def download_models():
 
33
  from huggingface_hub import hf_hub_download
34
-
35
  os.makedirs(MODEL_DIR, exist_ok=True)
 
 
 
 
 
36
 
37
- model_path = hf_hub_download(
38
- repo_id=MODEL_REPO,
39
- filename=MODEL_FILE,
40
- local_dir=MODEL_DIR
41
- )
42
-
43
- mmproj_path = hf_hub_download(
44
- repo_id=MODEL_REPO,
45
- filename=MMPROJ_FILE,
46
- local_dir=MODEL_DIR
47
- )
48
-
49
- return model_path, mmproj_path
50
 
51
-
52
- # --- INICIAR LLAMA SERVER ---
53
  def start_server():
 
54
  global server_process
55
-
56
  if server_process is not None and server_process.poll() is None:
57
- return True
58
 
59
- model_path, mmproj_path = download_models()
60
 
61
  env = os.environ.copy()
62
  env["LD_LIBRARY_PATH"] = (
@@ -66,99 +82,75 @@ def start_server():
66
 
67
  cmd = [
68
  LLAMA_SERVER,
69
- "-m", model_path,
70
- "--mmproj", mmproj_path,
71
  "--host", "127.0.0.1",
72
  "--port", "8080",
73
- "-c", "8192",
74
  "-ngl", "99",
75
- "--jinja"
 
76
  ]
77
 
78
- print("Iniciando llama-server...")
79
- print(" ".join(cmd))
80
-
81
- server_process = subprocess.Popen(
82
- cmd,
83
- env=env,
84
- stdout=subprocess.PIPE,
85
- stderr=subprocess.STDOUT,
86
- text=True
87
- )
88
 
89
- def log_reader():
90
- for line in iter(server_process.stdout.readline, ""):
91
- print(f"[LLAMA] {line.strip()}")
92
-
93
- threading.Thread(target=log_reader, daemon=True).start()
94
-
95
- # Espera hasta 4 minutos porque en Space puede tardar cargando GGUF + mmproj
96
- for _ in range(120):
97
  try:
98
- response = requests.get(f"{SERVER_URL}/health", timeout=2)
99
- if response.status_code == 200:
100
- print("llama-server iniciado correctamente.")
101
- return True
102
  except Exception:
103
- pass
104
-
105
- time.sleep(2)
106
 
107
- print("Error: llama-server no respondió en /health.")
108
- return False
109
 
110
 
111
- # --- IMAGEN A BASE64 ---
112
  def image_to_base64(image_path: str) -> str:
 
113
  with Image.open(image_path) as img:
 
114
  if img.mode in ("RGBA", "P"):
115
  img = img.convert("RGB")
116
-
117
  buffer = io.BytesIO()
118
  img.save(buffer, format="JPEG", quality=85)
119
-
120
  return base64.b64encode(buffer.getvalue()).decode("utf-8")
121
 
122
 
123
- # --- RESPUESTA DEL MODELO ---
124
- def respond(message, image_path, history):
125
- if not start_server():
126
- yield "⚠️ Error: El servidor llama.cpp no inició. Revisa los logs de [LLAMA]."
127
- return
 
 
128
 
129
  messages = [{"role": "system", "content": CLARITYGUARD_PROMPT}]
130
 
131
- # Gradio 6 usa historial tipo messages: {"role": "...", "content": "..."}
132
- for item in history or []:
133
- if isinstance(item, dict) and "role" in item and "content" in item:
134
- clean_content = str(item["content"]).replace(" [📎 imagen adjunta]", "")
135
- messages.append({
136
- "role": item["role"],
137
- "content": clean_content
138
- })
139
 
 
140
  if image_path:
141
- image_b64 = image_to_base64(image_path)
142
-
143
- user_content = [
144
- {
145
- "type": "image_url",
146
- "image_url": {
147
- "url": f"data:image/jpeg;base64,{image_b64}"
148
- }
149
- },
150
- {
151
- "type": "text",
152
- "text": message or "Analiza esta imagen."
153
- }
154
- ]
155
  else:
156
- user_content = message or ""
157
 
158
- messages.append({
159
- "role": "user",
160
- "content": user_content
161
- })
162
 
163
  try:
164
  response = requests.post(
@@ -166,140 +158,131 @@ def respond(message, image_path, history):
166
  json={
167
  "messages": messages,
168
  "stream": True,
169
- "temperature": 0.3,
170
- "max_tokens": 1024
171
  },
172
  stream=True,
173
- timeout=300
174
  )
175
 
176
- if response.status_code != 200:
177
- yield f"⚠️ Error del servidor llama.cpp: {response.status_code}\n\n{response.text}"
178
- return
179
-
180
  full_response = ""
181
-
182
  for line in response.iter_lines():
183
- if not line:
184
- continue
185
-
186
- decoded = line.decode("utf-8")
187
-
188
- if not decoded.startswith("data: "):
189
- continue
190
-
191
- content = decoded[6:]
192
-
193
- if content.strip() == "[DONE]":
194
- break
195
-
196
- try:
197
- data = json.loads(content)
198
- delta = data["choices"][0].get("delta", {})
199
- token = delta.get("content", "")
200
-
201
- if token:
202
- full_response += token
203
- yield full_response
204
-
205
- except Exception:
206
- continue
207
 
208
  except Exception as e:
209
- yield f"⚠️ Error consultando llama.cpp: {str(e)}"
210
 
211
 
212
- # --- INTERFAZ GRADIO 6 ---
213
  with gr.Blocks(title="ClarityGuard v4.4") as demo:
214
- gr.Markdown("# 🔍 ClarityGuard v4.4")
215
 
216
- chatbot = gr.Chatbot(height=520)
 
 
 
 
 
 
217
 
218
  with gr.Row():
219
  msg_input = gr.Textbox(
220
- placeholder="Mensaje...",
221
- scale=4
 
 
222
  )
223
-
224
  image_input = gr.Image(
 
225
  type="filepath",
226
- scale=1
 
 
227
  )
228
 
229
  with gr.Row():
230
- submit_btn = gr.Button("🔍 Analizar", variant="primary")
231
- clear_btn = gr.Button("🗑️ Limpiar")
 
 
 
 
 
 
 
 
 
 
 
 
232
 
233
  def user_action(message, image, history):
 
234
  history = history or []
235
-
236
- display = message or ""
237
-
238
  if image:
239
- display += " [📎 imagen adjunta]"
240
-
241
- history.append({
242
- "role": "user",
243
- "content": display
244
- })
245
-
246
- return "", None, history
247
 
248
  def bot_action(message, image, history):
249
- history = history or []
250
-
251
- real_msg = message or ""
252
-
253
- if not real_msg.strip() and image:
254
- real_msg = "Analiza la imagen."
255
-
256
- clean_history = []
257
-
258
- for item in history[:-1]:
259
- if isinstance(item, dict):
260
- clean_history.append({
261
- "role": item["role"],
262
- "content": str(item["content"]).replace(" [📎 imagen adjunta]", "")
263
- })
264
-
265
- history.append({
266
- "role": "assistant",
267
- "content": ""
268
- })
269
 
270
- for chunk in respond(real_msg, image, clean_history):
271
- history[-1]["content"] = chunk
 
272
  yield history
273
 
 
274
  submit_btn.click(
275
  user_action,
276
  inputs=[msg_input, image_input, chatbot],
277
- outputs=[msg_input, image_input, chatbot]
278
  ).then(
279
  bot_action,
280
  inputs=[msg_input, image_input, chatbot],
281
- outputs=[chatbot]
282
  )
283
 
284
  msg_input.submit(
285
  user_action,
286
  inputs=[msg_input, image_input, chatbot],
287
- outputs=[msg_input, image_input, chatbot]
288
  ).then(
289
  bot_action,
290
  inputs=[msg_input, image_input, chatbot],
291
- outputs=[chatbot]
292
  )
293
 
294
- clear_btn.click(
295
- lambda: ([], "", None),
296
- outputs=[chatbot, msg_input, image_input]
297
- )
298
 
299
 
300
  if __name__ == "__main__":
 
301
  demo.launch(
302
  server_name="0.0.0.0",
303
  server_port=7860,
304
- theme=gr.themes.Soft()
 
305
  )
 
9
  from PIL import Image
10
  import io
11
 
12
+ # --- CONFIGURACIÓN DE MODELO Y RUTAS ---
13
  MODEL_REPO = "CharlieBonito/clarity-guard-gemma4-7b"
14
  MODEL_FILE = "Checkpoint-375-Ollama-Clean-7.5B-Q4_K_M.gguf"
15
  MMPROJ_FILE = "mmproj-Checkpoint-375-Ollama-Clean-BF16.gguf"
 
16
  LLAMA_SERVER = "/opt/llama-cpp/llama-server"
17
  MODEL_DIR = "/app/models"
 
 
18
  server_process = None
19
+ SERVER_URL = "http://127.0.0.1:8080"
20
 
21
+ # --- SYSTEM PROMPT (ClarityGuard v4.4) ---
22
  CLARITYGUARD_PROMPT = """# CLARITYGUARD ASSISTANT — NEURO-INCLUSIVE EDITION v4.4
23
  **Language policy:** Reply in the same language the user uses.
24
+ **Response initialization:** Every response must begin with a natural opener: "Got it.", "Sure!", "Hi there!" or "Understood."
25
+ ---
26
+ ## IDENTITY AND PURPOSE
27
+ You are **ClarityGuard**, specialized in clarity support for neurodivergent and autistic people in workplace and personal settings.
28
+ **Core function:** Determine whether the user's confusion originates in the **structure of the message**—not in a "failure" of the user.
29
+ **Foundational principle:** When a message lacks a clear subject, defined action, concrete date, or measurable criterion, confusion is the logical response to incomplete input. It is a **protocol mismatch**, not a cognitive error.
30
+ ---
31
+ ## ANALYSIS PROCESS (internal - never show to user)
32
+ C: [0–10] | F: [0–10] | R: [0–10] | V: [0–10] | A: [0–10]
33
+ TOTAL: [sum] / 50
34
+ Response modes:
35
+ - 0–10: Clear message. Confirm briefly.
36
+ - 11–20: Name the ambiguous element, suggest one clarification question.
37
+ - 21–30: Full analysis + clarification suggestion.
38
+ - 31–50: Full 4-step response + cognitive protection.
39
+ ---
40
+ ## RESPONSE STRUCTURE (4 STEPS)
41
+ ### STEP 1 — ANALYSIS
42
+ 🔍 **[ClarityGuard] C.F.R.V.A. score: XX/50 → [level]**
43
+ Explain what creates confusion using descriptive language about message structure.
44
+ ### STEP 2 — COGNITIVE PROTECTION (only if score ≥ 21)
45
+ 🔒 **Your confusion is not a failure. It is the correct response to an incomplete message.**
46
+ ### STEP 3 — CONCRETE ACTION (Read-Back)
47
+ ✍️ **Clarification suggestion:**
48
+ Offer a concrete clarification question.
49
+ ### STEP 4 — FOLLOW-UP PLAN (only if score ≥ 31)
50
+ ⏰ If clarification is still abstract, apply adjective decomposition.
51
  ---
52
+ ## OPERATIONAL RULES
53
+ 1. If the message is clear, say so. 2. If ambiguous, name the missing element. 3. Protect against self-invalidation when score ≥ 21. 4. Never diagnose the sender. 5. Never attribute confusion to the user's cognitive profile. 6. Match length to channel. 7. Reply in the user's language. 8. Never output internal scoring.
54
+ ---
55
+ **Version:** ClarityGuard v4.4 — Neuro-inclusive"""
56
 
57
 
 
58
  def download_models():
59
+ """Descarga los modelos desde el Hub de Hugging Face."""
60
  from huggingface_hub import hf_hub_download
 
61
  os.makedirs(MODEL_DIR, exist_ok=True)
62
+ print(f"[ClarityGuard] Descargando modelos en {MODEL_DIR}...")
63
+ m_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE, local_dir=MODEL_DIR)
64
+ mm_path = hf_hub_download(repo_id=MODEL_REPO, filename=MMPROJ_FILE, local_dir=MODEL_DIR)
65
+ print("[ClarityGuard] Modelos descargados.")
66
+ return m_path, mm_path
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
 
 
69
  def start_server():
70
+ """Inicia el binario llama-server con soporte CUDA y multimodal."""
71
  global server_process
 
72
  if server_process is not None and server_process.poll() is None:
73
+ return server_process
74
 
75
+ m_path, mm_path = download_models()
76
 
77
  env = os.environ.copy()
78
  env["LD_LIBRARY_PATH"] = (
 
82
 
83
  cmd = [
84
  LLAMA_SERVER,
85
+ "-m", m_path,
86
+ "--mmproj", mm_path,
87
  "--host", "127.0.0.1",
88
  "--port", "8080",
89
+ "-c", "16384",
90
  "-ngl", "99",
91
+ "--jinja",
92
+ "--log-disable",
93
  ]
94
 
95
+ print(f"[ClarityGuard] Lanzando servidor: {' '.join(cmd)}")
96
+ server_process = subprocess.Popen(cmd, env=env)
 
 
 
 
 
 
 
 
97
 
98
+ for _ in range(45):
 
 
 
 
 
 
 
99
  try:
100
+ if requests.get(f"{SERVER_URL}/health", timeout=1).status_code == 200:
101
+ print("[ClarityGuard] Servidor Llama-CPP listo.")
102
+ return server_process
 
103
  except Exception:
104
+ time.sleep(2)
 
 
105
 
106
+ print("[ClarityGuard] Advertencia: el servidor puede no estar listo.")
107
+ return server_process
108
 
109
 
 
110
  def image_to_base64(image_path: str) -> str:
111
+ """Convierte una imagen a base64 para enviarla al servidor."""
112
  with Image.open(image_path) as img:
113
+ # Convertir a RGB si es necesario (por ejemplo, PNG con transparencia)
114
  if img.mode in ("RGBA", "P"):
115
  img = img.convert("RGB")
 
116
  buffer = io.BytesIO()
117
  img.save(buffer, format="JPEG", quality=85)
 
118
  return base64.b64encode(buffer.getvalue()).decode("utf-8")
119
 
120
 
121
+ def respond(message: str, image_path, history: list):
122
+ """
123
+ Genera la respuesta del modelo.
124
+ history: lista de tuplas (user_msg, assistant_msg)
125
+ image_path: ruta a imagen opcional (str o None)
126
+ """
127
+ start_server()
128
 
129
  messages = [{"role": "system", "content": CLARITYGUARD_PROMPT}]
130
 
131
+ # Historial previo
132
+ for user_msg, assistant_msg in history:
133
+ if assistant_msg:
134
+ messages.append({"role": "user", "content": user_msg})
135
+ messages.append({"role": "assistant", "content": assistant_msg})
 
 
 
136
 
137
+ # Mensaje actual — con o sin imagen
138
  if image_path:
139
+ try:
140
+ img_b64 = image_to_base64(image_path)
141
+ user_content = [
142
+ {
143
+ "type": "image_url",
144
+ "image_url": {"url": f"data:image/jpeg;base64,{img_b64}"},
145
+ },
146
+ {"type": "text", "text": message if message.strip() else "Analiza este mensaje de la imagen."},
147
+ ]
148
+ except Exception as e:
149
+ user_content = message + f"\n[Error cargando imagen: {e}]"
 
 
 
150
  else:
151
+ user_content = message
152
 
153
+ messages.append({"role": "user", "content": user_content})
 
 
 
154
 
155
  try:
156
  response = requests.post(
 
158
  json={
159
  "messages": messages,
160
  "stream": True,
161
+ "temperature": 0.7,
162
+ "max_tokens": 2048,
163
  },
164
  stream=True,
165
+ timeout=120,
166
  )
167
 
 
 
 
 
168
  full_response = ""
 
169
  for line in response.iter_lines():
170
+ if line:
171
+ chunk_decoded = line.decode("utf-8")
172
+ if chunk_decoded.startswith("data: "):
173
+ content = chunk_decoded[6:]
174
+ if content.strip() == "[DONE]":
175
+ break
176
+ try:
177
+ data = json.loads(content)
178
+ if "choices" in data:
179
+ delta = data["choices"][0].get("delta", {}).get("content", "")
180
+ full_response += delta
181
+ yield full_response
182
+ except Exception:
183
+ continue
 
 
 
 
 
 
 
 
 
 
184
 
185
  except Exception as e:
186
+ yield f"⚠️ Error de conexión con el servidor: {str(e)}"
187
 
188
 
189
+ # --- INTERFAZ DE GRADIO ---
190
  with gr.Blocks(title="ClarityGuard v4.4") as demo:
 
191
 
192
+ gr.Markdown(
193
+ """# 🔍 ClarityGuard v4.4
194
+ Análisis de comunicación neuro-inclusiva. Pega un mensaje, adjunta una captura de pantalla o ambos."""
195
+ )
196
+
197
+ # type="tuples" es obligatorio en Gradio 6 para usar listas de pares
198
+ chatbot = gr.Chatbot(height=520, type="tuples", label="ClarityGuard")
199
 
200
  with gr.Row():
201
  msg_input = gr.Textbox(
202
+ label="Mensaje a analizar",
203
+ placeholder="Pega aquí el texto que quieres analizar...",
204
+ lines=3,
205
+ scale=4,
206
  )
 
207
  image_input = gr.Image(
208
+ label="📎 Captura / Imagen",
209
  type="filepath",
210
+ sources=["upload", "clipboard"],
211
+ scale=1,
212
+ height=120,
213
  )
214
 
215
  with gr.Row():
216
+ submit_btn = gr.Button("🔍 Analizar", variant="primary", scale=3)
217
+ clear_btn = gr.Button("🗑️ Limpiar", scale=1)
218
+
219
+ gr.Examples(
220
+ examples=[
221
+ ["\"Nos vemos el lunes por la tarde.\"", None],
222
+ ["\"Necesitamos arreglar esto ASAP.\"", None],
223
+ ["\"Sé más proactivo en las reuniones.\"", None],
224
+ ["\"Estaré de vuelta en 5 minutos.\"", None],
225
+ ],
226
+ inputs=[msg_input, image_input],
227
+ )
228
+
229
+ # --- Handlers ---
230
 
231
  def user_action(message, image, history):
232
+ """Agrega el turno del usuario al historial y limpia los inputs."""
233
  history = history or []
234
+ display_msg = message or ""
 
 
235
  if image:
236
+ display_msg = (display_msg + " [📎 imagen adjunta]").strip()
237
+ return "", None, history + [[display_msg, None]]
 
 
 
 
 
 
238
 
239
  def bot_action(message, image, history):
240
+ """Genera la respuesta del bot con streaming."""
241
+ # El mensaje real (sin el tag de imagen) para enviarlo al modelo
242
+ real_message = message or ""
243
+ if not real_message.strip() and image:
244
+ real_message = "Analiza este mensaje de la imagen."
245
+
246
+ history_pairs = [
247
+ [h[0].replace(" [📎 imagen adjunta]", ""), h[1]]
248
+ for h in history[:-1]
249
+ if h[1] is not None
250
+ ]
 
 
 
 
 
 
 
 
 
251
 
252
+ history[-1][1] = ""
253
+ for chunk in respond(real_message, image, history_pairs):
254
+ history[-1][1] = chunk
255
  yield history
256
 
257
+ # Guardamos message e image antes de limpiarlos para usarlos en bot_action
258
  submit_btn.click(
259
  user_action,
260
  inputs=[msg_input, image_input, chatbot],
261
+ outputs=[msg_input, image_input, chatbot],
262
  ).then(
263
  bot_action,
264
  inputs=[msg_input, image_input, chatbot],
265
+ outputs=[chatbot],
266
  )
267
 
268
  msg_input.submit(
269
  user_action,
270
  inputs=[msg_input, image_input, chatbot],
271
+ outputs=[msg_input, image_input, chatbot],
272
  ).then(
273
  bot_action,
274
  inputs=[msg_input, image_input, chatbot],
275
+ outputs=[chatbot],
276
  )
277
 
278
+ clear_btn.click(lambda: (None, [], None), outputs=[msg_input, chatbot, image_input], queue=False)
 
 
 
279
 
280
 
281
  if __name__ == "__main__":
282
+ threading.Thread(target=start_server, daemon=True).start()
283
  demo.launch(
284
  server_name="0.0.0.0",
285
  server_port=7860,
286
+ ssr_mode=False,
287
+ theme=gr.themes.Soft(),
288
  )