quynong commited on
Commit
309ceeb
·
verified ·
1 Parent(s): b3d3bc2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +79 -7
app.py CHANGED
@@ -40,6 +40,9 @@ from rule_detector import DEFAULT_RULEBASE_ALLOWED_ENTITY_TYPES, detect_by_rules
40
  DEFAULT_MODEL_ID = os.getenv("MODEL_ID", "AITeamUIT/gliner2-multi-v1-e3-25-6")
41
  DEFAULT_THRESHOLD = float(os.getenv("THRESHOLD", "0.5"))
42
  DEFAULT_CHUNK_CHARS = int(os.getenv("CHUNK_CHARS", "1000"))
 
 
 
43
  MAX_WIDTH = int(os.getenv("MAX_WIDTH", "30"))
44
 
45
  PII_LABEL_DESCRIPTIONS: Dict[str, str] = {
@@ -235,7 +238,76 @@ def manual_chunks(text: str, max_chars: int) -> List[Tuple[str, int]]:
235
  return chunks
236
 
237
 
238
- def find_all_occurrences(text: str, value: str) -> List[Tuple[int, int]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  spans = []
240
  if not value:
241
  return spans
@@ -417,24 +489,24 @@ def run_inference(text: str, model_id: str, threshold: float, use_rules: bool, u
417
  schema = build_schema(model, use_descriptions)
418
  all_predictions: List[Dict] = []
419
 
420
- chunks = manual_chunks(text, DEFAULT_CHUNK_CHARS)
421
- for chunk_idx, (chunk_text, chunk_start) in enumerate(chunks):
422
  # Store chunk info
423
- debug_info["chunks"].append({"index": chunk_idx, "start": chunk_start, "end": chunk_start + len(chunk_text), "text": chunk_text})
424
 
425
  # Tokenize chunk (show what WhitespaceTokenSplitter produces)
426
- tokens = tokenize_chunk(chunk_text)
427
  debug_info["tokens_per_chunk"].append({"chunk_index": chunk_idx, "tokens": [(t, s, e) for t, s, e in tokens]})
428
 
429
  raw = model.extract(
430
- chunk_text,
431
  schema,
432
  threshold=threshold,
433
  include_spans=not emit_duplicates,
434
  include_confidence=not emit_duplicates,
435
  format_results=emit_duplicates,
436
  )
437
- predictions = normalize_predictions(raw, chunk_text, emit_duplicates=emit_duplicates)
438
 
439
  # Store per-chunk raw predictions (before offset adjustment)
440
  debug_info["raw_preds_per_chunk"].append({"chunk_index": chunk_idx, "predictions": [dict(p) for p in predictions]})
 
40
  DEFAULT_MODEL_ID = os.getenv("MODEL_ID", "AITeamUIT/gliner2-multi-v1-e3-25-6")
41
  DEFAULT_THRESHOLD = float(os.getenv("THRESHOLD", "0.5"))
42
  DEFAULT_CHUNK_CHARS = int(os.getenv("CHUNK_CHARS", "1000"))
43
+ DEFAULT_CHUNK_TOKENS = int(os.getenv("CHUNK_TOKENS", "768"))
44
+ CHUNK_SAFETY_MARGIN = int(os.getenv("CHUNK_SAFETY_MARGIN", "20"))
45
+ MAX_CHARS_PER_CHUNK = int(os.getenv("MAX_CHARS_PER_CHUNK", "8000"))
46
  MAX_WIDTH = int(os.getenv("MAX_WIDTH", "30"))
47
 
48
  PII_LABEL_DESCRIPTIONS: Dict[str, str] = {
 
238
  return chunks
239
 
240
 
241
+ def word_token_count(text: str) -> int:
242
+ """Count word tokens the way the (patched) model tokenizer splits them."""
243
+ try:
244
+ from gliner2.processor import WhitespaceTokenSplitter
245
+
246
+ splitter = WhitespaceTokenSplitter()
247
+ return sum(1 for _ in splitter(text, lower=False))
248
+ except Exception:
249
+ return len(text.split())
250
+
251
+
252
+ class RecursiveWordChunker:
253
+ """Label-aware recursive chunker built on chonkie.RecursiveChunker.
254
+
255
+ The chunk-size budget is measured in the model's own word-token space and
256
+ already subtracts the entity-schema label tokens, so each emitted chunk plus
257
+ the labels fits the encoder. Recursion falls back paragraph -> sentence ->
258
+ whitespace. ``chunk()`` returns ``(chunk_text, start_index)`` tuples.
259
+ """
260
+
261
+ def __init__(self, budget_words: int, max_chars: int = MAX_CHARS_PER_CHUNK, min_chars_per_chunk: int = 24):
262
+ # chonkie is a pip dependency (not a cross-folder import).
263
+ from chonkie.chunker.recursive import RecursiveChunker
264
+ from chonkie.types import RecursiveLevel, RecursiveRules
265
+
266
+ self.budget = budget_words
267
+ self.max_chars = max_chars
268
+ self._chunker = RecursiveChunker(
269
+ tokenizer=word_token_count,
270
+ chunk_size=budget_words,
271
+ rules=RecursiveRules(
272
+ levels=[
273
+ RecursiveLevel(delimiters=["\n\n", "\r\n", "\n", "\r"]),
274
+ RecursiveLevel(delimiters=[". ", "! ", "? ", ".\n", "!\n", "?\n"]),
275
+ RecursiveLevel(whitespace=True),
276
+ ]
277
+ ),
278
+ min_characters_per_chunk=min_chars_per_chunk,
279
+ )
280
+
281
+ def chunk(self, text: str) -> List[Tuple[str, int]]:
282
+ return [(c.text, int(c.start_index)) for c in self._chunker.chunk(text)]
283
+
284
+
285
+ @lru_cache(maxsize=4)
286
+ def build_word_chunker(
287
+ chunk_tokens: int = DEFAULT_CHUNK_TOKENS,
288
+ safety_margin: int = CHUNK_SAFETY_MARGIN,
289
+ max_chars: int = MAX_CHARS_PER_CHUNK,
290
+ ) -> RecursiveWordChunker:
291
+ """Build a :class:`RecursiveWordChunker` with a label-aware token budget.
292
+
293
+ The label tokens (``"<LABEL> <description>"`` plus a +2 separator budget each)
294
+ are subtracted from ``chunk_tokens`` so the encoder never overflows.
295
+ """
296
+ labels_words = [f"{k} {v}" for k, v in PII_LABEL_DESCRIPTIONS.items()]
297
+ label_token_count = sum(word_token_count(lbl) + 2 for lbl in labels_words)
298
+ budget = max(64, chunk_tokens - label_token_count - safety_margin)
299
+ return RecursiveWordChunker(budget_words=budget, max_chars=max_chars)
300
+
301
+
302
+ def chunk_text(text: str) -> List[Tuple[str, int]]:
303
+ """Split text into (chunk_text, start_index) using the recursive word chunker.
304
+
305
+ Falls back to a plain whitespace chunker only if chonkie is unavailable.
306
+ """
307
+ try:
308
+ return build_word_chunker(DEFAULT_CHUNK_TOKENS).chunk(text)
309
+ except Exception:
310
+ return manual_chunks(text, DEFAULT_CHUNK_CHARS)
311
  spans = []
312
  if not value:
313
  return spans
 
489
  schema = build_schema(model, use_descriptions)
490
  all_predictions: List[Dict] = []
491
 
492
+ chunks = chunk_text(text)
493
+ for chunk_idx, (chunk_text_value, chunk_start) in enumerate(chunks):
494
  # Store chunk info
495
+ debug_info["chunks"].append({"index": chunk_idx, "start": chunk_start, "end": chunk_start + len(chunk_text_value), "text": chunk_text_value})
496
 
497
  # Tokenize chunk (show what WhitespaceTokenSplitter produces)
498
+ tokens = tokenize_chunk(chunk_text_value)
499
  debug_info["tokens_per_chunk"].append({"chunk_index": chunk_idx, "tokens": [(t, s, e) for t, s, e in tokens]})
500
 
501
  raw = model.extract(
502
+ chunk_text_value,
503
  schema,
504
  threshold=threshold,
505
  include_spans=not emit_duplicates,
506
  include_confidence=not emit_duplicates,
507
  format_results=emit_duplicates,
508
  )
509
+ predictions = normalize_predictions(raw, chunk_text_value, emit_duplicates=emit_duplicates)
510
 
511
  # Store per-chunk raw predictions (before offset adjustment)
512
  debug_info["raw_preds_per_chunk"].append({"chunk_index": chunk_idx, "predictions": [dict(p) for p in predictions]})