Spaces:
Running on Zero
Running on Zero
Commit Β·
ec0b40d
1
Parent(s): 72674a7
streaming scan: verdict renders before SHAP, honest timing labels
Browse files
app.py
CHANGED
|
@@ -163,6 +163,10 @@ body{background:var(--bg)!important}
|
|
| 163 |
|
| 164 |
/* ββ SHAP ββ */
|
| 165 |
.sh{display:flex;flex-direction:column;gap:.42rem}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
.sh-row{display:flex;align-items:center;gap:.7rem}
|
| 167 |
.sh-w{width:120px;text-align:right;font-size:.74rem;font-weight:600;color:var(--muted);
|
| 168 |
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
@@ -319,7 +323,24 @@ def _explain(message):
|
|
| 319 |
|
| 320 |
|
| 321 |
# ββ renderers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
p = round(max(0.0, min(1.0, r.get("confidence", 0))) * 100)
|
| 324 |
k, colour, lvl, icon = risk(r.get("confidence", 0), r.get("risk_level", ""))
|
| 325 |
label = r.get("label", "ham")
|
|
@@ -329,13 +350,24 @@ def h_verdict(r, msg="", ms=None):
|
|
| 329 |
out.append(f'<div class="v-ic ic-{k}">{icon}</div>')
|
| 330 |
out.append(f'<div><div class="v-title">{title}</div><div class="v-sub">'
|
| 331 |
f'{lvl} Β· detected as <b>{label}</b> Β· {esc(r.get("language","?"))}'
|
| 332 |
-
|
| 333 |
out.append(f'<div class="v-score"><b>{p}</b><i>%</i></div></div>')
|
| 334 |
out.append(f'<div class="meter"><div class="meter-f" style="width:{p}%;background:{colour}"></div>'
|
| 335 |
f'<div class="meter-t" style="left:55%"></div></div>')
|
| 336 |
out.append('<div class="meter-s"><span>0% Β· ham</span><span>decision threshold 55%</span>'
|
| 337 |
'<span>100% Β· scam</span></div>')
|
| 338 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
if r.get("green_channel"):
|
| 340 |
out.append(f'<div class="alert alert-g" style="margin-top:1rem">'
|
| 341 |
f'<b>π‘ Green Channel cleared</b> β {esc(r.get("green_reason",""))}</div>')
|
|
@@ -414,26 +446,55 @@ def h_warn(r):
|
|
| 414 |
# Handlers
|
| 415 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 416 |
@spaces.GPU
|
| 417 |
-
def scan(message):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 418 |
if not message or not message.strip():
|
| 419 |
-
|
| 420 |
-
|
|
|
|
|
|
|
| 421 |
t0 = time.perf_counter()
|
| 422 |
r = _run(message)
|
| 423 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
ex = _explain(message)
|
| 425 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
|
| 427 |
|
| 428 |
@spaces.GPU
|
| 429 |
-
def mob_detail(which, custom=""):
|
| 430 |
-
"""Single seeded-message analysis
|
| 431 |
i = SEED_LABELS.index(which) if which in SEED_LABELS else 0
|
| 432 |
msg = custom.strip() or SEED_SMS[i]["b"]
|
|
|
|
| 433 |
r = _run(msg)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
ex = _explain(msg)
|
| 435 |
-
|
| 436 |
-
+ h_verdict(r, msg) + h_signals(r) + h_shap(r, ex) + h_warn(r))
|
| 437 |
|
| 438 |
|
| 439 |
def mob_scan_all(progress=gr.Progress()):
|
|
@@ -559,7 +620,7 @@ with gr.Blocks(title="ScamShield β Smishing Detector") as demo:
|
|
| 559 |
|
| 560 |
for e in (btn.click, inp.submit):
|
| 561 |
e(fn=scan, inputs=inp, outputs=[verdict, signals, shap, warn],
|
| 562 |
-
api_name="scan", show_progress="
|
| 563 |
|
| 564 |
with gr.TabItem("Mobile app"):
|
| 565 |
with gr.Row():
|
|
|
|
| 163 |
|
| 164 |
/* ββ SHAP ββ */
|
| 165 |
.sh{display:flex;flex-direction:column;gap:.42rem}
|
| 166 |
+
.sh-spin{width:13px;height:13px;border:2px solid var(--line2);border-top-color:var(--brand);
|
| 167 |
+
border-radius:50%;animation:shspin .7s linear infinite;display:inline-block;margin-right:.5rem;
|
| 168 |
+
vertical-align:-2px}
|
| 169 |
+
@keyframes shspin{to{transform:rotate(360deg)}}
|
| 170 |
.sh-row{display:flex;align-items:center;gap:.7rem}
|
| 171 |
.sh-w{width:120px;text-align:right;font-size:.74rem;font-weight:600;color:var(--muted);
|
| 172 |
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
|
|
| 323 |
|
| 324 |
|
| 325 |
# ββ renderers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 326 |
+
SHAP_PENDING = (
|
| 327 |
+
'<div class="card"><div class="card-t">Why this verdict Β· SHAP word impact</div>'
|
| 328 |
+
'<div class="row"><span class="sh-spin"></span> Computing word attributionβ¦</div>'
|
| 329 |
+
'<div class="rows" style="margin-top:.8rem">'
|
| 330 |
+
'<div class="sh-row"><div class="sh-w" style="color:var(--faint)">β¦</div>'
|
| 331 |
+
'<div class="sh-t"><div class="sh-b" style="left:50%;width:6%;background:var(--brand);opacity:.35"></div></div>'
|
| 332 |
+
'<div class="sh-v"></div></div>'
|
| 333 |
+
'<div class="sh-row"><div class="sh-w" style="color:var(--faint)">β¦</div>'
|
| 334 |
+
'<div class="sh-t"><div class="sh-b" style="left:50%;width:4%;background:var(--brand);opacity:.25"></div></div>'
|
| 335 |
+
'<div class="sh-v"></div></div>'
|
| 336 |
+
'<div class="sh-row"><div class="sh-w" style="color:var(--faint)">β¦</div>'
|
| 337 |
+
'<div class="sh-t"><div class="sh-b" style="left:50%;width:5%;background:var(--brand);opacity:.2"></div></div>'
|
| 338 |
+
'<div class="sh-v"></div></div>'
|
| 339 |
+
'</div></div>'
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def h_verdict(r, msg="", ms=None, neural_ms=None):
|
| 344 |
p = round(max(0.0, min(1.0, r.get("confidence", 0))) * 100)
|
| 345 |
k, colour, lvl, icon = risk(r.get("confidence", 0), r.get("risk_level", ""))
|
| 346 |
label = r.get("label", "ham")
|
|
|
|
| 350 |
out.append(f'<div class="v-ic ic-{k}">{icon}</div>')
|
| 351 |
out.append(f'<div><div class="v-title">{title}</div><div class="v-sub">'
|
| 352 |
f'{lvl} Β· detected as <b>{label}</b> Β· {esc(r.get("language","?"))}'
|
| 353 |
+
"</div></div>")
|
| 354 |
out.append(f'<div class="v-score"><b>{p}</b><i>%</i></div></div>')
|
| 355 |
out.append(f'<div class="meter"><div class="meter-f" style="width:{p}%;background:{colour}"></div>'
|
| 356 |
f'<div class="meter-t" style="left:55%"></div></div>')
|
| 357 |
out.append('<div class="meter-s"><span>0% Β· ham</span><span>decision threshold 55%</span>'
|
| 358 |
'<span>100% Β· scam</span></div>')
|
| 359 |
|
| 360 |
+
# Timing is honest about which stage it reflects: while streaming, `neural_ms`
|
| 361 |
+
# equals `ms`, so we only claim a verdict timing β never a "total".
|
| 362 |
+
if ms and neural_ms and neural_ms == ms:
|
| 363 |
+
out.append(f'<div class="meter-s" style="margin-top:.3rem">'
|
| 364 |
+
f'<span>verdict {ms} ms on GPU</span>'
|
| 365 |
+
f'<span>attribution computingβ¦</span></div>')
|
| 366 |
+
elif ms:
|
| 367 |
+
out.append(f'<div class="meter-s" style="margin-top:.3rem">'
|
| 368 |
+
f'<span>verdict {neural_ms} ms</span>'
|
| 369 |
+
f'<span>total {ms} ms on GPU</span></div>')
|
| 370 |
+
|
| 371 |
if r.get("green_channel"):
|
| 372 |
out.append(f'<div class="alert alert-g" style="margin-top:1rem">'
|
| 373 |
f'<b>π‘ Green Channel cleared</b> β {esc(r.get("green_reason",""))}</div>')
|
|
|
|
| 446 |
# Handlers
|
| 447 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 448 |
@spaces.GPU
|
| 449 |
+
def scan(message, progress=gr.Progress()):
|
| 450 |
+
"""
|
| 451 |
+
Streaming scan: yields the verdict as soon as the neural pass finishes, then
|
| 452 |
+
yields again once SHAP attribution lands.
|
| 453 |
+
|
| 454 |
+
Judging flow wants the verdict fast (~1-2s) but also wants the word-level
|
| 455 |
+
attribution on screen. Blocking on SHAP hides the verdict behind the slowest
|
| 456 |
+
step, so this is a generator rather than a single return.
|
| 457 |
+
"""
|
| 458 |
if not message or not message.strip():
|
| 459 |
+
yield ('<div class="card"><div class="row">Type or paste a message to analyse it.</div></div>',
|
| 460 |
+
"", "", "")
|
| 461 |
+
return
|
| 462 |
+
|
| 463 |
t0 = time.perf_counter()
|
| 464 |
r = _run(message)
|
| 465 |
+
neural_ms = int((time.perf_counter() - t0) * 1000)
|
| 466 |
+
|
| 467 |
+
# First yield: verdict + signals, SHAP panel still loading.
|
| 468 |
+
# Pass neural_ms as BOTH args so h_verdict knows SHAP has not run yet.
|
| 469 |
+
yield (h_verdict(r, message, neural_ms, neural_ms),
|
| 470 |
+
h_signals(r),
|
| 471 |
+
SHAP_PENDING,
|
| 472 |
+
h_warn(r))
|
| 473 |
+
|
| 474 |
+
progress(0.5, desc="Generating word-level attributionβ¦")
|
| 475 |
ex = _explain(message)
|
| 476 |
+
total_ms = int((time.perf_counter() - t0) * 1000)
|
| 477 |
+
|
| 478 |
+
# Second yield: SHAP resolved, timing folded into the verdict card.
|
| 479 |
+
yield (h_verdict(r, message, total_ms, neural_ms),
|
| 480 |
+
h_signals(r),
|
| 481 |
+
h_shap(r, ex),
|
| 482 |
+
h_warn(r))
|
| 483 |
|
| 484 |
|
| 485 |
@spaces.GPU
|
| 486 |
+
def mob_detail(which, custom="", progress=gr.Progress()):
|
| 487 |
+
"""Single seeded-message analysis β same streaming treatment as the scanner."""
|
| 488 |
i = SEED_LABELS.index(which) if which in SEED_LABELS else 0
|
| 489 |
msg = custom.strip() or SEED_SMS[i]["b"]
|
| 490 |
+
head = f'<div class="card"><div class="card-t">Analysis Β· {esc(SEED_SMS[i]["n"])}</div></div>'
|
| 491 |
r = _run(msg)
|
| 492 |
+
|
| 493 |
+
yield head + h_verdict(r, msg) + h_signals(r) + SHAP_PENDING + h_warn(r)
|
| 494 |
+
|
| 495 |
+
progress(0.5, desc="Generating word-level attributionβ¦")
|
| 496 |
ex = _explain(msg)
|
| 497 |
+
yield head + h_verdict(r, msg) + h_signals(r) + h_shap(r, ex) + h_warn(r)
|
|
|
|
| 498 |
|
| 499 |
|
| 500 |
def mob_scan_all(progress=gr.Progress()):
|
|
|
|
| 620 |
|
| 621 |
for e in (btn.click, inp.submit):
|
| 622 |
e(fn=scan, inputs=inp, outputs=[verdict, signals, shap, warn],
|
| 623 |
+
api_name="scan", show_progress="minimal")
|
| 624 |
|
| 625 |
with gr.TabItem("Mobile app"):
|
| 626 |
with gr.Row():
|