histlearn commited on
Commit
e2ed4f2
Β·
verified Β·
1 Parent(s): b2d619d

feat: endpoint FT-Solo inicial (Qwen3-Embedding-4B + LoRA fold 01 + linear head)

Browse files

Sobe o cΓ³digo do endpoint (app.py, inference.py, config.py) e os artefatos do FT-Solo (adapter LoRA + cabeΓ§a linear do melhor fold segundo o manifesto). Gerado via Colab a partir do tar + zip no Drive.

Files changed (4) hide show
  1. app.py +83 -88
  2. app.py.bak_20260424_031409 +364 -0
  3. styles.css +152 -110
  4. styles.css.bak_20260424_031409 +177 -0
app.py CHANGED
@@ -1,25 +1,26 @@
1
  """Gradio app β€” endpoint de utilidade para community notes em PT-BR.
2
-
3
  ExpΓ΅e:
4
  - UI web com trΓͺs abas: Prever / Explicar / Sobre.
5
  - API HTTP em /gradio_api/call/predict e /gradio_api/call/explain (gerada
6
  automaticamente pelo Gradio a partir dos api_name).
7
-
8
  Para clientes Python, use gradio_client:
9
-
10
  from gradio_client import Client
11
  c = Client("<user>/<space>", hf_token="hf_...")
12
  score = c.predict("texto da nota...", api_name="/predict")
13
  """
14
  from __future__ import annotations
15
-
16
  import html
17
  import logging
18
  import os
19
  import traceback
20
-
 
21
  import gradio as gr
22
-
23
  from config import (
24
  CONFIDENCE_BOUNDS_ALTA,
25
  CONFIDENCE_BOUNDS_MEDIA,
@@ -27,26 +28,27 @@ from config import (
27
  )
28
  from inference import DEVICE, explain_occlusion, predict_one, warmup
29
 
30
- from pathlib import Path
31
- # se o styles.css estiver na mesma pasta do app.py
32
- STYLE_PATH = Path(__file__).resolve().parent / "styles.css"
33
- CUSTOM_CSS = ""
34
- if STYLE_PATH.exists():
35
- CUSTOM_CSS = STYLE_PATH.read_text(encoding="utf-8")
36
-
37
  logging.basicConfig(
38
  level=logging.INFO,
39
  format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
40
  )
41
  log = logging.getLogger("app")
42
-
43
-
 
 
 
 
 
 
 
 
44
  # ---------------------------------------------------------------------------
45
  # Warm-up agressivo β€” queremos que o primeiro request nΓ£o pague cold-start
46
  # ---------------------------------------------------------------------------
47
  MODEL_READY: bool
48
  MODEL_ERROR: str | None
49
-
50
  try:
51
  warmup()
52
  MODEL_READY = True
@@ -56,8 +58,8 @@ except Exception as exc: # noqa: BLE001 β€” queremos pegar qualquer falha de ca
56
  MODEL_READY = False
57
  MODEL_ERROR = f"{type(exc).__name__}: {exc}"
58
  log.error("Falha ao carregar modelo no startup:\n%s", traceback.format_exc())
59
-
60
-
61
  # ---------------------------------------------------------------------------
62
  # Helpers de apresentaΓ§Γ£o
63
  # ---------------------------------------------------------------------------
@@ -69,50 +71,43 @@ def _confidence_band(p: float) -> str:
69
  if p <= lo_m or p >= hi_m:
70
  return "MΓ©dia"
71
  return "Baixa"
72
-
73
-
74
  def _label(p: float) -> str:
75
  return "Útil" if p >= THRESHOLD_UTIL else "NΓ£o-ΓΊtil"
76
-
77
-
78
  def _score_card_html(p: float) -> str:
79
- """Card principal do resultado β€” badge de label + badge de confianΓ§a + probabilidade."""
80
  lbl = _label(p)
81
  band = _confidence_band(p)
82
-
83
- lbl_colors = {"Útil": ("#d8f3dc", "#1b4332"), "Não-útil": ("#fde2e4", "#9d0208")}
84
- band_colors = {
85
- "Alta": ("#d8f3dc", "#1b4332"),
86
- "MΓ©dia": ("#fff3bf", "#7c5c00"),
87
- "Baixa": ("#e9ecef", "#495057"),
88
- }
89
- lbg, lfg = lbl_colors[lbl]
90
- bbg, bfg = band_colors[band]
91
-
92
  return f"""
93
- <div style="background:#fff;border:1px solid #e9ecef;border-radius:16px;
94
- padding:18px 22px;box-shadow:0 4px 14px rgba(0,0,0,0.04);
95
- font-family:system-ui, -apple-system, sans-serif;">
96
- <div style="display:flex;justify-content:space-between;align-items:center;
97
- gap:12px;flex-wrap:wrap;">
98
  <div style="display:flex;gap:8px;flex-wrap:wrap;">
99
- <span style="background:{lbg};color:{lfg};padding:4px 12px;
100
- border-radius:999px;font-size:13px;font-weight:700;">{lbl}</span>
101
- <span style="background:{bbg};color:{bfg};padding:4px 12px;
102
- border-radius:999px;font-size:13px;font-weight:700;">
103
- ConfianΓ§a {band}
104
- </span>
105
  </div>
 
106
  <div style="text-align:right;">
107
- <div style="font-size:12px;color:#6c757d;">P(ΓΊtil)</div>
108
- <div style="font-size:32px;font-weight:800;color:#2b2d42;
109
- font-variant-numeric:tabular-nums;">{p:.3f}</div>
110
  </div>
111
  </div>
112
  </div>
113
  """
114
-
115
-
116
  def _contrib_color(v: float, v_max: float) -> str:
117
  if v_max <= 0:
118
  return "transparent"
@@ -121,8 +116,8 @@ def _contrib_color(v: float, v_max: float) -> str:
121
  if v > 0:
122
  return f"rgba(95, 168, 143, {alpha:.3f})" # verde (PALETA['util'] do notebook)
123
  return f"rgba(224, 123, 107, {alpha:.3f})" # coral (PALETA['nao_util'])
124
-
125
-
126
  def _highlighted_text_html(tokens: list[str], contribs: list[float]) -> str:
127
  if not tokens:
128
  return "<em>(sem palavras para destacar)</em>"
@@ -140,15 +135,15 @@ def _highlighted_text_html(tokens: list[str], contribs: list[float]) -> str:
140
  + " ".join(spans)
141
  + "</div>"
142
  )
143
-
144
-
145
  def _top_tokens_table_html(
146
  tokens: list[str], contribs: list[float], k: int = 5
147
  ) -> str:
148
  pairs = list(zip(tokens, contribs))
149
  pos = sorted([p for p in pairs if p[1] > 0], key=lambda x: -x[1])[:k]
150
  neg = sorted([p for p in pairs if p[1] < 0], key=lambda x: x[1])[:k]
151
-
152
  def _row(tok: str, v: float, side: str) -> str:
153
  color = "#1b4332" if side == "pos" else "#9d0208"
154
  sign = "+" if v > 0 else ""
@@ -158,11 +153,11 @@ def _top_tokens_table_html(
158
  f'<td style="padding:5px 8px;text-align:right;color:{color};'
159
  f'font-variant-numeric:tabular-nums;">{sign}{v:.4f}</td></tr>'
160
  )
161
-
162
  empty = '<tr><td colspan="2" style="padding:6px;color:#9aa1aa;"><em>β€”</em></td></tr>'
163
  pos_rows = "".join(_row(t, v, "pos") for t, v in pos) or empty
164
  neg_rows = "".join(_row(t, v, "neg") for t, v in neg) or empty
