yungisimon commited on
Commit
c1d3043
·
verified ·
1 Parent(s): a795fc8

Upload context_cued.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. context_cued.py +109 -0
context_cued.py CHANGED
@@ -548,3 +548,112 @@ def hybrid_with_context(text: str, model_spans: list[dict]) -> list[dict]:
548
  return uniq
549
 
550
  CONTEXT_TYPES = set(_CUES) # the 12 Group-A types this tier owns
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
548
  return uniq
549
 
550
  CONTEXT_TYPES = set(_CUES) # the 12 Group-A types this tier owns
551
+
552
+
553
+ # =========================================================================== #
554
+ # P2 — Group-B cue-gated capture (financial.amount / identity.date_of_birth /
555
+ # contact.phone / contact.postal_code). High-precision: BOTH a cue AND a
556
+ # value-shape must hold, so the precision-trap docs gain no new false positives.
557
+ # These ADD spans, authoritative for their type. Distinct from the Group-A ID
558
+ # tier above (those have no shape; these are shape + cue).
559
+ # --------------------------------------------------------------------------- #
560
+ # Shapes (reuse the shipped SNAP shapes so boundaries match the rest of the decode).
561
+ _GB_PHONE = re.compile(
562
+ r"(?<!\d)(?:\+?\d{1,3}[ \-.]?)?(?:\(\d{2,4}\)[ \-.]?)?\d{2,4}[ \-.]?\d{3}[ \-.]?\d{3,4}(?!\d)")
563
+ _GB_DATE = re.compile(
564
+ r"\b(?:\d{1,2}[\/.\-]\d{1,2}[\/.\-]\d{2,4}"
565
+ r"|\d{4}[\/.\-]\d{1,2}[\/.\-]\d{1,2}"
566
+ r"|(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[a-z]*\.?\s+\d{1,2},?\s+\d{4}"
567
+ r"|\d{1,2}\.?\s+(?:Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|"
568
+ r"janv|f[ée]vr|mars|avr|mai|juin|juil|ao[ûu]t|sept|oct|nov|d[ée]c|"
569
+ r"ene|abr|ago|dic|gen|mag|giu|lug|set|ott)[a-zéûô]*\.?\s+\d{4}"
570
+ r"|\d{1,2}\s*(?:de\s+)?(?:[A-Za-zÀ-ÿ]{3,12})\s*(?:de\s+)?\d{4})\b")
571
+ _GB_AMOUNT = re.compile(
572
+ r"(?:[$€£¥₹₩]\s?\d[\d.,]*(?:\s?[KMB])?"
573
+ r"|\b(?:USD|EUR|GBP|JPY|CHF|CAD|AUD|INR|KRW)\s?\d[\d.,]*(?:\s?[KMB])?\b"
574
+ r"|\b\d[\d.,]*\s?(?:USD|EUR|GBP|JPY|CHF|INR|KRW|dollars|euros|pounds|yen)\b"
575
+ r"|\b\d[\d.,]*\s?[$€£¥₹₩])")
576
+ _GB_POSTAL = re.compile(
577
+ r"\b(?:\d{5}(?:-\d{4})?|[A-Z]{1,2}\d[A-Z\d]?\s?\d[A-Z]{2}|\d{4}\s?[A-Z]{2}|\d{4})\b")
578
+
579
+ # Cues (multilingual). Amount: also fires on a bare currency symbol adjacent to a
580
+ # number even WITHOUT a word cue (the symbol IS the cue). Phone reuses tel/mobile/...
581
+ _GB_CUES = {
582
+ "financial.amount": re.compile(
583
+ r"(?i)\b(?:amount|total|salary|balance|fee|price|sum|due|cost|charge|payment|"
584
+ r"betrag|gehalt|saldo|geb[üu]hr|preis|summe|montant|salaire|solde|frais|prix|"
585
+ r"importe|salario|saldo|precio|importo|stipendio|prezzo|valor|sal[áa]rio|"
586
+ r"金额|金額|급여|잔액|사용료|сумма|оклад|баланс|المبلغ|الراتب|"
587
+ r"kwota|wynagrodzenie|bel[oø]p|bel[ée]ag|summa|sum[ma]|m[ćc]e)\b"),
588
+ "identity.date_of_birth": re.compile(
589
+ r"(?i)(?:\bDOB\b|\bD\.?O\.?B\.?|date\s*of\s*birth|born(?:\s*on)?|"
590
+ r"geburtsdatum|geb\.?\s*am|geboren|date\s*de\s*naissance|n[ée]\s*le|"
591
+ r"fecha\s*de\s*nacimiento|nacid[oa]\s*el|data\s*di\s*nascita|nat[oa]\s*il|"
592
+ r"data\s*de\s*nascimento|geboortedatum|data\s*urodzenia|"
593
+ r"生年月日|生日|出生日期|생년월일|дата\s*рождения|تاريخ\s*الميلاد|"
594
+ r"ng[àa]y\s*sinh|วันเกิด|f[öo]delsedatum|syntym[äa]aika)"),
595
+ "contact.phone": re.compile(
596
+ r"(?i)\b(?:tel|telephone|t[ée]l[ée]phone|phone|mobile|mob|cell|cellphone|"
597
+ r"call|fax|whatsapp|handy|telefon|tel[ée]fono|telefono|telefoon|"
598
+ r"電話|手机|手機|전화|휴대폰|телефон|موبايل|هاتف|"
599
+ r"telefon|puhelin|tlf|s[đd]t|เบอร์โทร|m[óo]vil|celular|n[úu]mero)\b"),
600
+ "contact.postal_code": re.compile(
601
+ r"(?i)\b(?:zip|zip\s*code|postal\s*code|postcode|post\s*code|plz|postleitzahl|"
602
+ r"code\s*postal|c[óo]digo\s*postal|cap|codice\s*postale|c[ée]p|postcode|"
603
+ r"kod\s*pocztowy|邮编|邮政编码|郵便番号|우편번호|почтовый\s*индекс|"
604
+ r"الرمز\s*البريدي|postnummer|postinumero|m[ãa]\s*b[uư]u)\b"),
605
+ }
606
+ # Amount also fires on bare currency symbol -> number (symbol is the cue).
607
+ _GB_BARE_AMOUNT = re.compile(
608
+ r"(?:[$€£¥₹₩]\s?\d[\d.,]*(?:\s?[KMB])?|\b\d[\d.,]*\s?[$€£¥₹₩])")
609
+
610
+ # precision: a value must sit within this many chars AFTER the cue (cue->value gate).
611
+ _GB_WINDOW = 24
612
+ _GB_SHAPES = {
613
+ "financial.amount": _GB_AMOUNT,
614
+ "identity.date_of_birth": _GB_DATE,
615
+ "contact.phone": _GB_PHONE,
616
+ "contact.postal_code": _GB_POSTAL,
617
+ }
618
+
619
+
620
+ def group_b_cue_spans(text: str) -> list[dict]:
621
+ """Cue+shape gated Group-B spans. A span is emitted only when a value of the
622
+ right SHAPE appears within _GB_WINDOW chars after a type cue (or, for amounts,
623
+ when a bare currency symbol abuts the number). High precision by construction:
624
+ no cue -> no span, so the precision traps stay clean."""
625
+ out = []
626
+ claimed = [False] * len(text)
627
+ cand = [] # (start, end, type, specificity)
628
+
629
+ for typ, cue_rx in _GB_CUES.items():
630
+ shape = _GB_SHAPES[typ]
631
+ for cm in cue_rx.finditer(text):
632
+ window = text[cm.end():cm.end() + _GB_WINDOW]
633
+ vm = shape.search(window)
634
+ if not vm:
635
+ continue
636
+ # value must START within the window (not be far downstream prose)
637
+ vs = cm.end() + vm.start()
638
+ ve = cm.end() + vm.end()
639
+ # require only separators/space between cue and value
640
+ gap = text[cm.end():vs]
641
+ if not re.fullmatch(r"[\s:#=.\-/() :№\"']{0,24}", gap):
642
+ continue
643
+ cand.append((vs, ve, typ, cm.end() - cm.start() + 100))
644
+
645
+ # bare currency-symbol amounts (symbol is the cue)
646
+ for vm in _GB_BARE_AMOUNT.finditer(text):
647
+ cand.append((vm.start(), vm.end(), "financial.amount", 50))
648
+
649
+ cand.sort(key=lambda c: (-c[3], -(c[1] - c[0])))
650
+ for vs, ve, typ, _spec in cand:
651
+ if vs < 0 or ve > len(text) or any(claimed[vs:ve]):
652
+ continue
653
+ if len(text[vs:ve].strip()) < 3:
654
+ continue
655
+ for i in range(vs, ve):
656
+ claimed[i] = True
657
+ out.append({"start": vs, "end": ve, "type": typ, "text": text[vs:ve]})
658
+ out.sort(key=lambda d: (d["start"], d["end"]))
659
+ return out