ExistedYear commited on
Commit
ec0b40d
Β·
1 Parent(s): 72674a7

streaming scan: verdict renders before SHAP, honest timing labels

Browse files
Files changed (1) hide show
  1. app.py +73 -12
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
- def h_verdict(r, msg="", ms=None):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- + (f' Β· {ms} ms' if ms else "") + "</div></div>")
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
- return ('<div class="card"><div class="row">Type or paste a message to analyse it.</div></div>',
420
- "", "", "")
 
 
421
  t0 = time.perf_counter()
422
  r = _run(message)
423
- ms = int((time.perf_counter() - t0) * 1000)
 
 
 
 
 
 
 
 
 
424
  ex = _explain(message)
425
- return h_verdict(r, message, ms), h_signals(r), h_shap(r, ex), h_warn(r)
 
 
 
 
 
 
426
 
427
 
428
  @spaces.GPU
429
- def mob_detail(which, custom=""):
430
- """Single seeded-message analysis. Kept on GPU for a responsive detail view."""
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
- return (f'<div class="card"><div class="card-t">Analysis Β· {esc(SEED_SMS[i]["n"])}</div></div>'
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="hidden")
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():