165
-
166
  return f"""
167
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;
168
  font-family:system-ui, -apple-system, sans-serif;">
@@ -180,8 +175,8 @@ def _top_tokens_table_html(
180
  </div>
181
  </div>
182
  """
183
-
184
-
185
  # ---------------------------------------------------------------------------
186
  # Handlers β€” retornam HTML para a UI + JSON para a API
187
  # ---------------------------------------------------------------------------
@@ -195,7 +190,7 @@ def handle_predict(text: str):
195
  f"<em>Modelo indisponΓ­vel: {html.escape(err)}</em>",
196
  {"error": "model_unavailable", "detail": err},
197
  )
198
-
199
  p = predict_one(text)
200
  return (
201
  _score_card_html(p),
@@ -205,8 +200,8 @@ def handle_predict(text: str):
205
  "confidence_band": _confidence_band(p),
206
  },
207
  )
208
-
209
-
210
  def handle_explain(text: str):
211
  text = (text or "").strip()
212
  if not text:
@@ -219,12 +214,12 @@ def handle_explain(text: str):
219
  "",
220
  {"error": "model_unavailable", "detail": err},
221
  )
222
-
223
  result = explain_occlusion(text)
224
  p = result["proba_full"]
225
  tokens = result["tokens"]
226
  contribs = result["contributions"]
227
-
228
  return (
229
  _score_card_html(p),
230
  _highlighted_text_html(tokens, contribs),
@@ -237,8 +232,8 @@ def handle_explain(text: str):
237
  "contributions": contribs,
238
  },
239
  )
240
-
241
-
242
  # ---------------------------------------------------------------------------
243
  # UI
244
  # ---------------------------------------------------------------------------
@@ -247,26 +242,26 @@ EXAMPLE_UTIL = (
247
  "A fonte correta pode ser conferida no link: https://www.gov.br/saude/..."
248
  )
249
  EXAMPLE_NAO = "Essa nota Γ© claramente desnecessΓ‘ria, Γ© opiniΓ£o pessoal do autor."
250
-
251
  INTRO_MD = """
252
  # Notinhas β€” endpoint de utilidade (FT-Solo)
253
-
254
  Classificador de utilidade para **community notes em portuguΓͺs**, baseado em
255
  **Qwen3-Embedding-4B + LoRA + cabeΓ§a linear** (modo fiel do FT-Solo, fold 01).
256
-
257
  - **Prever** β€” score + label + faixa de confianΓ§a.
258
  - **Explicar** β€” o mesmo + contribuiΓ§Γ£o de cada palavra via leave-one-out.
259
  - **Sobre** β€” detalhes tΓ©cnicos e limitaΓ§Γ΅es.
260
  """
261
-
262
-
263
  with gr.Blocks(
264
  title="Notinhas β€” endpoint de utilidade (FT-Solo)",
265
  theme=gr.themes.Base(),
266
  css=CUSTOM_CSS,
267
  ) as demo:
268
  gr.Markdown(INTRO_MD)
269
-
270
  if not MODEL_READY:
271
  gr.Markdown(
272
  f"""
@@ -277,7 +272,7 @@ with gr.Blocks(
277
  > **Settings β†’ Variables and secrets**.
278
  """
279
  )
280
-
281
  with gr.Tab("Prever"):
282
  with gr.Row():
283
  with gr.Column(scale=2):
