minhvtt commited on
Commit
31c2cb4
·
verified ·
1 Parent(s): a62c554

Update app/services/classifier.py

Browse files
Files changed (1) hide show
  1. app/services/classifier.py +60 -25
app/services/classifier.py CHANGED
@@ -109,12 +109,10 @@ ALLOWED_REASON_CODES = {
109
  GAME_PROMPT = """
110
  You are a high-precision parental-control classifier.
111
  Primary objective: minimize false positives.
112
-
113
  Input fields:
114
  - suspected_game_signal: boolean
115
  - extracted_urls: list of urls/domains from OCR
116
  - ocr_text: raw OCR text from screenshot
117
-
118
  Decision policy:
119
  1) Return "game" only if at least one strong signal exists:
120
  - known game domain/platform in extracted_urls, OR
@@ -123,7 +121,6 @@ Decision policy:
123
  2) Return "uncertain" only when signals are conflicting and neither side is clearly dominant.
124
  3) Return "not_game" for weak/generic words (game, games, play) without concrete game context.
125
  4) Prioritize precision over recall.
126
-
127
  Output:
128
  Return EXACT JSON only using this schema:
129
  {
@@ -131,7 +128,6 @@ Return EXACT JSON only using this schema:
131
  "confidence": 0.0-1.0,
132
  "reason": "reason_code"
133
  }
134
-
135
  Allowed reason codes:
136
  - game_domain_match
137
  - game_ui_terms
@@ -139,7 +135,6 @@ Allowed reason codes:
139
  - weak_generic_terms
140
  - conflicting_signals
141
  - no_game_evidence
142
-
143
  Do not output markdown, prose, or extra keys.
144
  """.strip()
145
 
@@ -285,31 +280,30 @@ def _load_ocr_reader() -> Any | None:
285
  if _ocr_error is not None:
286
  return None
287
 
288
- easyocr_error: str | None = None
289
  try:
290
- import easyocr
291
 
292
  _ocr_reader = {
293
- "backend": "easyocr",
294
- "reader": easyocr.Reader(["en"], gpu=False),
295
  }
296
- logger.info("ocr reader initialized backend=easyocr")
297
  return _ocr_reader
298
  except Exception as exc:
299
- easyocr_error = str(exc)
300
- logger.warning("ocr easyocr init failed: %s", easyocr_error)
301
 
302
  try:
303
- from rapidocr_onnxruntime import RapidOCR
304
 
305
  _ocr_reader = {
306
- "backend": "rapidocr",
307
- "reader": RapidOCR(),
308
  }
309
- logger.info("ocr reader initialized backend=rapidocr")
310
  return _ocr_reader
311
  except Exception as exc:
312
- _ocr_error = f"easyocr={easyocr_error}; rapidocr={exc}"
313
  logger.warning("ocr reader init failed all backends: %s", _ocr_error)
314
  return None
315
 
@@ -350,13 +344,7 @@ def _classify_game_with_llm(ocr_text: str, urls: list[str], suspected_game: bool
350
  provider=provider_name,
351
  token=token or None,
352
  )
353
- response = client.text_generation(
354
- messages=prompt_messages,
355
- max_tokens=160
356
- )
357
- content = ""
358
- if response.choices:
359
- content = response.choices[0].message.content or ""
360
  parsed = _parse_llm_json(content)
361
  if parsed is None:
362
  logger.warning(
@@ -388,6 +376,53 @@ def _classify_game_with_llm(ocr_text: str, urls: list[str], suspected_game: bool
388
  return None
389
 
390
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  def _is_model_not_supported_error(exc: Exception) -> bool:
392
  text = repr(exc).lower()
393
  return "model_not_supported" in text or "not supported by any provider" in text
@@ -574,4 +609,4 @@ def _extract_nsfw_score(scores: list[float], labels: list[str]) -> float:
574
  return scores[idx]
575
  if len(scores) >= 2:
576
  return scores[1]
577
- return scores[0] if scores else 0.0
 
109
  GAME_PROMPT = """
110
  You are a high-precision parental-control classifier.
111
  Primary objective: minimize false positives.
 
112
  Input fields:
113
  - suspected_game_signal: boolean
114
  - extracted_urls: list of urls/domains from OCR
115
  - ocr_text: raw OCR text from screenshot
 
116
  Decision policy:
117
  1) Return "game" only if at least one strong signal exists:
118
  - known game domain/platform in extracted_urls, OR
 
121
  2) Return "uncertain" only when signals are conflicting and neither side is clearly dominant.
122
  3) Return "not_game" for weak/generic words (game, games, play) without concrete game context.
123
  4) Prioritize precision over recall.
 
124
  Output:
125
  Return EXACT JSON only using this schema:
126
  {
 
128
  "confidence": 0.0-1.0,
129
  "reason": "reason_code"
130
  }
 
131
  Allowed reason codes:
132
  - game_domain_match
133
  - game_ui_terms
 
135
  - weak_generic_terms
136
  - conflicting_signals
137
  - no_game_evidence
 
138
  Do not output markdown, prose, or extra keys.
139
  """.strip()
140
 
 
280
  if _ocr_error is not None:
281
  return None
282
 
 
283
  try:
284
+ from rapidocr_onnxruntime import RapidOCR
285
 
286
  _ocr_reader = {
287
+ "backend": "rapidocr",
288
+ "reader": RapidOCR(),
289
  }
290
+ logger.info("ocr reader initialized backend=rapidocr")
291
  return _ocr_reader
292
  except Exception as exc:
293
+ rapidocr_error = str(exc)
294
+ logger.warning("ocr rapidocr init failed: %s", rapidocr_error)
295
 
296
  try:
297
+ import easyocr
298
 
299
  _ocr_reader = {
300
+ "backend": "easyocr",
301
+ "reader": easyocr.Reader(["en","vi"], gpu=False),
302
  }
303
+ logger.info("ocr reader initialized backend=easyocr")
304
  return _ocr_reader
305
  except Exception as exc:
306
+ _ocr_error = f"rapidocr={rapidocr_error}; easyocr={exc}"
307
  logger.warning("ocr reader init failed all backends: %s", _ocr_error)
308
  return None
309
 
 
344
  provider=provider_name,
345
  token=token or None,
346
  )
347
+ content = _run_hf_completion(client, prompt_messages, user_payload)
 
 
 
 
 
 
348
  parsed = _parse_llm_json(content)
349
  if parsed is None:
350
  logger.warning(
 
376
  return None
377
 
378
 
379
+ def _run_hf_completion(client: Any, prompt_messages: list[dict[str, str]], user_payload: dict[str, Any]) -> str:
380
+ chat_completion = getattr(client, "chat_completion", None)
381
+ if callable(chat_completion):
382
+ response = chat_completion(messages=prompt_messages, max_tokens=160)
383
+ return _extract_hf_response_text(response)
384
+
385
+ chat_api = getattr(client, "chat", None)
386
+ chat_completions = getattr(chat_api, "completions", None)
387
+ chat_create = getattr(chat_completions, "create", None)
388
+ if callable(chat_create):
389
+ response = chat_create(messages=prompt_messages, max_tokens=160)
390
+ return _extract_hf_response_text(response)
391
+
392
+ prompt = f"{GAME_PROMPT}\n\nInput JSON:\n{json.dumps(user_payload, ensure_ascii=True)}\n\nReturn exact JSON only."
393
+ response = client.text_generation(
394
+ prompt=prompt,
395
+ max_new_tokens=160,
396
+ return_full_text=False,
397
+ )
398
+ return _extract_hf_response_text(response)
399
+
400
+
401
+ def _extract_hf_response_text(response: Any) -> str:
402
+ if response is None:
403
+ return ""
404
+ if isinstance(response, str):
405
+ return response
406
+ if isinstance(response, dict):
407
+ choices = response.get("choices") or []
408
+ if choices:
409
+ message = choices[0].get("message") or {}
410
+ if isinstance(message, dict):
411
+ return str(message.get("content") or "")
412
+ return str(message or "")
413
+ return str(response.get("generated_text") or response.get("text") or "")
414
+
415
+ choices = getattr(response, "choices", None)
416
+ if choices:
417
+ first_choice = choices[0]
418
+ message = getattr(first_choice, "message", None)
419
+ if message is not None:
420
+ return str(getattr(message, "content", "") or "")
421
+ return str(getattr(first_choice, "text", "") or "")
422
+
423
+ return str(getattr(response, "generated_text", "") or getattr(response, "text", "") or response)
424
+
425
+
426
  def _is_model_not_supported_error(exc: Exception) -> bool:
427
  text = repr(exc).lower()
428
  return "model_not_supported" in text or "not supported by any provider" in text
 
609
  return scores[idx]
610
  if len(scores) >= 2:
611
  return scores[1]
612
+ return scores[0] if scores else 0.0