Spaces:
Running
Running
feat: endpoint FT-Solo inicial (Qwen3-Embedding-4B + LoRA fold 01 + linear head)
Browse filesSobe 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.
- app.py +83 -88
- app.py.bak_20260424_031409 +364 -0
- styles.css +152 -110
- 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 β
|
| 80 |
lbl = _label(p)
|
| 81 |
band = _confidence_band(p)
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
return f"""
|
| 93 |
-
<div
|
| 94 |
-
|
| 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
|
| 100 |
-
|
| 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
|
| 108 |
-
<div
|
| 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 |
-
/* ββ
|
| 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 |
-
|
| 21 |
-
--font-
|
| 22 |
-
--font-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
-
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
font-family: var(--font-sans);
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
display:
|
| 46 |
-
|
| 47 |
-
|
| 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:
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
background:
|
| 88 |
-
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
.
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
}
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 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 |
-
|
|
|
|
| 118 |
font-family: var(--font-mono);
|
| 119 |
-
font-size:
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
}
|
| 124 |
|
| 125 |
-
/*
|
| 126 |
-
.
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 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 |
+
}
|