Spaces:
Running
Running
Update app/services/classifier.py
Browse files- 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
|
| 291 |
|
| 292 |
_ocr_reader = {
|
| 293 |
-
"backend": "
|
| 294 |
-
"reader":
|
| 295 |
}
|
| 296 |
-
logger.info("ocr reader initialized backend=
|
| 297 |
return _ocr_reader
|
| 298 |
except Exception as exc:
|
| 299 |
-
|
| 300 |
-
logger.warning("ocr
|
| 301 |
|
| 302 |
try:
|
| 303 |
-
|
| 304 |
|
| 305 |
_ocr_reader = {
|
| 306 |
-
"backend": "
|
| 307 |
-
"reader":
|
| 308 |
}
|
| 309 |
-
logger.info("ocr reader initialized backend=
|
| 310 |
return _ocr_reader
|
| 311 |
except Exception as exc:
|
| 312 |
-
_ocr_error = f"
|
| 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 |
-
|
| 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
|