Mustafa Öztürk commited on
Commit
1bbaeae
·
1 Parent(s): 2647c15

Batch EN detox inference and safer gibberish early-exit

Browse files
Files changed (1) hide show
  1. 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
- tr_pending = []
276
- tr_pending_texts = []
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
- if gb.get("label", "").lower() == "noise" and gb.get("score", 0.0) > 0.98:
308
- early_exit_count += 1
309
- results[idx] = (
310
- "🗑️ GIBBERISH/SPAM",
311
- "Metin anlamsız karakter dizileri içeriyor.",
312
- "LOW",
313
- "en",
314
- temiz,
315
- {"gibberish": gb, "action": "MONITOR", "detox": {}},
316
- )
317
- continue
318
-
319
- raw_res = state["D_EN"].predict(temiz)
320
- detoxify_call_count += 1
321
- res = {k: float(v) for k, v in raw_res.items()}
322
- tox_score = float(res.get("toxicity", 0.0))
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
- tr_pending.append((idx, temiz, profanity_hits, insult_hits))
347
- tr_pending_texts.append(temiz)
348
-
349
- if tr_pending_texts:
350
- off_scores = _tr_off_scores_batched(tr_pending_texts, state, batch_size=batch_size)
351
- for pending, off_score in zip(tr_pending, off_scores):
352
- idx, temiz, profanity_hits, insult_hits = pending
353
- result, detoxify_called = _tr_result_with_off_score(temiz, profanity_hits, insult_hits, off_score, state, dil)
354
- if detoxify_called:
355
- detoxify_call_count += 1
356
- results[idx] = result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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