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

fix zerogpu quota: drop duration holds, cache results, cpu bulk scan, bound shap evals

Browse files
app.py CHANGED
@@ -281,17 +281,41 @@ def risk(conf, level=""):
281
  return "lo", "var(--lo)", "Low risk", "🟒"
282
 
283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  def _run(message):
 
 
 
 
285
  r = predictor.predict(message)
286
  r["_tokens"] = [w for w in str(message).lower().split() if w.isalpha() and len(w) > 3][:14]
287
- return r
288
 
289
 
290
  def _explain(message):
 
 
 
 
291
  try:
292
- return explainer.explain_text(message, num_features=12)
 
293
  except Exception:
294
- return {"top_spam_words": [], "top_ham_words": []}
 
295
 
296
 
297
  # ── renderers ────────────────────────────────────────────────────────────────
@@ -389,7 +413,7 @@ def h_warn(r):
389
  # ─────────────────────────────────────────────────────────────────────────────
390
  # Handlers
391
  # ─────────────────────────────────────────────────────────────────────────────
392
- @spaces.GPU(duration=90)
393
  def scan(message):
394
  if not message or not message.strip():
395
  return ('<div class="card"><div class="row">Type or paste a message to analyse it.</div></div>',
@@ -403,6 +427,7 @@ def scan(message):
403
 
404
  @spaces.GPU
405
  def mob_detail(which, custom=""):
 
406
  i = SEED_LABELS.index(which) if which in SEED_LABELS else 0
407
  msg = custom.strip() or SEED_SMS[i]["b"]
408
  r = _run(msg)
@@ -411,8 +436,12 @@ def mob_detail(which, custom=""):
411
  + h_verdict(r, msg) + h_signals(r) + h_shap(r, ex) + h_warn(r))
412
 
413
 
414
- @spaces.GPU
415
  def mob_scan_all(progress=gr.Progress()):
 
 
 
 
 
416
  rows, threats = [], 0
417
  for i, m in enumerate(SEED_SMS):
418
  progress(i / len(SEED_SMS), desc=f"Scanning {i+1}/{len(SEED_SMS)} Β· {m['n']}")
@@ -445,7 +474,7 @@ def mob_scan_all(progress=gr.Progress()):
445
  # ─────────────────────────────────────────────────────────────────────────────
446
  # JSON API β€” used by the Android app and any external client
447
  # ─────────────────────────────────────────────────────────────────────────────
448
- @spaces.GPU(duration=60)
449
  def predict_api(message: str) -> dict:
450
  return _run(message)
451
 
 
281
  return "lo", "var(--lo)", "Low risk", "🟒"
282
 
283
 
284
+ _CACHE, _CACHE_MAX = {}, 256
285
+
286
+
287
+ def _cache_get(key):
288
+ return _CACHE.get(key)
289
+
290
+
291
+ def _cache_put(key, value):
292
+ if len(_CACHE) >= _CACHE_MAX:
293
+ _CACHE.clear()
294
+ _CACHE[key] = value
295
+ return value
296
+
297
+
298
  def _run(message):
299
+ key = ("run", message)
300
+ hit = _cache_get(key)
301
+ if hit is not None:
302
+ return hit
303
  r = predictor.predict(message)
304
  r["_tokens"] = [w for w in str(message).lower().split() if w.isalpha() and len(w) > 3][:14]
305
+ return _cache_put(key, r)
306
 
307
 
308
  def _explain(message):
309
+ key = ("ex", message)
310
+ hit = _cache_get(key)
311
+ if hit is not None:
312
+ return hit
313
  try:
314
+ # 8 features keeps the SHAP kernel small β€” this is the expensive part.
315
+ out = explainer.explain_text(message, num_features=8)
316
  except Exception:
317
+ out = {"top_spam_words": [], "top_ham_words": []}
318
+ return _cache_put(key, out)
319
 
320
 
321
  # ── renderers ────────────────────────────────────────────────────────────────
 
413
  # ─────────────────────────────────────────────────────────────────────────────
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>',
 
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)
 
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()):
440
+ """Bulk scan of the seeded inbox.
441
+
442
+ Deliberately NOT decorated with @spaces.GPU β€” it runs on CPU so that walking
443
+ 16 messages does not burn the ZeroGPU quota. Single-message scans stay on GPU.
444
+ """
445
  rows, threats = [], 0
446
  for i, m in enumerate(SEED_SMS):
447
  progress(i / len(SEED_SMS), desc=f"Scanning {i+1}/{len(SEED_SMS)} Β· {m['n']}")
 
474
  # ─────────────────────────────────────────────────────────────────────────────
475
  # JSON API β€” used by the Android app and any external client
476
  # ─────────────────────────────────────────────────────────────────────────────
477
+ @spaces.GPU
478
  def predict_api(message: str) -> dict:
479
  return _run(message)
480
 
smishing_detector/explainability/shap_explainer.py CHANGED
@@ -50,15 +50,33 @@ class SmishingExplainer:
50
 
51
  return predict_fn
52
 
53
- def explain_text(self, message: str, num_features: int = 10) -> dict:
 
 
 
 
 
 
 
 
54
  predict_fn = self._get_prediction_function()
55
 
 
 
 
 
 
 
56
  tokenizer = self.predictor.tokenizer
57
  masker = shap.maskers.Text(tokenizer)
58
 
59
  explainer = shap.Explainer(predict_fn, masker)
60
 
61
- shap_values = explainer([message])
 
 
 
 
62
 
63
  tokens = [str(t) for t in shap_values.data[0]]
64
  values = shap_values.values[0]
 
50
 
51
  return predict_fn
52
 
53
+ def explain_text(self, message: str, num_features: int = 10,
54
+ max_tokens: int = 24, max_evals: int = 96) -> dict:
55
+ """
56
+ Word-level SHAP attribution.
57
+
58
+ max_tokens / max_evals bound the compute: the Text masker perturbs every
59
+ token, so cost grows roughly as 2x token count. Capping them keeps a single
60
+ explanation to a handful of forward passes instead of an unbounded loop.
61
+ """
62
  predict_fn = self._get_prediction_function()
63
 
64
+ # Bound the text SHAP has to work on β€” long messages give diminishing
65
+ # returns well before the model even reaches MAX_LEN tokens.
66
+ words = str(message).split()
67
+ if len(words) > max_tokens:
68
+ message = " ".join(words[:max_tokens])
69
+
70
  tokenizer = self.predictor.tokenizer
71
  masker = shap.maskers.Text(tokenizer)
72
 
73
  explainer = shap.Explainer(predict_fn, masker)
74
 
75
+ try:
76
+ shap_values = explainer([message], max_evals=max_evals)
77
+ except Exception:
78
+ # Fall back to the uncapped path if the masker rejects max_evals.
79
+ shap_values = explainer([message])
80
 
81
  tokens = [str(t) for t in shap_values.data[0]]
82
  values = shap_values.values[0]