@@ -292,14 +287,14 @@ with gr.Blocks(
292
  with gr.Column(scale=3):
293
  out_card_p = gr.HTML(label="Resultado")
294
  out_json_p = gr.JSON(label="Resposta da API")
295
-
296
  btn_p.click(
297
  handle_predict,
298
  inputs=[inp_p],
299
  outputs=[out_card_p, out_json_p],
300
  api_name="predict",
301
  )
302
-
303
  with gr.Tab("Explicar"):
304
  with gr.Row():
305
  with gr.Column(scale=2):
@@ -316,44 +311,44 @@ with gr.Blocks(
316
  out_hl = gr.HTML(label="ContribuiΓ§Γ£o por palavra")
317
  out_tbl = gr.HTML(label="Top tokens por lado")
318
  out_json_e = gr.JSON(label="Resposta da API")
319
-
320
  btn_e.click(
321
  handle_explain,
322
  inputs=[inp_e],
323
  outputs=[out_card_e, out_hl, out_tbl, out_json_e],
324
  api_name="explain",
325
  )
326
-
327
  with gr.Tab("Sobre"):
328
  gr.Markdown(
329
  f"""
330
  ### Detalhes tΓ©cnicos
331
-
332
  - **Modelo base**: `Qwen/Qwen3-Embedding-4B` (embedding, 2.560 dims, last-token pooling).
333
  - **AdaptaΓ§Γ£o**: LoRA treinado com alvo `label_binary_strict` (recorte A do projeto).
334
  - **CabeΓ§a**: `nn.Linear(2560, 1)` β†’ sigmoid.
335
  - **Prompt de instruΓ§Γ£o** (idΓͺntico ao treino):
336
-
337
  > `Instruct: Represent the following Brazilian Portuguese community note for binary classification of helpfulness.`
338
  > `Query: <texto>`
339
-
340
  - **max_length**: 256 tokens.
341
  - **Dispositivo atual**: `{DEVICE}`.
342
  - **Fold servido**: 01 (melhor fold segundo o manifesto do pipeline).
343
-
344
  ### MΓ©todo de explicaΓ§Γ£o
345
-
346
  A aba **Explicar** usa **occlusion word-level** (leave-one-out): para cada palavra
347
  separada por espaΓ§o, calculamos `Ξ” = P(texto completo) βˆ’ P(texto sem a palavra)`.
348
-
349
  - Ξ” positivo β‡’ palavra puxando para **ΓΊtil** (verde).
350
  - Ξ” negativo β‡’ palavra puxando para **nΓ£o-ΓΊtil** (coral).
351
-
352
  Γ‰ uma aproximaΓ§Γ£o rΓ‘pida do SHAP Partition usado no notebook de explicabilidade
353
  (~1–2 s vs ~12–15 s em GPU), com resultados visualmente comparΓ‘veis para notas curtas.
354
-
355
  ### LimitaΓ§Γ΅es
356
-
357
  - O rΓ³tulo `helpful` mede **aceitabilidade bipartidΓ‘ria**, nΓ£o qualidade editorial.
358
  A galeria curada do notebook mostra casos onde vizinhos semΓ’nticos idΓͺnticos
359
  recebem rΓ³tulos opostos por razΓ΅es polΓ­ticas.
@@ -362,11 +357,11 @@ separada por espaΓ§o, calculamos `Ξ” = P(texto completo) βˆ’ P(texto sem a palav
362
  subir para ensemble dos 5 folds (mΓ©dia de probabilidades).
363
  """
364
  )
365
-
366
-
367
  if __name__ == "__main__":
368
  demo.queue(default_concurrency_limit=1).launch(
369
  server_name="0.0.0.0",
370
  server_port=int(os.environ.get("PORT", 7860)),
371
  show_api=True,
372
- )
 
1
  """Gradio app β€” endpoint de utilidade para community notes em PT-BR.
2
+
3
  ExpΓ΅e:
4
  - UI web com trΓͺs abas: Prever / Explicar / Sobre.
5
  - API HTTP em /gradio_api/call/predict e /gradio_api/call/explain (gerada
6
  automaticamente pelo Gradio a partir dos api_name).
7
+
8
  Para clientes Python, use gradio_client:
9
+
10
  from gradio_client import Client
11
  c = Client("<user>/<space>", hf_token="hf_...")
12
  score = c.predict("texto da nota...", api_name="/predict")
13
  """
14
  from __future__ import annotations
15
+
16
  import html
17
  import logging
18
  import os
19
  import traceback
20
+ from pathlib import Path
21
+
22
  import gradio as gr
23
+
24
  from config import (
25
  CONFIDENCE_BOUNDS_ALTA,
26
  CONFIDENCE_BOUNDS_MEDIA,
 
28
  )
29
  from inference import DEVICE, explain_occlusion, predict_one, warmup
30
 
 
 
 
 
 
 
 
31
  logging.basicConfig(
32
  level=logging.INFO,
33
  format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
34
  )
35
  log = logging.getLogger("app")
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # CSS do projeto
39
+ # ---------------------------------------------------------------------------
40
+ APP_DIR = Path(__file__).resolve().parent
41
+ STYLE_PATH = APP_DIR / "styles.css"
42
+ CUSTOM_CSS = STYLE_PATH.read_text(encoding="utf-8") if STYLE_PATH.exists() else ""
43
+
44
+
45
+
46
  # ---------------------------------------------------------------------------
47
  # Warm-up agressivo β€” queremos que o primeiro request nΓ£o pague cold-start
48
  # ---------------------------------------------------------------------------
49
  MODEL_READY: bool
50
  MODEL_ERROR: str | None
51
+
52
  try:
53
  warmup()
54
  MODEL_READY = True
 
58
  MODEL_READY = False
59
  MODEL_ERROR = f"{type(exc).__name__}: {exc}"
60
  log.error("Falha ao carregar modelo no startup:\n%s", traceback.format_exc())
61
+
62
+
63
  # ---------------------------------------------------------------------------
64
  # Helpers de apresentaΓ§Γ£o
65
  # ---------------------------------------------------------------------------
 
71
  if p <= lo_m or p >= hi_m:
72
  return "MΓ©dia"
73
  return "Baixa"
74
+
75
+
76
  def _label(p: float) -> str:
77
  return "Útil" if p >= THRESHOLD_UTIL else "NΓ£o-ΓΊtil"
78
+
79
+
80
  def _score_card_html(p: float) -> str:
81
+ """Card principal do resultado β€” usando classes CSS do projeto."""
82
  lbl = _label(p)
83
  band = _confidence_band(p)
84
+
85
+ lbl_class = "notinhas-badge-util" if lbl == "Útil" else "notinhas-badge-nao-util"
86
+
87
+ if band == "Alta":
88
+ band_class = lbl_class
89
+ elif band == "MΓ©dia":
90
+ band_class = "notinhas-badge-media"
91
+ else:
92
+ band_class = "notinhas-badge-baixa"
93
+
94
  return f"""
95
+ <div class="notinhas-card">
96
+ <div style="display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;">
 
 
 
97
  <div style="display:flex;gap:8px;flex-wrap:wrap;">
98
+ <span class="notinhas-badge {lbl_class}">{lbl}</span>
99
+ <span class="notinhas-badge {band_class}">ConfianΓ§a {band}</span>
 
 
 
 
100
  </div>
101
+
102
  <div style="text-align:right;">
103
+ <div class="notinhas-score-label">P(ΓΊtil)</div>
104
+ <div class="notinhas-score-value">{p:.3f}</div>
 
105
  </div>
106
  </div>
107
  </div>
108
  """
109
+
110
+
111
  def _contrib_color(v: float, v_max: float) -> str:
112
  if v_max <= 0:
113
  return "transparent"
 
116
  if v > 0:
117
  return f"rgba(95, 168, 143, {alpha:.3f})" # verde (PALETA['util'] do notebook)
118
  return f"rgba(224, 123, 107, {alpha:.3f})" # coral (PALETA['nao_util'])
119
+
120
+
121
  def _highlighted_text_html(tokens: list[str], contribs: list[float]) -> str:
122
  if not tokens:
123
  return "<em>(sem palavras para destacar)</em>"
 
135
  + " ".join(spans)
136
  + "</div>"
137
  )
138
+
139
+
140
  def _top_tokens_table_html(
141
  tokens: list[str], contribs: list[float], k: int = 5
142
  ) -> str:
143
  pairs = list(zip(tokens, contribs))
144
  pos = sorted([p for p in pairs if p[1] > 0], key=lambda x: -x[1])[:k]
145
  neg = sorted([p for p in pairs if p[1] < 0], key=lambda x: x[1])[:k]
146
+
147
  def _row(tok: str, v: float, side: str) -> str:
148
  color = "#1b4332" if side == "pos" else "#9d0208"
149
  sign = "+" if v > 0 else ""
 
153
  f'<td style="padding:5px 8px;text-align:right;color:{color};'
154
  f'font-variant-numeric:tabular-nums;">{sign}{v:.4f}</td></tr>'
155
  )
156
+
157
  empty = '<tr><td colspan="2" style="padding:6px;color:#9aa1aa;"><em>β€”</em></td></tr>'
158
  pos_rows = "".join(_row(t, v, "pos") for t, v in pos) or empty
159
  neg_rows = "".join(_row(t, v, "neg") for t, v in neg) or empty
160
+
161
  return f"""
162
  <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;
163
  font-family:system-ui, -apple-system, sans-serif;">
 
175
  </div>
176
  </div>
177
  """
178
+
179
+
180
  # ---------------------------------------------------------------------------
181
  # Handlers β€” retornam HTML para a UI + JSON para a API
182
  # ---------------------------------------------------------------------------
 
190
  f"<em>Modelo indisponΓ­vel: {html.escape(err)}</em>",
191
  {"error": "model_unavailable", "detail": err},
192
  )
193
+
194
  p = predict_one(text)
195
  return (
196
  _score_card_html(p),
 
200
  "confidence_band": _confidence_band(p),
201
  },
202
  )
203
+
204
+
205
  def handle_explain(text: str):
206
  text = (text or "").strip()
207
  if not text:
 
214
  "",
215
  {"error": "model_unavailable", "detail": err},
216
  )
217
+
218
  result = explain_occlusion(text)
219
  p = result["proba_full"]
220
  tokens = result["tokens"]
221
  contribs = result["contributions"]
222
+
223
  return (
224
  _score_card_html(p),
225
  _highlighted_text_html(tokens, contribs),
 
232
  "contributions": contribs,
233
  },
234
  )
235
+
236
+
237
  # ---------------------------------------------------------------------------
238
  # UI
239
  # ---------------------------------------------------------------------------
 
242
  "A fonte correta pode ser conferida no link: https://www.gov.br/saude/..."
243
  )
244
  EXAMPLE_NAO = "Essa nota Γ© claramente desnecessΓ‘ria, Γ© opiniΓ£o pessoal do autor."
245
+
246
  INTRO_MD = """
247
  # Notinhas β€” endpoint de utilidade (FT-Solo)
248
+
249
  Classificador de utilidade para **community notes em portuguΓͺs**, baseado em
250
  **Qwen3-Embedding-4B + LoRA + cabeΓ§a linear** (modo fiel do FT-Solo, fold 01).
251
+
252
  - **Prever** β€” score + label + faixa de confianΓ§a.
253
  - **Explicar** β€” o mesmo + contribuiΓ§Γ£o de cada palavra via leave-one-out.
254
  - **Sobre** β€” detalhes tΓ©cnicos e limitaΓ§Γ΅es.
255
  """
256
+
257
+
258
  with gr.Blocks(
259
  title="Notinhas β€” endpoint de utilidade (FT-Solo)",
260
  theme=gr.themes.Base(),
261
  css=CUSTOM_CSS,
262
  ) as demo:
