Spaces:
Runtime error
Runtime error
Mustafa Öztürk commited on
Commit ·
1bbaeae
1
Parent(s): 2647c15
Batch EN detox inference and safer gibberish early-exit
Browse files- app/services/moderation_service.py +133 -51
app/services/moderation_service.py
CHANGED
|
@@ -121,6 +121,79 @@ def _blacklist_early_result(profanity_hits, insult_hits, dil, cleaned_text):
|
|
| 121 |
return verdict["decision"], verdict["reason"], verdict["risk_level"], dil, cleaned_text, detail
|
| 122 |
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
def _tr_off_scores_batched(text_list, state, batch_size: int = 8):
|
| 125 |
if not text_list:
|
| 126 |
return []
|
|
@@ -212,13 +285,14 @@ def run_moderation(text: str, platform_dil: str = "tr"):
|
|
| 212 |
return _blacklist_early_result(profanity_hits, insult_hits, dil, temiz)
|
| 213 |
|
| 214 |
if dil == "en":
|
|
|
|
| 215 |
if state["GB_PIPE"] is not None:
|
| 216 |
gb_raw = state["GB_PIPE"](temiz)[0]
|
| 217 |
gb = {
|
| 218 |
"label": str(gb_raw.get("label", "")),
|
| 219 |
"score": float(gb_raw.get("score", 0.0)),
|
| 220 |
}
|
| 221 |
-
if gb.get("label", "").lower() == "noise" and gb.get("score", 0.0) > 0.98:
|
| 222 |
early_exit_count += 1
|
| 223 |
_log_pipeline_counts(early_exit_count, detoxify_call_count)
|
| 224 |
return (
|
|
@@ -272,8 +346,8 @@ def run_moderation_batch(texts, platform_dil: str = "tr", batch_size: int = 8):
|
|
| 272 |
detoxify_call_count = 0
|
| 273 |
|
| 274 |
results = [None] * len(texts)
|
| 275 |
-
|
| 276 |
-
|
| 277 |
|
| 278 |
for idx, text in enumerate(texts):
|
| 279 |
temiz = clean_text_nfkc(text)
|
|
@@ -298,62 +372,70 @@ def run_moderation_batch(texts, platform_dil: str = "tr", batch_size: int = 8):
|
|
| 298 |
|
| 299 |
if dil == "en":
|
| 300 |
gb = None
|
|
|
|
| 301 |
if state["GB_PIPE"] is not None:
|
| 302 |
gb_raw = state["GB_PIPE"](temiz)[0]
|
| 303 |
gb = {
|
| 304 |
"label": str(gb_raw.get("label", "")),
|
| 305 |
"score": float(gb_raw.get("score", 0.0)),
|
| 306 |
}
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
ins_score = float(res.get("insult", 0.0))
|
| 324 |
-
identity_attack = float(res.get("identity_attack", 0.0))
|
| 325 |
-
detail = {
|
| 326 |
-
"detox": res,
|
| 327 |
-
"insult": ins_score,
|
| 328 |
-
"toxicity": tox_score,
|
| 329 |
-
"identity_attack": identity_attack,
|
| 330 |
-
"hits": profanity_hits,
|
| 331 |
-
"insult_hits": insult_hits,
|
| 332 |
-
"gibberish": gb if state["GB_PIPE"] is not None else None,
|
| 333 |
-
}
|
| 334 |
-
verdict = calculate_verdict(
|
| 335 |
-
profanity_hits,
|
| 336 |
-
insult_hits,
|
| 337 |
-
{
|
| 338 |
-
"off_score": 0.0,
|
| 339 |
-
"detox_toxicity": tox_score,
|
| 340 |
-
},
|
| 341 |
-
)
|
| 342 |
-
detail.update({"action": ACTION_MAP.get(verdict["risk_level"], "MONITOR")})
|
| 343 |
-
results[idx] = (verdict["decision"], verdict["reason"], verdict["risk_level"], dil, temiz, detail)
|
| 344 |
continue
|
| 345 |
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
if
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 357 |
|
| 358 |
_log_pipeline_counts(early_exit_count, detoxify_call_count)
|
| 359 |
|
|
|
|
| 121 |
return verdict["decision"], verdict["reason"], verdict["risk_level"], dil, cleaned_text, detail
|
| 122 |
|
| 123 |
|
| 124 |
+
def _is_obvious_gibberish(cleaned_text: str) -> bool:
|
| 125 |
+
letters = re.sub(r"[^a-z]", "", cleaned_text.lower())
|
| 126 |
+
if not letters:
|
| 127 |
+
return True
|
| 128 |
+
|
| 129 |
+
tokens = [t for t in cleaned_text.split() if t]
|
| 130 |
+
if len(tokens) <= 1:
|
| 131 |
+
return True
|
| 132 |
+
|
| 133 |
+
vowel_ratio = sum(1 for c in letters if c in "aeiou") / max(len(letters), 1)
|
| 134 |
+
unique_ratio = len(set(letters)) / max(len(letters), 1)
|
| 135 |
+
if len(letters) >= 7 and vowel_ratio < 0.18:
|
| 136 |
+
return True
|
| 137 |
+
if len(letters) >= 10 and unique_ratio < 0.25:
|
| 138 |
+
return True
|
| 139 |
+
|
| 140 |
+
return False
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def _split_en_detox_result(raw_res, expected_len: int):
|
| 144 |
+
if isinstance(raw_res, list):
|
| 145 |
+
parsed = []
|
| 146 |
+
for item in raw_res:
|
| 147 |
+
if isinstance(item, dict):
|
| 148 |
+
parsed.append({k: float(v) for k, v in item.items()})
|
| 149 |
+
if len(parsed) == expected_len:
|
| 150 |
+
return parsed
|
| 151 |
+
|
| 152 |
+
if isinstance(raw_res, dict):
|
| 153 |
+
out = [dict() for _ in range(expected_len)]
|
| 154 |
+
for key, value in raw_res.items():
|
| 155 |
+
if isinstance(value, (list, tuple)) and len(value) == expected_len:
|
| 156 |
+
for i in range(expected_len):
|
| 157 |
+
out[i][key] = float(value[i])
|
| 158 |
+
elif hasattr(value, "__len__") and not isinstance(value, (str, bytes)):
|
| 159 |
+
try:
|
| 160 |
+
if len(value) == expected_len:
|
| 161 |
+
for i in range(expected_len):
|
| 162 |
+
out[i][key] = float(value[i])
|
| 163 |
+
continue
|
| 164 |
+
except TypeError:
|
| 165 |
+
pass
|
| 166 |
+
for i in range(expected_len):
|
| 167 |
+
out[i][key] = float(value)
|
| 168 |
+
else:
|
| 169 |
+
for i in range(expected_len):
|
| 170 |
+
out[i][key] = float(value)
|
| 171 |
+
if all(out[i] for i in range(expected_len)):
|
| 172 |
+
return out
|
| 173 |
+
|
| 174 |
+
return []
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
def _predict_en_detox_batched(text_list, state, batch_size: int = 8):
|
| 178 |
+
if not text_list:
|
| 179 |
+
return []
|
| 180 |
+
|
| 181 |
+
scores = []
|
| 182 |
+
effective_batch = max(1, int(batch_size))
|
| 183 |
+
for i in range(0, len(text_list), effective_batch):
|
| 184 |
+
chunk = text_list[i : i + effective_batch]
|
| 185 |
+
raw_chunk = state["D_EN"].predict(chunk)
|
| 186 |
+
parsed = _split_en_detox_result(raw_chunk, len(chunk))
|
| 187 |
+
|
| 188 |
+
# Fallback for unexpected return shape from Detoxify implementation.
|
| 189 |
+
if not parsed:
|
| 190 |
+
parsed = [{k: float(v) for k, v in state["D_EN"].predict(text).items()} for text in chunk]
|
| 191 |
+
|
| 192 |
+
scores.extend(parsed)
|
| 193 |
+
|
| 194 |
+
return scores
|
| 195 |
+
|
| 196 |
+
|
| 197 |
def _tr_off_scores_batched(text_list, state, batch_size: int = 8):
|
| 198 |
if not text_list:
|
| 199 |
return []
|
|
|
|
| 285 |
return _blacklist_early_result(profanity_hits, insult_hits, dil, temiz)
|
| 286 |
|
| 287 |
if dil == "en":
|
| 288 |
+
gb = None
|
| 289 |
if state["GB_PIPE"] is not None:
|
| 290 |
gb_raw = state["GB_PIPE"](temiz)[0]
|
| 291 |
gb = {
|
| 292 |
"label": str(gb_raw.get("label", "")),
|
| 293 |
"score": float(gb_raw.get("score", 0.0)),
|
| 294 |
}
|
| 295 |
+
if gb.get("label", "").lower() == "noise" and gb.get("score", 0.0) > 0.98 and _is_obvious_gibberish(temiz):
|
| 296 |
early_exit_count += 1
|
| 297 |
_log_pipeline_counts(early_exit_count, detoxify_call_count)
|
| 298 |
return (
|
|
|
|
| 346 |
detoxify_call_count = 0
|
| 347 |
|
| 348 |
results = [None] * len(texts)
|
| 349 |
+
pending = []
|
| 350 |
+
pending_texts = []
|
| 351 |
|
| 352 |
for idx, text in enumerate(texts):
|
| 353 |
temiz = clean_text_nfkc(text)
|
|
|
|
| 372 |
|
| 373 |
if dil == "en":
|
| 374 |
gb = None
|
| 375 |
+
gb_is_noise = False
|
| 376 |
if state["GB_PIPE"] is not None:
|
| 377 |
gb_raw = state["GB_PIPE"](temiz)[0]
|
| 378 |
gb = {
|
| 379 |
"label": str(gb_raw.get("label", "")),
|
| 380 |
"score": float(gb_raw.get("score", 0.0)),
|
| 381 |
}
|
| 382 |
+
gb_is_noise = gb.get("label", "").lower() == "noise" and gb.get("score", 0.0) > 0.98
|
| 383 |
+
|
| 384 |
+
if gb_is_noise and _is_obvious_gibberish(temiz):
|
| 385 |
+
early_exit_count += 1
|
| 386 |
+
results[idx] = (
|
| 387 |
+
"🗑️ GIBBERISH/SPAM",
|
| 388 |
+
"Metin anlamsız karakter dizileri içeriyor.",
|
| 389 |
+
"LOW",
|
| 390 |
+
"en",
|
| 391 |
+
temiz,
|
| 392 |
+
{"gibberish": gb, "action": "MONITOR", "detox": {}},
|
| 393 |
+
)
|
| 394 |
+
continue
|
| 395 |
+
|
| 396 |
+
pending.append((idx, temiz, profanity_hits, insult_hits, gb))
|
| 397 |
+
pending_texts.append(temiz)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 398 |
continue
|
| 399 |
|
| 400 |
+
pending.append((idx, temiz, profanity_hits, insult_hits, None))
|
| 401 |
+
pending_texts.append(temiz)
|
| 402 |
+
|
| 403 |
+
if pending_texts:
|
| 404 |
+
if dil == "en":
|
| 405 |
+
en_scores = _predict_en_detox_batched(pending_texts, state, batch_size=batch_size)
|
| 406 |
+
detoxify_call_count += len(pending_texts)
|
| 407 |
+
for pending_item, res in zip(pending, en_scores):
|
| 408 |
+
idx, temiz, profanity_hits, insult_hits, gb = pending_item
|
| 409 |
+
tox_score = float(res.get("toxicity", 0.0))
|
| 410 |
+
ins_score = float(res.get("insult", 0.0))
|
| 411 |
+
identity_attack = float(res.get("identity_attack", 0.0))
|
| 412 |
+
detail = {
|
| 413 |
+
"detox": res,
|
| 414 |
+
"insult": ins_score,
|
| 415 |
+
"toxicity": tox_score,
|
| 416 |
+
"identity_attack": identity_attack,
|
| 417 |
+
"hits": profanity_hits,
|
| 418 |
+
"insult_hits": insult_hits,
|
| 419 |
+
"gibberish": gb if state["GB_PIPE"] is not None else None,
|
| 420 |
+
}
|
| 421 |
+
verdict = calculate_verdict(
|
| 422 |
+
profanity_hits,
|
| 423 |
+
insult_hits,
|
| 424 |
+
{
|
| 425 |
+
"off_score": 0.0,
|
| 426 |
+
"detox_toxicity": tox_score,
|
| 427 |
+
},
|
| 428 |
+
)
|
| 429 |
+
detail.update({"action": ACTION_MAP.get(verdict["risk_level"], "MONITOR")})
|
| 430 |
+
results[idx] = (verdict["decision"], verdict["reason"], verdict["risk_level"], dil, temiz, detail)
|
| 431 |
+
else:
|
| 432 |
+
off_scores = _tr_off_scores_batched(pending_texts, state, batch_size=batch_size)
|
| 433 |
+
for pending_item, off_score in zip(pending, off_scores):
|
| 434 |
+
idx, temiz, profanity_hits, insult_hits, _ = pending_item
|
| 435 |
+
result, detoxify_called = _tr_result_with_off_score(temiz, profanity_hits, insult_hits, off_score, state, dil)
|
| 436 |
+
if detoxify_called:
|
| 437 |
+
detoxify_call_count += 1
|
| 438 |
+
results[idx] = result
|
| 439 |
|
| 440 |
_log_pipeline_counts(early_exit_count, detoxify_call_count)
|
| 441 |
|