Update app.py
Browse files
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
| 421 |
-
for chunk_idx, (
|
| 422 |
# Store chunk info
|
| 423 |
-
debug_info["chunks"].append({"index": chunk_idx, "start": chunk_start, "end": chunk_start + len(
|
| 424 |
|
| 425 |
# Tokenize chunk (show what WhitespaceTokenSplitter produces)
|
| 426 |
-
tokens = tokenize_chunk(
|
| 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 |
-
|
| 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,
|
| 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]})
|