263
  gr.Markdown(INTRO_MD)
264
+
265
  if not MODEL_READY:
266
  gr.Markdown(
267
  f"""
 
272
  > **Settings β†’ Variables and secrets**.
273
  """
274
  )
275
+
276
  with gr.Tab("Prever"):
277
  with gr.Row():
278
  with gr.Column(scale=2):
 
287
  with gr.Column(scale=3):
288
  out_card_p = gr.HTML(label="Resultado")
289
  out_json_p = gr.JSON(label="Resposta da API")
290
+
291
  btn_p.click(
292
  handle_predict,
293
  inputs=[inp_p],
294
  outputs=[out_card_p, out_json_p],
295
  api_name="predict",
296
  )
297
+
298
  with gr.Tab("Explicar"):
299
  with gr.Row():
300
  with gr.Column(scale=2):
 
311
  out_hl = gr.HTML(label="ContribuiΓ§Γ£o por palavra")
312
  out_tbl = gr.HTML(label="Top tokens por lado")
313
  out_json_e = gr.JSON(label="Resposta da API")
314
+
315
  btn_e.click(
316
  handle_explain,
317
  inputs=[inp_e],
318
  outputs=[out_card_e, out_hl, out_tbl, out_json_e],
319
  api_name="explain",
320
  )
321
+
322
  with gr.Tab("Sobre"):
323
  gr.Markdown(
324
  f"""
325
  ### Detalhes tΓ©cnicos
326
+
327
  - **Modelo base**: `Qwen/Qwen3-Embedding-4B` (embedding, 2.560 dims, last-token pooling).
328
  - **AdaptaΓ§Γ£o**: LoRA treinado com alvo `label_binary_strict` (recorte A do projeto).
329
  - **CabeΓ§a**: `nn.Linear(2560, 1)` β†’ sigmoid.
330
  - **Prompt de instruΓ§Γ£o** (idΓͺntico ao treino):
331
+
332
  > `Instruct: Represent the following Brazilian Portuguese community note for binary classification of helpfulness.`
333
  > `Query: <texto>`
334
+
335
  - **max_length**: 256 tokens.
336
  - **Dispositivo atual**: `{DEVICE}`.
337
  - **Fold servido**: 01 (melhor fold segundo o manifesto do pipeline).
338
+
339
  ### MΓ©todo de explicaΓ§Γ£o
340
+
341
  A aba **Explicar** usa **occlusion word-level** (leave-one-out): para cada palavra
342
  separada por espaΓ§o, calculamos `Ξ” = P(texto completo) βˆ’ P(texto sem a palavra)`.
343
+
344
  - Ξ” positivo β‡’ palavra puxando para **ΓΊtil** (verde).
345
  - Ξ” negativo β‡’ palavra puxando para **nΓ£o-ΓΊtil** (coral).
346
+
347
  Γ‰ uma aproximaΓ§Γ£o rΓ‘pida do SHAP Partition usado no notebook de explicabilidade
348
  (~1–2 s vs ~12–15 s em GPU), com resultados visualmente comparΓ‘veis para notas curtas.
349
+
350
  ### LimitaΓ§Γ΅es
351
+
352
  - O rΓ³tulo `helpful` mede **aceitabilidade bipartidΓ‘ria**, nΓ£o qualidade editorial.
353
  A galeria curada do notebook mostra casos onde vizinhos semΓ’nticos idΓͺnticos
354
  recebem rΓ³tulos opostos por razΓ΅es polΓ­ticas.
 
357
  subir para ensemble dos 5 folds (mΓ©dia de probabilidades).
358
  """
359
  )
360
+
361
+
362
  if __name__ == "__main__":
