minhvtt commited on
Commit
9c745f0
·
verified ·
1 Parent(s): 7dca8cd

Update app/services/classifier.py

Browse files
Files changed (1) hide show
  1. app/services/classifier.py +48 -10
app/services/classifier.py CHANGED
@@ -57,24 +57,50 @@ SENSITIVE_KEYWORDS = {
57
 
58
  URL_REGEX = re.compile(r"(?:https?://)?(?:www\.)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:/[^\s]*)?")
59
 
 
 
 
 
 
 
 
 
 
60
  GAME_PROMPT = """
61
- You are a strict classifier for parental-control screenshots.
 
 
62
  Input fields:
63
  - suspected_game_signal: boolean
64
  - extracted_urls: list of urls/domains from OCR
65
  - ocr_text: raw OCR text from screenshot
66
- Task:
67
- - Decide if screenshot likely indicates gaming/web-game activity.
68
- - Return only compact JSON with this schema:
 
 
 
 
 
 
 
 
 
69
  {
70
  "verdict": "game" | "not_game" | "uncertain",
71
  "confidence": 0.0-1.0,
72
- "reason": "short reason"
73
  }
74
- Rules:
75
- - If clear game domain or game UI terms appear, lean game.
76
- - If evidence is weak or contradictory, return uncertain.
77
- - Never output markdown, prose, or extra keys.
 
 
 
 
 
 
78
  """.strip()
79
 
80
 
@@ -260,11 +286,23 @@ def _parse_llm_json(content: str) -> dict[str, Any] | None:
260
  confidence = 0.5
261
 
262
  confidence = max(0.0, min(1.0, confidence))
263
- reason = str(data.get("reason", "llm-decision"))[:200]
 
264
 
265
  return {"verdict": verdict, "confidence": confidence, "reason": reason}
266
 
267
 
 
 
 
 
 
 
 
 
 
 
 
268
  def _domain_from_url(url: str) -> str:
269
  parsed = urlparse(url)
270
  host = parsed.netloc or parsed.path
 
57
 
58
  URL_REGEX = re.compile(r"(?:https?://)?(?:www\.)?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(?:/[^\s]*)?")
59
 
60
+ ALLOWED_REASON_CODES = {
61
+ "game_domain_match",
62
+ "game_ui_terms",
63
+ "signal_plus_ocr",
64
+ "weak_generic_terms",
65
+ "conflicting_signals",
66
+ "no_game_evidence",
67
+ }
68
+
69
  GAME_PROMPT = """
70
+ You are a high-precision parental-control classifier.
71
+ Primary objective: minimize false positives.
72
+
73
  Input fields:
74
  - suspected_game_signal: boolean
75
  - extracted_urls: list of urls/domains from OCR
76
  - ocr_text: raw OCR text from screenshot
77
+
78
+ Decision policy:
79
+ 1) Return "game" only if at least one strong signal exists:
80
+ - known game domain/platform in extracted_urls, OR
81
+ - explicit in-game UI terms in ocr_text (ranked, lobby, battle pass, matchmaking, etc.), OR
82
+ - suspected_game_signal=true AND at least one medium OCR signal.
83
+ 2) Return "uncertain" only when signals are conflicting and neither side is clearly dominant.
84
+ 3) Return "not_game" for weak/generic words (game, games, play) without concrete game context.
85
+ 4) Prioritize precision over recall.
86
+
87
+ Output:
88
+ Return EXACT JSON only using this schema:
89
  {
90
  "verdict": "game" | "not_game" | "uncertain",
91
  "confidence": 0.0-1.0,
92
+ "reason": "reason_code"
93
  }
94
+
95
+ Allowed reason codes:
96
+ - game_domain_match
97
+ - game_ui_terms
98
+ - signal_plus_ocr
99
+ - weak_generic_terms
100
+ - conflicting_signals
101
+ - no_game_evidence
102
+
103
+ Do not output markdown, prose, or extra keys.
104
  """.strip()
105
 
106
 
 
286
  confidence = 0.5
287
 
288
  confidence = max(0.0, min(1.0, confidence))
289
+ raw_reason = str(data.get("reason", "")).strip().lower()
290
+ reason = _normalize_reason_code(verdict, raw_reason)
291
 
292
  return {"verdict": verdict, "confidence": confidence, "reason": reason}
293
 
294
 
295
+ def _normalize_reason_code(verdict: str, reason: str) -> str:
296
+ if reason in ALLOWED_REASON_CODES:
297
+ return reason
298
+
299
+ if verdict == "game":
300
+ return "game_ui_terms"
301
+ if verdict == "uncertain":
302
+ return "conflicting_signals"
303
+ return "no_game_evidence"
304
+
305
+
306
  def _domain_from_url(url: str) -> str:
307
  parsed = urlparse(url)
308
  host = parsed.netloc or parsed.path