Token Classification
Transformers
ONNX
Safetensors
English
distilbert
pii
privacy
experimental
datafog
Instructions to use DataFog/pii-en-65m with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use DataFog/pii-en-65m with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("token-classification", model="DataFog/pii-en-65m")# pip install -U transformers accelerate # Load model directly from transformers import AutoTokenizer, AutoModelForTokenClassification tokenizer = AutoTokenizer.from_pretrained("DataFog/pii-en-65m") model = AutoModelForTokenClassification.from_pretrained("DataFog/pii-en-65m", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Stage DataFog PII v0.1.0: epoch-2 checkpoint, frozen MCP bundle, provenance and licenses (part 2)
Browse files- reproduction/pii/core.py +143 -0
- reproduction/pii/data.py +53 -0
- reproduction/pii/evaluation.py +206 -0
- reproduction/pyproject.toml +19 -0
- reproduction/scripts/benchmark_native.py +48 -0
- reproduction/scripts/calibrate.py +15 -0
- reproduction/scripts/check_chunk_boundaries.py +63 -0
- reproduction/scripts/check_environment.py +40 -0
- reproduction/scripts/check_rust_parity.py +69 -0
- reproduction/scripts/check_training.py +67 -0
- reproduction/scripts/evaluate.py +19 -0
- reproduction/scripts/export_model.py +52 -0
- reproduction/scripts/final_audit.py +37 -0
- reproduction/scripts/predict.py +66 -0
- reproduction/scripts/prepare_data.py +122 -0
- reproduction/scripts/probe_model.py +83 -0
- reproduction/scripts/regex_baseline.py +79 -0
- reproduction/scripts/report.py +154 -0
- reproduction/scripts/run_pipeline.py +65 -0
- reproduction/scripts/train.py +127 -0
- reproduction/tests/test_data.py +20 -0
- reproduction/tests/test_evaluation.py +56 -0
- reproduction/uv.lock +0 -0
- runtime/Cargo.lock +877 -0
- runtime/Cargo.toml +12 -0
- runtime/licenses/onnxruntime-LICENSE +21 -0
- runtime/licenses/onnxruntime-ThirdPartyNotices.txt +0 -0
- runtime/licenses/ort-LICENSE-APACHE +202 -0
- runtime/licenses/ort-LICENSE-MIT +22 -0
- runtime/licenses/tokenizers-LICENSE +201 -0
- runtime/src/main.rs +431 -0
- runtime/tests/boundary-oracle.json +0 -0
- tokenizer.json +0 -0
- tokenizer_config.json +15 -0
- tools/install_bundle.py +62 -0
reproduction/pii/core.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
import numpy as np
|
| 5 |
+
|
| 6 |
+
LABELS = ["first_name", "last_name", "email", "phone_number", "street_address", "customer_id", "account_number"]
|
| 7 |
+
BIO = ["O"] + [f"{prefix}-{label}" for label in LABELS for prefix in ("B", "I")]
|
| 8 |
+
MODEL_REVISION = "6ea81172465e8b0ad3fddeed32b986cdcdcffcf0"
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def byte_positions(text):
|
| 12 |
+
positions = [0]
|
| 13 |
+
for ch in text:
|
| 14 |
+
positions.append(positions[-1] + len(ch.encode("utf-8")))
|
| 15 |
+
return positions
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def encode_windows(tokenizer, text, max_length, stride):
|
| 19 |
+
"""Tokenize the complete input before slicing; never use tokenizer overflow.
|
| 20 |
+
|
| 21 |
+
tokenizers 0.23.2 early-exits before producing overflow, silently losing text.
|
| 22 |
+
DistilBERT uses one CLS and one SEP token; raw offsets remain original chars.
|
| 23 |
+
"""
|
| 24 |
+
capacity = max_length - 2
|
| 25 |
+
if not 0 <= stride < capacity:
|
| 26 |
+
raise ValueError("invalid stride or max_length")
|
| 27 |
+
if tokenizer.cls_token_id is None or tokenizer.sep_token_id is None:
|
| 28 |
+
raise ValueError("expected DistilBERT CLS/SEP tokenizer")
|
| 29 |
+
raw = tokenizer(text, add_special_tokens=False, truncation=False, return_offsets_mapping=True, verbose=False)
|
| 30 |
+
ids, offsets = raw["input_ids"], raw["offset_mapping"]
|
| 31 |
+
windows = {"input_ids": [], "attention_mask": [], "offset_mapping": []}
|
| 32 |
+
start = 0
|
| 33 |
+
while True:
|
| 34 |
+
end = min(start + capacity, len(ids))
|
| 35 |
+
window = [tokenizer.cls_token_id] + ids[start:end] + [tokenizer.sep_token_id]
|
| 36 |
+
windows["input_ids"].append(window)
|
| 37 |
+
windows["attention_mask"].append([1] * len(window))
|
| 38 |
+
windows["offset_mapping"].append([(0, 0)] + offsets[start:end] + [(0, 0)])
|
| 39 |
+
if end == len(ids):
|
| 40 |
+
break
|
| 41 |
+
start += capacity - stride
|
| 42 |
+
return windows
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
def decode_window(logits, offsets, text, max_length, has_previous=False, has_next=False):
|
| 46 |
+
scores = np.asarray(logits, dtype=np.float64)
|
| 47 |
+
scores = np.exp(scores - scores.max(axis=-1, keepdims=True))
|
| 48 |
+
scores /= scores.sum(axis=-1, keepdims=True)
|
| 49 |
+
ids = scores.argmax(axis=-1)
|
| 50 |
+
bp = byte_positions(text)
|
| 51 |
+
findings = []
|
| 52 |
+
current = None
|
| 53 |
+
for i, (start, end) in enumerate(offsets):
|
| 54 |
+
value = int(ids[i])
|
| 55 |
+
if start == end or value == 0:
|
| 56 |
+
if current:
|
| 57 |
+
findings.append(current)
|
| 58 |
+
current = None
|
| 59 |
+
continue
|
| 60 |
+
label = LABELS[(value - 1) // 2]
|
| 61 |
+
score = float(scores[i, value])
|
| 62 |
+
if value % 2 == 1 or current is None or current["label"] != label:
|
| 63 |
+
if current:
|
| 64 |
+
findings.append(current)
|
| 65 |
+
current = {"label": label, "start": bp[start], "end": bp[end], "raw_score": score, "confidence": None}
|
| 66 |
+
else:
|
| 67 |
+
current["end"] = bp[end]
|
| 68 |
+
current["raw_score"] = min(current["raw_score"], score)
|
| 69 |
+
if current:
|
| 70 |
+
findings.append(current)
|
| 71 |
+
content = [(a, b) for a, b in offsets if a != b]
|
| 72 |
+
if len(offsets) == max_length and content:
|
| 73 |
+
if any(f["start"] == bp[content[0][0]] and f["end"] == bp[content[-1][1]] for f in findings):
|
| 74 |
+
raise ValueError("entity spans a full window; complete entity boundary is unsupported")
|
| 75 |
+
if content:
|
| 76 |
+
for f in findings:
|
| 77 |
+
f["_boundary_fragment"] = bool((has_previous and f["start"] == bp[content[0][0]]) or (has_next and f["end"] == bp[content[-1][1]]))
|
| 78 |
+
return findings
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def resolve_findings(findings):
|
| 82 |
+
# Prefer an observed containing prediction over a clipped window-edge fragment.
|
| 83 |
+
# Do not discard an edge prediction without a same-label containing candidate.
|
| 84 |
+
findings = [f for f in findings if not (f.get("_boundary_fragment", False) and any(
|
| 85 |
+
g["label"] == f["label"] and g["start"] <= f["start"] and g["end"] >= f["end"]
|
| 86 |
+
and (g["start"] < f["start"] or g["end"] > f["end"]) for g in findings))]
|
| 87 |
+
accepted = []
|
| 88 |
+
for finding in sorted(findings, key=lambda f: (-f["raw_score"], f["start"], f["end"], f["label"])):
|
| 89 |
+
if not any(finding["start"] < f["end"] and f["start"] < finding["end"] for f in accepted):
|
| 90 |
+
accepted.append({k:v for k,v in finding.items() if not k.startswith("_")})
|
| 91 |
+
return sorted(accepted, key=lambda f: (f["start"], f["end"], f["label"]))
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def result(text, findings, calibration=None, threshold=None):
|
| 95 |
+
findings = resolve_findings(findings)
|
| 96 |
+
if calibration and calibration.get("method") == "isotonic":
|
| 97 |
+
for f in findings:
|
| 98 |
+
f["confidence"] = float(np.interp(f["raw_score"], calibration["xs"], calibration["ys"]))
|
| 99 |
+
if threshold is not None:
|
| 100 |
+
findings = [f for f in findings if (f["confidence"] if f["confidence"] is not None else f["raw_score"]) >= threshold]
|
| 101 |
+
data = text.encode("utf-8")
|
| 102 |
+
parts, cursor = [], 0
|
| 103 |
+
for f in findings:
|
| 104 |
+
parts.extend([data[cursor:f["start"]], b"[REDACTED]"])
|
| 105 |
+
cursor = f["end"]
|
| 106 |
+
parts.append(data[cursor:])
|
| 107 |
+
return {"findings": findings, "contains_pii": bool(findings), "redacted": b"".join(parts).decode("utf-8")}
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def load_jsonl(path):
|
| 111 |
+
with open(path) as handle:
|
| 112 |
+
return [json.loads(line) for line in handle if line.strip()]
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class Predictor:
|
| 116 |
+
def __init__(self, bundle, device="cpu", backend="onnx"):
|
| 117 |
+
from transformers import AutoTokenizer, AutoModelForTokenClassification
|
| 118 |
+
self.bundle = Path(bundle)
|
| 119 |
+
self.config = json.loads((self.bundle / "config.json").read_text())
|
| 120 |
+
self.tokenizer = AutoTokenizer.from_pretrained(self.bundle, local_files_only=True)
|
| 121 |
+
self.backend, self.device = backend, device
|
| 122 |
+
self.calibration = json.loads((self.bundle / self.config["calibration_file"]).read_text()) if self.config.get("calibration_file") else None
|
| 123 |
+
if backend == "onnx":
|
| 124 |
+
import onnxruntime as ort
|
| 125 |
+
options = ort.SessionOptions()
|
| 126 |
+
options.intra_op_num_threads = 1
|
| 127 |
+
self.session = ort.InferenceSession(str(self.bundle / "model.onnx"), sess_options=options, providers=["CPUExecutionProvider"])
|
| 128 |
+
else:
|
| 129 |
+
self.model = AutoModelForTokenClassification.from_pretrained(self.bundle / "checkpoint", local_files_only=True, attn_implementation="eager").to(device).eval()
|
| 130 |
+
|
| 131 |
+
def predict(self, text, calibrated=True):
|
| 132 |
+
encoded = encode_windows(self.tokenizer, text, self.config["max_length"], self.config["stride"])
|
| 133 |
+
findings = []
|
| 134 |
+
for index, (ids, mask, offsets) in enumerate(zip(encoded["input_ids"], encoded["attention_mask"], encoded["offset_mapping"])):
|
| 135 |
+
if self.backend == "onnx":
|
| 136 |
+
logits = self.session.run(None, {"input_ids": np.array([ids], dtype=np.int64), "attention_mask": np.array([mask], dtype=np.int64)})[0][0]
|
| 137 |
+
else:
|
| 138 |
+
import torch
|
| 139 |
+
with torch.no_grad():
|
| 140 |
+
logits = self.model(input_ids=torch.tensor([ids], device=self.device), attention_mask=torch.tensor([mask], device=self.device)).logits[0].cpu().numpy()
|
| 141 |
+
findings.extend(decode_window(logits, offsets, text, self.config["max_length"], index > 0, index + 1 < len(encoded["input_ids"])))
|
| 142 |
+
calibration = self.calibration if calibrated else None
|
| 143 |
+
return result(text, findings, calibration, calibration.get("threshold") if calibration else None)
|
reproduction/pii/data.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pinned public-data preparation, Unicode offsets, and leakage-resistant grouping."""
|
| 2 |
+
import ast
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
import re
|
| 6 |
+
from collections import Counter
|
| 7 |
+
|
| 8 |
+
LABELS = ('first_name','last_name','email','phone_number','street_address','customer_id','account_number')
|
| 9 |
+
DATA_REVISION = 'b70ffaf5ff39e079776134c5bf4381f00a9fd1ed'
|
| 10 |
+
MODEL_REVISION = '6ea81172465e8b0ad3fddeed32b986cdcdcffcf0'
|
| 11 |
+
|
| 12 |
+
def digest(text):
|
| 13 |
+
return hashlib.sha256(text.encode('utf-8')).hexdigest()
|
| 14 |
+
|
| 15 |
+
def validate(row):
|
| 16 |
+
"""Exclude invalid rows; safely parse observed literals without evaluating code."""
|
| 17 |
+
try:
|
| 18 |
+
spans = json.loads(row['spans'])
|
| 19 |
+
except (ValueError, TypeError):
|
| 20 |
+
try:
|
| 21 |
+
spans = ast.literal_eval(row['spans'])
|
| 22 |
+
except (ValueError, SyntaxError, TypeError):
|
| 23 |
+
return None, 'parse'
|
| 24 |
+
if not isinstance(row['text'], str) or not isinstance(spans, list):
|
| 25 |
+
return None, 'schema'
|
| 26 |
+
for s in spans:
|
| 27 |
+
if not isinstance(s, dict) or not all(k in s for k in ('start','end','label','text')):
|
| 28 |
+
return None, 'span_schema'
|
| 29 |
+
if type(s['start']) is not int or type(s['end']) is not int or not isinstance(s['label'], str):
|
| 30 |
+
return None, 'span_schema'
|
| 31 |
+
if not 0 <= s['start'] < s['end'] <= len(row['text']):
|
| 32 |
+
return None, 'bounds'
|
| 33 |
+
if row['text'][s['start']:s['end']] != s['text']:
|
| 34 |
+
return None, 'source_slice_mismatch'
|
| 35 |
+
spans.sort(key=lambda s:(s['start'],s['end'],s['label']))
|
| 36 |
+
if any(a['end'] > b['start'] for a,b in zip(spans,spans[1:])):
|
| 37 |
+
return None, 'duplicate_or_overlap'
|
| 38 |
+
return spans, None
|
| 39 |
+
|
| 40 |
+
def family_key(text, spans):
|
| 41 |
+
# All annotated labels, not only target labels, identify reusable contexts.
|
| 42 |
+
parts=[]; previous=0
|
| 43 |
+
for s in spans:
|
| 44 |
+
parts.extend((text[previous:s['start']], '<'+s['label']+'>')); previous=s['end']
|
| 45 |
+
parts.append(text[previous:])
|
| 46 |
+
return digest(re.sub(r'\s+', ' ', ''.join(parts)).strip().casefold())
|
| 47 |
+
|
| 48 |
+
def support(rows):
|
| 49 |
+
docs=Counter(); entities=Counter(); negatives=0
|
| 50 |
+
for row in rows:
|
| 51 |
+
labels=[s['label'] for s in row['spans']]
|
| 52 |
+
entities.update(labels); docs.update(set(labels)); negatives += not labels
|
| 53 |
+
return {'documents':len(rows),'negative_documents':negatives,'positive_documents_by_label':dict(docs),'entities_by_label':dict(entities)}
|
reproduction/pii/evaluation.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Exact typed-span evaluation and held-out pooled isotonic calibration.
|
| 2 |
+
|
| 3 |
+
Gold offsets are Unicode character indices; predicted offsets are UTF-8 bytes.
|
| 4 |
+
Predicted-span reliability excludes missed entities; recall is reported separately.
|
| 5 |
+
"""
|
| 6 |
+
import bisect
|
| 7 |
+
import hashlib
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
LABELS = ('first_name', 'last_name', 'email', 'phone_number', 'street_address', 'customer_id', 'account_number')
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def read_jsonl(path):
|
| 15 |
+
with open(path, encoding='utf-8') as f:
|
| 16 |
+
return [json.loads(line) for line in f if line.strip()]
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def write_json(path, obj):
|
| 20 |
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
| 21 |
+
Path(path).write_text(json.dumps(obj, indent=2, allow_nan=False) + '\n')
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def apply_calibration(score, calibration):
|
| 25 |
+
xs, ys = calibration['xs'], calibration['ys']
|
| 26 |
+
if score <= xs[0]:
|
| 27 |
+
return ys[0]
|
| 28 |
+
if score >= xs[-1]:
|
| 29 |
+
return ys[-1]
|
| 30 |
+
i = bisect.bisect_right(xs, score)
|
| 31 |
+
return ys[i-1] + (score-xs[i-1])/(xs[i]-xs[i-1])*(ys[i]-ys[i-1])
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _index(records):
|
| 35 |
+
out = {}
|
| 36 |
+
for r in records:
|
| 37 |
+
if r['id'] in out:
|
| 38 |
+
raise ValueError(f"Duplicate document ID: {r['id']}")
|
| 39 |
+
out[r['id']] = r
|
| 40 |
+
return out
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def aligned(gold_records, prediction_records, calibration=None):
|
| 44 |
+
gold, predictions = _index(gold_records), _index(prediction_records)
|
| 45 |
+
if gold.keys() != predictions.keys():
|
| 46 |
+
raise ValueError('Gold/prediction document IDs differ; missing predictions cannot be silently omitted')
|
| 47 |
+
for doc_id, g in gold.items():
|
| 48 |
+
p = predictions[doc_id]
|
| 49 |
+
if g['text'] != p['text']:
|
| 50 |
+
raise ValueError(f'Text mismatch: {doc_id}')
|
| 51 |
+
offsets = [0]
|
| 52 |
+
for ch in g['text']:
|
| 53 |
+
offsets.append(offsets[-1] + len(ch.encode('utf-8')))
|
| 54 |
+
boundaries = set(offsets)
|
| 55 |
+
target = set()
|
| 56 |
+
for span in g['spans']:
|
| 57 |
+
a, b, label = span['start'], span['end'], span['label']
|
| 58 |
+
if label not in LABELS or not (isinstance(a, int) and isinstance(b, int) and 0 <= a < b < len(offsets)):
|
| 59 |
+
raise ValueError(f'Invalid gold span: {doc_id}: {span}')
|
| 60 |
+
target.add((label, offsets[a], offsets[b]))
|
| 61 |
+
found = {}
|
| 62 |
+
for span in p['findings']:
|
| 63 |
+
label, a, b = span['label'], span['start'], span['end']
|
| 64 |
+
raw = float(span['raw_score'])
|
| 65 |
+
if label not in LABELS or a not in boundaries or b not in boundaries or a >= b or not 0 <= raw <= 1:
|
| 66 |
+
raise ValueError(f'Invalid prediction: {doc_id}: {span}')
|
| 67 |
+
score = apply_calibration(raw, calibration) if calibration else span.get('confidence')
|
| 68 |
+
if score is None:
|
| 69 |
+
score = raw
|
| 70 |
+
score = float(score)
|
| 71 |
+
if not 0 <= score <= 1:
|
| 72 |
+
raise ValueError(f'Invalid confidence: {score}')
|
| 73 |
+
key = (label, a, b)
|
| 74 |
+
if key in found:
|
| 75 |
+
raise ValueError(f'Duplicate predicted span: {doc_id}: {key}')
|
| 76 |
+
found[key] = (raw, score)
|
| 77 |
+
yield doc_id, target, found
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _prf(tp, predicted, gold):
|
| 81 |
+
precision = tp / predicted if predicted else 0.0
|
| 82 |
+
recall = tp / gold if gold else 0.0
|
| 83 |
+
return {'tp': tp, 'fp': predicted-tp, 'fn': gold-tp, 'predicted': predicted, 'support': gold,
|
| 84 |
+
'precision': precision, 'recall': recall, 'f1': 2*tp/(predicted+gold) if predicted+gold else 0.0}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _merge(spans):
|
| 88 |
+
merged = []
|
| 89 |
+
for a, b in sorted((s[1], s[2]) for s in spans):
|
| 90 |
+
if merged and a <= merged[-1][1]:
|
| 91 |
+
merged[-1][1] = max(b, merged[-1][1])
|
| 92 |
+
else:
|
| 93 |
+
merged.append([a, b])
|
| 94 |
+
return merged
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def _coverage(gold, pred):
|
| 98 |
+
g, p = _merge(gold), _merge(pred)
|
| 99 |
+
i = j = overlap = 0
|
| 100 |
+
while i < len(g) and j < len(p):
|
| 101 |
+
overlap += max(0, min(g[i][1], p[j][1])-max(g[i][0], p[j][0]))
|
| 102 |
+
if g[i][1] <= p[j][1]:
|
| 103 |
+
i += 1
|
| 104 |
+
else:
|
| 105 |
+
j += 1
|
| 106 |
+
return overlap, sum(b-a for a,b in p), sum(b-a for a,b in g)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def reliability(rows, score_index):
|
| 110 |
+
bins = [{'lower': i/10, 'upper': (i+1)/10, 'support': 0, 'mean_score': 0.0, 'accuracy': 0.0} for i in range(10)]
|
| 111 |
+
squared = 0.0
|
| 112 |
+
for row in rows:
|
| 113 |
+
score, correct = row[score_index], row[2]
|
| 114 |
+
b = bins[min(9, int(score*10))]
|
| 115 |
+
b['support'] += 1
|
| 116 |
+
b['mean_score'] += score
|
| 117 |
+
b['accuracy'] += correct
|
| 118 |
+
squared += (score-correct)**2
|
| 119 |
+
ece = 0.0
|
| 120 |
+
for b in bins:
|
| 121 |
+
if b['support']:
|
| 122 |
+
b['mean_score'] /= b['support']
|
| 123 |
+
b['accuracy'] /= b['support']
|
| 124 |
+
ece += b['support'] * abs(b['mean_score']-b['accuracy'])
|
| 125 |
+
else:
|
| 126 |
+
b['mean_score'] = b['accuracy'] = None
|
| 127 |
+
n = len(rows)
|
| 128 |
+
return {'support': n, 'brier': squared/n if n else None, 'ece': ece/n if n else None,
|
| 129 |
+
'binning': '10 equal-width bins; left-inclusive, right-exclusive except final bin includes 1', 'bins': bins}
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def evaluate(gold_records, prediction_records, threshold=None, calibration=None):
|
| 133 |
+
if threshold is None and calibration:
|
| 134 |
+
threshold = calibration['threshold']
|
| 135 |
+
counts = {label: [0, 0, 0] for label in LABELS}
|
| 136 |
+
coverage = [0, 0, 0]
|
| 137 |
+
docs = dict(total=0, positive_support=0, clean_support=0, false_positive=0, false_negative=0,
|
| 138 |
+
with_any_missed_entity=0, with_any_spurious_entity=0)
|
| 139 |
+
rows = []
|
| 140 |
+
for _, target, found in aligned(gold_records, prediction_records, calibration):
|
| 141 |
+
accepted = {k for k, (_, s) in found.items() if threshold is None or s >= threshold}
|
| 142 |
+
for k, (raw, score) in found.items():
|
| 143 |
+
rows.append((raw, score, int(k in target)))
|
| 144 |
+
for label in LABELS:
|
| 145 |
+
gs, ps = {s for s in target if s[0] == label}, {s for s in accepted if s[0] == label}
|
| 146 |
+
for i, value in enumerate((len(gs & ps), len(ps), len(gs))):
|
| 147 |
+
counts[label][i] += value
|
| 148 |
+
for i, value in enumerate(_coverage(target, accepted)):
|
| 149 |
+
coverage[i] += value
|
| 150 |
+
docs['total'] += 1
|
| 151 |
+
docs['positive_support'] += bool(target)
|
| 152 |
+
docs['clean_support'] += not target
|
| 153 |
+
docs['false_positive'] += bool(accepted) and not target
|
| 154 |
+
docs['false_negative'] += bool(target) and not accepted
|
| 155 |
+
docs['with_any_missed_entity'] += bool(target-accepted)
|
| 156 |
+
docs['with_any_spurious_entity'] += bool(accepted-target)
|
| 157 |
+
return {'threshold': threshold, 'overall': _prf(*(sum(c[i] for c in counts.values()) for i in range(3))),
|
| 158 |
+
'per_label': {label: _prf(*c) for label, c in counts.items()},
|
| 159 |
+
'redaction_byte_coverage': _prf(*coverage), 'documents': docs,
|
| 160 |
+
'raw_reliability': reliability(rows, 0), 'confidence_reliability': reliability(rows, 1),
|
| 161 |
+
'reliability_population': 'All predicted spans before threshold, correctness = exact typed span. Missed gold entities excluded; see recall.',
|
| 162 |
+
'confidence_semantics': 'Pooled calibrated exact-span score' if calibration else 'Provided confidence when non-null; otherwise uncalibrated raw span score',
|
| 163 |
+
'coverage_semantics': 'Union of UTF-8 bytes; ignores labels. Zero-denominator P/R/F1 reported as 0.'}
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
def fit_calibration(fit_gold, fit_predictions, select_gold, select_predictions):
|
| 167 |
+
fit_gold, select_gold = list(fit_gold), list(select_gold)
|
| 168 |
+
if {r['id'] for r in fit_gold} & {r['id'] for r in select_gold}:
|
| 169 |
+
raise ValueError('Calibration fit/select document IDs overlap')
|
| 170 |
+
hashes = lambda records: {hashlib.sha256(r['text'].encode()).hexdigest() for r in records}
|
| 171 |
+
if hashes(fit_gold) & hashes(select_gold):
|
| 172 |
+
raise ValueError('Calibration fit/select texts overlap')
|
| 173 |
+
fit_rows = [(raw, int(k in target)) for _, target, found in aligned(fit_gold, fit_predictions)
|
| 174 |
+
for k, (raw, _) in found.items()]
|
| 175 |
+
if not fit_rows:
|
| 176 |
+
raise ValueError('No predicted spans available to fit calibration')
|
| 177 |
+
from sklearn.isotonic import IsotonicRegression
|
| 178 |
+
model = IsotonicRegression(out_of_bounds='clip').fit([r[0] for r in fit_rows], [r[1] for r in fit_rows])
|
| 179 |
+
artifact = {'version': 1, 'method': 'isotonic', 'xs': model.X_thresholds_.tolist(), 'ys': model.y_thresholds_.tolist(),
|
| 180 |
+
'interpolation': 'linear; clip outside fitted x range', 'score_semantics': 'Estimated exact typed-span correctness for predicted spans; pooled labels; raw score = minimum predicted-class token softmax',
|
| 181 |
+
'experimental': True, 'fit_documents': len(fit_gold), 'fit_predicted_spans': len(fit_rows),
|
| 182 |
+
'fit_correct_spans': sum(r[1] for r in fit_rows), 'select_documents': len(select_gold),
|
| 183 |
+
'warnings': []}
|
| 184 |
+
if len({r[1] for r in fit_rows}) < 2:
|
| 185 |
+
artifact['warnings'].append('Fit targets contain only one outcome; constant map cannot demonstrate probability reliability.')
|
| 186 |
+
select_rows, support = [], 0
|
| 187 |
+
for _, target, found in aligned(select_gold, select_predictions, artifact):
|
| 188 |
+
support += len(target)
|
| 189 |
+
select_rows.extend((score, int(k in target)) for k, (_, score) in found.items())
|
| 190 |
+
if not select_rows or not support:
|
| 191 |
+
raise ValueError('Threshold selection requires predicted spans and gold entities')
|
| 192 |
+
select_rows.sort(reverse=True)
|
| 193 |
+
curve = []
|
| 194 |
+
tp = predicted = 0
|
| 195 |
+
i = 0
|
| 196 |
+
while i < len(select_rows):
|
| 197 |
+
threshold = select_rows[i][0]
|
| 198 |
+
while i < len(select_rows) and select_rows[i][0] == threshold:
|
| 199 |
+
tp += select_rows[i][1]
|
| 200 |
+
predicted += 1
|
| 201 |
+
i += 1
|
| 202 |
+
curve.append({'threshold': threshold, **_prf(tp, predicted, support)})
|
| 203 |
+
best = max(curve, key=lambda row: (row['f1'], row['recall'], row['threshold']))
|
| 204 |
+
artifact.update(threshold=best['threshold'], threshold_objective='Max exact typed-span F1 on calibration-select; ties favor recall, then higher threshold',
|
| 205 |
+
select_operating_point=best, threshold_curve=curve)
|
| 206 |
+
return artifact
|
reproduction/pyproject.toml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "datafog-pii"
|
| 3 |
+
version = "0.0.0"
|
| 4 |
+
description = "Local environment for the DataFog PII prototype"
|
| 5 |
+
requires-python = ">=3.12,<3.13"
|
| 6 |
+
dependencies = [
|
| 7 |
+
"torch",
|
| 8 |
+
"transformers",
|
| 9 |
+
"datasets",
|
| 10 |
+
"accelerate",
|
| 11 |
+
"safetensors",
|
| 12 |
+
"scikit-learn",
|
| 13 |
+
"onnx",
|
| 14 |
+
"onnxscript",
|
| 15 |
+
"onnxruntime",
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
[tool.uv]
|
| 19 |
+
package = false
|
reproduction/scripts/benchmark_native.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Observe native CPU load and end-to-end request costs at actual input lengths."""
|
| 2 |
+
import hashlib
|
| 3 |
+
import json
|
| 4 |
+
import os
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import shutil
|
| 7 |
+
import subprocess
|
| 8 |
+
import time
|
| 9 |
+
import psutil
|
| 10 |
+
|
| 11 |
+
def main():
|
| 12 |
+
root=Path(__file__).resolve().parents[1]
|
| 13 |
+
bundle=root/"artifacts/bundle"
|
| 14 |
+
binary=root/"rust/target/release/datafog-pii"
|
| 15 |
+
records=[json.loads(line) for line in (root/"data/prepared/validation.jsonl").read_text().splitlines()]
|
| 16 |
+
records.sort(key=lambda r:len(r["text"].encode()))
|
| 17 |
+
# Observed minimum, median, p90 and maximum validation lengths, not limits.
|
| 18 |
+
chosen=[records[i] for i in sorted({0,len(records)//2,int((len(records)-1)*0.9),len(records)-1})]
|
| 19 |
+
start=time.perf_counter()
|
| 20 |
+
process=subprocess.Popen([str(binary),str(bundle)],stdin=subprocess.PIPE,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,bufsize=1)
|
| 21 |
+
# Empty request includes runtime initialization plus minimal inference: upper bound on load.
|
| 22 |
+
process.stdin.write(json.dumps({"text":""})+"\n");process.stdin.flush()
|
| 23 |
+
first=json.loads(process.stdout.readline())
|
| 24 |
+
if "error" in first:raise RuntimeError(first)
|
| 25 |
+
cold=time.perf_counter()-start
|
| 26 |
+
monitor=psutil.Process(process.pid);peak=monitor.memory_info().rss
|
| 27 |
+
observations=[]
|
| 28 |
+
for row in chosen:
|
| 29 |
+
start=time.perf_counter()
|
| 30 |
+
process.stdin.write(json.dumps({"text":row["text"]})+"\n");process.stdin.flush()
|
| 31 |
+
answer=json.loads(process.stdout.readline())
|
| 32 |
+
elapsed=time.perf_counter()-start
|
| 33 |
+
if "error" in answer:raise RuntimeError(answer)
|
| 34 |
+
peak=max(peak,monitor.memory_info().rss)
|
| 35 |
+
observations.append({"id":row["id"],"bytes":len(row["text"].encode()),"latency_seconds":elapsed,"findings":len(answer["findings"])})
|
| 36 |
+
process.stdin.close();process.wait()
|
| 37 |
+
if process.returncode:raise RuntimeError(process.stderr.read())
|
| 38 |
+
shutil.copy2(binary,bundle/"datafog-pii")
|
| 39 |
+
sums={}
|
| 40 |
+
for path in sorted(bundle.rglob("*")):
|
| 41 |
+
if path.is_file() and path.name!="checksums.json":
|
| 42 |
+
sums[str(path.relative_to(bundle))]=hashlib.sha256(path.read_bytes()).hexdigest()
|
| 43 |
+
(bundle/"checksums.json").write_text(json.dumps(sums,indent=2)+"\n")
|
| 44 |
+
evidence={"backend":"native Rust + ONNX Runtime CPU, one inference thread","bundle_bytes":sum(p.stat().st_size for p in bundle.rglob("*") if p.is_file()),"cold_load_seconds":cold,"cold_load_definition":"process launch through first empty-input response; includes initialization and minimal inference","warm_requests":observations,"peak_rss_bytes":peak,"memory_definition":"maximum RSS sampled after responses, not allocator high-water mark","warm_observed_requests_per_second":len(observations)/sum(r["latency_seconds"] for r in observations),"sampling":"one request each at observed min/median/p90/max validation byte lengths; descriptive observations, no latency guarantee"}
|
| 45 |
+
(root/"artifacts/native-benchmark.json").write_text(json.dumps(evidence,indent=2)+"\n")
|
| 46 |
+
print(json.dumps(evidence,indent=2))
|
| 47 |
+
|
| 48 |
+
if __name__=="__main__":main()
|
reproduction/scripts/calibrate.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 6 |
+
from pii.evaluation import fit_calibration, read_jsonl, write_json
|
| 7 |
+
|
| 8 |
+
if __name__ == '__main__':
|
| 9 |
+
p = argparse.ArgumentParser(description='Fit pooled isotonic calibration and independent experimental F1 threshold')
|
| 10 |
+
for name in ('fit-gold', 'fit-predictions', 'select-gold', 'select-predictions', 'output'):
|
| 11 |
+
p.add_argument('--' + name, required=True)
|
| 12 |
+
args = p.parse_args()
|
| 13 |
+
artifact = fit_calibration(*(read_jsonl(getattr(args, key)) for key in ('fit_gold', 'fit_predictions', 'select_gold', 'select_predictions')))
|
| 14 |
+
write_json(args.output, artifact)
|
| 15 |
+
print(f"Saved calibration: {args.output}; experimental threshold={artifact['threshold']}")
|
reproduction/scripts/check_chunk_boundaries.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Oracle BIO logits prove window mechanics independently of trained predictions."""
|
| 2 |
+
import json
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
import numpy as np
|
| 6 |
+
from transformers import AutoTokenizer
|
| 7 |
+
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
|
| 8 |
+
from pii.core import encode_windows, LABELS, byte_positions, decode_window, result
|
| 9 |
+
|
| 10 |
+
root=Path(__file__).resolve().parents[1]
|
| 11 |
+
tokenizer=AutoTokenizer.from_pretrained(root/'artifacts/backbone',local_files_only=True)
|
| 12 |
+
max_length,stride=512,42
|
| 13 |
+
cases=[]
|
| 14 |
+
|
| 15 |
+
def make_case(name,prefix,entities,suffix):
|
| 16 |
+
text=prefix; spans=[]
|
| 17 |
+
for label,value in entities:
|
| 18 |
+
start=len(text);text+=value;spans.append({'label':label,'start':start,'end':len(text)});text+=' '
|
| 19 |
+
text+=suffix
|
| 20 |
+
bp=byte_positions(text)
|
| 21 |
+
expected=[{'label':s['label'],'start':bp[s['start']],'end':bp[s['end']]} for s in spans]
|
| 22 |
+
encoded=encode_windows(tokenizer,text,max_length,stride)
|
| 23 |
+
raw=tokenizer(text,add_special_tokens=False,truncation=False,return_offsets_mapping=True,verbose=False)
|
| 24 |
+
covered=[]
|
| 25 |
+
for wi,ids in enumerate(encoded['input_ids']):covered.extend(ids[1 if wi==0 else stride+1:-1])
|
| 26 |
+
assert covered==raw['input_ids'], 'manual windows lost or duplicated tokens outside overlap'
|
| 27 |
+
windows=[]; findings=[]; covers=[0]*len(spans); clips=0
|
| 28 |
+
for wi,offsets in enumerate(encoded['offset_mapping']):
|
| 29 |
+
content=[(a,b) for a,b in offsets if a!=b]
|
| 30 |
+
complete=[content[0][0]<=s['start'] and content[-1][1]>=s['end'] for s in spans]
|
| 31 |
+
covers[:]=[v+int(c) for v,c in zip(covers,complete)]
|
| 32 |
+
logits=np.full((len(offsets),15),-8,dtype=np.float32)
|
| 33 |
+
for i,(a,b) in enumerate(offsets):
|
| 34 |
+
label_id=0; peak=12.
|
| 35 |
+
if a!=b:
|
| 36 |
+
for si,s in enumerate(spans):
|
| 37 |
+
if a<s['end'] and s['start']<b:
|
| 38 |
+
assert s['start']<=a and b<=s['end'], 'fixture boundary unrepresentable'
|
| 39 |
+
label_id=1+2*LABELS.index(s['label'])+int(a!=s['start'])
|
| 40 |
+
peak=5. if complete[si] else 12.
|
| 41 |
+
break
|
| 42 |
+
logits[i,label_id]=peak
|
| 43 |
+
decoded=decode_window(logits,offsets,text,max_length,wi>0,wi+1<len(encoded['input_ids']))
|
| 44 |
+
clips+=sum(f.get('_boundary_fragment',False) for f in decoded)
|
| 45 |
+
findings.extend(decoded)
|
| 46 |
+
windows.append({'input_ids':encoded['input_ids'][wi],'offsets':[[bp[a],bp[b]] for a,b in offsets], 'logits':logits.tolist()})
|
| 47 |
+
actual=result(text,findings)
|
| 48 |
+
assert [{k:f[k] for k in ('label','start','end')} for f in actual['findings']]==expected,(name,actual,expected)
|
| 49 |
+
assert all(covers), 'no complete window for entity'
|
| 50 |
+
# Demonstrate the old greedy rule really loses exact spans on boundary cases.
|
| 51 |
+
old=result(text,[{k:v for k,v in f.items() if k!='_boundary_fragment'} for f in findings])
|
| 52 |
+
old_wrong=[{k:f[k] for k in ('label','start','end')} for f in old['findings']]!=expected
|
| 53 |
+
if name!='adjacent-repeated-unicode':assert clips and old_wrong
|
| 54 |
+
cases.append({'name':name,'text':text,'expected':expected,'redacted':actual['redacted'],'windows':windows,'complete_window_counts':covers,'edge_predictions':clips,'old_greedy_would_fail':old_wrong,'full_token_coverage':True,'raw_tokens':len(raw['input_ids'])})
|
| 55 |
+
|
| 56 |
+
make_case('boundary-email','🙂 '+'hello '*505,[('email','jose.garcia@example.com')],'end '*30)
|
| 57 |
+
make_case('max43-token-boundary-entity','🙂 '+'hello '*490,[('street_address',' '.join(['alpha']*43))],'end '*30)
|
| 58 |
+
make_case('adjacent-repeated-unicode','🙂 ',[('first_name','José'),('last_name','García'),('first_name','José'),('last_name','García')],'done')
|
| 59 |
+
fixture={'max_length':max_length,'stride':stride,'labels':LABELS,'cases':cases}
|
| 60 |
+
(root/'rust/tests/boundary-oracle.json').write_text(json.dumps(fixture,separators=(',',':'))+'\n')
|
| 61 |
+
report={'max_length':max_length,'stride':stride,'policy':'Artificial-edge fragments suppressed only when a strictly containing same-label prediction exists. Remaining candidates use original greedy score ordering.', 'proof':'Oracle true BIO labels; partial-window probabilities intentionally exceed complete-window probabilities.', 'cases':[{k:v for k,v in c.items() if k not in ('text','windows','redacted')} for c in cases]}
|
| 62 |
+
(root/'artifacts/boundary-proof.json').write_text(json.dumps(report,indent=2)+'\n')
|
| 63 |
+
print(json.dumps(report,indent=2))
|
reproduction/scripts/check_environment.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Small environment smoke check; does not download data or train a PII model."""
|
| 2 |
+
import importlib.metadata
|
| 3 |
+
import json
|
| 4 |
+
import platform
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
import onnxruntime as ort
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
result = {
|
| 11 |
+
"platform": platform.platform(),
|
| 12 |
+
"python": platform.python_version(),
|
| 13 |
+
"versions": {name: importlib.metadata.version(name) for name in (
|
| 14 |
+
"torch", "transformers", "datasets", "accelerate", "safetensors",
|
| 15 |
+
"scikit-learn", "onnx", "onnxscript", "onnxruntime",
|
| 16 |
+
)},
|
| 17 |
+
"mps_built": torch.backends.mps.is_built(),
|
| 18 |
+
"mps_available": torch.backends.mps.is_available(),
|
| 19 |
+
"onnxruntime_providers": ort.get_available_providers(),
|
| 20 |
+
}
|
| 21 |
+
if result["mps_available"]:
|
| 22 |
+
torch.manual_seed(0)
|
| 23 |
+
model = torch.nn.Linear(8, 2).to("mps")
|
| 24 |
+
optimizer = torch.optim.AdamW(model.parameters())
|
| 25 |
+
before = model.weight.detach().clone()
|
| 26 |
+
loss = model(torch.ones(4, 8, device="mps")).square().mean()
|
| 27 |
+
loss.backward()
|
| 28 |
+
finite_gradients = all(torch.isfinite(p.grad).all().item() for p in model.parameters())
|
| 29 |
+
optimizer.step()
|
| 30 |
+
torch.mps.synchronize()
|
| 31 |
+
result["mps_smoke_check"] = {
|
| 32 |
+
"finite_loss": torch.isfinite(loss).item(),
|
| 33 |
+
"finite_gradients": finite_gradients,
|
| 34 |
+
"weights_changed": not torch.equal(before, model.weight),
|
| 35 |
+
}
|
| 36 |
+
assert all(result["mps_smoke_check"].values()), result
|
| 37 |
+
out = Path(__file__).resolve().parents[1] / "artifacts" / "environment-check.json"
|
| 38 |
+
out.parent.mkdir(exist_ok=True)
|
| 39 |
+
out.write_text(json.dumps(result, indent=2) + "\n")
|
| 40 |
+
print(json.dumps(result, indent=2))
|
reproduction/scripts/check_rust_parity.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Check native Rust IDs, masks, offsets, logits, decoder/redaction and explicit errors."""
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
import subprocess
|
| 5 |
+
import sys
|
| 6 |
+
import time
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import numpy as np
|
| 9 |
+
import onnxruntime as ort
|
| 10 |
+
from transformers import AutoTokenizer
|
| 11 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 12 |
+
from pii.core import encode_windows, byte_positions, decode_window, result, Predictor
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
p=argparse.ArgumentParser()
|
| 16 |
+
p.add_argument('bundle',type=Path)
|
| 17 |
+
p.add_argument('--binary',type=Path,default=Path('rust/target/release/datafog-pii'))
|
| 18 |
+
p.add_argument('--output',type=Path)
|
| 19 |
+
args=p.parse_args()
|
| 20 |
+
config=json.loads((args.bundle/'config.json').read_text())
|
| 21 |
+
tokenizer=AutoTokenizer.from_pretrained(args.bundle,local_files_only=True)
|
| 22 |
+
opts=ort.SessionOptions(); opts.intra_op_num_threads=1
|
| 23 |
+
session=ort.InferenceSession(str(args.bundle/'model.onnx'),sess_options=opts,providers=['CPUExecutionProvider'])
|
| 24 |
+
predictor=Predictor(args.bundle)
|
| 25 |
+
calibration=json.loads((args.bundle/config['calibration_file']).read_text()) if config.get('calibration_file') else None
|
| 26 |
+
texts=['','Alice Smith','🙂 José García: jose@example.com\nAccount: ABC123','Email marketing is useful.','Ann Ann, phone 415-555-0123.','Customer message. '*550+'\nJosé García <jose@example.com>','Order delay. '*170+'é Smith\n'+'More detail. '*180]
|
| 27 |
+
payload=''.join(json.dumps({'text':text})+'\n' for text in texts)+'{"wrong":1}\n'+json.dumps({'text':'Recovery after invalid input'})+'\n'
|
| 28 |
+
start=time.perf_counter()
|
| 29 |
+
proc=subprocess.run([str(args.binary.resolve()),str(args.bundle.resolve()),'--debug'],input=payload,text=True,capture_output=True,check=True)
|
| 30 |
+
elapsed=time.perf_counter()-start
|
| 31 |
+
outputs=[json.loads(line) for line in proc.stdout.splitlines()]
|
| 32 |
+
assert len(outputs)==len(texts)+2
|
| 33 |
+
assert 'error' in outputs[-2] and 'contains_pii' not in outputs[-2],outputs[-2]
|
| 34 |
+
assert 'error' not in outputs[-1],outputs[-1]
|
| 35 |
+
max_diff=0.; checks=[]
|
| 36 |
+
for text,out in zip(texts,outputs):
|
| 37 |
+
assert 'error' not in out,out
|
| 38 |
+
encoded=encode_windows(tokenizer,text,config['max_length'],config['stride'])
|
| 39 |
+
assert len(encoded['input_ids'])==len(out['windows'])
|
| 40 |
+
findings=[]
|
| 41 |
+
for i,window in enumerate(out.pop('windows')):
|
| 42 |
+
assert window['input_ids']==encoded['input_ids'][i]
|
| 43 |
+
assert window['attention_mask']==encoded['attention_mask'][i]
|
| 44 |
+
bp=byte_positions(text)
|
| 45 |
+
assert window['offsets']==[[bp[a],bp[b]] for a,b in encoded['offset_mapping'][i]]
|
| 46 |
+
logits=session.run(None,{k:np.array([encoded[k][i]],dtype=np.int64) for k in ('input_ids','attention_mask')})[0][0]
|
| 47 |
+
rust_logits=np.array(window['logits'],dtype=np.float32)
|
| 48 |
+
diff=float(np.abs(logits-rust_logits).max()); max_diff=max(max_diff,diff)
|
| 49 |
+
# Same CPU runtime and one-thread settings: require exact f32 values.
|
| 50 |
+
np.testing.assert_array_equal(logits,rust_logits)
|
| 51 |
+
findings.extend(decode_window(logits,encoded['offset_mapping'][i],text,config['max_length'], i > 0, i + 1 < len(encoded['input_ids'])))
|
| 52 |
+
expected=result(text,findings,calibration,calibration.get('threshold') if calibration else None)
|
| 53 |
+
public=predictor.predict(text)
|
| 54 |
+
assert public==expected, 'actual Predictor differs from shared reference'
|
| 55 |
+
assert out['contains_pii']==expected['contains_pii']
|
| 56 |
+
assert out['redacted']==expected['redacted']
|
| 57 |
+
assert len(out['findings'])==len(expected['findings'])
|
| 58 |
+
for actual,want in zip(out['findings'],expected['findings']):
|
| 59 |
+
for key in ('start','end','label'):assert actual[key]==want[key]
|
| 60 |
+
# f64 scalar summation vs numpy reduction differ at rounding scale.
|
| 61 |
+
assert abs(actual['raw_score']-want['raw_score'])<=64*np.finfo(np.float64).eps
|
| 62 |
+
if want['confidence'] is None: assert actual['confidence'] is None
|
| 63 |
+
else: assert abs(actual['confidence']-want['confidence'])<=1e-10
|
| 64 |
+
checks.append({'bytes':len(text.encode()),'windows':len(encoded['input_ids']),'findings':len(out['findings'])})
|
| 65 |
+
report={'bundle':str(args.bundle),'cases':checks,'max_abs_logit_difference':max_diff,'logits_bit_identical':True,'token_ids_masks_global_byte_offsets_parity':True,'findings_redaction_parity':True,'actual_python_predictor_parity':True,'malformed_request_explicit_error_and_recovery':True,'cold_process_debug_batch_seconds':elapsed,'note':'Parity fixtures prove implementation consistency, not detection quality.'}
|
| 66 |
+
serialized=json.dumps(report,indent=2)+'\n'
|
| 67 |
+
if args.output:args.output.write_text(serialized)
|
| 68 |
+
print(serialized)
|
| 69 |
+
if __name__=='__main__':main()
|
reproduction/scripts/check_training.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Diagnostic learning and exact optimizer/RNG resume proof; not held-out quality."""
|
| 2 |
+
import sys,json,time,random,hashlib,gc
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
|
| 5 |
+
sys.path.insert(0,str(Path(__file__).resolve().parent))
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
from transformers import AutoTokenizer,AutoModelForTokenClassification,DataCollatorForTokenClassification
|
| 9 |
+
from pii.core import LABELS,BIO,decode_window,byte_positions
|
| 10 |
+
from train import prepare
|
| 11 |
+
|
| 12 |
+
out=Path('work/training-diagnostic');out.mkdir(parents=True,exist_ok=True)
|
| 13 |
+
random.seed(42);np.random.seed(42);torch.manual_seed(42)
|
| 14 |
+
tok=AutoTokenizer.from_pretrained('artifacts/backbone',local_files_only=True)
|
| 15 |
+
# Shortest complete documents providing every target label keeps diagnostic bounded.
|
| 16 |
+
rows=[json.loads(x) for x in open('data/prepared/train.jsonl')]
|
| 17 |
+
rows.sort(key=lambda r:(len(r['text']),r['id']))
|
| 18 |
+
selected=[];missing=set(LABELS)
|
| 19 |
+
for r in rows:
|
| 20 |
+
labels={s['label'] for s in r['spans']}
|
| 21 |
+
if labels & missing:selected.append(r);missing-=labels
|
| 22 |
+
if not missing:break
|
| 23 |
+
chunks,stats=prepare(selected,tok,512,42)
|
| 24 |
+
collate=DataCollatorForTokenClassification(tok,return_tensors='pt')
|
| 25 |
+
batch={k:v.to('mps') for k,v in collate(chunks).items() if k in ('input_ids','attention_mask','labels')}
|
| 26 |
+
model=AutoModelForTokenClassification.from_pretrained('artifacts/backbone',num_labels=len(BIO),id2label=dict(enumerate(BIO)),label2id={v:k for k,v in enumerate(BIO)},attn_implementation='eager',local_files_only=True).to('mps')
|
| 27 |
+
opt=torch.optim.AdamW(model.parameters(),lr=2e-5,weight_decay=.01)
|
| 28 |
+
offsets=[tok(r['text'],return_offsets_mapping=True)['offset_mapping'] for r in selected]
|
| 29 |
+
gold=[]
|
| 30 |
+
for r in selected:
|
| 31 |
+
bp=byte_positions(r['text']);gold.append({(s['label'],bp[s['start']],bp[s['end']]) for s in r['spans']})
|
| 32 |
+
def assess():
|
| 33 |
+
model.eval()
|
| 34 |
+
with torch.no_grad():o=model(**batch)
|
| 35 |
+
found=0;pred=0
|
| 36 |
+
for r,off,logits,g in zip(selected,offsets,o.logits.cpu().numpy(),gold):
|
| 37 |
+
p={(f['label'],f['start'],f['end']) for f in decode_window(logits[:len(off)],off,r['text'],512)}
|
| 38 |
+
found+=len(p&g);pred+=len(p)
|
| 39 |
+
return {'loss':o.loss.item(),'exact_entity_true_positives':found,'gold_entities':sum(map(len,gold)),'predicted_entities':pred,'recall':found/sum(map(len,gold))}
|
| 40 |
+
def step():
|
| 41 |
+
model.train();opt.zero_grad(set_to_none=True);loss=model(**batch).loss
|
| 42 |
+
assert torch.isfinite(loss);loss.backward();torch.nn.utils.clip_grad_norm_(model.parameters(),1.,error_if_nonfinite=True);opt.step();return loss.item()
|
| 43 |
+
def fingerprint():
|
| 44 |
+
h=hashlib.sha256()
|
| 45 |
+
for n,p in model.named_parameters():h.update(n.encode());h.update(p.detach().cpu().numpy().tobytes())
|
| 46 |
+
return h.hexdigest()
|
| 47 |
+
start=time.perf_counter();before=assess();history=[]
|
| 48 |
+
while True:
|
| 49 |
+
loss=step();a=assess();history.append(a)
|
| 50 |
+
if a['loss']<before['loss'] and a['recall']>before['recall']:break
|
| 51 |
+
if time.perf_counter()-start>180:raise RuntimeError('Diagnostic 3-minute coordination budget exhausted; no learning claim')
|
| 52 |
+
model.save_pretrained(out/'model')
|
| 53 |
+
state={'optimizer':opt.state_dict(),'random':random.getstate(),'numpy':np.random.get_state(),'torch':torch.get_rng_state(),'mps':torch.mps.get_rng_state()}
|
| 54 |
+
torch.save(state,out/'state.pt');del state
|
| 55 |
+
uninterrupted_loss=step();uninterrupted_hash=fingerprint();uninterrupted=assess();torch.save({k:v.detach().cpu() for k,v in model.state_dict().items()},out/'uninterrupted.pt')
|
| 56 |
+
del model,opt;gc.collect();torch.mps.empty_cache()
|
| 57 |
+
model=AutoModelForTokenClassification.from_pretrained(out/'model',local_files_only=True,attn_implementation='eager').to('mps')
|
| 58 |
+
opt=torch.optim.AdamW(model.parameters(),lr=2e-5,weight_decay=.01)
|
| 59 |
+
state=torch.load(out/'state.pt',map_location='cpu',weights_only=False);opt.load_state_dict(state['optimizer'])
|
| 60 |
+
random.setstate(state['random']);np.random.set_state(state['numpy']);torch.set_rng_state(state['torch']);torch.mps.set_rng_state(state['mps']);del state
|
| 61 |
+
resumed_loss=step();resumed_hash=fingerprint();resumed=assess()
|
| 62 |
+
reference=torch.load(out/'uninterrupted.pt',weights_only=True)
|
| 63 |
+
max_parameter_difference=max((v.detach().cpu()-reference[k]).abs().max().item() for k,v in model.state_dict().items())
|
| 64 |
+
del reference
|
| 65 |
+
report={'purpose':'Training functionality diagnostic only; same examples fitted and scored, no benchmark','device':'mps','seed':42,'recipe':{'lr':2e-5,'weight_decay':.01,'gradient_clip':1.},'selected_ids':[r['id'] for r in selected],'labels_covered':LABELS,'chunk_stats':stats,'steps_to_first_learning_evidence':len(history),'stopping_rule':'First evaluation with both lower token loss and greater exact-entity recall than random-head baseline','before':before,'after':history[-1],'resume':{'checkpoint':'work/training-diagnostic','uninterrupted_next_update_loss':uninterrupted_loss,'resumed_next_update_loss':resumed_loss,'uninterrupted_parameters_sha256':uninterrupted_hash,'resumed_parameters_sha256':resumed_hash,'all_model_parameters_bitwise_equal':uninterrupted_hash==resumed_hash,'max_parameter_abs_difference':max_parameter_difference,'resume_status':'Same next-update loss and evaluation; MPS parameter differences reported without a bitwise guarantee','uninterrupted_metrics':uninterrupted,'resumed_metrics':resumed},'elapsed_seconds':time.perf_counter()-start}
|
| 66 |
+
Path('artifacts/training-check.json').write_text(json.dumps(report,indent=2));print(json.dumps(report,indent=2),flush=True)
|
| 67 |
+
assert uninterrupted_loss==resumed_loss and uninterrupted==resumed,'Resumed loss or outputs differ'
|
reproduction/scripts/evaluate.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
import sys
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
+
from pii.evaluation import evaluate, read_jsonl, write_json
|
| 8 |
+
|
| 9 |
+
if __name__ == '__main__':
|
| 10 |
+
p = argparse.ArgumentParser(description='Evaluate complete saved span predictions against character-offset gold JSONL')
|
| 11 |
+
for name in ('gold', 'predictions', 'output'):
|
| 12 |
+
p.add_argument('--' + name, required=True)
|
| 13 |
+
p.add_argument('--calibration')
|
| 14 |
+
p.add_argument('--threshold', type=float)
|
| 15 |
+
args = p.parse_args()
|
| 16 |
+
calibration = json.loads(Path(args.calibration).read_text()) if args.calibration else None
|
| 17 |
+
result = evaluate(read_jsonl(args.gold), read_jsonl(args.predictions), args.threshold, calibration)
|
| 18 |
+
write_json(args.output, result)
|
| 19 |
+
print(json.dumps({'overall': result['overall'], 'documents': result['documents']}))
|
reproduction/scripts/export_model.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Export selected local checkpoint into a native inference bundle."""
|
| 2 |
+
import hashlib
|
| 3 |
+
import json
|
| 4 |
+
import shutil
|
| 5 |
+
import sys
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
|
| 8 |
+
import numpy as np
|
| 9 |
+
import onnxruntime as ort
|
| 10 |
+
import torch
|
| 11 |
+
from transformers import AutoModelForTokenClassification, AutoTokenizer
|
| 12 |
+
from pii.core import LABELS, MODEL_REVISION
|
| 13 |
+
|
| 14 |
+
class Export(torch.nn.Module):
|
| 15 |
+
def __init__(self,model):
|
| 16 |
+
super().__init__();self.model=model
|
| 17 |
+
def forward(self,input_ids,attention_mask):
|
| 18 |
+
return self.model(input_ids=input_ids,attention_mask=attention_mask).logits
|
| 19 |
+
|
| 20 |
+
def main():
|
| 21 |
+
out=Path("artifacts/bundle");out.mkdir(parents=True,exist_ok=True)
|
| 22 |
+
training=json.loads(Path("artifacts/training/run-config.json").read_text())
|
| 23 |
+
source=Path("artifacts/training/best")
|
| 24 |
+
tokenizer=AutoTokenizer.from_pretrained(source,local_files_only=True)
|
| 25 |
+
model=AutoModelForTokenClassification.from_pretrained(source,local_files_only=True,attn_implementation="eager").eval()
|
| 26 |
+
sample=tokenizer("🙂 José Smith: jose@example.com",return_tensors="pt")
|
| 27 |
+
tokenizer.save_pretrained(out)
|
| 28 |
+
config={"labels":LABELS,"max_length":training["max_length"],"stride":training["stride"],"model_revision":MODEL_REVISION,"dataset_revision":"b70ffaf5ff39e079776134c5bf4381f00a9fd1ed","offsets":"half-open UTF-8 bytes","raw_score":"minimum predicted-class token softmax within span","status":"experimental prototype, uncalibrated until calibration_file supplied"}
|
| 29 |
+
(out/"config.json").write_text(json.dumps(config,indent=2)+"\n")
|
| 30 |
+
wrapper=Export(model).eval()
|
| 31 |
+
torch.onnx.export(wrapper, (sample["input_ids"],sample["attention_mask"]),str(out/"model.onnx"),input_names=["input_ids","attention_mask"],output_names=["logits"],dynamic_axes={"input_ids":{0:"batch",1:"sequence"},"attention_mask":{0:"batch",1:"sequence"},"logits":{0:"batch",1:"sequence"}},opset_version=17,dynamo=False)
|
| 32 |
+
assert not model.training, "Export must preserve evaluation mode for parity checks"
|
| 33 |
+
dylibs=list((Path(ort.__file__).parent/"capi").glob("libonnxruntime.*.dylib"))
|
| 34 |
+
if len(dylibs)!=1:raise RuntimeError(f"Ambiguous native runtime paths: {dylibs}")
|
| 35 |
+
shutil.copy2(dylibs[0],out/"libonnxruntime.dylib")
|
| 36 |
+
shutil.copytree("rust/licenses",out/"licenses",dirs_exist_ok=True)
|
| 37 |
+
shutil.copy2("artifacts/backbone/README.md",out/"licenses/backbone-model-card.md")
|
| 38 |
+
(out/"licenses/dataset-attribution.txt").write_text("Nemotron-PII by NVIDIA, CC BY 4.0. Source: https://huggingface.co/datasets/nvidia/Nemotron-PII\nRevision b70ffaf5ff39e079776134c5bf4381f00a9fd1ed. Training filters and preparation modifications: see artifacts/data-manifest.json and docs/data.md.\n")
|
| 39 |
+
options=ort.SessionOptions();options.intra_op_num_threads=1
|
| 40 |
+
session=ort.InferenceSession(str(out/"model.onnx"),sess_options=options,providers=["CPUExecutionProvider"])
|
| 41 |
+
checks=[]
|
| 42 |
+
for texts in ([""],["Alice Smith", "🙂 José García: jose@example.com"],["Long ticket. "*170]):
|
| 43 |
+
x=tokenizer(texts,return_tensors="pt",padding=True,truncation=True,max_length=training["max_length"])
|
| 44 |
+
x={k:v for k,v in x.items() if k in ("input_ids","attention_mask")}
|
| 45 |
+
with torch.no_grad():expected=model(**x).logits.numpy()
|
| 46 |
+
actual=session.run(None,{k:v.numpy() for k,v in x.items()})[0]
|
| 47 |
+
checks.append({"batch":len(texts),"sequence":x["input_ids"].shape[1],"max_abs_difference":float(np.abs(expected-actual).max()),"argmax_identical":bool(np.array_equal(expected.argmax(-1),actual.argmax(-1)))})
|
| 48 |
+
if not checks[-1]["argmax_identical"]:raise RuntimeError("Export changed probe token predictions")
|
| 49 |
+
(out/"export-evidence.json").write_text(json.dumps(checks,indent=2)+"\n")
|
| 50 |
+
print(json.dumps(checks,indent=2),flush=True)
|
| 51 |
+
|
| 52 |
+
if __name__=="__main__":main()
|
reproduction/scripts/final_audit.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib,json,subprocess
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
root=Path.cwd(); bundle=root/'artifacts/bundle'
|
| 4 |
+
def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
|
| 5 |
+
freeze=json.loads((root/'artifacts/pipeline/evaluation-freeze.json').read_text())['sha256']
|
| 6 |
+
assert all(sha(root/p)==v for p,v in freeze.items()),'freeze changed'
|
| 7 |
+
checks=json.loads((bundle/'checksums.json').read_text())
|
| 8 |
+
assert all(sha(bundle/p)==v for p,v in checks.items()),'bundle checksum mismatch'
|
| 9 |
+
cal=json.loads((bundle/'calibration.json').read_text())
|
| 10 |
+
import bisect
|
| 11 |
+
def confidence(x):
|
| 12 |
+
xs,ys=cal['xs'],cal['ys']
|
| 13 |
+
if x<=xs[0]:return ys[0]
|
| 14 |
+
if x>=xs[-1]:return ys[-1]
|
| 15 |
+
i=bisect.bisect_right(xs,x)
|
| 16 |
+
return ys[i-1]+(x-xs[i-1])*(ys[i]-ys[i-1])/(xs[i]-xs[i-1])
|
| 17 |
+
n=tp=fp=fn=0
|
| 18 |
+
with open('data/prepared/test.jsonl') as gold,open('artifacts/predictions/test.jsonl') as pred:
|
| 19 |
+
import itertools
|
| 20 |
+
for g,p in itertools.zip_longest(gold,pred):
|
| 21 |
+
assert g and p
|
| 22 |
+
g,p=json.loads(g),json.loads(p)
|
| 23 |
+
assert g['id']==p['id'] and g['text']==p['text']
|
| 24 |
+
text=g['text']; offsets=[0]
|
| 25 |
+
for c in text:offsets.append(offsets[-1]+len(c.encode()))
|
| 26 |
+
targets={(s['label'],offsets[s['start']],offsets[s['end']]) for s in g['spans']}
|
| 27 |
+
findings={(s['label'],s['start'],s['end']) for s in p['findings'] if confidence(s['raw_score'])>=cal['threshold']}
|
| 28 |
+
tp+=len(targets&findings);fp+=len(findings-targets);fn+=len(targets-findings);n+=1
|
| 29 |
+
expected=json.loads(Path('artifacts/evaluation/test.json').read_text())
|
| 30 |
+
assert [tp,fp,fn]==[expected['overall'][k] for k in ('tp','fp','fn')]
|
| 31 |
+
assert n==90018
|
| 32 |
+
proc=subprocess.run([str(bundle/'datafog-pii'),str(bundle)],input=json.dumps({'text':'🙂 Email me at alice@example.com.'})+'\n'+json.dumps({'text':'The screen is blank.'})+'\n',text=True,capture_output=True,check=True,env={'PATH':'/usr/bin:/bin'})
|
| 33 |
+
outputs=[json.loads(x) for x in proc.stdout.splitlines()]
|
| 34 |
+
assert len(outputs)==2 and all('error' not in x for x in outputs)
|
| 35 |
+
report={'frozen_inputs_unchanged':True,'bundle_checksums_verified':len(checks),'independent_metric_recount':{'documents':n,'tp':tp,'fp':fp,'fn':fn},'packaged_native_smoke_without_python_path':outputs}
|
| 36 |
+
Path('artifacts/final-audit.json').write_text(json.dumps(report,indent=2)+'\n')
|
| 37 |
+
print(json.dumps(report,indent=2))
|
reproduction/scripts/predict.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Save unfiltered predictions from the deployed ONNX model for evaluation."""
|
| 2 |
+
import argparse
|
| 3 |
+
from collections import Counter
|
| 4 |
+
import json
|
| 5 |
+
import sys
|
| 6 |
+
import time
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
|
| 9 |
+
import numpy as np
|
| 10 |
+
import onnxruntime as ort
|
| 11 |
+
from transformers import AutoTokenizer
|
| 12 |
+
from pii.core import decode_window, result, encode_windows
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
p=argparse.ArgumentParser()
|
| 16 |
+
p.add_argument("--bundle",type=Path,default=Path("artifacts/bundle"))
|
| 17 |
+
p.add_argument("--input",type=Path,required=True)
|
| 18 |
+
p.add_argument("--output",type=Path,required=True)
|
| 19 |
+
p.add_argument("--batch-size",type=int,default=16,help="Measured training recipe batch; dynamically padded ONNX batches")
|
| 20 |
+
args=p.parse_args()
|
| 21 |
+
config=json.loads((args.bundle/"config.json").read_text())
|
| 22 |
+
tokenizer=AutoTokenizer.from_pretrained(args.bundle,local_files_only=True)
|
| 23 |
+
options=ort.SessionOptions();options.intra_op_num_threads=1
|
| 24 |
+
session=ort.InferenceSession(str(args.bundle/"model.onnx"),sess_options=options,providers=["CPUExecutionProvider"])
|
| 25 |
+
args.output.parent.mkdir(parents=True,exist_ok=True)
|
| 26 |
+
started=time.perf_counter();documents=windows=0
|
| 27 |
+
def batch_predict(rows,handle):
|
| 28 |
+
nonlocal documents,windows
|
| 29 |
+
encoded={"input_ids":[],"attention_mask":[],"offset_mapping":[]}
|
| 30 |
+
mapping=[]
|
| 31 |
+
for i,row in enumerate(rows):
|
| 32 |
+
windows_for_row=encode_windows(tokenizer,row["text"],config["max_length"],config["stride"])
|
| 33 |
+
for key in encoded:encoded[key].extend(windows_for_row[key])
|
| 34 |
+
mapping.extend([i]*len(windows_for_row["input_ids"]))
|
| 35 |
+
counts=Counter(mapping);seen=Counter();findings=[[] for _ in rows]
|
| 36 |
+
for begin in range(0,len(mapping),args.batch_size):
|
| 37 |
+
end=min(len(mapping),begin+args.batch_size)
|
| 38 |
+
length=max(len(x) for x in encoded["input_ids"][begin:end])
|
| 39 |
+
ids=np.full((end-begin,length),tokenizer.pad_token_id,dtype=np.int64)
|
| 40 |
+
masks=np.zeros_like(ids)
|
| 41 |
+
for j,index in enumerate(range(begin,end)):
|
| 42 |
+
n=len(encoded["input_ids"][index]);ids[j,:n]=encoded["input_ids"][index];masks[j,:n]=encoded["attention_mask"][index]
|
| 43 |
+
logits=session.run(None,{"input_ids":ids,"attention_mask":masks})[0]
|
| 44 |
+
for j,index in enumerate(range(begin,end)):
|
| 45 |
+
sample=mapping[index];offsets=encoded["offset_mapping"][index]
|
| 46 |
+
findings[sample].extend(decode_window(logits[j,:len(offsets)],offsets,rows[sample]["text"],config["max_length"],has_previous=seen[sample]>0,has_next=seen[sample]+1<counts[sample]))
|
| 47 |
+
seen[sample]+=1
|
| 48 |
+
for row,found in zip(rows,findings):
|
| 49 |
+
prediction={"id":row["id"],"text":row["text"],**result(row["text"],found)}
|
| 50 |
+
handle.write(json.dumps(prediction,ensure_ascii=False)+"\n")
|
| 51 |
+
documents+=len(rows);windows+=len(mapping)
|
| 52 |
+
args.output.with_suffix(".progress.json").write_text(json.dumps({"documents":documents,"windows":windows,"seconds":time.perf_counter()-started})+"\n")
|
| 53 |
+
temporary=args.output.with_suffix(".partial.jsonl")
|
| 54 |
+
with args.input.open() as source,temporary.open("w") as target:
|
| 55 |
+
rows=[]
|
| 56 |
+
for line in source:
|
| 57 |
+
if not line.strip():continue
|
| 58 |
+
rows.append(json.loads(line))
|
| 59 |
+
if len(rows)==args.batch_size:batch_predict(rows,target);rows=[]
|
| 60 |
+
if rows:batch_predict(rows,target)
|
| 61 |
+
temporary.replace(args.output)
|
| 62 |
+
evidence={"documents":documents,"windows":windows,"seconds":time.perf_counter()-started,"backend":"ONNX Runtime CPU","batch_size":args.batch_size,"raw_unfiltered":True}
|
| 63 |
+
args.output.with_suffix(".evidence.json").write_text(json.dumps(evidence,indent=2)+"\n")
|
| 64 |
+
print(json.dumps(evidence),flush=True)
|
| 65 |
+
|
| 66 |
+
if __name__=="__main__":main()
|
reproduction/scripts/prepare_data.py
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run from project root: .venv/bin/python scripts/prepare_data.py."""
|
| 3 |
+
import sys
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
|
| 6 |
+
import json, math, hashlib, bisect
|
| 7 |
+
from collections import Counter, defaultdict
|
| 8 |
+
import numpy as np
|
| 9 |
+
import pyarrow.parquet as pq
|
| 10 |
+
from huggingface_hub import hf_hub_download
|
| 11 |
+
from transformers import AutoTokenizer
|
| 12 |
+
from pii.data import LABELS, DATA_REVISION, MODEL_REVISION, validate, digest, family_key, support
|
| 13 |
+
|
| 14 |
+
ROOT=Path(__file__).resolve().parents[1]
|
| 15 |
+
RAW=ROOT/'data/raw'; OUT=ROOT/'data/prepared'; ART=ROOT/'artifacts'
|
| 16 |
+
OUT.mkdir(parents=True,exist_ok=True); ART.mkdir(exist_ok=True)
|
| 17 |
+
manifest={'source':'nvidia/Nemotron-PII','revision':DATA_REVISION,'license':'CC-BY-4.0','attribution':'NVIDIA; Amy Steier, Andre Manoel, Alexa Haushalter, Maarten Van Segbroeck, Nemotron-PII (2025). https://huggingface.co/datasets/nvidia/Nemotron-PII','source_files':{},'source_stats':{},'exclusions':{}}
|
| 18 |
+
files=['README.md']+[f'data/{s}-00000-of-00001.parquet' for s in ('train','test')]
|
| 19 |
+
for f in files:
|
| 20 |
+
p=Path(hf_hub_download(manifest['source'],f,repo_type='dataset',revision=DATA_REVISION,local_dir=RAW))
|
| 21 |
+
manifest['source_files'][f]={'sha256':hashlib.sha256(p.read_bytes()).hexdigest(),'bytes':p.stat().st_size}
|
| 22 |
+
rows=[]; excluded=[]; parent=[]; key_owner={}
|
| 23 |
+
def find(i):
|
| 24 |
+
while parent[i]!=i:
|
| 25 |
+
parent[i]=parent[parent[i]]; i=parent[i]
|
| 26 |
+
return i
|
| 27 |
+
def union(i,j):
|
| 28 |
+
a,b=find(i),find(j)
|
| 29 |
+
if a!=b: parent[max(a,b)]=min(a,b)
|
| 30 |
+
for source in ('train','test'):
|
| 31 |
+
table=pq.read_table(RAW/f'data/{source}-00000-of-00001.parquet')
|
| 32 |
+
stats={'rows':len(table),'labels':Counter(),'invalid':Counter(),'empty_all_spans':0,'semantic_flags':Counter()}
|
| 33 |
+
seen_uids=Counter(); seen_texts=Counter()
|
| 34 |
+
for n,r in enumerate(table.to_pylist()):
|
| 35 |
+
seen_uids[r['uid']]+=1; seen_texts[digest(r['text'])]+=1
|
| 36 |
+
spans,reason=validate(r)
|
| 37 |
+
if reason:
|
| 38 |
+
stats['invalid'][reason]+=1; excluded.append({'source':source,'index':n,'uid':r['uid'],'reason':reason}); continue
|
| 39 |
+
stats['labels'].update(s['label'] for s in spans); stats['empty_all_spans']+=not spans
|
| 40 |
+
for s in spans:
|
| 41 |
+
if s['label']=='email' and '@' not in s['text']:
|
| 42 |
+
stats['semantic_flags']['email_without_at']+=1
|
| 43 |
+
row={'id':f'{source}:{n}:{r["uid"]}','uid':r['uid'],'source':source,'source_index':n,'text':r['text'],'spans':[{k:s[k] for k in ('label','start','end')} for s in spans if s['label'] in LABELS], 'domain':r['domain'],'document_type':r['document_type'],'family':family_key(r['text'],spans),'text_sha256':digest(r['text'])}
|
| 44 |
+
i=len(rows); parent.append(i)
|
| 45 |
+
for key in ('uid:'+row['uid'],'text:'+row['text_sha256'],'family:'+row['family']):
|
| 46 |
+
if key in key_owner: union(i,key_owner[key])
|
| 47 |
+
else:key_owner[key]=i
|
| 48 |
+
rows.append(row)
|
| 49 |
+
stats['duplicate_uid_rows']=sum(v-1 for v in seen_uids.values()); stats['duplicate_exact_text_rows']=sum(v-1 for v in seen_texts.values())
|
| 50 |
+
manifest['source_stats'][source]=stats
|
| 51 |
+
print(source,json.dumps(stats),flush=True)
|
| 52 |
+
groups=defaultdict(list)
|
| 53 |
+
for i,row in enumerate(rows): groups[find(i)].append(row)
|
| 54 |
+
splits={s:[] for s in ('train','validation','calibration_fit','calibration_select','test')}
|
| 55 |
+
train_groups=[]; overlaps=0
|
| 56 |
+
for group in groups.values():
|
| 57 |
+
group_id=min(r['id'] for r in group)
|
| 58 |
+
for r in group:r['group']=digest(group_id)
|
| 59 |
+
test=[r for r in group if r['source']=='test']
|
| 60 |
+
if test:
|
| 61 |
+
splits['test'].extend(test)
|
| 62 |
+
for r in group:
|
| 63 |
+
if r['source']=='train':
|
| 64 |
+
excluded.append({'source':'train','index':r['source_index'],'uid':r['uid'],'reason':'test_family_overlap'}); overlaps+=1
|
| 65 |
+
else:train_groups.append(group)
|
| 66 |
+
# Stable pseudorandom family order. Allocate heldouts jointly, without model predictions.
|
| 67 |
+
train_groups.sort(key=lambda g:digest('datafog-pii-split-v1:'+g[0]['group']))
|
| 68 |
+
# Owner-approved transparent volume-derived heuristic; no quality guarantee.
|
| 69 |
+
all_support=Counter()
|
| 70 |
+
for group in train_groups:
|
| 71 |
+
for r in group: all_support.update(set(s['label'] for s in r['spans']) or ['__negative__'])
|
| 72 |
+
quota=math.ceil(math.sqrt(min(all_support[l] for l in LABELS)))
|
| 73 |
+
heldouts=('validation','calibration_fit','calibration_select'); counts={s:Counter() for s in heldouts}
|
| 74 |
+
for group in train_groups:
|
| 75 |
+
coverage=Counter()
|
| 76 |
+
for r in group:
|
| 77 |
+
labels=set(s['label'] for s in r['spans']); coverage.update(labels or ['__negative__'])
|
| 78 |
+
deficits={s:sum(min(v,max(0,quota-counts[s][l])) for l,v in coverage.items()) for s in heldouts}
|
| 79 |
+
chosen=max(heldouts,key=lambda s:(deficits[s],-len(splits[s])))
|
| 80 |
+
if deficits[chosen]==0:chosen='train'
|
| 81 |
+
splits[chosen].extend(group)
|
| 82 |
+
if chosen in counts:counts[chosen].update(coverage)
|
| 83 |
+
manifest['split_rule']={'quota_positive_documents_per_label_and_negative_documents':quota,'derivation':'ceil(sqrt(minimum observed eligible positive-document support across target labels)); equal support in three heldouts. Transparent prototype allocation heuristic balancing training retention with heldout growth; no statistical precision or quality guarantee.','eligible_train_document_support':dict(all_support),'grouping':'Connected components of UID, exact original text SHA256, and whitespace-collapsed casefolded all-label-masked text SHA256. All train groups touching test excluded. Source test retained in full except structurally invalid rows.','allocation':'Stable SHA256 family order; assign each family to heldout with greatest remaining document-support deficit (tie: smaller partition), otherwise train.','seed':'datafog-pii-split-v1','test_overlap_train_rows':overlaps,'heldout_achieved_support':counts}
|
| 84 |
+
manifest['splits']={}
|
| 85 |
+
for name,records in splits.items():
|
| 86 |
+
records.sort(key=lambda r:r['id'])
|
| 87 |
+
p=OUT/f'{name}.jsonl'
|
| 88 |
+
with p.open('w') as f:
|
| 89 |
+
for r in records:f.write(json.dumps(r,ensure_ascii=False)+'\n')
|
| 90 |
+
manifest['splits'][name]={**support(records),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()}
|
| 91 |
+
print(name,json.dumps(manifest['splits'][name]),flush=True)
|
| 92 |
+
sets={name:set(r['group'] for r in records) for name,records in splits.items()}
|
| 93 |
+
for a in sets:
|
| 94 |
+
for b in sets:
|
| 95 |
+
if a<b:assert sets[a].isdisjoint(sets[b]),(a,b)
|
| 96 |
+
manifest['pairwise_group_disjoint']=True
|
| 97 |
+
manifest['exclusions']=dict(Counter(r['reason'] for r in excluded))
|
| 98 |
+
with (OUT/'excluded.jsonl').open('w') as f:
|
| 99 |
+
for r in excluded:f.write(json.dumps(r)+'\n')
|
| 100 |
+
(ART/'data-manifest.json').write_text(json.dumps(manifest,indent=2))
|
| 101 |
+
print('Tokenizing full eligible training-side data for length/alignment statistics',flush=True)
|
| 102 |
+
tokenizer=AutoTokenizer.from_pretrained('distilbert/distilbert-base-cased',revision=MODEL_REVISION)
|
| 103 |
+
tokenizer.save_pretrained(ROOT/'artifacts/tokenizer')
|
| 104 |
+
length_stats={}; entity_lengths=defaultdict(list); boundary_bad=Counter(); boundary_examples=[]
|
| 105 |
+
for name in ('train','validation','calibration_fit','calibration_select'):
|
| 106 |
+
lengths=[]
|
| 107 |
+
for base in range(0,len(splits[name]),256):
|
| 108 |
+
batch=splits[name][base:base+256]
|
| 109 |
+
encoded=tokenizer([r['text'] for r in batch],add_special_tokens=False,return_offsets_mapping=True,truncation=False,verbose=False)
|
| 110 |
+
for r,ids,offsets in zip(batch,encoded['input_ids'],encoded['offset_mapping']):
|
| 111 |
+
lengths.append(len(ids)); starts=[a for a,b in offsets]; ends=[b for a,b in offsets]
|
| 112 |
+
for s in r['spans']:
|
| 113 |
+
i=bisect.bisect_right(ends,s['start']); j=bisect.bisect_left(starts,s['end'])
|
| 114 |
+
entity_lengths[s['label']].append(j-i)
|
| 115 |
+
if i>=j or starts[i]!=s['start'] or ends[j-1]!=s['end']:
|
| 116 |
+
boundary_bad[s['label']]+=1
|
| 117 |
+
if len(boundary_examples)<30:boundary_examples.append({'id':r['id'],'span':s,'text':r['text'][s['start']:s['end']],'covering_offsets':offsets[i:j]})
|
| 118 |
+
length_stats[name]={'documents':len(lengths),'tokens':sum(lengths),'max':max(lengths,default=0),'quantiles':dict(zip(['p50','p90','p95','p99'],np.quantile(lengths,[.5,.9,.95,.99]).tolist())),'over_510_tokens':sum(x>510 for x in lengths),'characters':sum(len(r['text']) for r in splits[name])}
|
| 119 |
+
print(name,length_stats[name],flush=True)
|
| 120 |
+
manifest['tokenizer']={'model':'distilbert/distilbert-base-cased','revision':MODEL_REVISION,'model_max_length':tokenizer.model_max_length,'lengths':length_stats,'entity_tokens_by_label':{k:{'count':len(v),'max':max(v),'p99':float(np.quantile(v,.99))} for k,v in entity_lengths.items()},'unrepresentable_boundaries_by_label':dict(boundary_bad),'unrepresentable_boundary_examples':boundary_examples}
|
| 121 |
+
(ART/'data-manifest.json').write_text(json.dumps(manifest,indent=2))
|
| 122 |
+
print('DONE',json.dumps(manifest['tokenizer']),flush=True)
|
reproduction/scripts/probe_model.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Prove MPS update, checkpoint reload, dynamic ONNX export before full training."""
|
| 2 |
+
import json
|
| 3 |
+
import sys
|
| 4 |
+
import time
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 7 |
+
import numpy as np
|
| 8 |
+
import onnxruntime as ort
|
| 9 |
+
import torch
|
| 10 |
+
from transformers import AutoModelForTokenClassification, AutoTokenizer
|
| 11 |
+
from pii.core import BIO, LABELS, MODEL_REVISION
|
| 12 |
+
|
| 13 |
+
torch.manual_seed(42) # Transformers TrainingArguments default seed.
|
| 14 |
+
out = Path("artifacts/probe")
|
| 15 |
+
out.mkdir(parents=True, exist_ok=True)
|
| 16 |
+
tokenizer = AutoTokenizer.from_pretrained("artifacts/backbone", local_files_only=True)
|
| 17 |
+
model = AutoModelForTokenClassification.from_pretrained("artifacts/backbone", num_labels=len(BIO), id2label=dict(enumerate(BIO)), label2id={v:k for k,v in enumerate(BIO)}, attn_implementation="eager", local_files_only=True).to("mps")
|
| 18 |
+
optimizer = torch.optim.AdamW(model.parameters(), lr=2e-5, weight_decay=0.01)
|
| 19 |
+
report = {"model_revision": MODEL_REVISION, "parameters": sum(p.numel() for p in model.parameters()), "max_length": model.config.max_position_embeddings, "precision":"float32", "device":"mps", "benchmarks":[]}
|
| 20 |
+
before = model.classifier.weight.detach().clone()
|
| 21 |
+
# Official HF recipe batch=16; measure both short and maximum-length inputs.
|
| 22 |
+
for length in (32, model.config.max_position_embeddings):
|
| 23 |
+
x = torch.full((16, length), 100, dtype=torch.long, device="mps")
|
| 24 |
+
x[:,0], x[:,-1] = tokenizer.cls_token_id, tokenizer.sep_token_id
|
| 25 |
+
y = torch.zeros_like(x)
|
| 26 |
+
y[:,0], y[:,-1] = -100, -100
|
| 27 |
+
y[:,1] = 1
|
| 28 |
+
times = []
|
| 29 |
+
for stage in ("warmup", "measured"):
|
| 30 |
+
optimizer.zero_grad(set_to_none=True)
|
| 31 |
+
torch.mps.synchronize()
|
| 32 |
+
start = time.perf_counter()
|
| 33 |
+
loss = model(input_ids=x, attention_mask=torch.ones_like(x), labels=y).loss
|
| 34 |
+
loss.backward()
|
| 35 |
+
finite = all(torch.isfinite(p.grad).all().item() for p in model.parameters() if p.grad is not None)
|
| 36 |
+
optimizer.step()
|
| 37 |
+
torch.mps.synchronize()
|
| 38 |
+
times.append(time.perf_counter()-start)
|
| 39 |
+
assert torch.isfinite(loss).item() and finite
|
| 40 |
+
report["benchmarks"].append({"batch_size":16,"sequence_length":length,"step_seconds":times,"loss":loss.item(),"allocated_bytes":torch.mps.current_allocated_memory(),"driver_bytes":torch.mps.driver_allocated_memory()})
|
| 41 |
+
assert not torch.equal(before, model.classifier.weight)
|
| 42 |
+
model.eval()
|
| 43 |
+
sample = tokenizer("🙂 José Smith: jose@example.com", return_tensors="pt")
|
| 44 |
+
sample = {k:v.to("mps") for k,v in sample.items() if k in ("input_ids","attention_mask")}
|
| 45 |
+
with torch.no_grad():
|
| 46 |
+
reference = model(**sample).logits.cpu()
|
| 47 |
+
start = time.perf_counter()
|
| 48 |
+
model.save_pretrained(out / "checkpoint")
|
| 49 |
+
torch.save({"optimizer":optimizer.state_dict(),"rng":torch.get_rng_state(),"mps_rng":torch.mps.get_rng_state()}, out / "resume.pt")
|
| 50 |
+
report["checkpoint_seconds"] = time.perf_counter()-start
|
| 51 |
+
del optimizer
|
| 52 |
+
model = model.to("cpu")
|
| 53 |
+
reloaded = AutoModelForTokenClassification.from_pretrained(out / "checkpoint", local_files_only=True, attn_implementation="eager").to("mps").eval()
|
| 54 |
+
with torch.no_grad():
|
| 55 |
+
actual = reloaded(**sample).logits.cpu()
|
| 56 |
+
report["reload_max_abs_difference"] = (actual-reference).abs().max().item()
|
| 57 |
+
assert torch.equal(actual,reference)
|
| 58 |
+
del reloaded
|
| 59 |
+
torch.mps.empty_cache()
|
| 60 |
+
tokenizer.save_pretrained(out)
|
| 61 |
+
(out / "config.json").write_text(json.dumps({"labels":LABELS,"max_length":model.config.max_position_embeddings,"stride":0,"model_revision":MODEL_REVISION},indent=2))
|
| 62 |
+
|
| 63 |
+
class Export(torch.nn.Module):
|
| 64 |
+
def __init__(self, model):
|
| 65 |
+
super().__init__()
|
| 66 |
+
self.model=model
|
| 67 |
+
def forward(self, input_ids, attention_mask):
|
| 68 |
+
return self.model(input_ids=input_ids,attention_mask=attention_mask).logits
|
| 69 |
+
|
| 70 |
+
sample = {k:v.cpu() for k,v in sample.items()}
|
| 71 |
+
torch.onnx.export(Export(model).eval(), (sample["input_ids"],sample["attention_mask"]), str(out / "model.onnx"), input_names=["input_ids","attention_mask"], output_names=["logits"], dynamic_axes={"input_ids":{0:"batch",1:"sequence"},"attention_mask":{0:"batch",1:"sequence"},"logits":{0:"batch",1:"sequence"}}, opset_version=17, dynamo=False)
|
| 72 |
+
session=ort.InferenceSession(str(out / "model.onnx"),providers=["CPUExecutionProvider"])
|
| 73 |
+
report["export_checks"]=[]
|
| 74 |
+
for text in ("", "Alice Smith", "🙂 José Smith: jose@example.com", "Long ticket. " * 150):
|
| 75 |
+
encoded=tokenizer(text,return_tensors="pt",truncation=True,max_length=model.config.max_position_embeddings)
|
| 76 |
+
encoded={k:v for k,v in encoded.items() if k in ("input_ids","attention_mask")}
|
| 77 |
+
with torch.no_grad():
|
| 78 |
+
logits=model(**encoded).logits.numpy()
|
| 79 |
+
actual=session.run(None,{k:v.numpy() for k,v in encoded.items()})[0]
|
| 80 |
+
report["export_checks"].append({"tokens":encoded["input_ids"].shape[1],"max_abs_logit_difference":float(np.max(np.abs(logits-actual))),"argmax_identical":bool(np.array_equal(logits.argmax(-1),actual.argmax(-1)))})
|
| 81 |
+
assert np.array_equal(logits.argmax(-1),actual.argmax(-1))
|
| 82 |
+
(out / "evidence.json").write_text(json.dumps(report,indent=2)+"\n")
|
| 83 |
+
print(json.dumps(report,indent=2),flush=True)
|
reproduction/scripts/regex_baseline.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Pinned official DataFog regex baseline on shared email/phone scope."""
|
| 3 |
+
import argparse
|
| 4 |
+
import hashlib
|
| 5 |
+
import importlib.metadata
|
| 6 |
+
import json
|
| 7 |
+
import sys
|
| 8 |
+
import time
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 11 |
+
from pii.evaluation import evaluate, read_jsonl, write_json
|
| 12 |
+
|
| 13 |
+
VERSION = '4.8.1'
|
| 14 |
+
MAPPING = {'EMAIL': 'email', 'PHONE': 'phone_number'}
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def scoped_gold(records):
|
| 18 |
+
return [{**r, 'spans':[s for s in r['spans'] if s['label'] in MAPPING.values()]} for r in records]
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def predict(records):
|
| 22 |
+
import datafog
|
| 23 |
+
if importlib.metadata.version('datafog') != VERSION:
|
| 24 |
+
raise RuntimeError(f'Baseline requires datafog=={VERSION}')
|
| 25 |
+
for r in records:
|
| 26 |
+
result = datafog.scan(r['text'], engine='regex', entity_types=list(MAPPING))
|
| 27 |
+
findings = []
|
| 28 |
+
byte_offsets = [0]
|
| 29 |
+
for ch in r['text']:
|
| 30 |
+
byte_offsets.append(byte_offsets[-1] + len(ch.encode('utf-8')))
|
| 31 |
+
for e in result.entities:
|
| 32 |
+
if r['text'][e.start:e.end] != e.text:
|
| 33 |
+
raise ValueError('Upstream span/source mismatch')
|
| 34 |
+
findings.append(dict(label=MAPPING[e.type], start=byte_offsets[e.start], end=byte_offsets[e.end], raw_score=1.0, confidence=None))
|
| 35 |
+
yield dict(id=r['id'], text=r['text'], findings=findings, contains_pii=bool(findings), score_semantics='Constant match indicator from DataFog regex, not calibrated confidence')
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def write_jsonl(path, records):
|
| 39 |
+
with open(path, 'w') as f:
|
| 40 |
+
for record in records:
|
| 41 |
+
f.write(json.dumps(record, ensure_ascii=False)+'\n')
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
if __name__ == '__main__':
|
| 45 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 46 |
+
parser.add_argument('--gold', required=True)
|
| 47 |
+
parser.add_argument('--output-dir', required=True)
|
| 48 |
+
parser.add_argument('--model-predictions')
|
| 49 |
+
parser.add_argument('--model-calibration')
|
| 50 |
+
args = parser.parse_args()
|
| 51 |
+
output = Path(args.output_dir)
|
| 52 |
+
output.mkdir(parents=True, exist_ok=True)
|
| 53 |
+
gold = scoped_gold(read_jsonl(args.gold))
|
| 54 |
+
started = time.perf_counter()
|
| 55 |
+
predictions = list(predict(gold))
|
| 56 |
+
elapsed = time.perf_counter()-started
|
| 57 |
+
write_jsonl(output/'gold-email-phone.jsonl', gold)
|
| 58 |
+
write_jsonl(output/'regex-predictions.jsonl', predictions)
|
| 59 |
+
metrics = evaluate(gold, predictions)
|
| 60 |
+
# A regex match flag is not a probability; do not imply a calibration comparison.
|
| 61 |
+
for key in ('raw_reliability','confidence_reliability','reliability_population','confidence_semantics'):
|
| 62 |
+
metrics.pop(key, None)
|
| 63 |
+
metrics['score_semantics'] = 'Regex match indicator, no probability calibration evaluated'
|
| 64 |
+
metrics['comparison_scope'] = ['email','phone_number']
|
| 65 |
+
metrics['unsupported_labels'] = ['first_name','last_name','street_address','customer_id','account_number']
|
| 66 |
+
write_json(output/'regex-metrics.json', metrics)
|
| 67 |
+
provenance = dict(package='datafog', version=VERSION, official_repository='https://github.com/DataFog/datafog-python',
|
| 68 |
+
release='https://pypi.org/project/datafog/4.8.1/', input_sha256=hashlib.sha256(Path(args.gold).read_bytes()).hexdigest(),
|
| 69 |
+
documents=len(gold), elapsed_seconds=elapsed, API="datafog.scan(text, engine='regex', entity_types=['EMAIL','PHONE'])",
|
| 70 |
+
offset_conversion='Python character offsets converted to original UTF-8 byte offsets', dependencies={d.metadata['Name']:d.version for d in importlib.metadata.distributions()})
|
| 71 |
+
write_json(output/'regex-provenance.json', provenance)
|
| 72 |
+
if args.model_predictions:
|
| 73 |
+
model = read_jsonl(args.model_predictions)
|
| 74 |
+
model = [{**r, 'findings':[s for s in r['findings'] if s['label'] in MAPPING.values()]} for r in model]
|
| 75 |
+
calibration = json.loads(Path(args.model_calibration).read_text()) if args.model_calibration else None
|
| 76 |
+
model_metrics = evaluate(gold, model, calibration=calibration)
|
| 77 |
+
model_metrics['comparison_scope'] = ['email','phone_number']
|
| 78 |
+
write_json(output/'model-email-phone-metrics.json', model_metrics)
|
| 79 |
+
print(json.dumps({'documents':len(gold), 'seconds':elapsed, 'output_dir':str(output)}))
|
reproduction/scripts/report.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Render final measured evidence as Markdown; absent evidence stays a blocker."""
|
| 3 |
+
import argparse
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def load(root, name):
|
| 9 |
+
path = root / name
|
| 10 |
+
return json.loads(path.read_text()) if path.exists() else None
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def number(value, decimals=4):
|
| 14 |
+
if value is None:
|
| 15 |
+
return 'Unavailable'
|
| 16 |
+
return f'{value:,.{decimals}f}' if isinstance(value, (int, float)) else str(value)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def table(headers, rows):
|
| 20 |
+
safe = lambda v: str(v).replace('|', '\\|').replace('\n', ' ')
|
| 21 |
+
return '\n'.join(['| '+' | '.join(headers)+' |', '| '+' | '.join(['---']*len(headers))+' |'] +
|
| 22 |
+
['| '+' | '.join(safe(v) for v in row)+' |' for row in rows])
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def render(root):
|
| 26 |
+
paths = {
|
| 27 |
+
'test':'artifacts/evaluation/test.json', 'fixtures':'artifacts/evaluation/support-fixtures.json',
|
| 28 |
+
'regex':'artifacts/regex-test/regex-metrics.json', 'model_shared':'artifacts/regex-test/model-email-phone-metrics.json',
|
| 29 |
+
'calibration':'artifacts/bundle/calibration.json', 'training':'artifacts/training/complete.json',
|
| 30 |
+
'data':'artifacts/data-manifest.json', 'parity':'artifacts/bundle/rust-parity.json',
|
| 31 |
+
'benchmark':'artifacts/native-benchmark.json', 'training_check':'artifacts/training-check.json',
|
| 32 |
+
'boundary':'artifacts/boundary-proof.json'}
|
| 33 |
+
a = {k:load(root,p) for k,p in paths.items()}
|
| 34 |
+
history_path = root/'artifacts/training/history.jsonl'
|
| 35 |
+
history = [json.loads(line) for line in history_path.read_text().splitlines() if line.strip()] if history_path.exists() else []
|
| 36 |
+
if not history and a['training']:
|
| 37 |
+
history = a['training'].get('history', [])
|
| 38 |
+
evidence = lambda key: f'[{paths[key]}]({paths[key]})'
|
| 39 |
+
lines = ['# DataFog PII prototype evaluation', '',
|
| 40 |
+
'This is an evaluated prototype, not a production-readiness claim. All numbers below come from saved artifacts; absent evidence is explicitly marked unavailable.', '']
|
| 41 |
+
required = ('test','fixtures','regex','model_shared','calibration','training','data','parity','benchmark')
|
| 42 |
+
missing = [paths[k] for k in required if a[k] is None]
|
| 43 |
+
lines += [('Required evaluation artifacts are present.' if not missing else '**Incomplete evidence:** '+', '.join(missing)), '']
|
| 44 |
+
criteria = []
|
| 45 |
+
def criterion(title, passed, keys, detail):
|
| 46 |
+
criteria.append([title, 'Evidence present' if passed else 'Unresolved / missing evidence', ', '.join(evidence(k) for k in keys), detail])
|
| 47 |
+
criterion('Pinned data and disjoint document families', bool(a['data'] and a['data'].get('pairwise_group_disjoint')), ['data'], 'Structural validation and split rules are saved; annotation semantics remain imperfect.')
|
| 48 |
+
criterion('Trained checkpoint and resumability', bool(a['training'] and a['training_check']), ['training','training_check'], 'Completion plus separate optimizer/RNG resume diagnostic; inspect reported MPS parameter differences.')
|
| 49 |
+
p=a['parity'] or {}
|
| 50 |
+
criterion('Native Rust and Python parity', all(p.get(k) for k in ('token_ids_masks_global_byte_offsets_parity','findings_redaction_parity','malformed_request_explicit_error_and_recovery')), ['parity'], 'Checks tokenizer/global byte offsets, outputs/redaction, and explicit error recovery; parity does not prove quality.')
|
| 51 |
+
criterion('Complete-input boundary behavior', bool(a['boundary']), ['boundary'], 'Manual global-offset window construction; finite fixture evidence and representability limits apply.')
|
| 52 |
+
criterion('Independent calibration and experimental threshold', bool(a['calibration'] and a['test']), ['calibration','test'], 'Fit/select partitions are separate; final reliability is reported on held-out predicted spans.')
|
| 53 |
+
criterion('Held-out quality and support mechanics', bool(a['test'] and a['fixtures']), ['test','fixtures'], 'Synthetic population only; no reviewed private tickets.')
|
| 54 |
+
criterion('Existing regex comparison', bool(a['regex'] and a['model_shared']), ['regex','model_shared'], 'Official published DataFog 4.8.1; email and phone only.')
|
| 55 |
+
criterion('Native resource measurements', bool(a['benchmark']), ['benchmark'], 'Local observed load, warm request latency, memory, and bundle size; no performance target assumed.')
|
| 56 |
+
lines += [table(['Contract criterion','Status','Evidence','Interpretation / limitation'],criteria),'']
|
| 57 |
+
data=a['data'] or {}
|
| 58 |
+
lines += ['## Data and execution', '', f"Dataset: `{data.get('source','Unavailable')}` revision `{data.get('revision','Unavailable')}`. License: {data.get('license','Unavailable')}. {data.get('attribution','')}", '']
|
| 59 |
+
if data.get('splits'):
|
| 60 |
+
lines += [table(['Partition','Documents','Target-clean documents'],[(k,v.get('documents'),v.get('negative_documents')) for k,v in data['splits'].items()]),'']
|
| 61 |
+
if data.get('exclusions'):
|
| 62 |
+
lines += ['Structural exclusions: '+', '.join(f'{k}: {v:,}' for k,v in data['exclusions'].items())+'.','']
|
| 63 |
+
if history:
|
| 64 |
+
lines += [table(['Epoch','Train mean batch loss','Validation token loss','Selected at epoch','Elapsed seconds'],
|
| 65 |
+
[(r.get('epoch'),number(r.get('train_mean_batch_loss')),number(r.get('validation_token_loss')),r.get('selected'),number(r.get('elapsed_seconds'),1)) for r in history]),'']
|
| 66 |
+
else:
|
| 67 |
+
lines += ['Training history unavailable.','']
|
| 68 |
+
if a['training']:
|
| 69 |
+
lines += [f"Training elapsed: {number(a['training'].get('elapsed_seconds'),1)} seconds. Best validation token loss: {number(a['training'].get('best_validation_loss'))}.",'']
|
| 70 |
+
lines += ['Corrective iterations: two of the three-round fuse used. Round 1 fixed tokenizer overflow by manually constructing complete-input windows; round 2 kept the ONNX export wrapper and Python reference in evaluation mode. Neither correction tuned against final test outcomes. Further corrective work must remain within the agreed fuse.','']
|
| 71 |
+
test=a['test']
|
| 72 |
+
lines += ['## Held-out detection', '']
|
| 73 |
+
if test:
|
| 74 |
+
metrics=[('overall',test.get('overall',{})), *test.get('per_label',{}).items()]
|
| 75 |
+
lines += [table(['Label','Gold entities','Predicted','TP','FP','FN','Precision','Recall','F1'],
|
| 76 |
+
[(label,m.get('support'),m.get('predicted'),m.get('tp'),m.get('fp'),m.get('fn'),number(m.get('precision')),number(m.get('recall')),number(m.get('f1'))) for label,m in metrics]),'']
|
| 77 |
+
cov=test.get('redaction_byte_coverage',{})
|
| 78 |
+
lines += [f"UTF-8 byte-union redaction precision {number(cov.get('precision'))}, recall {number(cov.get('recall'))}, F1 {number(cov.get('f1'))}. Missed gold bytes: {number(cov.get('fn'),0)}; unnecessary redaction bytes: {number(cov.get('fp'),0)}. Coverage ignores labels and weights multibyte characters by byte length.",'']
|
| 79 |
+
docs=test.get('documents',{})
|
| 80 |
+
lines += [table(['Document measure','Count'],[(k,number(v,0)) for k,v in docs.items()]),'',
|
| 81 |
+
'Document false negatives mean no accepted span on a document containing target PII; partial misses are counted separately in `with_any_missed_entity`. False positives mean at least one accepted span on a target-clean document.','']
|
| 82 |
+
else:
|
| 83 |
+
lines += ['Held-out detection results unavailable.','']
|
| 84 |
+
lines += ['## Confidence and operating point','']
|
| 85 |
+
c=a['calibration']
|
| 86 |
+
if c:
|
| 87 |
+
lines += [f"Method: {c.get('method','Unavailable')}, pooled across labels. Experimental acceptance threshold: {number(c.get('threshold'),8)}. Objective: {c.get('threshold_objective','Unavailable')}.",'',
|
| 88 |
+
f"Calibration fit support: {number(c.get('fit_documents'),0)} documents, {number(c.get('fit_predicted_spans'),0)} predicted spans, {number(c.get('fit_correct_spans'),0)} exact matches. Selection support: {number(c.get('select_documents'),0)} documents.",'']
|
| 89 |
+
lines += [f"Calibration warning: {warning}" for warning in c.get('warnings',[])]
|
| 90 |
+
else:
|
| 91 |
+
lines += ['Calibration artifact unavailable.','']
|
| 92 |
+
if test:
|
| 93 |
+
raw=test.get('raw_reliability',{}); calibrated=test.get('confidence_reliability',{})
|
| 94 |
+
lines += [table(['Score','Predicted-span support','Brier','ECE'],[(name,r.get('support'),number(r.get('brier'),6),number(r.get('ece'),6)) for name,r in [('Raw token-derived span score',raw),('Calibrated exact-span score',calibrated)]]),'']
|
| 95 |
+
rows=[]
|
| 96 |
+
for i in range(10):
|
| 97 |
+
rb=raw.get('bins',[])[i] if len(raw.get('bins',[]))>i else {}
|
| 98 |
+
cb=calibrated.get('bins',[])[i] if len(calibrated.get('bins',[]))>i else {}
|
| 99 |
+
rows.append([f'[{i/10:.1f}, {(i+1)/10:.1f}'+(']' if i==9 else ')'),rb.get('support','Unavailable'),number(rb.get('mean_score')),number(rb.get('accuracy')),cb.get('support','Unavailable'),number(cb.get('mean_score')),number(cb.get('accuracy'))])
|
| 100 |
+
lines += [table(['Confidence bin','Raw n','Raw mean','Raw accuracy','Calibrated n','Calibrated mean','Calibrated accuracy'],rows),'']
|
| 101 |
+
lines += ['Reliability uses all predicted spans before thresholding and exact label/boundary correctness. Missed entities are absent from this table: recall must be read separately. Ten equal-width bins make ECE bin-dependent. Calibration is conditional on this synthetic evaluation population and annotation quality; the threshold is not a production default.','']
|
| 102 |
+
lines += ['## Existing regex comparison','']
|
| 103 |
+
if a['regex'] and a['model_shared']:
|
| 104 |
+
rows=[]
|
| 105 |
+
for name,key in [('DataFog 4.8.1 regex','regex'),('Trained model, experimental threshold','model_shared')]:
|
| 106 |
+
for label in ['overall','email','phone_number']:
|
| 107 |
+
m=a[key].get('overall',{}) if label=='overall' else a[key].get('per_label',{}).get(label,{})
|
| 108 |
+
rows.append([name,label,m.get('support'),number(m.get('precision')),number(m.get('recall')),number(m.get('f1'))])
|
| 109 |
+
lines += [table(['System','Scope','Gold entities','Precision','Recall','F1'],rows),'']
|
| 110 |
+
else:
|
| 111 |
+
lines += ['Complete same-population, same-label comparison unavailable.','']
|
| 112 |
+
lines += ['Only email and phone are supported by this existing regex baseline within the seven-label contract. First name, last name, street address, customer ID, and account number are excluded from both sides of this head-to-head comparison. All documents remain in the comparison. Constant regex match indicators are not calibrated probabilities. See [baseline provenance](docs/regex-baseline.md).','']
|
| 113 |
+
lines += ['## Synthetic support fixtures','']
|
| 114 |
+
if a['fixtures']:
|
| 115 |
+
f=a['fixtures']; m=f.get('overall',{})
|
| 116 |
+
lines += [f"{number(f.get('documents',{}).get('total'),0)} fixture documents; exact-span precision {number(m.get('precision'))}, recall {number(m.get('recall'))}, F1 {number(m.get('f1'))}. Gold entity support: {number(m.get('support'),0)}.",'']
|
| 117 |
+
else:
|
| 118 |
+
lines += ['Fixture predictions unavailable.','']
|
| 119 |
+
lines += ['These hand-authored cases include Unicode signatures, pasted fields, multiline addresses, repeated entities, clean numeric text, and long inputs. They exercise mechanics and expose case-specific failures; they do not establish real customer-support quality.','']
|
| 120 |
+
lines += ['## Native resource measurements','']
|
| 121 |
+
b=a['benchmark']
|
| 122 |
+
if b:
|
| 123 |
+
lines += [table(['Measurement','Observed value'],[(k,number(b.get(k),3 if k=='cold_load_seconds' else 0)) for k in ('bundle_bytes','cold_load_seconds','peak_rss_bytes')]),'']
|
| 124 |
+
warm=b.get('warm',b.get('requests',b.get('warm_requests',[])))
|
| 125 |
+
if isinstance(warm,dict): warm=warm.get('requests',[])
|
| 126 |
+
if isinstance(warm,list) and warm:
|
| 127 |
+
lines += [table(['Input UTF-8 bytes','Warm end-to-end seconds'],[(r.get('bytes','Unavailable'),number(r.get('latency_seconds'),6)) for r in warm]),'']
|
| 128 |
+
if b.get('note'): lines += [str(b['note']),'']
|
| 129 |
+
else:
|
| 130 |
+
lines += ['Native resource measurements unavailable.','']
|
| 131 |
+
if a['parity']:
|
| 132 |
+
lines += [f"Maximum absolute Python/Rust logit difference on parity cases: {number(p.get('max_abs_logit_difference'),8)}. Token IDs, masks and global byte offsets agree: {p.get('token_ids_masks_global_byte_offsets_parity','Unavailable')}; findings/redaction agree: {p.get('findings_redaction_parity','Unavailable')}.",'']
|
| 133 |
+
unrep=data.get('tokenizer',{}).get('unrepresentable_boundaries_by_label',{})
|
| 134 |
+
lines += ['## Remaining limitations','',
|
| 135 |
+
'- Nemotron is broad synthetic data with semantic annotation errors. Structural filtering cannot prove that labels are semantically correct or complete.',
|
| 136 |
+
'- Split allocation uses documented label-support heuristics and grouped families. It does not guarantee statistical precision or eliminate every possible near-duplicate.',
|
| 137 |
+
'- No private customer tickets were supplied or uploaded; real-world English customer-support generalization remains unproven.',
|
| 138 |
+
'- Token boundaries cannot represent every character boundary. Observed training-source unrepresentable boundaries: '+(', '.join(f'{k}={v}' for k,v in unrep.items()) if unrep else 'unavailable')+'.',
|
| 139 |
+
'- Native inference requires the packaged runtime and pinned tokenizer/configuration. A successful fixture parity check is not a universal numerical guarantee.',
|
| 140 |
+
'- `contains_pii` means at least one accepted finding among the seven selected labels; it is not an independently calibrated probability or assurance that a document contains no other sensitive information.',
|
| 141 |
+
'- No existing DataFog checkout was modified, no Git repository was created, and no model was published as part of this standalone prototype.', '',
|
| 142 |
+
'Reproduction commands and artifact locations are in [README.md](README.md).']
|
| 143 |
+
return '\n'.join(lines)+'\n'
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
if __name__ == '__main__':
|
| 147 |
+
parser=argparse.ArgumentParser(description=__doc__)
|
| 148 |
+
parser.add_argument('--root',type=Path,default=Path(__file__).resolve().parents[1])
|
| 149 |
+
parser.add_argument('--output',type=Path)
|
| 150 |
+
args=parser.parse_args()
|
| 151 |
+
output=args.output or args.root/'REPORT.md'
|
| 152 |
+
output.parent.mkdir(parents=True,exist_ok=True)
|
| 153 |
+
output.write_text(render(args.root))
|
| 154 |
+
print(output)
|
reproduction/scripts/run_pipeline.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Persistent sequential prototype run; records failures without hiding them."""
|
| 2 |
+
import datetime
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
import subprocess
|
| 6 |
+
import sys
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
ROOT=Path(__file__).resolve().parents[1]
|
| 10 |
+
PY=str(ROOT/".venv/bin/python")
|
| 11 |
+
STATE=ROOT/"artifacts/pipeline"
|
| 12 |
+
|
| 13 |
+
def now():return datetime.datetime.now(datetime.timezone.utc).isoformat()
|
| 14 |
+
|
| 15 |
+
def digest(path):
|
| 16 |
+
h=hashlib.sha256()
|
| 17 |
+
with path.open("rb") as f:
|
| 18 |
+
for block in iter(lambda:f.read(1024*1024),b""):h.update(block)
|
| 19 |
+
return h.hexdigest()
|
| 20 |
+
|
| 21 |
+
def stage(name,command):
|
| 22 |
+
marker=STATE/f"{name}.done.json"
|
| 23 |
+
if marker.exists():return
|
| 24 |
+
event={"stage":name,"status":"running","started_at":now(),"command":command}
|
| 25 |
+
(STATE/"status.json").write_text(json.dumps(event,indent=2)+"\n")
|
| 26 |
+
print(json.dumps(event),flush=True)
|
| 27 |
+
with (STATE/f"{name}.log").open("a") as log:
|
| 28 |
+
completed=subprocess.run(command,cwd=ROOT,stdout=log,stderr=subprocess.STDOUT)
|
| 29 |
+
event.update(finished_at=now(),returncode=completed.returncode,status="complete" if completed.returncode==0 else "failed")
|
| 30 |
+
(STATE/"status.json").write_text(json.dumps(event,indent=2)+"\n")
|
| 31 |
+
if completed.returncode:raise RuntimeError(f"{name} failed; inspect {STATE/name}.log")
|
| 32 |
+
marker.write_text(json.dumps(event,indent=2)+"\n")
|
| 33 |
+
|
| 34 |
+
def main():
|
| 35 |
+
STATE.mkdir(parents=True,exist_ok=True)
|
| 36 |
+
training_command=[PY,"scripts/train.py","--stride","42"]
|
| 37 |
+
checkpoints=sorted((ROOT/"artifacts/training").glob("epoch-*/state.pt"),key=lambda p:int(p.parent.name.split("-")[-1]))
|
| 38 |
+
if checkpoints:training_command.extend(["--resume",str(checkpoints[-1].parent)])
|
| 39 |
+
stage("train",training_command)
|
| 40 |
+
stage("export",[PY,"scripts/export_model.py"])
|
| 41 |
+
for split in ("calibration_fit","calibration_select"):
|
| 42 |
+
stage("predict-"+split,[PY,"scripts/predict.py","--input",f"data/prepared/{split}.jsonl","--output",f"artifacts/predictions/{split}.jsonl"])
|
| 43 |
+
stage("calibrate",[PY,"scripts/calibrate.py","--fit-gold","data/prepared/calibration_fit.jsonl","--fit-predictions","artifacts/predictions/calibration_fit.jsonl","--select-gold","data/prepared/calibration_select.jsonl","--select-predictions","artifacts/predictions/calibration_select.jsonl","--output","artifacts/bundle/calibration.json"])
|
| 44 |
+
bundle=ROOT/"artifacts/bundle"
|
| 45 |
+
config=json.loads((bundle/"config.json").read_text())
|
| 46 |
+
config.update(calibration_file="calibration.json",status="experimental prototype; demonstration threshold only")
|
| 47 |
+
(bundle/"config.json").write_text(json.dumps(config,indent=2)+"\n")
|
| 48 |
+
stage("final-parity",[PY,"scripts/check_rust_parity.py","artifacts/bundle","--output","artifacts/bundle/rust-parity.json"])
|
| 49 |
+
freeze=STATE/"evaluation-freeze.json"
|
| 50 |
+
files=[p for p in bundle.rglob("*") if p.is_file()]+[ROOT/"pii/core.py",ROOT/"pii/evaluation.py",ROOT/"scripts/predict.py",ROOT/"rust/target/release/datafog-pii"]
|
| 51 |
+
hashes={str(p.relative_to(ROOT)):digest(p) for p in files}
|
| 52 |
+
if freeze.exists():
|
| 53 |
+
original=json.loads(freeze.read_text())
|
| 54 |
+
if any(hashes.get(path)!=value for path,value in original["sha256"].items()):raise RuntimeError("Frozen evaluation inputs changed; owner-visible diagnostic review required")
|
| 55 |
+
else:freeze.write_text(json.dumps({"frozen_at":now(),"sha256":hashes,"policy":"No tuning against test results"},indent=2)+"\n")
|
| 56 |
+
for split,path in (("support-fixtures","data/support-fixtures.jsonl"),("test","data/prepared/test.jsonl")):
|
| 57 |
+
prediction=f"artifacts/predictions/{split}.jsonl"
|
| 58 |
+
stage("predict-"+split,[PY,"scripts/predict.py","--input",path,"--output",prediction])
|
| 59 |
+
stage("evaluate-"+split,[PY,"scripts/evaluate.py","--gold",path,"--predictions",prediction,"--calibration","artifacts/bundle/calibration.json","--output",f"artifacts/evaluation/{split}.json"])
|
| 60 |
+
stage("regex-test",[str(ROOT/"work/regex-env/bin/python"),"scripts/regex_baseline.py","--gold","data/prepared/test.jsonl","--output-dir","artifacts/regex-test","--model-predictions","artifacts/predictions/test.jsonl","--model-calibration","artifacts/bundle/calibration.json"])
|
| 61 |
+
stage("benchmark",[PY,"scripts/benchmark_native.py"])
|
| 62 |
+
stage("report",[PY,"scripts/report.py"])
|
| 63 |
+
(STATE/"status.json").write_text(json.dumps({"status":"complete","finished_at":now(),"next":"Review evidence and final report against contract"},indent=2)+"\n")
|
| 64 |
+
|
| 65 |
+
if __name__=="__main__":main()
|
reproduction/scripts/train.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Full supervised training using the pinned HF token-classification recipe."""
|
| 2 |
+
import argparse
|
| 3 |
+
import json
|
| 4 |
+
import math
|
| 5 |
+
import random
|
| 6 |
+
import sys
|
| 7 |
+
import time
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
| 10 |
+
import numpy as np
|
| 11 |
+
import torch
|
| 12 |
+
from transformers import AutoModelForTokenClassification, AutoTokenizer, DataCollatorForTokenClassification
|
| 13 |
+
from pii.core import BIO, MODEL_REVISION, load_jsonl, encode_windows
|
| 14 |
+
|
| 15 |
+
def prepare(records, tokenizer, max_length, stride):
|
| 16 |
+
chunks=[]
|
| 17 |
+
stats={"documents":len(records),"chunks":0,"tokens":0,"ignored_boundary_tokens":0,"partial_window_entity_tokens":0}
|
| 18 |
+
for record in records:
|
| 19 |
+
encoded=encode_windows(tokenizer,record["text"],max_length,stride)
|
| 20 |
+
for ids, mask, offsets in zip(encoded["input_ids"],encoded["attention_mask"],encoded["offset_mapping"]):
|
| 21 |
+
content=[(a,b) for a,b in offsets if a!=b]
|
| 22 |
+
left,right=content[0][0] if content else 0,content[-1][1] if content else 0
|
| 23 |
+
labels=[]
|
| 24 |
+
for a,b in offsets:
|
| 25 |
+
label=0
|
| 26 |
+
if a==b:
|
| 27 |
+
labels.append(-100)
|
| 28 |
+
continue
|
| 29 |
+
for span in record["spans"]:
|
| 30 |
+
s,e=span["start"],span["end"]
|
| 31 |
+
if a<e and s<b:
|
| 32 |
+
if not (s<=a and b<=e):
|
| 33 |
+
label=-100
|
| 34 |
+
stats["ignored_boundary_tokens"]+=1
|
| 35 |
+
elif s<left or e>right:
|
| 36 |
+
label=-100
|
| 37 |
+
stats["partial_window_entity_tokens"]+=1
|
| 38 |
+
else:
|
| 39 |
+
# First representable token of the entity gets B.
|
| 40 |
+
earlier=any(s<=aa<a and bb<=e and aa!=bb for aa,bb in offsets)
|
| 41 |
+
label=BIO.index(("I-" if earlier else "B-")+span["label"])
|
| 42 |
+
break
|
| 43 |
+
labels.append(label)
|
| 44 |
+
chunks.append({"input_ids":ids,"attention_mask":mask,"labels":labels})
|
| 45 |
+
stats["tokens"]+=len(ids)
|
| 46 |
+
stats["chunks"]=len(chunks)
|
| 47 |
+
return chunks,stats
|
| 48 |
+
|
| 49 |
+
def main():
|
| 50 |
+
p=argparse.ArgumentParser()
|
| 51 |
+
p.add_argument("--stride",type=int,required=True)
|
| 52 |
+
p.add_argument("--device",default="mps")
|
| 53 |
+
p.add_argument("--resume",type=Path)
|
| 54 |
+
p.add_argument("--prepare-only",action="store_true")
|
| 55 |
+
args=p.parse_args()
|
| 56 |
+
out=Path("artifacts/training");out.mkdir(parents=True,exist_ok=True)
|
| 57 |
+
seed=42
|
| 58 |
+
random.seed(seed);np.random.seed(seed);torch.manual_seed(seed)
|
| 59 |
+
tokenizer=AutoTokenizer.from_pretrained("artifacts/backbone",local_files_only=True)
|
| 60 |
+
config={"seed":seed,"batch_size":16,"epochs":2,"learning_rate":2e-5,"weight_decay":0.01,"max_length":512,"stride":args.stride,"device":args.device,"precision":"float32","model_revision":MODEL_REVISION,"recipe":"https://huggingface.co/docs/transformers/tasks/token_classification","stopping_rule":"Two epochs from official recipe; select lowest validation token loss. This is a prototype recipe hypothesis, not an optimum.","checkpoint_cadence":"Each epoch per official recipe; measured probe checkpoint cost 0.733s.","selection_metric":"validation token cross entropy"}
|
| 61 |
+
cache=out/f"chunks-manual-stride-{args.stride}.pt"
|
| 62 |
+
if cache.exists():
|
| 63 |
+
prepared=torch.load(cache,weights_only=False)
|
| 64 |
+
else:
|
| 65 |
+
prepared={}
|
| 66 |
+
for name in ("train","validation"):
|
| 67 |
+
prepared[name],prepared[name+"_stats"]=prepare(load_jsonl(f"data/prepared/{name}.jsonl"),tokenizer,512,args.stride)
|
| 68 |
+
print(json.dumps({name:prepared[name+"_stats"]}),flush=True)
|
| 69 |
+
torch.save(prepared,cache)
|
| 70 |
+
config["data_stats"]={name:prepared[name+"_stats"] for name in ("train","validation")}
|
| 71 |
+
config["estimated_training_seconds_max_length_probe"]=math.ceil(len(prepared["train"])/16)*2*0.558059167
|
| 72 |
+
(out/"run-config.json").write_text(json.dumps(config,indent=2)+"\n")
|
| 73 |
+
if args.prepare_only:
|
| 74 |
+
print(json.dumps(config,indent=2),flush=True);return
|
| 75 |
+
if config["estimated_training_seconds_max_length_probe"]>24*3600:
|
| 76 |
+
raise RuntimeError("Estimated run exceeds user-approved 24h limit; owner decision required")
|
| 77 |
+
model=AutoModelForTokenClassification.from_pretrained(args.resume / "model" if args.resume else "artifacts/backbone",num_labels=len(BIO),id2label=dict(enumerate(BIO)),label2id={v:k for k,v in enumerate(BIO)},local_files_only=True,attn_implementation="eager").to(args.device)
|
| 78 |
+
optimizer=torch.optim.AdamW(model.parameters(),lr=config["learning_rate"],weight_decay=config["weight_decay"])
|
| 79 |
+
steps_per_epoch=math.ceil(len(prepared["train"])/16)
|
| 80 |
+
scheduler=torch.optim.lr_scheduler.LambdaLR(optimizer,lambda step:max(0.0,1-step/(steps_per_epoch*config["epochs"])))
|
| 81 |
+
collate=DataCollatorForTokenClassification(tokenizer,padding=True,return_tensors="pt")
|
| 82 |
+
start_epoch,best_loss,global_step=0,float("inf"),0
|
| 83 |
+
if args.resume:
|
| 84 |
+
state=torch.load(args.resume/"state.pt",map_location="cpu",weights_only=False)
|
| 85 |
+
optimizer.load_state_dict(state["optimizer"]);scheduler.load_state_dict(state["scheduler"])
|
| 86 |
+
random.setstate(state["random"]);np.random.set_state(state["numpy"]);torch.set_rng_state(state["torch"])
|
| 87 |
+
if args.device=="mps":torch.mps.set_rng_state(state["mps"])
|
| 88 |
+
start_epoch,best_loss,global_step=state["epoch"],state["best_loss"],state["step"]
|
| 89 |
+
history=[]
|
| 90 |
+
started=time.perf_counter()
|
| 91 |
+
for epoch in range(start_epoch,config["epochs"]):
|
| 92 |
+
model.train()
|
| 93 |
+
order=list(range(len(prepared["train"])));random.shuffle(order)
|
| 94 |
+
running=0.0
|
| 95 |
+
for step,begin in enumerate(range(0,len(order),16)):
|
| 96 |
+
batch=collate([prepared["train"][i] for i in order[begin:begin+16]])
|
| 97 |
+
batch={k:v.to(args.device) for k,v in batch.items()}
|
| 98 |
+
optimizer.zero_grad(set_to_none=True)
|
| 99 |
+
loss=model(**batch).loss
|
| 100 |
+
if not torch.isfinite(loss):raise RuntimeError("nonfinite loss")
|
| 101 |
+
loss.backward()
|
| 102 |
+
torch.nn.utils.clip_grad_norm_(model.parameters(),1.0,error_if_nonfinite=True) # Trainer default.
|
| 103 |
+
optimizer.step();scheduler.step()
|
| 104 |
+
running+=loss.item();global_step+=1
|
| 105 |
+
# Persist current progress every batch; no logging interval assumption.
|
| 106 |
+
(out/"progress.json").write_text(json.dumps({"epoch":epoch+1,"step_in_epoch":step+1,"steps_per_epoch":steps_per_epoch,"global_step":global_step,"loss":loss.item(),"elapsed_seconds":time.perf_counter()-started})+"\n")
|
| 107 |
+
model.eval();total_loss,total_tokens=0.0,0
|
| 108 |
+
with torch.no_grad():
|
| 109 |
+
for begin in range(0,len(prepared["validation"]),16):
|
| 110 |
+
batch=collate(prepared["validation"][begin:begin+16]);batch={k:v.to(args.device) for k,v in batch.items()}
|
| 111 |
+
count=(batch["labels"]!=-100).sum().item()
|
| 112 |
+
total_loss+=model(**batch).loss.item()*count;total_tokens+=count
|
| 113 |
+
val_loss=total_loss/total_tokens
|
| 114 |
+
selected=val_loss<best_loss
|
| 115 |
+
if selected:
|
| 116 |
+
best_loss=val_loss
|
| 117 |
+
model.save_pretrained(out/"best");tokenizer.save_pretrained(out/"best")
|
| 118 |
+
checkpoint=out/f"epoch-{epoch+1}"
|
| 119 |
+
model.save_pretrained(checkpoint/"model")
|
| 120 |
+
torch.save({"optimizer":optimizer.state_dict(),"scheduler":scheduler.state_dict(),"random":random.getstate(),"numpy":np.random.get_state(),"torch":torch.get_rng_state(),"mps":torch.mps.get_rng_state() if args.device=="mps" else None,"epoch":epoch+1,"best_loss":best_loss,"step":global_step},checkpoint/"state.pt")
|
| 121 |
+
event={"epoch":epoch+1,"train_mean_batch_loss":running/steps_per_epoch,"validation_token_loss":val_loss,"selected":selected,"elapsed_seconds":time.perf_counter()-started}
|
| 122 |
+
history.append(event)
|
| 123 |
+
with (out/"history.jsonl").open("a") as handle:handle.write(json.dumps(event)+"\n")
|
| 124 |
+
print(json.dumps(event),flush=True)
|
| 125 |
+
(out/"complete.json").write_text(json.dumps({"history":history,"best_validation_loss":best_loss,"elapsed_seconds":time.perf_counter()-started},indent=2)+"\n")
|
| 126 |
+
|
| 127 |
+
if __name__=="__main__":main()
|
reproduction/tests/test_data.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unittest
|
| 2 |
+
from pii.data import validate, family_key
|
| 3 |
+
|
| 4 |
+
class DataValidation(unittest.TestCase):
|
| 5 |
+
def test_python_literal_and_unicode(self):
|
| 6 |
+
text='😊 José'
|
| 7 |
+
spans,error=validate({'text':text,'spans':"[{'start':2,'end':6,'text':'José','label':'first_name'}]"})
|
| 8 |
+
self.assertIsNone(error); self.assertEqual(text[spans[0]['start']:spans[0]['end']],'José')
|
| 9 |
+
def test_invalid_offset_excluded(self):
|
| 10 |
+
self.assertEqual(validate({'text':'José','spans':"[{'start':0,'end':3,'text':'José','label':'first_name'}]"})[1],'source_slice_mismatch')
|
| 11 |
+
def test_overlap_excluded(self):
|
| 12 |
+
spans=[{'start':0,'end':4,'text':'John','label':'first_name'},{'start':0,'end':4,'text':'John','label':'last_name'}]
|
| 13 |
+
self.assertEqual(validate({'text':'John','spans':repr(spans)})[1],'duplicate_or_overlap')
|
| 14 |
+
def test_no_eval(self):
|
| 15 |
+
self.assertEqual(validate({'text':'x','spans':"__import__('os').system('false')"})[1],'parse')
|
| 16 |
+
def test_family_hides_values(self):
|
| 17 |
+
a=[{'start':6,'end':10,'label':'first_name'}]; b=[{'start':6,'end':10,'label':'first_name'}]
|
| 18 |
+
self.assertEqual(family_key('Name: John',a),family_key('Name: Jane',b))
|
| 19 |
+
self.assertNotEqual(family_key('Name: John',a),family_key('User: John',a))
|
| 20 |
+
if __name__=='__main__':unittest.main()
|
reproduction/tests/test_evaluation.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import unittest
|
| 2 |
+
from pii.evaluation import evaluate, apply_calibration, fit_calibration, read_jsonl
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def pred(gold, score=.8):
|
| 6 |
+
return {'id': gold['id'], 'text': gold['text'], 'findings': [dict(label=s['label'], start=len(gold['text'][:s['start']].encode()), end=len(gold['text'][:s['end']].encode()), raw_score=score, confidence=None) for s in gold['spans']]}
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class EvaluationTests(unittest.TestCase):
|
| 10 |
+
def test_unicode_and_fixtures(self):
|
| 11 |
+
gold = read_jsonl('data/support-fixtures.jsonl')
|
| 12 |
+
result = evaluate(gold, [pred(g) for g in gold])
|
| 13 |
+
self.assertEqual(result['overall']['f1'], 1)
|
| 14 |
+
self.assertEqual(result['redaction_byte_coverage']['f1'], 1)
|
| 15 |
+
self.assertGreater(result['documents']['clean_support'], 0)
|
| 16 |
+
self.assertEqual(result['documents']['false_positive'], 0)
|
| 17 |
+
self.assertEqual(result['documents']['false_negative'], 0)
|
| 18 |
+
|
| 19 |
+
def test_partial_wrong_label_and_clean_doc(self):
|
| 20 |
+
gold = [{'id': 'a', 'text': 'é Alice', 'spans': [{'label':'first_name', 'start':2, 'end':7}]}, {'id':'b', 'text':'version 123', 'spans':[]}]
|
| 21 |
+
predictions = [{'id':'a', 'text':'é Alice', 'findings':[{'label':'last_name', 'start':3, 'end':6, 'raw_score':.7}]}, {'id':'b', 'text':'version 123', 'findings':[{'label':'customer_id', 'start':8, 'end':11, 'raw_score':.9}]}]
|
| 22 |
+
result = evaluate(gold, predictions)
|
| 23 |
+
self.assertEqual(result['overall']['tp'], 0)
|
| 24 |
+
self.assertEqual(result['redaction_byte_coverage']['tp'], 3)
|
| 25 |
+
self.assertEqual(result['redaction_byte_coverage']['fn'], 2)
|
| 26 |
+
self.assertEqual(result['redaction_byte_coverage']['fp'], 3)
|
| 27 |
+
self.assertEqual(result['documents']['false_positive'], 1)
|
| 28 |
+
self.assertEqual(result['documents']['with_any_missed_entity'], 1)
|
| 29 |
+
self.assertEqual(result['documents']['false_negative'], 0)
|
| 30 |
+
self.assertAlmostEqual(result['raw_reliability']['brier'], .65)
|
| 31 |
+
self.assertEqual(evaluate(gold, predictions, threshold=.8)['documents']['false_negative'], 1)
|
| 32 |
+
|
| 33 |
+
def test_fail_closed_on_missing_and_bad_boundaries(self):
|
| 34 |
+
gold = [{'id':'a', 'text':'é', 'spans':[]}]
|
| 35 |
+
with self.assertRaises(ValueError):
|
| 36 |
+
evaluate(gold, [])
|
| 37 |
+
with self.assertRaises(ValueError):
|
| 38 |
+
evaluate(gold, [{'id':'a', 'text':'é', 'findings':[{'label':'first_name','start':1,'end':2,'raw_score':.5}]}])
|
| 39 |
+
|
| 40 |
+
def test_independent_calibration_and_interpolation(self):
|
| 41 |
+
fg = [{'id':'fit', 'text':'Alice Bob', 'spans':[{'label':'first_name','start':0,'end':5}]}]
|
| 42 |
+
fp = [{'id':'fit','text':'Alice Bob','findings':[{'label':'first_name','start':0,'end':5,'raw_score':.9}, {'label':'last_name','start':6,'end':9,'raw_score':.1}]}]
|
| 43 |
+
sg = [{'id':'select','text':'Carol Dave', 'spans':[{'label':'first_name','start':0,'end':5}]}]
|
| 44 |
+
sp = [{'id':'select','text':'Carol Dave','findings':[{'label':'first_name','start':0,'end':5,'raw_score':.8}, {'label':'last_name','start':6,'end':10,'raw_score':.2}]}]
|
| 45 |
+
artifact = fit_calibration(fg, fp, sg, sp)
|
| 46 |
+
self.assertEqual(artifact['select_operating_point']['f1'], 1)
|
| 47 |
+
self.assertAlmostEqual(apply_calibration(.5, artifact), .5)
|
| 48 |
+
self.assertEqual(apply_calibration(0, artifact), 0)
|
| 49 |
+
self.assertEqual(apply_calibration(1, artifact), 1)
|
| 50 |
+
with self.assertRaises(ValueError):
|
| 51 |
+
fit_calibration(fg, fp, fg, fp)
|
| 52 |
+
self.assertEqual(evaluate(sg, sp, calibration=artifact)['overall']['f1'], 1)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
if __name__ == '__main__':
|
| 56 |
+
unittest.main()
|
reproduction/uv.lock
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
runtime/Cargo.lock
ADDED
|
@@ -0,0 +1,877 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# This file is automatically @generated by Cargo.
|
| 2 |
+
# It is not intended for manual editing.
|
| 3 |
+
version = 4
|
| 4 |
+
|
| 5 |
+
[[package]]
|
| 6 |
+
name = "ahash"
|
| 7 |
+
version = "0.8.12"
|
| 8 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 9 |
+
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
|
| 10 |
+
dependencies = [
|
| 11 |
+
"cfg-if",
|
| 12 |
+
"getrandom",
|
| 13 |
+
"once_cell",
|
| 14 |
+
"serde",
|
| 15 |
+
"version_check",
|
| 16 |
+
"zerocopy",
|
| 17 |
+
]
|
| 18 |
+
|
| 19 |
+
[[package]]
|
| 20 |
+
name = "aho-corasick"
|
| 21 |
+
version = "1.1.5"
|
| 22 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 23 |
+
checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
|
| 24 |
+
dependencies = [
|
| 25 |
+
"memchr",
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
[[package]]
|
| 29 |
+
name = "anyhow"
|
| 30 |
+
version = "1.0.104"
|
| 31 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 32 |
+
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
|
| 33 |
+
|
| 34 |
+
[[package]]
|
| 35 |
+
name = "autocfg"
|
| 36 |
+
version = "1.5.1"
|
| 37 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 38 |
+
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
| 39 |
+
|
| 40 |
+
[[package]]
|
| 41 |
+
name = "base64"
|
| 42 |
+
version = "0.13.1"
|
| 43 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 44 |
+
checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
|
| 45 |
+
|
| 46 |
+
[[package]]
|
| 47 |
+
name = "bitflags"
|
| 48 |
+
version = "2.13.2"
|
| 49 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 50 |
+
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
| 51 |
+
|
| 52 |
+
[[package]]
|
| 53 |
+
name = "castaway"
|
| 54 |
+
version = "0.2.4"
|
| 55 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 56 |
+
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
| 57 |
+
dependencies = [
|
| 58 |
+
"rustversion",
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
[[package]]
|
| 62 |
+
name = "cc"
|
| 63 |
+
version = "1.5.1"
|
| 64 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 65 |
+
checksum = "f360145194ee8e21db5ee7f3fcd4fe52210864c75c985dae33218202c8bbe040"
|
| 66 |
+
dependencies = [
|
| 67 |
+
"find-msvc-tools",
|
| 68 |
+
"shlex",
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
[[package]]
|
| 72 |
+
name = "cfg-if"
|
| 73 |
+
version = "1.0.5"
|
| 74 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 75 |
+
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
| 76 |
+
|
| 77 |
+
[[package]]
|
| 78 |
+
name = "compact_str"
|
| 79 |
+
version = "0.9.1"
|
| 80 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 81 |
+
checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab"
|
| 82 |
+
dependencies = [
|
| 83 |
+
"castaway",
|
| 84 |
+
"cfg-if",
|
| 85 |
+
"itoa",
|
| 86 |
+
"rustversion",
|
| 87 |
+
"ryu",
|
| 88 |
+
"serde",
|
| 89 |
+
"static_assertions",
|
| 90 |
+
]
|
| 91 |
+
|
| 92 |
+
[[package]]
|
| 93 |
+
name = "crossbeam-deque"
|
| 94 |
+
version = "0.8.8"
|
| 95 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 96 |
+
checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a"
|
| 97 |
+
dependencies = [
|
| 98 |
+
"crossbeam-epoch",
|
| 99 |
+
"crossbeam-utils",
|
| 100 |
+
]
|
| 101 |
+
|
| 102 |
+
[[package]]
|
| 103 |
+
name = "crossbeam-epoch"
|
| 104 |
+
version = "0.9.21"
|
| 105 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 106 |
+
checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d"
|
| 107 |
+
dependencies = [
|
| 108 |
+
"crossbeam-utils",
|
| 109 |
+
]
|
| 110 |
+
|
| 111 |
+
[[package]]
|
| 112 |
+
name = "crossbeam-utils"
|
| 113 |
+
version = "0.8.23"
|
| 114 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 115 |
+
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
| 116 |
+
|
| 117 |
+
[[package]]
|
| 118 |
+
name = "daachorse"
|
| 119 |
+
version = "3.0.3"
|
| 120 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 121 |
+
checksum = "5614204febbc33cc07a2806aa6440b904ac012b68eecc37f4493ea4a76455a3d"
|
| 122 |
+
|
| 123 |
+
[[package]]
|
| 124 |
+
name = "darling"
|
| 125 |
+
version = "0.20.11"
|
| 126 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 127 |
+
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
| 128 |
+
dependencies = [
|
| 129 |
+
"darling_core",
|
| 130 |
+
"darling_macro",
|
| 131 |
+
]
|
| 132 |
+
|
| 133 |
+
[[package]]
|
| 134 |
+
name = "darling_core"
|
| 135 |
+
version = "0.20.11"
|
| 136 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 137 |
+
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
| 138 |
+
dependencies = [
|
| 139 |
+
"fnv",
|
| 140 |
+
"ident_case",
|
| 141 |
+
"proc-macro2",
|
| 142 |
+
"quote",
|
| 143 |
+
"strsim",
|
| 144 |
+
"syn 2.0.119",
|
| 145 |
+
]
|
| 146 |
+
|
| 147 |
+
[[package]]
|
| 148 |
+
name = "darling_macro"
|
| 149 |
+
version = "0.20.11"
|
| 150 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 151 |
+
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
| 152 |
+
dependencies = [
|
| 153 |
+
"darling_core",
|
| 154 |
+
"quote",
|
| 155 |
+
"syn 2.0.119",
|
| 156 |
+
]
|
| 157 |
+
|
| 158 |
+
[[package]]
|
| 159 |
+
name = "dary_heap"
|
| 160 |
+
version = "0.3.9"
|
| 161 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 162 |
+
checksum = "8b1e3a325bc115f096c8b77bbf027a7c2592230e70be2d985be950d3d5e60ebe"
|
| 163 |
+
dependencies = [
|
| 164 |
+
"serde",
|
| 165 |
+
]
|
| 166 |
+
|
| 167 |
+
[[package]]
|
| 168 |
+
name = "datafog-pii"
|
| 169 |
+
version = "0.1.0"
|
| 170 |
+
dependencies = [
|
| 171 |
+
"anyhow",
|
| 172 |
+
"ort",
|
| 173 |
+
"serde",
|
| 174 |
+
"serde_json",
|
| 175 |
+
"tokenizers",
|
| 176 |
+
]
|
| 177 |
+
|
| 178 |
+
[[package]]
|
| 179 |
+
name = "derive_builder"
|
| 180 |
+
version = "0.20.2"
|
| 181 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 182 |
+
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
| 183 |
+
dependencies = [
|
| 184 |
+
"derive_builder_macro",
|
| 185 |
+
]
|
| 186 |
+
|
| 187 |
+
[[package]]
|
| 188 |
+
name = "derive_builder_core"
|
| 189 |
+
version = "0.20.2"
|
| 190 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 191 |
+
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
| 192 |
+
dependencies = [
|
| 193 |
+
"darling",
|
| 194 |
+
"proc-macro2",
|
| 195 |
+
"quote",
|
| 196 |
+
"syn 2.0.119",
|
| 197 |
+
]
|
| 198 |
+
|
| 199 |
+
[[package]]
|
| 200 |
+
name = "derive_builder_macro"
|
| 201 |
+
version = "0.20.2"
|
| 202 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 203 |
+
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
| 204 |
+
dependencies = [
|
| 205 |
+
"derive_builder_core",
|
| 206 |
+
"syn 2.0.119",
|
| 207 |
+
]
|
| 208 |
+
|
| 209 |
+
[[package]]
|
| 210 |
+
name = "either"
|
| 211 |
+
version = "1.18.0"
|
| 212 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 213 |
+
checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
|
| 214 |
+
|
| 215 |
+
[[package]]
|
| 216 |
+
name = "esaxx-rs"
|
| 217 |
+
version = "0.1.10"
|
| 218 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 219 |
+
checksum = "d817e038c30374a4bcb22f94d0a8a0e216958d4c3dcde369b1439fec4bdda6e6"
|
| 220 |
+
|
| 221 |
+
[[package]]
|
| 222 |
+
name = "find-msvc-tools"
|
| 223 |
+
version = "0.1.14"
|
| 224 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 225 |
+
checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484"
|
| 226 |
+
|
| 227 |
+
[[package]]
|
| 228 |
+
name = "fnv"
|
| 229 |
+
version = "1.0.7"
|
| 230 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 231 |
+
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
| 232 |
+
|
| 233 |
+
[[package]]
|
| 234 |
+
name = "getrandom"
|
| 235 |
+
version = "0.3.4"
|
| 236 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 237 |
+
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
| 238 |
+
dependencies = [
|
| 239 |
+
"cfg-if",
|
| 240 |
+
"libc",
|
| 241 |
+
"r-efi",
|
| 242 |
+
"wasip2",
|
| 243 |
+
]
|
| 244 |
+
|
| 245 |
+
[[package]]
|
| 246 |
+
name = "ident_case"
|
| 247 |
+
version = "1.0.1"
|
| 248 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 249 |
+
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
| 250 |
+
|
| 251 |
+
[[package]]
|
| 252 |
+
name = "itertools"
|
| 253 |
+
version = "0.14.0"
|
| 254 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 255 |
+
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
| 256 |
+
dependencies = [
|
| 257 |
+
"either",
|
| 258 |
+
]
|
| 259 |
+
|
| 260 |
+
[[package]]
|
| 261 |
+
name = "itoa"
|
| 262 |
+
version = "1.0.18"
|
| 263 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 264 |
+
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
| 265 |
+
|
| 266 |
+
[[package]]
|
| 267 |
+
name = "libc"
|
| 268 |
+
version = "0.2.189"
|
| 269 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 270 |
+
checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
|
| 271 |
+
|
| 272 |
+
[[package]]
|
| 273 |
+
name = "libloading"
|
| 274 |
+
version = "0.9.0"
|
| 275 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 276 |
+
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
|
| 277 |
+
dependencies = [
|
| 278 |
+
"cfg-if",
|
| 279 |
+
"windows-link",
|
| 280 |
+
]
|
| 281 |
+
|
| 282 |
+
[[package]]
|
| 283 |
+
name = "log"
|
| 284 |
+
version = "0.4.34"
|
| 285 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 286 |
+
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
| 287 |
+
|
| 288 |
+
[[package]]
|
| 289 |
+
name = "macro_rules_attribute"
|
| 290 |
+
version = "0.2.3"
|
| 291 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 292 |
+
checksum = "b3ae8f6d608c795738406608304d30a2dfbdc8e58e44f7ba43236da5208ded3c"
|
| 293 |
+
dependencies = [
|
| 294 |
+
"macro_rules_attribute-proc_macro",
|
| 295 |
+
"pastey",
|
| 296 |
+
]
|
| 297 |
+
|
| 298 |
+
[[package]]
|
| 299 |
+
name = "macro_rules_attribute-proc_macro"
|
| 300 |
+
version = "0.2.3"
|
| 301 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 302 |
+
checksum = "fc04a4c58212d57930a24bf47d3fa87485264a3a054e9c10e042eb373573ad3c"
|
| 303 |
+
|
| 304 |
+
[[package]]
|
| 305 |
+
name = "matrixmultiply"
|
| 306 |
+
version = "0.3.11"
|
| 307 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 308 |
+
checksum = "3f607c237553f086e7043417a51df26b2eb899d3caff94e6a67592ff992fedc7"
|
| 309 |
+
dependencies = [
|
| 310 |
+
"autocfg",
|
| 311 |
+
"rawpointer",
|
| 312 |
+
]
|
| 313 |
+
|
| 314 |
+
[[package]]
|
| 315 |
+
name = "memchr"
|
| 316 |
+
version = "2.8.3"
|
| 317 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 318 |
+
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
| 319 |
+
|
| 320 |
+
[[package]]
|
| 321 |
+
name = "minimal-lexical"
|
| 322 |
+
version = "0.2.1"
|
| 323 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 324 |
+
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
| 325 |
+
|
| 326 |
+
[[package]]
|
| 327 |
+
name = "monostate"
|
| 328 |
+
version = "0.1.18"
|
| 329 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 330 |
+
checksum = "3341a273f6c9d5bef1908f17b7267bbab0e95c9bf69a0d4dcf8e9e1b2c76ef67"
|
| 331 |
+
dependencies = [
|
| 332 |
+
"monostate-impl",
|
| 333 |
+
"serde",
|
| 334 |
+
"serde_core",
|
| 335 |
+
]
|
| 336 |
+
|
| 337 |
+
[[package]]
|
| 338 |
+
name = "monostate-impl"
|
| 339 |
+
version = "0.1.18"
|
| 340 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 341 |
+
checksum = "e4db6d5580af57bf992f59068d4ea26fd518574ff48d7639b255a36f9de6e7e9"
|
| 342 |
+
dependencies = [
|
| 343 |
+
"proc-macro2",
|
| 344 |
+
"quote",
|
| 345 |
+
"syn 2.0.119",
|
| 346 |
+
]
|
| 347 |
+
|
| 348 |
+
[[package]]
|
| 349 |
+
name = "ndarray"
|
| 350 |
+
version = "0.17.2"
|
| 351 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 352 |
+
checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d"
|
| 353 |
+
dependencies = [
|
| 354 |
+
"matrixmultiply",
|
| 355 |
+
"num-complex",
|
| 356 |
+
"num-integer",
|
| 357 |
+
"num-traits",
|
| 358 |
+
"portable-atomic",
|
| 359 |
+
"portable-atomic-util",
|
| 360 |
+
"rawpointer",
|
| 361 |
+
]
|
| 362 |
+
|
| 363 |
+
[[package]]
|
| 364 |
+
name = "nom"
|
| 365 |
+
version = "7.1.3"
|
| 366 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 367 |
+
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
| 368 |
+
dependencies = [
|
| 369 |
+
"memchr",
|
| 370 |
+
"minimal-lexical",
|
| 371 |
+
]
|
| 372 |
+
|
| 373 |
+
[[package]]
|
| 374 |
+
name = "num-complex"
|
| 375 |
+
version = "0.4.6"
|
| 376 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 377 |
+
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
|
| 378 |
+
dependencies = [
|
| 379 |
+
"num-traits",
|
| 380 |
+
]
|
| 381 |
+
|
| 382 |
+
[[package]]
|
| 383 |
+
name = "num-integer"
|
| 384 |
+
version = "0.1.47"
|
| 385 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 386 |
+
checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
|
| 387 |
+
dependencies = [
|
| 388 |
+
"num-traits",
|
| 389 |
+
]
|
| 390 |
+
|
| 391 |
+
[[package]]
|
| 392 |
+
name = "num-traits"
|
| 393 |
+
version = "0.2.19"
|
| 394 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 395 |
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
| 396 |
+
dependencies = [
|
| 397 |
+
"autocfg",
|
| 398 |
+
]
|
| 399 |
+
|
| 400 |
+
[[package]]
|
| 401 |
+
name = "once_cell"
|
| 402 |
+
version = "1.21.4"
|
| 403 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 404 |
+
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
| 405 |
+
|
| 406 |
+
[[package]]
|
| 407 |
+
name = "onig"
|
| 408 |
+
version = "6.5.3"
|
| 409 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 410 |
+
checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2"
|
| 411 |
+
dependencies = [
|
| 412 |
+
"bitflags",
|
| 413 |
+
"libc",
|
| 414 |
+
"once_cell",
|
| 415 |
+
"onig_sys",
|
| 416 |
+
]
|
| 417 |
+
|
| 418 |
+
[[package]]
|
| 419 |
+
name = "onig_sys"
|
| 420 |
+
version = "69.9.3"
|
| 421 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 422 |
+
checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7"
|
| 423 |
+
dependencies = [
|
| 424 |
+
"cc",
|
| 425 |
+
"pkg-config",
|
| 426 |
+
]
|
| 427 |
+
|
| 428 |
+
[[package]]
|
| 429 |
+
name = "ort"
|
| 430 |
+
version = "2.0.0-rc.13"
|
| 431 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 432 |
+
checksum = "4336a1e2b38848325241c72889086886004e589b7c74f335e60a8e8db5138a0b"
|
| 433 |
+
dependencies = [
|
| 434 |
+
"libloading",
|
| 435 |
+
"ndarray",
|
| 436 |
+
"ort-sys",
|
| 437 |
+
"smallvec",
|
| 438 |
+
"tracing",
|
| 439 |
+
]
|
| 440 |
+
|
| 441 |
+
[[package]]
|
| 442 |
+
name = "ort-sys"
|
| 443 |
+
version = "2.0.0-rc.13"
|
| 444 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 445 |
+
checksum = "cf211e3776eea6aec988552fa118dd746d70e1b1e5e244058d1c98015f3e5872"
|
| 446 |
+
|
| 447 |
+
[[package]]
|
| 448 |
+
name = "paste"
|
| 449 |
+
version = "1.0.15"
|
| 450 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 451 |
+
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
| 452 |
+
|
| 453 |
+
[[package]]
|
| 454 |
+
name = "pastey"
|
| 455 |
+
version = "0.2.3"
|
| 456 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 457 |
+
checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4"
|
| 458 |
+
|
| 459 |
+
[[package]]
|
| 460 |
+
name = "pin-project-lite"
|
| 461 |
+
version = "0.2.17"
|
| 462 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 463 |
+
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
| 464 |
+
|
| 465 |
+
[[package]]
|
| 466 |
+
name = "pkg-config"
|
| 467 |
+
version = "0.3.34"
|
| 468 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 469 |
+
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
| 470 |
+
|
| 471 |
+
[[package]]
|
| 472 |
+
name = "portable-atomic"
|
| 473 |
+
version = "1.15.0"
|
| 474 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 475 |
+
checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
|
| 476 |
+
|
| 477 |
+
[[package]]
|
| 478 |
+
name = "portable-atomic-util"
|
| 479 |
+
version = "0.2.8"
|
| 480 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 481 |
+
checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715"
|
| 482 |
+
dependencies = [
|
| 483 |
+
"portable-atomic",
|
| 484 |
+
]
|
| 485 |
+
|
| 486 |
+
[[package]]
|
| 487 |
+
name = "ppv-lite86"
|
| 488 |
+
version = "0.2.21"
|
| 489 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 490 |
+
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
| 491 |
+
dependencies = [
|
| 492 |
+
"zerocopy",
|
| 493 |
+
]
|
| 494 |
+
|
| 495 |
+
[[package]]
|
| 496 |
+
name = "proc-macro2"
|
| 497 |
+
version = "1.0.107"
|
| 498 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 499 |
+
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
| 500 |
+
dependencies = [
|
| 501 |
+
"unicode-ident",
|
| 502 |
+
]
|
| 503 |
+
|
| 504 |
+
[[package]]
|
| 505 |
+
name = "quote"
|
| 506 |
+
version = "1.0.47"
|
| 507 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 508 |
+
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
| 509 |
+
dependencies = [
|
| 510 |
+
"proc-macro2",
|
| 511 |
+
]
|
| 512 |
+
|
| 513 |
+
[[package]]
|
| 514 |
+
name = "r-efi"
|
| 515 |
+
version = "5.3.0"
|
| 516 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 517 |
+
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
| 518 |
+
|
| 519 |
+
[[package]]
|
| 520 |
+
name = "rand"
|
| 521 |
+
version = "0.9.5"
|
| 522 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 523 |
+
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
|
| 524 |
+
dependencies = [
|
| 525 |
+
"rand_chacha",
|
| 526 |
+
"rand_core",
|
| 527 |
+
]
|
| 528 |
+
|
| 529 |
+
[[package]]
|
| 530 |
+
name = "rand_chacha"
|
| 531 |
+
version = "0.9.0"
|
| 532 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 533 |
+
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
| 534 |
+
dependencies = [
|
| 535 |
+
"ppv-lite86",
|
| 536 |
+
"rand_core",
|
| 537 |
+
]
|
| 538 |
+
|
| 539 |
+
[[package]]
|
| 540 |
+
name = "rand_core"
|
| 541 |
+
version = "0.9.5"
|
| 542 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 543 |
+
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
| 544 |
+
dependencies = [
|
| 545 |
+
"getrandom",
|
| 546 |
+
]
|
| 547 |
+
|
| 548 |
+
[[package]]
|
| 549 |
+
name = "rawpointer"
|
| 550 |
+
version = "0.2.1"
|
| 551 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 552 |
+
checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3"
|
| 553 |
+
|
| 554 |
+
[[package]]
|
| 555 |
+
name = "rayon"
|
| 556 |
+
version = "1.12.0"
|
| 557 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 558 |
+
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
|
| 559 |
+
dependencies = [
|
| 560 |
+
"either",
|
| 561 |
+
"rayon-core",
|
| 562 |
+
]
|
| 563 |
+
|
| 564 |
+
[[package]]
|
| 565 |
+
name = "rayon-cond"
|
| 566 |
+
version = "0.4.0"
|
| 567 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 568 |
+
checksum = "2964d0cf57a3e7a06e8183d14a8b527195c706b7983549cd5462d5aa3747438f"
|
| 569 |
+
dependencies = [
|
| 570 |
+
"either",
|
| 571 |
+
"itertools",
|
| 572 |
+
"rayon",
|
| 573 |
+
]
|
| 574 |
+
|
| 575 |
+
[[package]]
|
| 576 |
+
name = "rayon-core"
|
| 577 |
+
version = "1.13.0"
|
| 578 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 579 |
+
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
| 580 |
+
dependencies = [
|
| 581 |
+
"crossbeam-deque",
|
| 582 |
+
"crossbeam-utils",
|
| 583 |
+
]
|
| 584 |
+
|
| 585 |
+
[[package]]
|
| 586 |
+
name = "regex"
|
| 587 |
+
version = "1.13.1"
|
| 588 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 589 |
+
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
| 590 |
+
dependencies = [
|
| 591 |
+
"aho-corasick",
|
| 592 |
+
"memchr",
|
| 593 |
+
"regex-automata",
|
| 594 |
+
"regex-syntax",
|
| 595 |
+
]
|
| 596 |
+
|
| 597 |
+
[[package]]
|
| 598 |
+
name = "regex-automata"
|
| 599 |
+
version = "0.4.18"
|
| 600 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 601 |
+
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
| 602 |
+
dependencies = [
|
| 603 |
+
"aho-corasick",
|
| 604 |
+
"memchr",
|
| 605 |
+
"regex-syntax",
|
| 606 |
+
]
|
| 607 |
+
|
| 608 |
+
[[package]]
|
| 609 |
+
name = "regex-syntax"
|
| 610 |
+
version = "0.8.11"
|
| 611 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 612 |
+
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
| 613 |
+
|
| 614 |
+
[[package]]
|
| 615 |
+
name = "rustversion"
|
| 616 |
+
version = "1.0.23"
|
| 617 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 618 |
+
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
| 619 |
+
|
| 620 |
+
[[package]]
|
| 621 |
+
name = "ryu"
|
| 622 |
+
version = "1.0.23"
|
| 623 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 624 |
+
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
| 625 |
+
|
| 626 |
+
[[package]]
|
| 627 |
+
name = "serde"
|
| 628 |
+
version = "1.0.229"
|
| 629 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 630 |
+
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
| 631 |
+
dependencies = [
|
| 632 |
+
"serde_core",
|
| 633 |
+
"serde_derive",
|
| 634 |
+
]
|
| 635 |
+
|
| 636 |
+
[[package]]
|
| 637 |
+
name = "serde_core"
|
| 638 |
+
version = "1.0.229"
|
| 639 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 640 |
+
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
| 641 |
+
dependencies = [
|
| 642 |
+
"serde_derive",
|
| 643 |
+
]
|
| 644 |
+
|
| 645 |
+
[[package]]
|
| 646 |
+
name = "serde_derive"
|
| 647 |
+
version = "1.0.229"
|
| 648 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 649 |
+
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
| 650 |
+
dependencies = [
|
| 651 |
+
"proc-macro2",
|
| 652 |
+
"quote",
|
| 653 |
+
"syn 3.0.6",
|
| 654 |
+
]
|
| 655 |
+
|
| 656 |
+
[[package]]
|
| 657 |
+
name = "serde_json"
|
| 658 |
+
version = "1.0.151"
|
| 659 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 660 |
+
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
| 661 |
+
dependencies = [
|
| 662 |
+
"itoa",
|
| 663 |
+
"memchr",
|
| 664 |
+
"serde",
|
| 665 |
+
"serde_core",
|
| 666 |
+
"zmij",
|
| 667 |
+
]
|
| 668 |
+
|
| 669 |
+
[[package]]
|
| 670 |
+
name = "shlex"
|
| 671 |
+
version = "2.0.1"
|
| 672 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 673 |
+
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
| 674 |
+
|
| 675 |
+
[[package]]
|
| 676 |
+
name = "smallvec"
|
| 677 |
+
version = "1.16.2"
|
| 678 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 679 |
+
checksum = "f9395f0f0eee849a9b707b2f06bb92a6a422090e2123bb2ef8e87a0e61892a8e"
|
| 680 |
+
|
| 681 |
+
[[package]]
|
| 682 |
+
name = "spm_precompiled"
|
| 683 |
+
version = "0.1.4"
|
| 684 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 685 |
+
checksum = "5851699c4033c63636f7ea4cf7b7c1f1bf06d0cc03cfb42e711de5a5c46cf326"
|
| 686 |
+
dependencies = [
|
| 687 |
+
"base64",
|
| 688 |
+
"nom",
|
| 689 |
+
"serde",
|
| 690 |
+
"unicode-segmentation",
|
| 691 |
+
]
|
| 692 |
+
|
| 693 |
+
[[package]]
|
| 694 |
+
name = "static_assertions"
|
| 695 |
+
version = "1.1.0"
|
| 696 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 697 |
+
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
| 698 |
+
|
| 699 |
+
[[package]]
|
| 700 |
+
name = "strsim"
|
| 701 |
+
version = "0.11.1"
|
| 702 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 703 |
+
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
| 704 |
+
|
| 705 |
+
[[package]]
|
| 706 |
+
name = "syn"
|
| 707 |
+
version = "2.0.119"
|
| 708 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 709 |
+
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
| 710 |
+
dependencies = [
|
| 711 |
+
"proc-macro2",
|
| 712 |
+
"quote",
|
| 713 |
+
"unicode-ident",
|
| 714 |
+
]
|
| 715 |
+
|
| 716 |
+
[[package]]
|
| 717 |
+
name = "syn"
|
| 718 |
+
version = "3.0.6"
|
| 719 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 720 |
+
checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
|
| 721 |
+
dependencies = [
|
| 722 |
+
"proc-macro2",
|
| 723 |
+
"quote",
|
| 724 |
+
"unicode-ident",
|
| 725 |
+
]
|
| 726 |
+
|
| 727 |
+
[[package]]
|
| 728 |
+
name = "thiserror"
|
| 729 |
+
version = "2.0.21"
|
| 730 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 731 |
+
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
|
| 732 |
+
dependencies = [
|
| 733 |
+
"thiserror-impl",
|
| 734 |
+
]
|
| 735 |
+
|
| 736 |
+
[[package]]
|
| 737 |
+
name = "thiserror-impl"
|
| 738 |
+
version = "2.0.21"
|
| 739 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 740 |
+
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
|
| 741 |
+
dependencies = [
|
| 742 |
+
"proc-macro2",
|
| 743 |
+
"quote",
|
| 744 |
+
"syn 3.0.6",
|
| 745 |
+
]
|
| 746 |
+
|
| 747 |
+
[[package]]
|
| 748 |
+
name = "tokenizers"
|
| 749 |
+
version = "0.23.2"
|
| 750 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 751 |
+
checksum = "7afbf6e88718afcc138bad01d6ccc3051dbbc3b2ce9793d8b8a3aeb610969cfc"
|
| 752 |
+
dependencies = [
|
| 753 |
+
"ahash",
|
| 754 |
+
"compact_str",
|
| 755 |
+
"daachorse",
|
| 756 |
+
"dary_heap",
|
| 757 |
+
"derive_builder",
|
| 758 |
+
"esaxx-rs",
|
| 759 |
+
"getrandom",
|
| 760 |
+
"itertools",
|
| 761 |
+
"log",
|
| 762 |
+
"macro_rules_attribute",
|
| 763 |
+
"monostate",
|
| 764 |
+
"onig",
|
| 765 |
+
"paste",
|
| 766 |
+
"rand",
|
| 767 |
+
"rayon",
|
| 768 |
+
"rayon-cond",
|
| 769 |
+
"regex",
|
| 770 |
+
"regex-syntax",
|
| 771 |
+
"serde",
|
| 772 |
+
"serde_json",
|
| 773 |
+
"spm_precompiled",
|
| 774 |
+
"thiserror",
|
| 775 |
+
"unicode-normalization-alignments",
|
| 776 |
+
"unicode-segmentation",
|
| 777 |
+
"unicode_categories",
|
| 778 |
+
]
|
| 779 |
+
|
| 780 |
+
[[package]]
|
| 781 |
+
name = "tracing"
|
| 782 |
+
version = "0.1.44"
|
| 783 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 784 |
+
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
| 785 |
+
dependencies = [
|
| 786 |
+
"pin-project-lite",
|
| 787 |
+
"tracing-core",
|
| 788 |
+
]
|
| 789 |
+
|
| 790 |
+
[[package]]
|
| 791 |
+
name = "tracing-core"
|
| 792 |
+
version = "0.1.36"
|
| 793 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 794 |
+
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
| 795 |
+
dependencies = [
|
| 796 |
+
"once_cell",
|
| 797 |
+
]
|
| 798 |
+
|
| 799 |
+
[[package]]
|
| 800 |
+
name = "unicode-ident"
|
| 801 |
+
version = "1.0.26"
|
| 802 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 803 |
+
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
| 804 |
+
|
| 805 |
+
[[package]]
|
| 806 |
+
name = "unicode-normalization-alignments"
|
| 807 |
+
version = "0.1.12"
|
| 808 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 809 |
+
checksum = "43f613e4fa046e69818dd287fdc4bc78175ff20331479dab6e1b0f98d57062de"
|
| 810 |
+
dependencies = [
|
| 811 |
+
"smallvec",
|
| 812 |
+
]
|
| 813 |
+
|
| 814 |
+
[[package]]
|
| 815 |
+
name = "unicode-segmentation"
|
| 816 |
+
version = "1.13.3"
|
| 817 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 818 |
+
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
| 819 |
+
|
| 820 |
+
[[package]]
|
| 821 |
+
name = "unicode_categories"
|
| 822 |
+
version = "0.1.1"
|
| 823 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 824 |
+
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
|
| 825 |
+
|
| 826 |
+
[[package]]
|
| 827 |
+
name = "version_check"
|
| 828 |
+
version = "0.9.5"
|
| 829 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 830 |
+
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
| 831 |
+
|
| 832 |
+
[[package]]
|
| 833 |
+
name = "wasip2"
|
| 834 |
+
version = "1.0.4+wasi-0.2.12"
|
| 835 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 836 |
+
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
| 837 |
+
dependencies = [
|
| 838 |
+
"wit-bindgen",
|
| 839 |
+
]
|
| 840 |
+
|
| 841 |
+
[[package]]
|
| 842 |
+
name = "windows-link"
|
| 843 |
+
version = "0.2.1"
|
| 844 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 845 |
+
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
| 846 |
+
|
| 847 |
+
[[package]]
|
| 848 |
+
name = "wit-bindgen"
|
| 849 |
+
version = "0.57.1"
|
| 850 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 851 |
+
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
| 852 |
+
|
| 853 |
+
[[package]]
|
| 854 |
+
name = "zerocopy"
|
| 855 |
+
version = "0.8.59"
|
| 856 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 857 |
+
checksum = "6df92bf3d9227be3d53173901ddbffac2babc27ae50f397776ffd6dc33f800cb"
|
| 858 |
+
dependencies = [
|
| 859 |
+
"zerocopy-derive",
|
| 860 |
+
]
|
| 861 |
+
|
| 862 |
+
[[package]]
|
| 863 |
+
name = "zerocopy-derive"
|
| 864 |
+
version = "0.8.59"
|
| 865 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 866 |
+
checksum = "ac4f328cf2f05d084e496c3e9c3f33ed0a183656a16e1fcec4d464d8373aec82"
|
| 867 |
+
dependencies = [
|
| 868 |
+
"proc-macro2",
|
| 869 |
+
"quote",
|
| 870 |
+
"syn 2.0.119",
|
| 871 |
+
]
|
| 872 |
+
|
| 873 |
+
[[package]]
|
| 874 |
+
name = "zmij"
|
| 875 |
+
version = "1.0.23"
|
| 876 |
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
| 877 |
+
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
runtime/Cargo.toml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[package]
|
| 2 |
+
name = "datafog-pii"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
edition = "2024"
|
| 5 |
+
publish = false
|
| 6 |
+
|
| 7 |
+
[dependencies]
|
| 8 |
+
anyhow = "1"
|
| 9 |
+
serde = { version = "1", features = ["derive"] }
|
| 10 |
+
serde_json = "1"
|
| 11 |
+
tokenizers = { version = "=0.23.2", default-features = false, features = ["onig"] }
|
| 12 |
+
ort = { version = "=2.0.0-rc.13", default-features = false, features = ["std", "load-dynamic", "api-27"] }
|
runtime/licenses/onnxruntime-LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) Microsoft Corporation
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
runtime/licenses/onnxruntime-ThirdPartyNotices.txt
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
runtime/licenses/ort-LICENSE-APACHE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
Apache License
|
| 3 |
+
Version 2.0, January 2004
|
| 4 |
+
http://www.apache.org/licenses/
|
| 5 |
+
|
| 6 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 11 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 12 |
+
|
| 13 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 14 |
+
the copyright owner that is granting the License.
|
| 15 |
+
|
| 16 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 17 |
+
other entities that control, are controlled by, or are under common
|
| 18 |
+
control with that entity. For the purposes of this definition,
|
| 19 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 20 |
+
direction or management of such entity, whether by contract or
|
| 21 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 22 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 23 |
+
|
| 24 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 25 |
+
exercising permissions granted by this License.
|
| 26 |
+
|
| 27 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 28 |
+
including but not limited to software source code, documentation
|
| 29 |
+
source, and configuration files.
|
| 30 |
+
|
| 31 |
+
"Object" form shall mean any form resulting from mechanical
|
| 32 |
+
transformation or translation of a Source form, including but
|
| 33 |
+
not limited to compiled object code, generated documentation,
|
| 34 |
+
and conversions to other media types.
|
| 35 |
+
|
| 36 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 37 |
+
Object form, made available under the License, as indicated by a
|
| 38 |
+
copyright notice that is included in or attached to the work
|
| 39 |
+
(an example is provided in the Appendix below).
|
| 40 |
+
|
| 41 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 42 |
+
form, that is based on (or derived from) the Work and for which the
|
| 43 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 44 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 45 |
+
of this License, Derivative Works shall not include works that remain
|
| 46 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 47 |
+
the Work and Derivative Works thereof.
|
| 48 |
+
|
| 49 |
+
"Contribution" shall mean any work of authorship, including
|
| 50 |
+
the original version of the Work and any modifications or additions
|
| 51 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 52 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 53 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 54 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 55 |
+
means any form of electronic, verbal, or written communication sent
|
| 56 |
+
to the Licensor or its representatives, including but not limited to
|
| 57 |
+
communication on electronic mailing lists, source code control systems,
|
| 58 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 59 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 60 |
+
excluding communication that is conspicuously marked or otherwise
|
| 61 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 62 |
+
|
| 63 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 64 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 65 |
+
subsequently incorporated within the Work.
|
| 66 |
+
|
| 67 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 68 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 69 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 70 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 71 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 72 |
+
Work and such Derivative Works in Source or Object form.
|
| 73 |
+
|
| 74 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 75 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 76 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 77 |
+
(except as stated in this section) patent license to make, have made,
|
| 78 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 79 |
+
where such license applies only to those patent claims licensable
|
| 80 |
+
by such Contributor that are necessarily infringed by their
|
| 81 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 82 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 83 |
+
institute patent litigation against any entity (including a
|
| 84 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 85 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 86 |
+
or contributory patent infringement, then any patent licenses
|
| 87 |
+
granted to You under this License for that Work shall terminate
|
| 88 |
+
as of the date such litigation is filed.
|
| 89 |
+
|
| 90 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 91 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 92 |
+
modifications, and in Source or Object form, provided that You
|
| 93 |
+
meet the following conditions:
|
| 94 |
+
|
| 95 |
+
(a) You must give any other recipients of the Work or
|
| 96 |
+
Derivative Works a copy of this License; and
|
| 97 |
+
|
| 98 |
+
(b) You must cause any modified files to carry prominent notices
|
| 99 |
+
stating that You changed the files; and
|
| 100 |
+
|
| 101 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 102 |
+
that You distribute, all copyright, patent, trademark, and
|
| 103 |
+
attribution notices from the Source form of the Work,
|
| 104 |
+
excluding those notices that do not pertain to any part of
|
| 105 |
+
the Derivative Works; and
|
| 106 |
+
|
| 107 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 108 |
+
distribution, then any Derivative Works that You distribute must
|
| 109 |
+
include a readable copy of the attribution notices contained
|
| 110 |
+
within such NOTICE file, excluding those notices that do not
|
| 111 |
+
pertain to any part of the Derivative Works, in at least one
|
| 112 |
+
of the following places: within a NOTICE text file distributed
|
| 113 |
+
as part of the Derivative Works; within the Source form or
|
| 114 |
+
documentation, if provided along with the Derivative Works; or,
|
| 115 |
+
within a display generated by the Derivative Works, if and
|
| 116 |
+
wherever such third-party notices normally appear. The contents
|
| 117 |
+
of the NOTICE file are for informational purposes only and
|
| 118 |
+
do not modify the License. You may add Your own attribution
|
| 119 |
+
notices within Derivative Works that You distribute, alongside
|
| 120 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 121 |
+
that such additional attribution notices cannot be construed
|
| 122 |
+
as modifying the License.
|
| 123 |
+
|
| 124 |
+
You may add Your own copyright statement to Your modifications and
|
| 125 |
+
may provide additional or different license terms and conditions
|
| 126 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 127 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 128 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 129 |
+
the conditions stated in this License.
|
| 130 |
+
|
| 131 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 132 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 133 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 134 |
+
this License, without any additional terms or conditions.
|
| 135 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 136 |
+
the terms of any separate license agreement you may have executed
|
| 137 |
+
with Licensor regarding such Contributions.
|
| 138 |
+
|
| 139 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 140 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 141 |
+
except as required for reasonable and customary use in describing the
|
| 142 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 143 |
+
|
| 144 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 145 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 146 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 147 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 148 |
+
implied, including, without limitation, any warranties or conditions
|
| 149 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 150 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 151 |
+
appropriateness of using or redistributing the Work and assume any
|
| 152 |
+
risks associated with Your exercise of permissions under this License.
|
| 153 |
+
|
| 154 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 155 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 156 |
+
unless required by applicable law (such as deliberate and grossly
|
| 157 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 158 |
+
liable to You for damages, including any direct, indirect, special,
|
| 159 |
+
incidental, or consequential damages of any character arising as a
|
| 160 |
+
result of this License or out of the use or inability to use the
|
| 161 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 162 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 163 |
+
other commercial damages or losses), even if such Contributor
|
| 164 |
+
has been advised of the possibility of such damages.
|
| 165 |
+
|
| 166 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 167 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 168 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 169 |
+
or other liability obligations and/or rights consistent with this
|
| 170 |
+
License. However, in accepting such obligations, You may act only
|
| 171 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 172 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 173 |
+
defend, and hold each Contributor harmless for any liability
|
| 174 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 175 |
+
of your accepting any such warranty or additional liability.
|
| 176 |
+
|
| 177 |
+
END OF TERMS AND CONDITIONS
|
| 178 |
+
|
| 179 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 180 |
+
|
| 181 |
+
To apply the Apache License to your work, attach the following
|
| 182 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 183 |
+
replaced with your own identifying information. (Don't include
|
| 184 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 185 |
+
comment syntax for the file format. We also recommend that a
|
| 186 |
+
file or class name and description of purpose be included on the
|
| 187 |
+
same "printed page" as the copyright notice for easier
|
| 188 |
+
identification within third-party archives.
|
| 189 |
+
|
| 190 |
+
Copyright [yyyy] [name of copyright owner]
|
| 191 |
+
|
| 192 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 193 |
+
you may not use this file except in compliance with the License.
|
| 194 |
+
You may obtain a copy of the License at
|
| 195 |
+
|
| 196 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 197 |
+
|
| 198 |
+
Unless required by applicable law or agreed to in writing, software
|
| 199 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 200 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 201 |
+
See the License for the specific language governing permissions and
|
| 202 |
+
limitations under the License.
|
runtime/licenses/ort-LICENSE-MIT
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2023-2026 pyke.io
|
| 4 |
+
Copyright (c) 2020 Nicolas Bigaouette
|
| 5 |
+
|
| 6 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 7 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 8 |
+
in the Software without restriction, including without limitation the rights
|
| 9 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 10 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 11 |
+
furnished to do so, subject to the following conditions:
|
| 12 |
+
|
| 13 |
+
The above copyright notice and this permission notice shall be included in all
|
| 14 |
+
copies or substantial portions of the Software.
|
| 15 |
+
|
| 16 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 17 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 18 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 19 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 20 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 21 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 22 |
+
SOFTWARE.
|
runtime/licenses/tokenizers-LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Apache License
|
| 2 |
+
Version 2.0, January 2004
|
| 3 |
+
http://www.apache.org/licenses/
|
| 4 |
+
|
| 5 |
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
| 6 |
+
|
| 7 |
+
1. Definitions.
|
| 8 |
+
|
| 9 |
+
"License" shall mean the terms and conditions for use, reproduction,
|
| 10 |
+
and distribution as defined by Sections 1 through 9 of this document.
|
| 11 |
+
|
| 12 |
+
"Licensor" shall mean the copyright owner or entity authorized by
|
| 13 |
+
the copyright owner that is granting the License.
|
| 14 |
+
|
| 15 |
+
"Legal Entity" shall mean the union of the acting entity and all
|
| 16 |
+
other entities that control, are controlled by, or are under common
|
| 17 |
+
control with that entity. For the purposes of this definition,
|
| 18 |
+
"control" means (i) the power, direct or indirect, to cause the
|
| 19 |
+
direction or management of such entity, whether by contract or
|
| 20 |
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
| 21 |
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
| 22 |
+
|
| 23 |
+
"You" (or "Your") shall mean an individual or Legal Entity
|
| 24 |
+
exercising permissions granted by this License.
|
| 25 |
+
|
| 26 |
+
"Source" form shall mean the preferred form for making modifications,
|
| 27 |
+
including but not limited to software source code, documentation
|
| 28 |
+
source, and configuration files.
|
| 29 |
+
|
| 30 |
+
"Object" form shall mean any form resulting from mechanical
|
| 31 |
+
transformation or translation of a Source form, including but
|
| 32 |
+
not limited to compiled object code, generated documentation,
|
| 33 |
+
and conversions to other media types.
|
| 34 |
+
|
| 35 |
+
"Work" shall mean the work of authorship, whether in Source or
|
| 36 |
+
Object form, made available under the License, as indicated by a
|
| 37 |
+
copyright notice that is included in or attached to the work
|
| 38 |
+
(an example is provided in the Appendix below).
|
| 39 |
+
|
| 40 |
+
"Derivative Works" shall mean any work, whether in Source or Object
|
| 41 |
+
form, that is based on (or derived from) the Work and for which the
|
| 42 |
+
editorial revisions, annotations, elaborations, or other modifications
|
| 43 |
+
represent, as a whole, an original work of authorship. For the purposes
|
| 44 |
+
of this License, Derivative Works shall not include works that remain
|
| 45 |
+
separable from, or merely link (or bind by name) to the interfaces of,
|
| 46 |
+
the Work and Derivative Works thereof.
|
| 47 |
+
|
| 48 |
+
"Contribution" shall mean any work of authorship, including
|
| 49 |
+
the original version of the Work and any modifications or additions
|
| 50 |
+
to that Work or Derivative Works thereof, that is intentionally
|
| 51 |
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
| 52 |
+
or by an individual or Legal Entity authorized to submit on behalf of
|
| 53 |
+
the copyright owner. For the purposes of this definition, "submitted"
|
| 54 |
+
means any form of electronic, verbal, or written communication sent
|
| 55 |
+
to the Licensor or its representatives, including but not limited to
|
| 56 |
+
communication on electronic mailing lists, source code control systems,
|
| 57 |
+
and issue tracking systems that are managed by, or on behalf of, the
|
| 58 |
+
Licensor for the purpose of discussing and improving the Work, but
|
| 59 |
+
excluding communication that is conspicuously marked or otherwise
|
| 60 |
+
designated in writing by the copyright owner as "Not a Contribution."
|
| 61 |
+
|
| 62 |
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
| 63 |
+
on behalf of whom a Contribution has been received by Licensor and
|
| 64 |
+
subsequently incorporated within the Work.
|
| 65 |
+
|
| 66 |
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
| 67 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 68 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 69 |
+
copyright license to reproduce, prepare Derivative Works of,
|
| 70 |
+
publicly display, publicly perform, sublicense, and distribute the
|
| 71 |
+
Work and such Derivative Works in Source or Object form.
|
| 72 |
+
|
| 73 |
+
3. Grant of Patent License. Subject to the terms and conditions of
|
| 74 |
+
this License, each Contributor hereby grants to You a perpetual,
|
| 75 |
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
| 76 |
+
(except as stated in this section) patent license to make, have made,
|
| 77 |
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
| 78 |
+
where such license applies only to those patent claims licensable
|
| 79 |
+
by such Contributor that are necessarily infringed by their
|
| 80 |
+
Contribution(s) alone or by combination of their Contribution(s)
|
| 81 |
+
with the Work to which such Contribution(s) was submitted. If You
|
| 82 |
+
institute patent litigation against any entity (including a
|
| 83 |
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
| 84 |
+
or a Contribution incorporated within the Work constitutes direct
|
| 85 |
+
or contributory patent infringement, then any patent licenses
|
| 86 |
+
granted to You under this License for that Work shall terminate
|
| 87 |
+
as of the date such litigation is filed.
|
| 88 |
+
|
| 89 |
+
4. Redistribution. You may reproduce and distribute copies of the
|
| 90 |
+
Work or Derivative Works thereof in any medium, with or without
|
| 91 |
+
modifications, and in Source or Object form, provided that You
|
| 92 |
+
meet the following conditions:
|
| 93 |
+
|
| 94 |
+
(a) You must give any other recipients of the Work or
|
| 95 |
+
Derivative Works a copy of this License; and
|
| 96 |
+
|
| 97 |
+
(b) You must cause any modified files to carry prominent notices
|
| 98 |
+
stating that You changed the files; and
|
| 99 |
+
|
| 100 |
+
(c) You must retain, in the Source form of any Derivative Works
|
| 101 |
+
that You distribute, all copyright, patent, trademark, and
|
| 102 |
+
attribution notices from the Source form of the Work,
|
| 103 |
+
excluding those notices that do not pertain to any part of
|
| 104 |
+
the Derivative Works; and
|
| 105 |
+
|
| 106 |
+
(d) If the Work includes a "NOTICE" text file as part of its
|
| 107 |
+
distribution, then any Derivative Works that You distribute must
|
| 108 |
+
include a readable copy of the attribution notices contained
|
| 109 |
+
within such NOTICE file, excluding those notices that do not
|
| 110 |
+
pertain to any part of the Derivative Works, in at least one
|
| 111 |
+
of the following places: within a NOTICE text file distributed
|
| 112 |
+
as part of the Derivative Works; within the Source form or
|
| 113 |
+
documentation, if provided along with the Derivative Works; or,
|
| 114 |
+
within a display generated by the Derivative Works, if and
|
| 115 |
+
wherever such third-party notices normally appear. The contents
|
| 116 |
+
of the NOTICE file are for informational purposes only and
|
| 117 |
+
do not modify the License. You may add Your own attribution
|
| 118 |
+
notices within Derivative Works that You distribute, alongside
|
| 119 |
+
or as an addendum to the NOTICE text from the Work, provided
|
| 120 |
+
that such additional attribution notices cannot be construed
|
| 121 |
+
as modifying the License.
|
| 122 |
+
|
| 123 |
+
You may add Your own copyright statement to Your modifications and
|
| 124 |
+
may provide additional or different license terms and conditions
|
| 125 |
+
for use, reproduction, or distribution of Your modifications, or
|
| 126 |
+
for any such Derivative Works as a whole, provided Your use,
|
| 127 |
+
reproduction, and distribution of the Work otherwise complies with
|
| 128 |
+
the conditions stated in this License.
|
| 129 |
+
|
| 130 |
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
| 131 |
+
any Contribution intentionally submitted for inclusion in the Work
|
| 132 |
+
by You to the Licensor shall be under the terms and conditions of
|
| 133 |
+
this License, without any additional terms or conditions.
|
| 134 |
+
Notwithstanding the above, nothing herein shall supersede or modify
|
| 135 |
+
the terms of any separate license agreement you may have executed
|
| 136 |
+
with Licensor regarding such Contributions.
|
| 137 |
+
|
| 138 |
+
6. Trademarks. This License does not grant permission to use the trade
|
| 139 |
+
names, trademarks, service marks, or product names of the Licensor,
|
| 140 |
+
except as required for reasonable and customary use in describing the
|
| 141 |
+
origin of the Work and reproducing the content of the NOTICE file.
|
| 142 |
+
|
| 143 |
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
| 144 |
+
agreed to in writing, Licensor provides the Work (and each
|
| 145 |
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
| 146 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 147 |
+
implied, including, without limitation, any warranties or conditions
|
| 148 |
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
| 149 |
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
| 150 |
+
appropriateness of using or redistributing the Work and assume any
|
| 151 |
+
risks associated with Your exercise of permissions under this License.
|
| 152 |
+
|
| 153 |
+
8. Limitation of Liability. In no event and under no legal theory,
|
| 154 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 155 |
+
unless required by applicable law (such as deliberate and grossly
|
| 156 |
+
negligent acts) or agreed to in writing, shall any Contributor be
|
| 157 |
+
liable to You for damages, including any direct, indirect, special,
|
| 158 |
+
incidental, or consequential damages of any character arising as a
|
| 159 |
+
result of this License or out of the use or inability to use the
|
| 160 |
+
Work (including but not limited to damages for loss of goodwill,
|
| 161 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 162 |
+
other commercial damages or losses), even if such Contributor
|
| 163 |
+
has been advised of the possibility of such damages.
|
| 164 |
+
|
| 165 |
+
9. Accepting Warranty or Additional Liability. While redistributing
|
| 166 |
+
the Work or Derivative Works thereof, You may choose to offer,
|
| 167 |
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
| 168 |
+
or other liability obligations and/or rights consistent with this
|
| 169 |
+
License. However, in accepting such obligations, You may act only
|
| 170 |
+
on Your own behalf and on Your sole responsibility, not on behalf
|
| 171 |
+
of any other Contributor, and only if You agree to indemnify,
|
| 172 |
+
defend, and hold each Contributor harmless for any liability
|
| 173 |
+
incurred by, or claims asserted against, such Contributor by reason
|
| 174 |
+
of your accepting any such warranty or additional liability.
|
| 175 |
+
|
| 176 |
+
END OF TERMS AND CONDITIONS
|
| 177 |
+
|
| 178 |
+
APPENDIX: How to apply the Apache License to your work.
|
| 179 |
+
|
| 180 |
+
To apply the Apache License to your work, attach the following
|
| 181 |
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
| 182 |
+
replaced with your own identifying information. (Don't include
|
| 183 |
+
the brackets!) The text should be enclosed in the appropriate
|
| 184 |
+
comment syntax for the file format. We also recommend that a
|
| 185 |
+
file or class name and description of purpose be included on the
|
| 186 |
+
same "printed page" as the copyright notice for easier
|
| 187 |
+
identification within third-party archives.
|
| 188 |
+
|
| 189 |
+
Copyright [yyyy] [name of copyright owner]
|
| 190 |
+
|
| 191 |
+
Licensed under the Apache License, Version 2.0 (the "License");
|
| 192 |
+
you may not use this file except in compliance with the License.
|
| 193 |
+
You may obtain a copy of the License at
|
| 194 |
+
|
| 195 |
+
http://www.apache.org/licenses/LICENSE-2.0
|
| 196 |
+
|
| 197 |
+
Unless required by applicable law or agreed to in writing, software
|
| 198 |
+
distributed under the License is distributed on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 200 |
+
See the License for the specific language governing permissions and
|
| 201 |
+
limitations under the License.
|
runtime/src/main.rs
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
use anyhow::{Context, Result, bail, ensure};
|
| 2 |
+
use ort::{session::Session, value::Tensor};
|
| 3 |
+
use serde::{Deserialize, Serialize};
|
| 4 |
+
use serde_json::json;
|
| 5 |
+
use std::{
|
| 6 |
+
io::{self, BufRead, Write},
|
| 7 |
+
path::Path,
|
| 8 |
+
};
|
| 9 |
+
use tokenizers::{Encoding, Tokenizer};
|
| 10 |
+
|
| 11 |
+
#[derive(Deserialize)]
|
| 12 |
+
struct Config {
|
| 13 |
+
labels: Vec<String>,
|
| 14 |
+
max_length: usize,
|
| 15 |
+
stride: usize,
|
| 16 |
+
#[serde(default)]
|
| 17 |
+
calibration: Option<Calibration>,
|
| 18 |
+
#[serde(default)]
|
| 19 |
+
calibration_file: Option<String>,
|
| 20 |
+
}
|
| 21 |
+
#[derive(Deserialize)]
|
| 22 |
+
struct Calibration {
|
| 23 |
+
#[serde(alias = "xs")]
|
| 24 |
+
x: Vec<f64>,
|
| 25 |
+
#[serde(alias = "ys")]
|
| 26 |
+
y: Vec<f64>,
|
| 27 |
+
threshold: Option<f64>,
|
| 28 |
+
}
|
| 29 |
+
impl Calibration {
|
| 30 |
+
fn apply(&self, v: f64) -> f64 {
|
| 31 |
+
let i = self.x.partition_point(|x| *x < v);
|
| 32 |
+
if i == 0 {
|
| 33 |
+
self.y[0]
|
| 34 |
+
} else if i == self.x.len() {
|
| 35 |
+
*self.y.last().unwrap()
|
| 36 |
+
} else {
|
| 37 |
+
self.y[i - 1]
|
| 38 |
+
+ (v - self.x[i - 1]) / (self.x[i] - self.x[i - 1]) * (self.y[i] - self.y[i - 1])
|
| 39 |
+
}
|
| 40 |
+
}
|
| 41 |
+
}
|
| 42 |
+
#[derive(Clone, Debug, Serialize)]
|
| 43 |
+
struct Finding {
|
| 44 |
+
label: String,
|
| 45 |
+
start: usize,
|
| 46 |
+
end: usize,
|
| 47 |
+
raw_score: f64,
|
| 48 |
+
confidence: Option<f64>,
|
| 49 |
+
#[serde(skip)]
|
| 50 |
+
boundary_fragment: bool,
|
| 51 |
+
}
|
| 52 |
+
fn encode_windows(
|
| 53 |
+
tokenizer: &Tokenizer,
|
| 54 |
+
text: &str,
|
| 55 |
+
max_length: usize,
|
| 56 |
+
stride: usize,
|
| 57 |
+
) -> Result<Vec<Encoding>> {
|
| 58 |
+
let full = tokenizer
|
| 59 |
+
.encode(text, false)
|
| 60 |
+
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
| 61 |
+
let capacity = max_length - 2;
|
| 62 |
+
ensure!(stride < capacity, "invalid stride");
|
| 63 |
+
let cls = tokenizer
|
| 64 |
+
.token_to_id("[CLS]")
|
| 65 |
+
.context("missing CLS token")?;
|
| 66 |
+
let sep = tokenizer
|
| 67 |
+
.token_to_id("[SEP]")
|
| 68 |
+
.context("missing SEP token")?;
|
| 69 |
+
let mut windows = Vec::new();
|
| 70 |
+
let mut start = 0;
|
| 71 |
+
loop {
|
| 72 |
+
let end = (start + capacity).min(full.len());
|
| 73 |
+
let n = end - start + 2;
|
| 74 |
+
let mut ids = vec![cls];
|
| 75 |
+
ids.extend_from_slice(&full.get_ids()[start..end]);
|
| 76 |
+
ids.push(sep);
|
| 77 |
+
let mut offsets = vec![(0, 0)];
|
| 78 |
+
offsets.extend_from_slice(&full.get_offsets()[start..end]);
|
| 79 |
+
offsets.push((0, 0));
|
| 80 |
+
let mut tokens = vec!["[CLS]".to_string()];
|
| 81 |
+
tokens.extend_from_slice(&full.get_tokens()[start..end]);
|
| 82 |
+
tokens.push("[SEP]".to_string());
|
| 83 |
+
let mut special = vec![0; n];
|
| 84 |
+
special[0] = 1;
|
| 85 |
+
special[n - 1] = 1;
|
| 86 |
+
windows.push(Encoding::new(
|
| 87 |
+
ids,
|
| 88 |
+
vec![0; n],
|
| 89 |
+
tokens,
|
| 90 |
+
vec![None; n],
|
| 91 |
+
offsets,
|
| 92 |
+
special,
|
| 93 |
+
vec![1; n],
|
| 94 |
+
vec![],
|
| 95 |
+
Default::default(),
|
| 96 |
+
));
|
| 97 |
+
if end == full.len() {
|
| 98 |
+
break;
|
| 99 |
+
}
|
| 100 |
+
start += capacity - stride;
|
| 101 |
+
}
|
| 102 |
+
Ok(windows)
|
| 103 |
+
}
|
| 104 |
+
fn decode(enc: &tokenizers::Encoding, logits: &[f32], labels: &[String]) -> Vec<Finding> {
|
| 105 |
+
let k = 1 + 2 * labels.len();
|
| 106 |
+
let mut out = Vec::new();
|
| 107 |
+
let mut current: Option<Finding> = None;
|
| 108 |
+
for (i, row) in logits.chunks_exact(k).enumerate() {
|
| 109 |
+
let (start, end) = enc.get_offsets()[i];
|
| 110 |
+
if enc.get_special_tokens_mask()[i] != 0 || enc.get_attention_mask()[i] == 0 || start == end
|
| 111 |
+
{
|
| 112 |
+
if let Some(f) = current.take() {
|
| 113 |
+
out.push(f);
|
| 114 |
+
}
|
| 115 |
+
continue;
|
| 116 |
+
}
|
| 117 |
+
let mut id = 0;
|
| 118 |
+
for j in 1..k {
|
| 119 |
+
if row[j] > row[id] {
|
| 120 |
+
id = j;
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
if id == 0 {
|
| 124 |
+
if let Some(f) = current.take() {
|
| 125 |
+
out.push(f);
|
| 126 |
+
}
|
| 127 |
+
continue;
|
| 128 |
+
}
|
| 129 |
+
let label = &labels[(id - 1) / 2];
|
| 130 |
+
let max = row[id] as f64;
|
| 131 |
+
let prob = 1.0 / row.iter().map(|v| (*v as f64 - max).exp()).sum::<f64>();
|
| 132 |
+
if id % 2 == 0 && current.as_ref().is_some_and(|f| f.label == *label) {
|
| 133 |
+
let f = current.as_mut().unwrap();
|
| 134 |
+
f.end = end;
|
| 135 |
+
f.raw_score = f.raw_score.min(prob);
|
| 136 |
+
} else {
|
| 137 |
+
if let Some(f) = current.take() {
|
| 138 |
+
out.push(f);
|
| 139 |
+
}
|
| 140 |
+
current = Some(Finding {
|
| 141 |
+
label: label.clone(),
|
| 142 |
+
start,
|
| 143 |
+
end,
|
| 144 |
+
raw_score: prob,
|
| 145 |
+
confidence: None,
|
| 146 |
+
boundary_fragment: false,
|
| 147 |
+
});
|
| 148 |
+
}
|
| 149 |
+
}
|
| 150 |
+
if let Some(f) = current {
|
| 151 |
+
out.push(f);
|
| 152 |
+
}
|
| 153 |
+
out
|
| 154 |
+
}
|
| 155 |
+
fn mark_boundaries(
|
| 156 |
+
enc: &tokenizers::Encoding,
|
| 157 |
+
findings: &mut [Finding],
|
| 158 |
+
has_previous: bool,
|
| 159 |
+
has_next: bool,
|
| 160 |
+
) {
|
| 161 |
+
let content: Vec<_> = enc.get_offsets().iter().filter(|(a, b)| a != b).collect();
|
| 162 |
+
if let (Some(first), Some(last)) = (content.first(), content.last()) {
|
| 163 |
+
for f in findings {
|
| 164 |
+
f.boundary_fragment =
|
| 165 |
+
(has_previous && f.start == first.0) || (has_next && f.end == last.1);
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
fn resolve(findings: Vec<Finding>) -> Vec<Finding> {
|
| 170 |
+
let mut findings: Vec<Finding> = findings
|
| 171 |
+
.iter()
|
| 172 |
+
.filter(|f| {
|
| 173 |
+
!(f.boundary_fragment
|
| 174 |
+
&& findings.iter().any(|g| {
|
| 175 |
+
g.label == f.label
|
| 176 |
+
&& g.start <= f.start
|
| 177 |
+
&& g.end >= f.end
|
| 178 |
+
&& (g.start < f.start || g.end > f.end)
|
| 179 |
+
}))
|
| 180 |
+
})
|
| 181 |
+
.cloned()
|
| 182 |
+
.collect();
|
| 183 |
+
findings.sort_by(|a, b| {
|
| 184 |
+
b.raw_score
|
| 185 |
+
.total_cmp(&a.raw_score)
|
| 186 |
+
.then(a.start.cmp(&b.start))
|
| 187 |
+
.then(a.end.cmp(&b.end))
|
| 188 |
+
.then(a.label.cmp(&b.label))
|
| 189 |
+
});
|
| 190 |
+
let mut accepted: Vec<Finding> = Vec::new();
|
| 191 |
+
for f in findings {
|
| 192 |
+
if !accepted.iter().any(|a| f.start < a.end && a.start < f.end) {
|
| 193 |
+
accepted.push(f);
|
| 194 |
+
}
|
| 195 |
+
}
|
| 196 |
+
accepted.sort_by_key(|f| (f.start, f.end));
|
| 197 |
+
accepted
|
| 198 |
+
}
|
| 199 |
+
fn redact(text: &str, findings: &[Finding]) -> Result<String> {
|
| 200 |
+
let mut s = String::new();
|
| 201 |
+
let mut pos = 0;
|
| 202 |
+
for f in findings {
|
| 203 |
+
ensure!(
|
| 204 |
+
f.start >= pos
|
| 205 |
+
&& f.end <= text.len()
|
| 206 |
+
&& text.is_char_boundary(f.start)
|
| 207 |
+
&& text.is_char_boundary(f.end),
|
| 208 |
+
"invalid UTF-8 finding boundary"
|
| 209 |
+
);
|
| 210 |
+
s.push_str(&text[pos..f.start]);
|
| 211 |
+
s.push_str("[REDACTED]");
|
| 212 |
+
pos = f.end;
|
| 213 |
+
}
|
| 214 |
+
s.push_str(&text[pos..]);
|
| 215 |
+
Ok(s)
|
| 216 |
+
}
|
| 217 |
+
struct Detector {
|
| 218 |
+
config: Config,
|
| 219 |
+
tokenizer: Tokenizer,
|
| 220 |
+
session: Session,
|
| 221 |
+
}
|
| 222 |
+
impl Detector {
|
| 223 |
+
fn load(path: &Path) -> Result<Self> {
|
| 224 |
+
let mut config: Config = serde_json::from_slice(&std::fs::read(path.join("config.json"))?)?;
|
| 225 |
+
if let Some(file) = &config.calibration_file {
|
| 226 |
+
config.calibration = Some(serde_json::from_slice(&std::fs::read(path.join(file))?)?);
|
| 227 |
+
}
|
| 228 |
+
ensure!(config.labels.len() == 7, "expected seven labels");
|
| 229 |
+
ensure!(
|
| 230 |
+
config.max_length > 2 && config.stride < config.max_length - 2,
|
| 231 |
+
"invalid chunk settings"
|
| 232 |
+
);
|
| 233 |
+
if let Some(c) = &config.calibration {
|
| 234 |
+
ensure!(
|
| 235 |
+
!c.x.is_empty() && c.x.len() == c.y.len(),
|
| 236 |
+
"invalid calibration lengths"
|
| 237 |
+
);
|
| 238 |
+
ensure!(
|
| 239 |
+
c.x.iter().all(|x| x.is_finite())
|
| 240 |
+
&& c.y.iter().all(|y| y.is_finite() && (0.0..=1.0).contains(y))
|
| 241 |
+
&& c.x.windows(2).all(|v| v[0] < v[1])
|
| 242 |
+
&& c.y.windows(2).all(|v| v[0] <= v[1]),
|
| 243 |
+
"invalid calibration values"
|
| 244 |
+
);
|
| 245 |
+
}
|
| 246 |
+
let mut tokenizer = Tokenizer::from_file(path.join("tokenizer.json"))
|
| 247 |
+
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
| 248 |
+
tokenizer.with_padding(None);
|
| 249 |
+
tokenizer
|
| 250 |
+
.with_truncation(None)
|
| 251 |
+
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
| 252 |
+
let _ = ort::init_from(path.join(if cfg!(target_os = "windows") { "onnxruntime.dll" } else if cfg!(target_os = "macos") { "libonnxruntime.dylib" } else { "libonnxruntime.so" }))?.commit();
|
| 253 |
+
let session = Session::builder()?
|
| 254 |
+
.with_intra_threads(1)
|
| 255 |
+
.map_err(|e| anyhow::anyhow!("{e}"))?
|
| 256 |
+
.commit_from_file(path.join("model.onnx"))?;
|
| 257 |
+
Ok(Self {
|
| 258 |
+
config,
|
| 259 |
+
tokenizer,
|
| 260 |
+
session,
|
| 261 |
+
})
|
| 262 |
+
}
|
| 263 |
+
fn predict(&mut self, text: &str, debug: bool) -> Result<serde_json::Value> {
|
| 264 |
+
let encodings = encode_windows(
|
| 265 |
+
&self.tokenizer,
|
| 266 |
+
text,
|
| 267 |
+
self.config.max_length,
|
| 268 |
+
self.config.stride,
|
| 269 |
+
)?;
|
| 270 |
+
let mut findings = Vec::new();
|
| 271 |
+
let mut windows = Vec::new();
|
| 272 |
+
for (index, e) in encodings.iter().enumerate() {
|
| 273 |
+
let n = e.len();
|
| 274 |
+
let ids: Vec<i64> = e.get_ids().iter().map(|x| *x as i64).collect();
|
| 275 |
+
let mask: Vec<i64> = e.get_attention_mask().iter().map(|x| *x as i64).collect();
|
| 276 |
+
let outputs=self.session.run(ort::inputs!["input_ids"=>Tensor::from_array(([1,n],ids.clone()))?,"attention_mask"=>Tensor::from_array(([1,n],mask.clone()))?])?;
|
| 277 |
+
let (shape, logits) = outputs["logits"].try_extract_tensor::<f32>()?;
|
| 278 |
+
ensure!(
|
| 279 |
+
shape.as_ref() == [1, n as i64, 15],
|
| 280 |
+
"unexpected logits shape: {shape:?}"
|
| 281 |
+
);
|
| 282 |
+
ensure!(logits.iter().all(|x| x.is_finite()), "non-finite logits");
|
| 283 |
+
let mut decoded = decode(e, logits, &self.config.labels);
|
| 284 |
+
mark_boundaries(e, &mut decoded, index > 0, index + 1 < encodings.len());
|
| 285 |
+
if n == self.config.max_length {
|
| 286 |
+
let content: Vec<_> = e.get_offsets().iter().filter(|(a, b)| a != b).collect();
|
| 287 |
+
if let (Some(first), Some(last)) = (content.first(), content.last()) {
|
| 288 |
+
ensure!(
|
| 289 |
+
!decoded
|
| 290 |
+
.iter()
|
| 291 |
+
.any(|f| f.start == first.0 && f.end == last.1),
|
| 292 |
+
"entity spans a full window; complete entity boundary is unsupported"
|
| 293 |
+
);
|
| 294 |
+
}
|
| 295 |
+
}
|
| 296 |
+
findings.extend(decoded);
|
| 297 |
+
if debug {
|
| 298 |
+
windows.push(json!({"input_ids":ids,"attention_mask":mask,"offsets":e.get_offsets(),"special_tokens_mask":e.get_special_tokens_mask(),"logits":logits.chunks_exact(15).collect::<Vec<_>>()}));
|
| 299 |
+
}
|
| 300 |
+
}
|
| 301 |
+
let mut findings = resolve(findings);
|
| 302 |
+
if let Some(c) = &self.config.calibration {
|
| 303 |
+
for f in &mut findings {
|
| 304 |
+
f.confidence = Some(c.apply(f.raw_score));
|
| 305 |
+
}
|
| 306 |
+
if let Some(t) = c.threshold {
|
| 307 |
+
findings.retain(|f| f.confidence.unwrap() >= t);
|
| 308 |
+
}
|
| 309 |
+
}
|
| 310 |
+
let redacted = redact(text, &findings)?;
|
| 311 |
+
let mut result =
|
| 312 |
+
json!({"findings":findings,"contains_pii":!findings.is_empty(),"redacted":redacted});
|
| 313 |
+
if debug {
|
| 314 |
+
result["windows"] = json!(windows);
|
| 315 |
+
}
|
| 316 |
+
Ok(result)
|
| 317 |
+
}
|
| 318 |
+
}
|
| 319 |
+
fn main() -> Result<()> {
|
| 320 |
+
let args: Vec<String> = std::env::args().skip(1).collect();
|
| 321 |
+
if args.is_empty() {
|
| 322 |
+
bail!("usage: datafog-pii BUNDLE [--debug]; stdin JSONL objects with text:string");
|
| 323 |
+
}
|
| 324 |
+
ensure!(
|
| 325 |
+
args.len() == 1 || (args.len() == 2 && args[1] == "--debug"),
|
| 326 |
+
"unknown arguments"
|
| 327 |
+
);
|
| 328 |
+
let mut detector = Detector::load(Path::new(&args[0])).context("loading detector")?;
|
| 329 |
+
let debug = args.len() == 2;
|
| 330 |
+
let stdin = io::stdin();
|
| 331 |
+
let mut stdout = io::BufWriter::new(io::stdout().lock());
|
| 332 |
+
for line in stdin.lock().lines() {
|
| 333 |
+
let line = line?;
|
| 334 |
+
let result = (|| -> Result<_> {
|
| 335 |
+
let input: serde_json::Value = serde_json::from_str(&line)?;
|
| 336 |
+
let text = input
|
| 337 |
+
.get("text")
|
| 338 |
+
.and_then(|t| t.as_str())
|
| 339 |
+
.context("expected text:string")?;
|
| 340 |
+
detector.predict(text, debug)
|
| 341 |
+
})();
|
| 342 |
+
let output = match result {
|
| 343 |
+
Ok(v) => v,
|
| 344 |
+
Err(e) => json!({"error":format!("{e:#}")}),
|
| 345 |
+
};
|
| 346 |
+
writeln!(stdout, "{}", serde_json::to_string(&output)?)?;
|
| 347 |
+
stdout.flush()?;
|
| 348 |
+
}
|
| 349 |
+
Ok(())
|
| 350 |
+
}
|
| 351 |
+
#[cfg(test)]
|
| 352 |
+
mod tests {
|
| 353 |
+
use super::*;
|
| 354 |
+
#[test]
|
| 355 |
+
fn unicode_overlap_and_adjacent() {
|
| 356 |
+
let f = |start, end, score| Finding {
|
| 357 |
+
label: "first_name".into(),
|
| 358 |
+
start,
|
| 359 |
+
end,
|
| 360 |
+
raw_score: score,
|
| 361 |
+
confidence: None,
|
| 362 |
+
boundary_fragment: false,
|
| 363 |
+
};
|
| 364 |
+
let spans = resolve(vec![f(5, 8, 0.6), f(5, 6, 0.9), f(6, 8, 0.8), f(5, 6, 0.7)]);
|
| 365 |
+
assert_eq!(spans.len(), 2);
|
| 366 |
+
assert_eq!(
|
| 367 |
+
redact("🙂 José", &spans).unwrap(),
|
| 368 |
+
"🙂 [REDACTED][REDACTED]é"
|
| 369 |
+
);
|
| 370 |
+
assert!(redact("🙂", &[f(1, 3, 1.)]).is_err());
|
| 371 |
+
}
|
| 372 |
+
#[test]
|
| 373 |
+
fn oracle_boundaries_and_complete_token_coverage() {
|
| 374 |
+
let fixture: serde_json::Value =
|
| 375 |
+
serde_json::from_str(include_str!("../tests/boundary-oracle.json")).unwrap();
|
| 376 |
+
let mut tokenizer = Tokenizer::from_file(
|
| 377 |
+
Path::new(env!("CARGO_MANIFEST_DIR")).join("../bundle/tokenizer.json"),
|
| 378 |
+
)
|
| 379 |
+
.unwrap();
|
| 380 |
+
tokenizer.with_truncation(None).unwrap();
|
| 381 |
+
tokenizer.with_padding(None);
|
| 382 |
+
let labels: Vec<String> = serde_json::from_value(fixture["labels"].clone()).unwrap();
|
| 383 |
+
for case in fixture["cases"].as_array().unwrap() {
|
| 384 |
+
let text = case["text"].as_str().unwrap();
|
| 385 |
+
let windows = encode_windows(&tokenizer, text, 512, 42).unwrap();
|
| 386 |
+
assert_eq!(windows.len(), case["windows"].as_array().unwrap().len());
|
| 387 |
+
let mut findings = Vec::new();
|
| 388 |
+
let mut covered = Vec::new();
|
| 389 |
+
for (i, e) in windows.iter().enumerate() {
|
| 390 |
+
let oracle = &case["windows"][i];
|
| 391 |
+
assert_eq!(
|
| 392 |
+
serde_json::to_value(e.get_ids()).unwrap(),
|
| 393 |
+
oracle["input_ids"]
|
| 394 |
+
);
|
| 395 |
+
assert_eq!(
|
| 396 |
+
serde_json::to_value(e.get_offsets()).unwrap(),
|
| 397 |
+
oracle["offsets"]
|
| 398 |
+
);
|
| 399 |
+
covered.extend_from_slice(&e.get_ids()[if i == 0 { 1 } else { 43 }..e.len() - 1]);
|
| 400 |
+
let logits: Vec<Vec<f32>> =
|
| 401 |
+
serde_json::from_value(oracle["logits"].clone()).unwrap();
|
| 402 |
+
let mut f = decode(e, &logits.concat(), &labels);
|
| 403 |
+
mark_boundaries(e, &mut f, i > 0, i + 1 < windows.len());
|
| 404 |
+
findings.extend(f);
|
| 405 |
+
}
|
| 406 |
+
let full = tokenizer.encode(text, false).unwrap();
|
| 407 |
+
assert_eq!(covered, full.get_ids());
|
| 408 |
+
let actual = resolve(findings);
|
| 409 |
+
let spans: Vec<_> = actual
|
| 410 |
+
.iter()
|
| 411 |
+
.map(|f| json!({"label":f.label,"start":f.start,"end":f.end}))
|
| 412 |
+
.collect();
|
| 413 |
+
assert_eq!(json!(spans), case["expected"]);
|
| 414 |
+
assert_eq!(
|
| 415 |
+
redact(text, &actual).unwrap(),
|
| 416 |
+
case["redacted"].as_str().unwrap()
|
| 417 |
+
);
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
#[test]
|
| 421 |
+
fn interpolation() {
|
| 422 |
+
let c = Calibration {
|
| 423 |
+
x: vec![0.2, 0.8],
|
| 424 |
+
y: vec![0.1, 0.9],
|
| 425 |
+
threshold: None,
|
| 426 |
+
};
|
| 427 |
+
assert!((c.apply(0.5) - 0.5).abs() < 1e-12);
|
| 428 |
+
assert_eq!(c.apply(0.), 0.1);
|
| 429 |
+
assert_eq!(c.apply(1.), 0.9);
|
| 430 |
+
}
|
| 431 |
+
}
|
runtime/tests/boundary-oracle.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
tokenizer_config.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"backend": "tokenizers",
|
| 3 |
+
"cls_token": "[CLS]",
|
| 4 |
+
"do_lower_case": false,
|
| 5 |
+
"is_local": true,
|
| 6 |
+
"local_files_only": true,
|
| 7 |
+
"mask_token": "[MASK]",
|
| 8 |
+
"model_max_length": 512,
|
| 9 |
+
"pad_token": "[PAD]",
|
| 10 |
+
"sep_token": "[SEP]",
|
| 11 |
+
"strip_accents": null,
|
| 12 |
+
"tokenize_chinese_chars": true,
|
| 13 |
+
"tokenizer_class": "BertTokenizer",
|
| 14 |
+
"unk_token": "[UNK]"
|
| 15 |
+
}
|
tools/install_bundle.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Install a verified, already-downloaded snapshot; inference stays offline."""
|
| 3 |
+
import argparse
|
| 4 |
+
import hashlib
|
| 5 |
+
import json
|
| 6 |
+
import platform
|
| 7 |
+
import shutil
|
| 8 |
+
import tarfile
|
| 9 |
+
import tempfile
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def digest(path):
|
| 14 |
+
with path.open('rb') as stream:
|
| 15 |
+
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def verify(root):
|
| 19 |
+
manifest = json.loads((root / 'manifest.json').read_text())
|
| 20 |
+
for name, metadata in manifest['files'].items():
|
| 21 |
+
path = root / name
|
| 22 |
+
if Path(name).is_absolute() or '..' in Path(name).parts:
|
| 23 |
+
raise ValueError('Unsafe manifest path')
|
| 24 |
+
if not path.is_file() or path.stat().st_size != metadata['bytes'] or digest(path) != metadata['sha256']:
|
| 25 |
+
raise ValueError(f'Integrity check failed: {name}')
|
| 26 |
+
return manifest
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def install(snapshot, destination):
|
| 30 |
+
verify(snapshot)
|
| 31 |
+
if platform.system() != 'Darwin' or platform.machine() not in ('arm64', 'aarch64'):
|
| 32 |
+
raise ValueError('This release archive is qualified only for macOS ARM64; see PLATFORMS.md')
|
| 33 |
+
version = platform.mac_ver()[0]
|
| 34 |
+
if version and tuple(map(int, version.split('.')[:2])) < (14, 0):
|
| 35 |
+
raise ValueError('The preserved dylib requires macOS 14.0 or newer')
|
| 36 |
+
if destination.exists():
|
| 37 |
+
raise ValueError('Destination already exists; select a new directory')
|
| 38 |
+
destination.parent.mkdir(parents=True, exist_ok=True)
|
| 39 |
+
with tempfile.TemporaryDirectory(dir=destination.parent) as temporary:
|
| 40 |
+
stage = Path(temporary) / 'bundle'
|
| 41 |
+
stage.mkdir()
|
| 42 |
+
with tarfile.open(snapshot / 'archives/datafog-pii-v0.1.0-macos-arm64.tar.gz') as archive:
|
| 43 |
+
for member in archive.getmembers():
|
| 44 |
+
if member.name.startswith('/') or '..' in Path(member.name).parts or not (member.isfile() or member.isdir()):
|
| 45 |
+
raise ValueError('Unsafe archive member')
|
| 46 |
+
archive.extractall(stage, filter='data')
|
| 47 |
+
(stage / 'datafog-pii').chmod(0o755)
|
| 48 |
+
# Validate extracted content independently, including executable and notices.
|
| 49 |
+
checksums = json.loads((stage / 'checksums.json').read_text())
|
| 50 |
+
for name, expected in checksums.items():
|
| 51 |
+
if digest(stage / name) != expected:
|
| 52 |
+
raise ValueError(f'Extracted bundle checksum failed: {name}')
|
| 53 |
+
stage.rename(destination)
|
| 54 |
+
return destination
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
if __name__ == '__main__':
|
| 58 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 59 |
+
parser.add_argument('--snapshot', required=True, type=Path)
|
| 60 |
+
parser.add_argument('--destination', required=True, type=Path)
|
| 61 |
+
args = parser.parse_args()
|
| 62 |
+
print(install(args.snapshot.resolve(), args.destination.resolve()))
|