363
  demo.queue(default_concurrency_limit=1).launch(
364
  server_name="0.0.0.0",
365
  server_port=int(os.environ.get("PORT", 7860)),
366
  show_api=True,
367
+ )
app.py.bak_20260424_031409 ADDED
@@ -0,0 +1,364 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio app β€” endpoint de utilidade para community notes em PT-BR.
2
+
3
+ ExpΓ΅e:
4
+ - UI web com trΓͺs abas: Prever / Explicar / Sobre.
5
+ - API HTTP em /gradio_api/call/predict e /gradio_api/call/explain (gerada
6
+ automaticamente pelo Gradio a partir dos api_name).
7
+
8
+ Para clientes Python, use gradio_client:
9
+
10
+ from gradio_client import Client
11
+ c = Client("<user>/<space>", hf_token="hf_...")
12
+ score = c.predict("texto da nota...", api_name="/predict")
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import html
17
+ import logging
18
+ import os
19
+ import traceback
20
+
21
+ import gradio as gr
22
+
23
+ from config import (
24
+ CONFIDENCE_BOUNDS_ALTA,
25
+ CONFIDENCE_BOUNDS_MEDIA,
26
+ THRESHOLD_UTIL,
27
+ )
28
+ from inference import DEVICE, explain_occlusion, predict_one, warmup
29
+
30
+ logging.basicConfig(
31
+ level=logging.INFO,
32
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
33
+ )
34
+ log = logging.getLogger("app")
35
+
36
+
37
+ # ---------------------------------------------------------------------------
38
+ # Warm-up agressivo β€” queremos que o primeiro request nΓ£o pague cold-start
39
+ # ---------------------------------------------------------------------------
40
+ MODEL_READY: bool
41
+ MODEL_ERROR: str | None
42
+
43
+ try:
44
+ warmup()
45
+ MODEL_READY = True
46
+ MODEL_ERROR = None
47
+ log.info("Modelo carregado no startup. Device=%s", DEVICE)
48
+ except Exception as exc: # noqa: BLE001 β€” queremos pegar qualquer falha de carregamento
49
+ MODEL_READY = False
50
+ MODEL_ERROR = f"{type(exc).__name__}: {exc}"
51
+ log.error("Falha ao carregar modelo no startup:\n%s", traceback.format_exc())
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Helpers de apresentaΓ§Γ£o
56
+ # ---------------------------------------------------------------------------
57
+ def _confidence_band(p: float) -> str:
58
+ lo_a, hi_a = CONFIDENCE_BOUNDS_ALTA
59
+ lo_m, hi_m = CONFIDENCE_BOUNDS_MEDIA
60
+ if p <= lo_a or p >= hi_a:
61
+ return "Alta"
62
+ if p <= lo_m or p >= hi_m:
63
+ return "MΓ©dia"
64
+ return "Baixa"
65
+
66
+
67
+ def _label(p: float) -> str:
68
+ return "Útil" if p >= THRESHOLD_UTIL else "Não-útil"
69
+
70
+
71
+ def _score_card_html(p: float) -> str:
72
+ """Card principal do resultado β€” badge de label + badge de confianΓ§a + probabilidade."""
73
+ lbl = _label(p)
74
+ band = _confidence_band(p)
75
+
76
+ lbl_colors = {"Útil": ("#d8f3dc", "#1b4332"), "Não-útil": ("#fde2e4", "#9d0208")}
77
+ band_colors = {
78
+ "Alta": ("#d8f3dc", "#1b4332"),
79
+ "MΓ©dia": ("#fff3bf", "#7c5c00"),
80
+ "Baixa": ("#e9ecef", "#495057"),
81
+ }
82
+ lbg, lfg = lbl_colors[lbl]
83
+ bbg, bfg = band_colors[band]
84
+
85
+ return f"""
86
+ <div style="background:#fff;border:1px solid #e9ecef;border-radius:16px;
87
+ padding:18px 22px;box-shadow:0 4px 14px rgba(0,0,0,0.04);
88
+ font-family:system-ui, -apple-system, sans-serif;">
89
+ <div style="display:flex;justify-content:space-between;align-items:center;
90
+ gap:12px;flex-wrap:wrap;">
91
+ <div style="display:flex;gap:8px;flex-wrap:wrap;">
92
+ <span style="background:{lbg};color:{lfg};padding:4px 12px;
93
+ border-radius:999px;font-size:13px;font-weight:700;">{lbl}</span>
94
+ <span style="background:{bbg};color:{bfg};padding:4px 12px;
95
+ border-radius:999px;font-size:13px;font-weight:700;">
96
+ ConfianΓ§a {band}
97
+ </span>
98
+ </div>
99
+ <div style="text-align:right;">
100
+ <div style="font-size:12px;color:#6c757d;">P(ΓΊtil)</div>
101
+ <div style="font-size:32px;font-weight:800;color:#2b2d42;
102
+ font-variant-numeric:tabular-nums;">{p:.3f}</div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ """
107
+
108
+
109
+ def _contrib_color(v: float, v_max: float) -> str:
110
+ if v_max <= 0:
111
+ return "transparent"
112
+ intensity = min(1.0, abs(v) / v_max)
113
+ alpha = 0.15 + 0.65 * intensity # 0.15 .. 0.80
114
+ if v > 0:
115
+ return f"rgba(95, 168, 143, {alpha:.3f})" # verde (PALETA['util'] do notebook)
116
+ return f"rgba(224, 123, 107, {alpha:.3f})" # coral (PALETA['nao_util'])
117
+
118
+
119
+ def _highlighted_text_html(tokens: list[str], contribs: list[float]) -> str:
120
+ if not tokens:
121
+ return "<em>(sem palavras para destacar)</em>"
122
+ v_max = max((abs(c) for c in contribs), default=1e-9) or 1e-9
123
+ spans = []
124
+ for tok, c in zip(tokens, contribs):
125
+ bg = _contrib_color(c, v_max)
126
+ spans.append(
127
+ f'<span style="background:{bg};padding:2px 4px;border-radius:4px;'
128
+ f'margin:0 1px;" title="Ξ”={c:+.4f}">{html.escape(tok)}</span>'
129
+ )
130
+ return (
131
+ '<div style="font-size:15px;line-height:2;color:#212529;'
132
+ 'font-family:system-ui, -apple-system, sans-serif;padding:4px;">'
133
+ + " ".join(spans)
134
+ + "</div>"
135
+ )
136
+
137
+
138
+ def _top_tokens_table_html(
139
+ tokens: list[str], contribs: list[float], k: int = 5
140
+ ) -> str:
141
+ pairs = list(zip(tokens, contribs))
142
+ pos = sorted([p for p in pairs if p[1] > 0], key=lambda x: -x[1])[:k]
143
+ neg = sorted([p for p in pairs if p[1] < 0], key=lambda x: x[1])[:k]
144
+
145
+ def _row(tok: str, v: float, side: str) -> str:
146
+ color = "#1b4332" if side == "pos" else "#9d0208"
147
+ sign = "+" if v > 0 else ""
148
+ return (
149
+ f'<tr><td style="padding:5px 8px;color:{color};">'
150
+ f"{html.escape(tok)}</td>"
151
+ f'<td style="padding:5px 8px;text-align:right;color:{color};'
152
+ f'font-variant-numeric:tabular-nums;">{sign}{v:.4f}</td></tr>'
153
+ )
154
+
155
+ empty = '<tr><td colspan="2" style="padding:6px;color:#9aa1aa;"><em>β€”</em></td></tr>'
156
+ pos_rows = "".join(_row(t, v, "pos") for t, v in pos) or empty
157
+ neg_rows = "".join(_row(t, v, "neg") for t, v in neg) or empty
158
+
159
+ return f"""
160
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:14px;margin-top:12px;
161
+ font-family:system-ui, -apple-system, sans-serif;">
162
+ <div style="background:#fcfcfd;border:1px solid #eef2f7;border-radius:12px;padding:12px;">
163
+ <div style="font-size:13px;font-weight:700;color:#1b4332;margin-bottom:6px;">
164
+ Empurram para ΓΊtil
165
+ </div>
166
+ <table style="width:100%;border-collapse:collapse;font-size:13px;">{pos_rows}</table>
167
+ </div>
168
+ <div style="background:#fcfcfd;border:1px solid #eef2f7;border-radius:12px;padding:12px;">
169
+ <div style="font-size:13px;font-weight:700;color:#9d0208;margin-bottom:6px;">
170
+ Empurram para nΓ£o-ΓΊtil
171
+ </div>
172
+ <table style="width:100%;border-collapse:collapse;font-size:13px;">{neg_rows}</table>
173
+ </div>
174
+ </div>
175
+ """
176
+
177
+
178
+ # ---------------------------------------------------------------------------
179
+ # Handlers β€” retornam HTML para a UI + JSON para a API
180
+ # ---------------------------------------------------------------------------
181
+ def handle_predict(text: str):
182
+ text = (text or "").strip()
183
+ if not text:
184
+ return "<em>ForneΓ§a um texto.</em>", {"error": "empty_input"}
185
+ if not MODEL_READY:
186
+ err = MODEL_ERROR or "modelo indisponΓ­vel"
187
+ return (
188
+ f"<em>Modelo indisponΓ­vel: {html.escape(err)}</em>",
189
+ {"error": "model_unavailable", "detail": err},
190
+ )
191
+
192
+ p = predict_one(text)
193
+ return (
194
+ _score_card_html(p),
195
+ {
196
+ "proba_util": p,
197
+ "label": _label(p),
198
+ "confidence_band": _confidence_band(p),
199
+ },
200
+ )
201
+
202
+
203
+ def handle_explain(text: str):
204
+ text = (text or "").strip()
205
+ if not text:
206
+ return "<em>ForneΓ§a um texto.</em>", "", "", {"error": "empty_input"}
207
+ if not MODEL_READY:
208
+ err = MODEL_ERROR or "modelo indisponΓ­vel"
209
+ return (
210
+ f"<em>Modelo indisponΓ­vel: {html.escape(err)}</em>",
211
+ "",
212
+ "",
213
+ {"error": "model_unavailable", "detail": err},
214
+ )
215
+
216
+ result = explain_occlusion(text)
217
+ p = result["proba_full"]
218
+ tokens = result["tokens"]
219
+ contribs = result["contributions"]
220
+
221
+ return (
222
+ _score_card_html(p),
223
+ _highlighted_text_html(tokens, contribs),
224
+ _top_tokens_table_html(tokens, contribs),
225
+ {
226
+ "proba_util": p,
227
+ "label": _label(p),
228
+ "confidence_band": _confidence_band(p),
229
+ "tokens": tokens,
230
+ "contributions": contribs,
231
+ },
232
+ )
233
+
234
+
235
+ # ---------------------------------------------------------------------------
236
+ # UI
237
+ # ---------------------------------------------------------------------------
238
+ EXAMPLE_UTIL = (
239
+ "Segundo dados oficiais do MinistΓ©rio da SaΓΊde, o nΓΊmero citado no tweet Γ© falso. "
240
+ "A fonte correta pode ser conferida no link: https://www.gov.br/saude/..."
241
+ )
242
+ EXAMPLE_NAO = "Essa nota Γ© claramente desnecessΓ‘ria, Γ© opiniΓ£o pessoal do autor."
243
+
244
+ INTRO_MD = """
245
+ # Notinhas β€” endpoint de utilidade (FT-Solo)
246
+
247
+ Classificador de utilidade para **community notes em portuguΓͺs**, baseado em
248
+ **Qwen3-Embedding-4B + LoRA + cabeΓ§a linear** (modo fiel do FT-Solo, fold 01).
249
+
250
+ - **Prever** β€” score + label + faixa de confianΓ§a.
251
+ - **Explicar** β€” o mesmo + contribuiΓ§Γ£o de cada palavra via leave-one-out.
252
+ - **Sobre** β€” detalhes tΓ©cnicos e limitaΓ§Γ΅es.
253
+ """
254
+
255
+
256
+ with gr.Blocks(
257
+ title="Notinhas β€” endpoint de utilidade (FT-Solo)",
258
+ theme=gr.themes.Soft(primary_hue="emerald", neutral_hue="slate"),
259
+ ) as demo:
260
+ gr.Markdown(INTRO_MD)
261
+
262
+ if not MODEL_READY:
263
+ gr.Markdown(
264
+ f"""
265
+ > ⚠️ **Modelo não carregou.** Detalhe: `{html.escape(MODEL_ERROR or '')}`
266
+ >
267
+ > Verifique que `artifacts/fold_01_adapter/` e `artifacts/fold_01_head.pt` estΓ£o presentes
268
+ > no repositΓ³rio do Space. Se o modelo base exigir autenticaΓ§Γ£o, configure `HF_TOKEN` em
269
+ > **Settings β†’ Variables and secrets**.
270
+ """
271
+ )
272
+
273
+ with gr.Tab("Prever"):
274
+ with gr.Row():
275
+ with gr.Column(scale=2):
276
+ inp_p = gr.Textbox(
277
+ label="Texto da nota",
278
+ placeholder="Cole aqui o texto em portuguΓͺs...",
279
+ lines=7,
280
+ max_lines=25,
281
+ )
282
+ btn_p = gr.Button("Prever", variant="primary")
283
+ gr.Examples(examples=[[EXAMPLE_UTIL], [EXAMPLE_NAO]], inputs=[inp_p])
284
+ with gr.Column(scale=3):
285
+ out_card_p = gr.HTML(label="Resultado")
286
+ out_json_p = gr.JSON(label="Resposta da API")
287
+
288
+ btn_p.click(
289
+ handle_predict,
290
+ inputs=[inp_p],
291
+ outputs=[out_card_p, out_json_p],
292
+ api_name="predict",
293
+ )
294
+
295
+ with gr.Tab("Explicar"):
296
+ with gr.Row():
297
+ with gr.Column(scale=2):
298
+ inp_e = gr.Textbox(
299
+ label="Texto da nota",
300
+ placeholder="Cole aqui o texto em portuguΓͺs...",
301
+ lines=7,
302
+ max_lines=25,
303
+ )
304
+ btn_e = gr.Button("Explicar", variant="primary")
305
+ gr.Examples(examples=[[EXAMPLE_UTIL], [EXAMPLE_NAO]], inputs=[inp_e])
306
+ with gr.Column(scale=3):
307
+ out_card_e = gr.HTML(label="Resultado")
308
+ out_hl = gr.HTML(label="ContribuiΓ§Γ£o por palavra")
309
+ out_tbl = gr.HTML(label="Top tokens por lado")
310
+ out_json_e = gr.JSON(label="Resposta da API")
311
+
312
+ btn_e.click(
313
+ handle_explain,
314
+ inputs=[inp_e],
315
+ outputs=[out_card_e, out_hl, out_tbl, out_json_e],
316
+ api_name="explain",
317
+ )
318
+
319
+ with gr.Tab("Sobre"):
320
+ gr.Markdown(
321
+ f"""
322
+ ### Detalhes tΓ©cnicos
323
+
324
+ - **Modelo base**: `Qwen/Qwen3-Embedding-4B` (embedding, 2.560 dims, last-token pooling).
325
+ - **AdaptaΓ§Γ£o**: LoRA treinado com alvo `label_binary_strict` (recorte A do projeto).
326
+ - **CabeΓ§a**: `nn.Linear(2560, 1)` β†’ sigmoid.
327
+ - **Prompt de instruΓ§Γ£o** (idΓͺntico ao treino):
328
+
329
+ > `Instruct: Represent the following Brazilian Portuguese community note for binary classification of helpfulness.`
330
+ > `Query: <texto>`
331
+
332
+ - **max_length**: 256 tokens.
333
+ - **Dispositivo atual**: `{DEVICE}`.
334
+ - **Fold servido**: 01 (melhor fold segundo o manifesto do pipeline).
335
+
336
+ ### MΓ©todo de explicaΓ§Γ£o
337
+
338
+ A aba **Explicar** usa **occlusion word-level** (leave-one-out): para cada palavra
339
+ separada por espaΓ§o, calculamos `Ξ” = P(texto completo) βˆ’ P(texto sem a palavra)`.
340
+
341
+ - Ξ” positivo β‡’ palavra puxando para **ΓΊtil** (verde).
342
+ - Ξ” negativo β‡’ palavra puxando para **nΓ£o-ΓΊtil** (coral).
343
+
344
+ Γ‰ uma aproximaΓ§Γ£o rΓ‘pida do SHAP Partition usado no notebook de explicabilidade
345
+ (~1–2 s vs ~12–15 s em GPU), com resultados visualmente comparΓ‘veis para notas curtas.
346
+
347
+ ### LimitaΓ§Γ΅es
348
+
349
+ - O rΓ³tulo `helpful` mede **aceitabilidade bipartidΓ‘ria**, nΓ£o qualidade editorial.
350
+ A galeria curada do notebook mostra casos onde vizinhos semΓ’nticos idΓͺnticos
351
+ recebem rΓ³tulos opostos por razΓ΅es polΓ­ticas.
352
+ - Textos sΓ£o truncados em 256 tokens.
353
+ - Este endpoint serve um ΓΊnico fold. Para produΓ§Γ£o com ganho marginal de robustez,
354
+ subir para ensemble dos 5 folds (mΓ©dia de probabilidades).
355
+ """
356
+ )
357
+
358
+
359
+ if __name__ == "__main__":
360
+ demo.queue(default_concurrency_limit=1).launch(
361
+ server_name="0.0.0.0",
362
+ server_port=int(os.environ.get("PORT", 7860)),
363
+ show_api=True,
364
+ )
styles.css CHANGED
@@ -1,8 +1,4 @@
1
- /* ── Reset ─────────────────────────────────────────────────── */
2
- *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
3
- html { scroll-behavior: smooth; }
4
-
5
- /* ── VariΓ‘veis compartilhadas ─────────────────────────────── */
6
  :root {
7
  --bg: #fdfbf7;
8
  --text: #2c2c2c;
@@ -17,119 +13,165 @@ html { scroll-behavior: smooth; }
17
  --card-border: #ddd8d0;
18
  --soft: #f5f3ef;
19
 
20
- /* Fontes β€” podem ser sobrescritas por pΓ‘gina */
21
- --font-serif: 'Source Serif 4', Georgia, serif;
22
- --font-sans: 'DM Sans', system-ui, sans-serif;
23
- --font-mono: 'JetBrains Mono', monospace;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
 
25
- /* Aliases para compatibilidade com pΓ‘ginas que usam nomes antigos */
26
- --serif: var(--font-serif);
27
- --sans: var(--font-sans);
28
- --mono: var(--font-mono);
 
 
29
  }
30
 
31
- /* ── Base ──────────────────────────────────────────────────── */
32
- body {
 
 
 
 
 
 
 
 
 
 
33
  font-family: var(--font-sans);
34
- background: var(--bg);
35
- color: var(--text);
36
- line-height: 1.65;
37
- -webkit-font-smoothing: antialiased;
38
- }
39
-
40
- /* ── NavegaΓ§Γ£o principal ───────────────────────────────────── */
41
- .site-nav {
42
- position: sticky;
43
- top: 0;
44
- z-index: 1000;
45
- display: flex;
46
- align-items: center;
47
- gap: 6px;
48
- width: 100%;
49
- min-height: 52px;
50
- padding: 0 24px;
51
- background: var(--accent);
52
- box-shadow: 0 1px 12px rgba(0,0,0,.12);
53
- /* scrollΓ‘vel horizontalmente em telas estreitas, sem barra visΓ­vel */
54
- overflow-x: auto;
55
- -webkit-overflow-scrolling: touch;
56
- scrollbar-width: none;
57
- }
58
- .site-nav::-webkit-scrollbar { display: none; }
59
-
60
- .site-nav__brand {
61
- font-family: var(--font-mono);
62
- font-size: 11px;
63
- letter-spacing: 1.5px;
64
- text-transform: uppercase;
65
- color: #d4a017;
66
- padding: 15px 18px 15px 0;
67
- margin-right: 10px;
68
- text-decoration: none;
69
- white-space: nowrap;
70
- flex-shrink: 0;
71
- border-right: 1px solid rgba(255,255,255,.12);
72
- }
73
- .site-nav__link {
74
- color: rgba(255,255,255,.72);
75
- text-decoration: none;
76
- padding: 14px 16px;
77
  font-size: 13px;
78
- font-weight: 500;
79
- border-radius: 8px;
80
- white-space: nowrap;
81
- flex-shrink: 0;
82
- transition: background-color .18s ease, color .18s ease;
83
- }
84
- .site-nav__link:hover,
85
- .site-nav__link:focus-visible {
86
- color: #fff;
87
- background: rgba(255,255,255,.08);
88
- outline: none;
89
- }
90
- .site-nav__link.is-active,
91
- .site-nav__link[aria-current="page"] {
92
- color: #fff;
93
- font-weight: 600;
94
- background: rgba(255,255,255,.13);
95
- }
96
- @media (max-width: 640px) {
97
- .site-nav { padding: 0 14px; }
98
- .site-nav__brand { padding-right: 12px; margin-right: 6px; }
99
- }
100
-
101
- /* ── Layout ────────────────────────────────────────────────── */
102
- .container {
103
- max-width: 1180px;
104
- margin: 0 auto;
105
- padding: 32px 24px 60px;
106
- }
107
-
108
- /* ── UtilitΓ‘rios ───────────────────────────────────────────── */
109
- .kicker {
110
- font-family: var(--font-mono);
111
- font-size: 11px;
112
- letter-spacing: 2px;
113
- text-transform: uppercase;
114
  color: var(--muted);
115
- margin-bottom: 12px;
116
  }
117
- code {
 
118
  font-family: var(--font-mono);
119
- font-size: 12px;
120
- background: #f0ede6;
121
- padding: 1px 5px;
122
- border-radius: 4px;
123
  }
124
 
125
- /* ── RodapΓ© ────────────────────────────────────────────────── */
126
- .footer {
127
- margin-top: 40px;
128
- padding-top: 20px;
129
- border-top: 1px solid var(--grid);
130
- font-size: 11px;
131
- color: var(--muted);
132
- text-align: center;
133
- line-height: 1.6;
134
  }
135
- .footer a { color: var(--accent); }
 
1
+ /* ── VariΓ‘veis compartilhadas do projeto ───────────────────── */
 
 
 
 
2
  :root {
3
  --bg: #fdfbf7;
4
  --text: #2c2c2c;
 
13
  --card-border: #ddd8d0;
14
  --soft: #f5f3ef;
15
 
16
+ --font-serif: Georgia, 'Source Serif 4', serif;
17
+ --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
18
+ --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
19
+ }
20
+
21
+ /* Importante:
22
+ NÃO usamos reset global com margin:0/padding:0,
23
+ porque isso quebra componentes internos do Gradio.
24
+ */
25
+ *, *::before, *::after {
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ /* ── Base Gradio ───────────────────────────────────────────── */
30
+ .gradio-container {
31
+ background: var(--bg) !important;
32
+ color: var(--text) !important;
33
+ font-family: var(--font-sans) !important;
34
+ line-height: 1.55;
35
+ }
36
+
37
+ .gradio-container h1,
38
+ .gradio-container h2,
39
+ .gradio-container h3 {
40
+ color: var(--accent);
41
+ letter-spacing: -0.02em;
42
+ }
43
+
44
+ .gradio-container h1 {
45
+ font-family: var(--font-serif);
46
+ font-size: clamp(1.7rem, 2vw, 2.35rem);
47
+ margin-bottom: 0.45rem;
48
+ }
49
+
50
+ .gradio-container p {
51
+ margin-bottom: 0.65rem;
52
+ }
53
+
54
+ .gradio-container ul,
55
+ .gradio-container ol {
56
+ margin: 0.35rem 0 0.9rem 1.25rem;
57
+ padding-left: 1rem;
58
+ }
59
+
60
+ .gradio-container li {
61
+ margin: 0.15rem 0;
62
+ }
63
+
64
+ /* ── Abas: corrige PreverExplicarSobre colado ─────────────── */
65
+ .gradio-container [role="tablist"] {
66
+ gap: 0.35rem !important;
67
+ border-bottom: 1px solid var(--grid) !important;
68
+ margin-top: 0.9rem !important;
69
+ margin-bottom: 1rem !important;
70
+ }
71
+
72
+ .gradio-container button[role="tab"] {
73
+ padding: 0.55rem 0.85rem !important;
74
+ margin-right: 0.15rem !important;
75
+ border-radius: 10px 10px 0 0 !important;
76
+ color: var(--accent) !important;
77
+ font-weight: 600 !important;
78
+ }
79
+
80
+ .gradio-container button[role="tab"][aria-selected="true"] {
81
+ background: color-mix(in srgb, var(--accent) 10%, transparent) !important;
82
+ border-bottom-color: var(--accent) !important;
83
+ }
84
+
85
+ /* ── Campos, painΓ©is e botΓ΅es ─────────────────────────────── */
86
+ .gradio-container textarea,
87
+ .gradio-container input {
88
+ font-family: var(--font-sans) !important;
89
+ }
90
+
91
+ .gradio-container .block,
92
+ .gradio-container .panel,
93
+ .gradio-container .form {
94
+ border-radius: 16px !important;
95
+ }
96
 
97
+ .gradio-container button.primary,
98
+ .gradio-container .primary {
99
+ background: var(--accent) !important;
100
+ border-color: var(--accent) !important;
101
+ color: #fff !important;
102
+ font-weight: 700 !important;
103
  }
104
 
105
+ .gradio-container button.primary:hover,
106
+ .gradio-container .primary:hover {
107
+ filter: brightness(1.08);
108
+ }
109
+
110
+ /* ── Cards prΓ³prios do endpoint ───────────────────────────── */
111
+ .notinhas-card {
112
+ background: #ffffff;
113
+ border: 1px solid var(--card-border);
114
+ border-radius: 18px;
115
+ padding: 18px 22px;
116
+ box-shadow: 0 4px 14px rgba(38, 70, 83, 0.06);
117
  font-family: var(--font-sans);
118
+ }
119
+
120
+ .notinhas-soft-card {
121
+ background: var(--soft);
122
+ border: 1px solid var(--grid);
123
+ border-radius: 14px;
124
+ padding: 14px;
125
+ font-family: var(--font-sans);
126
+ }
127
+
128
+ .notinhas-badge {
129
+ display: inline-block;
130
+ padding: 4px 12px;
131
+ border-radius: 999px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  font-size: 13px;
133
+ font-weight: 700;
134
+ }
135
+
136
+ .notinhas-badge-util {
137
+ background: #d8f3dc;
138
+ color: #1b4332;
139
+ }
140
+
141
+ .notinhas-badge-nao-util {
142
+ background: #fde2e4;
143
+ color: #9d0208;
144
+ }
145
+
146
+ .notinhas-badge-media {
147
+ background: #fff3bf;
148
+ color: #7c5c00;
149
+ }
150
+
151
+ .notinhas-badge-baixa {
152
+ background: #e9ecef;
153
+ color: #495057;
154
+ }
155
+
156
+ .notinhas-score-label {
157
+ font-size: 12px;
 
 
 
 
 
 
 
 
 
 
 
158
  color: var(--muted);
 
159
  }
160
+
161
+ .notinhas-score-value {
162
  font-family: var(--font-mono);
163
+ font-size: 32px;
164
+ font-weight: 800;
165
+ color: var(--accent);
166
+ font-variant-numeric: tabular-nums;
167
  }
168
 
169
+ /* JSON e exemplos */
170
+ .gradio-container label,
171
+ .gradio-container .label-wrap {
172
+ color: var(--muted) !important;
173
+ }
174
+
175
+ .gradio-container .examples {
176
+ margin-top: 0.85rem !important;
 
177
  }
 
styles.css.bak_20260424_031409 ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ── VariΓ‘veis compartilhadas do projeto ───────────────────── */
2
+ :root {
3
+ --bg: #fdfbf7;
4
+ --text: #2c2c2c;
5
+ --muted: #7a7a7a;
6
+ --accent: #264653;
7
+ --crh: #2a9d8f;
8
+ --nmr: #e9c46a;
9
+ --crnh: #e76f51;
10
+ --info: #577590;
11
+ --info-bg: #57759014;
12
+ --grid: #e8e4dd;
13
+ --card-border: #ddd8d0;
14
+ --soft: #f5f3ef;
15
+
16
+ --font-serif: Georgia, 'Source Serif 4', serif;
17
+ --font-sans: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
18
+ --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
19
+ }
20
+
21
+ /* Importante:
22
+ NÃO usamos reset global com margin:0/padding:0,
23
+ porque isso quebra componentes internos do Gradio.
24
+ */
25
+ *, *::before, *::after {
26
+ box-sizing: border-box;
27
+ }
28
+
29
+ /* ── Base Gradio ───────────────────────────────────────────── */
30
+ .gradio-container {
31
+ background: var(--bg) !important;
32
+ color: var(--text) !important;
33
+ font-family: var(--font-sans) !important;
34
+ line-height: 1.55;
35
+ }
36
+
37
+ .gradio-container h1,
38
+ .gradio-container h2,
39
+ .gradio-container h3 {
40
+ color: var(--accent);
41
+ letter-spacing: -0.02em;
42
+ }
43
+
44
+ .gradio-container h1 {
45
+ font-family: var(--font-serif);
46
+ font-size: clamp(1.7rem, 2vw, 2.35rem);
47
+ margin-bottom: 0.45rem;
48
+ }
49
+
50
+ .gradio-container p {
51
+ margin-bottom: 0.65rem;
52
+ }
53
+
54
+ .gradio-container ul,
55
+ .gradio-container ol {
56
+ margin: 0.35rem 0 0.9rem 1.25rem;
57
+ padding-left: 1rem;
58
+ }
59
+
60
+ .gradio-container li {
61
+ margin: 0.15rem 0;
62
+ }
63
+
64
+ /* ── Abas: corrige PreverExplicarSobre colado ─────────────── */
65
+ .gradio-container [role="tablist"] {
66
+ gap: 0.35rem !important;
67
+ border-bottom: 1px solid var(--grid) !important;
68
+ margin-top: 0.9rem !important;
69
+ margin-bottom: 1rem !important;
70
+ }
71
+
72
+ .gradio-container button[role="tab"] {
73
+ padding: 0.55rem 0.85rem !important;
74
+ margin-right: 0.15rem !important;
75
+ border-radius: 10px 10px 0 0 !important;
76
+ color: var(--accent) !important;
77
+ font-weight: 600 !important;
78
+ }
79
+
80
+ .gradio-container button[role="tab"][aria-selected="true"] {
81
+ background: color-mix(in srgb, var(--accent) 10%, transparent) !important;
82
+ border-bottom-color: var(--accent) !important;
83
+ }
84
+
85
+ /* ── Campos, painΓ©is e botΓ΅es ─────────────────────────────── */
86
+ .gradio-container textarea,
87
+ .gradio-container input {
88
+ font-family: var(--font-sans) !important;
89
+ }
90
+
91
+ .gradio-container .block,
92
+ .gradio-container .panel,
93
+ .gradio-container .form {
94
+ border-radius: 16px !important;
95
+ }
96
+
97
+ .gradio-container button.primary,
98
+ .gradio-container .primary {
99
+ background: var(--accent) !important;
100
+ border-color: var(--accent) !important;
101
+ color: #fff !important;
102
+ font-weight: 700 !important;
103
+ }
104
+
105
+ .gradio-container button.primary:hover,
106
+ .gradio-container .primary:hover {
107
+ filter: brightness(1.08);
108
+ }
109
+
110
+ /* ── Cards prΓ³prios do endpoint ───────────────────────────── */
111
+ .notinhas-card {
112
+ background: #ffffff;
113
+ border: 1px solid var(--card-border);
114
+ border-radius: 18px;
115
+ padding: 18px 22px;
116
+ box-shadow: 0 4px 14px rgba(38, 70, 83, 0.06);
117
+ font-family: var(--font-sans);
118
+ }
119
+
120
+ .notinhas-soft-card {
121
+ background: var(--soft);
122
+ border: 1px solid var(--grid);
123
+ border-radius: 14px;
124
+ padding: 14px;
125
+ font-family: var(--font-sans);
126
+ }
127
+
128
+ .notinhas-badge {
129
+ display: inline-block;
130
+ padding: 4px 12px;
131
+ border-radius: 999px;
132
+ font-size: 13px;
133
+ font-weight: 700;
134
+ }
135
+
136
+ .notinhas-badge-util {
137
+ background: #d8f3dc;
138
+ color: #1b4332;
139
+ }
140
+
141
+ .notinhas-badge-nao-util {
142
+ background: #fde2e4;
143
+ color: #9d0208;
144
+ }
145
+
146
+ .notinhas-badge-media {
147
+ background: #fff3bf;
148
+ color: #7c5c00;
149
+ }
150
+
151
+ .notinhas-badge-baixa {
152
+ background: #e9ecef;
153
+ color: #495057;
154
+ }
155
+
156
+ .notinhas-score-label {
157
+ font-size: 12px;
158
+ color: var(--muted);
159
+ }
160
+
161
+ .notinhas-score-value {
162
+ font-family: var(--font-mono);
163
+ font-size: 32px;
164
+ font-weight: 800;
165
+ color: var(--accent);
166
+ font-variant-numeric: tabular-nums;
167
+ }
168
+
169
+ /* JSON e exemplos */
170
+ .gradio-container label,
171
+ .gradio-container .label-wrap {
172
+ color: var(--muted) !important;
173
+ }
174
+
175
+ .gradio-container .examples {
176
+ margin-top: 0.85rem !important;
177
+ }