granuscore 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
granuscore/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ from .granularity_predictor import HitGranularityPredictor
2
+ from .pipeline import GranuScore
3
+
4
+ def granuscore(
5
+ texts,
6
+ **kwargs,
7
+ ):
8
+ return GranuScore()(
9
+ texts,
10
+ **kwargs,
11
+ )
12
+
13
+ __all__ = ["GranuScore", "granuscore", "HitGranularityPredictor"]
@@ -0,0 +1,116 @@
1
+ import hashlib
2
+ import os
3
+ import shutil
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ import requests
8
+ from importlib.resources import files
9
+ from platformdirs import user_cache_dir
10
+
11
+
12
+ def _sha256(path: Path) -> str:
13
+ h = hashlib.sha256()
14
+ with path.open("rb") as f:
15
+ for chunk in iter(lambda: f.read(1024 * 1024), b""):
16
+ h.update(chunk)
17
+ return h.hexdigest()
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class ArtifactSpec:
22
+ name: str
23
+ subdir: str # "faiss" or "models"
24
+ url: str | None = None # if None: must be bundled or already cached
25
+ sha256: str | None = None
26
+
27
+
28
+ class ArtifactManager:
29
+ """
30
+ Resolution order:
31
+ 1) cache (and sha256 if provided)
32
+ 2) bundled assets inside the package -> copied to cache
33
+ 3) download from spec.url (public) -> cached
34
+ """
35
+
36
+ def __init__(self, app_name: str = "granuscore"):
37
+ self.cache_root = Path(user_cache_dir(app_name))
38
+ self.cache_root.mkdir(parents=True, exist_ok=True)
39
+
40
+ def ensure(self, spec: ArtifactSpec, force: bool = False) -> Path:
41
+ dst_dir = self.cache_root / spec.subdir
42
+ dst_dir.mkdir(parents=True, exist_ok=True)
43
+ dst = dst_dir / spec.name
44
+
45
+ # 1) cache hit
46
+ if dst.exists() and not force:
47
+ if spec.sha256 is None or _sha256(dst) == spec.sha256:
48
+ return dst
49
+ dst.unlink(missing_ok=True)
50
+
51
+ # 2) bundled asset
52
+ bundled = files("granuscore") / "../../assets" / spec.subdir / spec.name
53
+ if bundled.is_file() and not force:
54
+ self._copy_to_cache(bundled, dst)
55
+ self._verify_hash_if_needed(dst, spec)
56
+ return dst
57
+
58
+ # 3) public download
59
+ if not spec.url:
60
+ raise FileNotFoundError(
61
+ f"{spec.name} not found in cache and not bundled, and no public download URL provided."
62
+ )
63
+
64
+ self._download_public(spec.url, dst)
65
+ self._verify_hash_if_needed(dst, spec)
66
+ return dst
67
+
68
+ def _verify_hash_if_needed(self, path: Path, spec: ArtifactSpec) -> None:
69
+ if spec.sha256 is None:
70
+ return
71
+ got = _sha256(path)
72
+ if got != spec.sha256:
73
+ path.unlink(missing_ok=True)
74
+ raise ValueError(
75
+ f"Hash mismatch for {spec.name}. Expected {spec.sha256}, got {got}."
76
+ )
77
+
78
+ def _copy_to_cache(self, src_path: Path, dst: Path) -> None:
79
+ tmp = dst.with_suffix(dst.suffix + ".part")
80
+ with src_path.open("rb") as src, tmp.open("wb") as out:
81
+ shutil.copyfileobj(src, out)
82
+ tmp.replace(dst)
83
+
84
+ def _download_public(self, url: str, dst: Path) -> None:
85
+ tmp = dst.with_suffix(dst.suffix + ".part")
86
+
87
+ with requests.get(url, stream=True, timeout=60, allow_redirects=True) as r:
88
+ # Common mistake: GitHub "blob" links download HTML
89
+ if r.status_code >= 400 and "github.com/" in url and "/blob/" in url:
90
+ raise requests.HTTPError(
91
+ "You are using a GitHub 'blob' URL (HTML). Use a release asset URL "
92
+ "(.../releases/download/...) or a raw.githubusercontent.com URL."
93
+ )
94
+
95
+ r.raise_for_status()
96
+ total = int(r.headers.get("Content-Length", 0)) or None
97
+
98
+ try:
99
+ from tqdm import tqdm # type: ignore
100
+ bar = tqdm(total=total, unit="B", unit_scale=True, desc=f"Downloading {dst.name}")
101
+ except Exception:
102
+ bar = None
103
+
104
+ try:
105
+ with tmp.open("wb") as f:
106
+ for chunk in r.iter_content(chunk_size=1024 * 1024):
107
+ if not chunk:
108
+ continue
109
+ f.write(chunk)
110
+ if bar is not None:
111
+ bar.update(len(chunk))
112
+ finally:
113
+ if bar is not None:
114
+ bar.close()
115
+
116
+ os.replace(tmp, dst)
@@ -0,0 +1,53 @@
1
+ import numpy as np
2
+ from scipy.stats import percentileofscore
3
+
4
+
5
+ class PercentileOutput:
6
+ def __init__(self, path):
7
+ self.reference_scores = np.load(path)
8
+ self.percentiles: dict[float, float] | None = None
9
+
10
+ def _compute_percentiles(self):
11
+ self.percentiles = {
12
+ p: float(np.percentile(self.reference_scores, p))
13
+ for p in [33.33, 66.66]
14
+ }
15
+
16
+ def score_to_percentile(self, scores: float | list[float] | np.ndarray):
17
+ scores = np.asarray(scores)
18
+ return percentileofscore(self.reference_scores, scores)
19
+
20
+ def percentile_to_score(self, percentiles: float | list[float] | np.ndarray):
21
+ is_scalar = np.isscalar(percentiles)
22
+ arr = np.asarray([percentiles] if is_scalar else percentiles, dtype=float)
23
+
24
+ scores = np.percentile(self.reference_scores, arr)
25
+
26
+ if is_scalar:
27
+ return float(scores[0])
28
+ return scores
29
+
30
+ def bucket(self, scores: float | list[float] | np.ndarray) -> str:
31
+ """
32
+ Assign a granularity bucket based on score-space percentiles.
33
+
34
+ Args:
35
+ scores:
36
+ Granuscore percentile value.
37
+ Returns:
38
+ One of: "low", "medium", "high"
39
+ """
40
+ if self.percentiles is None:
41
+ self._compute_percentiles()
42
+
43
+ q1 = 33.33
44
+ q2 = 66.66
45
+
46
+ is_scalar = np.isscalar(scores)
47
+ arr = np.asarray([scores] if is_scalar else scores, dtype=float)
48
+
49
+ labels = np.where(arr < q1, "low", np.where(arr < q2, "medium", "high"))
50
+
51
+ if is_scalar:
52
+ return str(labels[0])
53
+ return labels.tolist()
granuscore/cache.py ADDED
@@ -0,0 +1,189 @@
1
+ from typing import List, Sequence, Protocol
2
+ from pathlib import Path
3
+ import sqlite3
4
+ import hashlib
5
+ import os
6
+ import threading
7
+ import numpy as np
8
+
9
+ __all__ = ["GranuscoreCache"]
10
+
11
+
12
+ def _md5(s: str) -> str:
13
+ return hashlib.md5(s.encode()).hexdigest()
14
+
15
+
16
+ class _Scorer(Protocol):
17
+ def score_wo_cache(self, answers: Sequence[str], encoding_batch_size: int | None = None) -> np.ndarray | list: ...
18
+
19
+
20
+ class GranuscoreCache:
21
+ """Thin wrapper around the Granuscore with SQLite caching."""
22
+
23
+ def __init__(
24
+ self,
25
+ scorer: "_Scorer",
26
+ hierarchy_model: str,
27
+ faiss_index: str,
28
+ lgb_model: str,
29
+ cache_dir: str | os.PathLike = None,
30
+ ) -> None:
31
+ self.scorer = scorer
32
+ self.hierarchy_model = hierarchy_model
33
+ self.faiss_index = faiss_index
34
+ self.lgb_model = lgb_model
35
+
36
+ if cache_dir is None:
37
+ cache_dir = os.getenv("GRANUSCORE_CACHE_DIR", f"{Path(__file__).parent}/.cache") # default is .cache dir next to this file
38
+ cache_path = Path(cache_dir)
39
+ cache_path.mkdir(parents=True, exist_ok=True)
40
+ self.db_path = cache_path / "granuscores.db"
41
+
42
+ self._init_db()
43
+
44
+ self._w_lock = threading.Lock()
45
+
46
+ def get(self, text: str) -> float:
47
+ """Return the granuscore for *text*, fetching from cache or scoring."""
48
+ text_hash = _md5(text)
49
+
50
+ # 1. try cache ------------------------------------------------------
51
+ row = self._select(text_hash)
52
+ if row is not None:
53
+ return row
54
+
55
+ score = self.scorer.score_wo_cache([text])[0]
56
+ self._insert(text_hash, text, score)
57
+ return score
58
+
59
+ def batch(self, texts: Sequence[str], encoding_batch_size: int | None = None) -> List[float]:
60
+ texts = list(texts)
61
+ if not texts:
62
+ return []
63
+
64
+ # Precompute hashes and preserve order
65
+ hashes = [_md5(t) for t in texts]
66
+
67
+ # 1) Bulk fetch cache hits
68
+ hit_map = self._select_many(hashes) # {text_hash: granuscore}
69
+
70
+ # 2) Identify misses (preserve order)
71
+ miss_texts = [t for t, h in zip(texts, hashes) if h not in hit_map]
72
+ miss_hashes = [h for h in hashes if h not in hit_map]
73
+
74
+ # 3) Score misses in one model call
75
+ if miss_texts:
76
+ miss_scores = self.scorer.score_wo_cache(miss_texts, encoding_batch_size)
77
+ miss_scores = np.asarray(miss_scores, dtype=float)
78
+
79
+ # 4) Bulk insert misses
80
+ self._insert_many(miss_hashes, miss_texts, miss_scores)
81
+
82
+ # update hit_map so we can fill outputs
83
+ hit_map.update({h: float(s) for h, s in zip(miss_hashes, miss_scores)})
84
+
85
+ # 5) Build output aligned to input order
86
+ return [float(hit_map[h]) for h in hashes]
87
+
88
+ # ------------------------------------------------------------------
89
+ # private db helpers
90
+ # ------------------------------------------------------------------
91
+ def _open_conn(self) -> sqlite3.Connection:
92
+ conn = sqlite3.connect(
93
+ self.db_path,
94
+ timeout=30,
95
+ check_same_thread=False,
96
+ isolation_level=None # autocommit
97
+ )
98
+ conn.execute("PRAGMA journal_mode=WAL;")
99
+ conn.execute("PRAGMA synchronous=NORMAL;")
100
+ return conn
101
+
102
+ def _init_db(self) -> None:
103
+ with sqlite3.connect(self.db_path) as conn:
104
+ conn.execute(
105
+ """
106
+ CREATE TABLE IF NOT EXISTS granuscores (
107
+ text_hash TEXT,
108
+ hierarchy_model TEXT,
109
+ faiss_index TEXT,
110
+ lgb_model TEXT,
111
+ granuscore REAL NOT NULL,
112
+ text TEXT,
113
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
114
+ PRIMARY KEY(text_hash, hierarchy_model, faiss_index, lgb_model)
115
+ )
116
+ """
117
+ )
118
+
119
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_model ON granuscores(hierarchy_model, faiss_index, lgb_model);")
120
+
121
+ def _select(self, text_hash: str) -> float | None:
122
+ with sqlite3.connect(self.db_path) as conn:
123
+ row = conn.execute(
124
+ "SELECT granuscore FROM granuscores WHERE text_hash=? AND hierarchy_model=? AND faiss_index=? AND lgb_model=?",
125
+ (text_hash, self.hierarchy_model, self.faiss_index, self.lgb_model),
126
+ ).fetchone()
127
+ if row:
128
+ return row[0]
129
+ return None
130
+
131
+ def _select_many(self, text_hashes: Sequence[str], chunk_size: int = 900) -> dict[str, float]:
132
+ """
133
+ Fetch many hashes at once. Chunked because SQLite has a limit on bound variables
134
+ (commonly 999).
135
+ """
136
+ out: dict[str, float] = {}
137
+ if not text_hashes:
138
+ return out
139
+
140
+ with sqlite3.connect(self.db_path) as conn:
141
+ for i in range(0, len(text_hashes), chunk_size):
142
+ chunk = text_hashes[i:i + chunk_size]
143
+ placeholders = ",".join(["?"] * len(chunk))
144
+
145
+ rows = conn.execute(
146
+ f"""
147
+ SELECT text_hash, granuscore
148
+ FROM granuscores
149
+ WHERE hierarchy_model = ?
150
+ AND faiss_index = ?
151
+ AND lgb_model = ?
152
+ AND text_hash IN ({placeholders})
153
+ """,
154
+ (self.hierarchy_model, self.faiss_index, self.lgb_model, *chunk),
155
+ ).fetchall()
156
+
157
+ out.update({h: float(s) for h, s in rows})
158
+
159
+ return out
160
+
161
+ def _insert(self, text_hash: str, text: str, granusccore: float) -> None:
162
+ with sqlite3.connect(self.db_path) as conn:
163
+ conn.execute(
164
+ "INSERT OR REPLACE INTO granuscores (text_hash, hierarchy_model, faiss_index, lgb_model, granuscore, text) VALUES (?,?,?,?,?,?)",
165
+ (text_hash, self.hierarchy_model, self.faiss_index, self.lgb_model, granusccore, text),
166
+ )
167
+
168
+ def _insert_many(self, hashes: Sequence[str], texts: Sequence[str], scores: Sequence[float]) -> None:
169
+ """
170
+ Bulk insert inside one transaction. Much faster than opening a connection per row.
171
+ """
172
+ if not hashes:
173
+ return
174
+
175
+ rows = [
176
+ (h, self.hierarchy_model, self.faiss_index, self.lgb_model, float(s), t)
177
+ for h, t, s in zip(hashes, texts, scores)
178
+ ]
179
+
180
+ with self._w_lock:
181
+ with sqlite3.connect(self.db_path) as conn:
182
+ conn.executemany(
183
+ """
184
+ INSERT OR REPLACE INTO granuscores
185
+ (text_hash, hierarchy_model, faiss_index, lgb_model, granuscore, text)
186
+ VALUES (?, ?, ?, ?, ?, ?)
187
+ """,
188
+ rows,
189
+ )
@@ -0,0 +1,300 @@
1
+ """
2
+ Claim splitting module for extracting atomic facts from text.
3
+
4
+ This module provides various strategies for splitting text into atomic claims,
5
+ from simple word-based splitting to sophisticated LLM-based extraction.
6
+ """
7
+
8
+ from abc import ABC, abstractmethod
9
+ from typing import List, Sequence, Literal, Dict, Any
10
+
11
+ import spacy
12
+ from spacy.matcher import Matcher
13
+
14
+ from granuscore.text_cleaner import TextCleaner
15
+ from granuscore.utils import ensure_spacy_model
16
+
17
+
18
+ SplitScope = Literal["document", "sentence"]
19
+
20
+
21
+ class UnitSplitter(ABC):
22
+ """
23
+ Abstract base class for unit splitting strategies.
24
+
25
+ Defines the interface for extracting referential units from text,
26
+ supporting both single-text and batch processing.
27
+ """
28
+
29
+ def __call__(self, batch: List[str], split_scope: SplitScope = "document") -> list[list[dict[str, Any]]]:
30
+ """Allow callable interface for batch processing."""
31
+ return self.get_referential_units_batch(batch, split_scope)
32
+
33
+ @abstractmethod
34
+ def get_referential_units(self, text: str, split_scope: SplitScope = "document") -> list[dict[str, Any]]:
35
+ """
36
+ Extract referential units from a single text.
37
+
38
+ Args:
39
+ text: Input text to process
40
+ split_scope: Defines the structural scope over inside which referential units should be split.
41
+ "document" splits the whole text.
42
+ "sentence" applies splitting within each sentence.
43
+
44
+ Returns:
45
+ List[Dict[str, Any]]
46
+ One entry per group (document or sentence) with:
47
+ - text: str
48
+ - referential_units: List[str]
49
+ """
50
+ pass
51
+
52
+ @abstractmethod
53
+ def get_referential_units_batch(self, texts: Sequence[str], split_scope: SplitScope = "document") -> list[list[dict[str, Any]]]:
54
+ """
55
+ Extract referential units from multiple texts.
56
+
57
+ Args:
58
+ texts: List of input texts to process
59
+ split_scope: Defines the structural scope over inside which referential units should be split.
60
+ "document" splits the whole text.
61
+ "sentence" applies splitting within each sentence.
62
+
63
+ Returns:
64
+ List[List[Dict[str, Any]]]:
65
+ A nested list structure where:
66
+ - the outer list indexes input texts,
67
+ - the second level indexes document- or sentence-level groups
68
+ (depending on ``split_scope``),
69
+ - each group is represented as a dictionary with the following keys:
70
+ - ``"text"`` (str):
71
+ The text span corresponding to the group.
72
+ - ``"referential_units"`` (List[str]):
73
+ The extracted referential units for the group.
74
+ """
75
+ pass
76
+
77
+
78
+ class SpacyNounPhraseSplitter(UnitSplitter):
79
+ """
80
+ Spacy-based noun phrase extractor.
81
+
82
+ Extracts noun phrases and significant tokens using spaCy's NLP pipeline.
83
+ Includes cleaning options for academic text (LaTeX, figures, etc.).
84
+ """
85
+
86
+ def __init__(
87
+ self,
88
+ model: str = "en_core_web_sm",
89
+ skip_brackets: bool = False,
90
+ skip_latex: bool = False,
91
+ skip_arxiv_meta: bool = False,
92
+ skip_figures: bool = False,
93
+ skip_pdf_noise: bool = False,
94
+ ):
95
+ """
96
+ Initialize with spaCy model and cleaning options.
97
+
98
+ Args:
99
+ model: spaCy model name to load
100
+ skip_brackets: Remove bracketed content
101
+ skip_latex: Remove LaTeX blocks
102
+ skip_arxiv_meta: Remove arXiv metadata
103
+ skip_figures: Remove figure captions
104
+ skip_pdf_noise: Remove PDF noise
105
+ """
106
+ ensure_spacy_model("en_core_web_sm")
107
+ self.nlp = spacy.load(model)
108
+ self._add_et_al_merging()
109
+ self.clean_kwargs = {
110
+ 'skip_brackets': skip_brackets,
111
+ 'skip_latex': skip_latex,
112
+ 'skip_arxiv_meta': skip_arxiv_meta,
113
+ 'skip_figures': skip_figures,
114
+ 'skip_pdf_noise': skip_pdf_noise,
115
+ }
116
+
117
+ def _add_et_al_merging(self):
118
+ """Add custom component to merge 'et al' into single tokens."""
119
+ matcher = Matcher(self.nlp.vocab)
120
+ matcher.add(
121
+ "ET_AL",
122
+ [[
123
+ {"IS_TITLE": True},
124
+ {"LOWER": "et"},
125
+ {"LOWER": "al"},
126
+ ]]
127
+ )
128
+
129
+ @spacy.Language.component("merge_et_al")
130
+ def merge_et_al(doc):
131
+ matches = matcher(doc)
132
+ with doc.retokenize() as retokenizer:
133
+ for _, start, end in matches:
134
+ retokenizer.merge(doc[start:end])
135
+ return doc
136
+
137
+ self.nlp.add_pipe("merge_et_al", before="parser")
138
+
139
+ @staticmethod
140
+ def _is_valid_noun_chunk(chunk) -> bool:
141
+ """Check if a noun chunk is valid for extraction."""
142
+ # Must be nominal
143
+ if chunk.root.pos_ not in {"NOUN", "PROPN", "PRON"}:
144
+ return False
145
+
146
+ # Reject punctuation-only
147
+ if all(tok.is_punct for tok in chunk):
148
+ return False
149
+
150
+ # Reject single hyphen or fragments
151
+ if chunk.text.strip() in {"-", "–", "—"}:
152
+ return False
153
+
154
+ return True
155
+
156
+
157
+ def _extract_from_doclike(self, text) -> list[str]:
158
+ """
159
+ Extract noun phrases and significant tokens from a spaCy Doc or Span.
160
+ Returns a flat list of units for that segment.
161
+ """
162
+ doclike = self.nlp(text)
163
+
164
+ spans = {
165
+ (chunk.start, chunk.end)
166
+ for chunk in doclike.noun_chunks
167
+ if self._is_valid_noun_chunk(chunk)
168
+ }
169
+
170
+ results: List[str] = []
171
+ i = 0
172
+ while i < len(doclike):
173
+ for start, end in spans:
174
+ if i == start:
175
+ results.append(doclike[start:end].text)
176
+ i = end
177
+ break
178
+ else:
179
+ tok = doclike[i]
180
+ if (not tok.is_stop or tok.like_num) and any(c.isalnum() for c in tok.text):
181
+ results.append(tok.text)
182
+ i += 1
183
+
184
+ return results
185
+
186
+ def get_referential_units(self, text: str, split_scope: SplitScope = "document") -> list[dict[str, Any]]:
187
+ """
188
+ Extract noun phrases and significant tokens, optionally grouped by sentence.
189
+
190
+ Returns
191
+ -------
192
+ List[Dict[str, Any]]
193
+ One entry per group (document or sentence) with:
194
+ - referential_units: List[str]
195
+ - cleaned_text: str
196
+ - original_text: str
197
+ """
198
+ text = TextCleaner.clean(text, **self.clean_kwargs)
199
+
200
+ if split_scope == "document":
201
+ return [{
202
+ "referential_units": self._extract_from_doclike(text),
203
+ "text": text,
204
+ }]
205
+
206
+ if split_scope == "sentence":
207
+ doc = self.nlp(text)
208
+ groups: List[Dict[str, Any]] = []
209
+ for sent in doc.sents:
210
+ sent_text = sent.text.strip()
211
+ if not sent_text:
212
+ continue
213
+ groups.append({
214
+ "referential_units": self._extract_from_doclike(sent_text),
215
+ "text": sent_text,
216
+ })
217
+ if not groups:
218
+ groups.append({
219
+ "referential_units": [],
220
+ "text": text,
221
+ })
222
+ return groups
223
+
224
+ raise ValueError('split_scope must be either "document" or "sentence"')
225
+
226
+ def get_referential_units_batch(self, texts: Sequence[str], split_scope: SplitScope = 'document') -> list[list[dict[str, Any]]]:
227
+ """Process multiple texts."""
228
+ return [self.get_referential_units(text, split_scope) for text in texts]
229
+
230
+
231
+ class SpacyObjectSplitter(SpacyNounPhraseSplitter):
232
+ DEP_KEEP = {
233
+ "nsubj", "nsubjpass", # subjects
234
+ "dobj", "obj", "iobj", # objects
235
+ "obl", # oblique nominal (where/when/how)
236
+ }
237
+
238
+ def _extract_from_doclike(self, text: str) -> list[str]:
239
+ """
240
+ Extract units in document order:
241
+ 1) noun chunks (preferred)
242
+ 2) otherwise, subject/object-like tokens (dep-based)
243
+ """
244
+ doc = self.nlp(text)
245
+
246
+ # spans for noun chunks
247
+ spans = {
248
+ (chunk.start, chunk.end)
249
+ for chunk in doc.noun_chunks
250
+ if self._is_valid_noun_chunk(chunk)
251
+ }
252
+ start_to_end = {start: end for start, end in spans}
253
+
254
+ results = []
255
+ seen = set()
256
+
257
+ i = 0
258
+ while i < len(doc):
259
+ end = start_to_end.get(i)
260
+ if end is not None:
261
+ text_unit = doc[i:end].text.strip()
262
+ if text_unit and text_unit not in seen:
263
+ results.append(text_unit)
264
+ seen.add(text_unit)
265
+ i = end
266
+ continue
267
+
268
+ tok = doc[i]
269
+ if tok.dep_ in self.DEP_KEEP and any(c.isalnum() for c in tok.text):
270
+ if tok.text not in seen:
271
+ results.append(tok.text)
272
+ seen.add(tok.text)
273
+
274
+ i += 1
275
+
276
+ return results
277
+
278
+ if __name__ == '__main__':
279
+ # Example: Using the noun phrase splitter
280
+ splitter = SpacyNounPhraseSplitter(
281
+ skip_brackets=True,
282
+ skip_latex=True,
283
+ skip_figures=True,
284
+ skip_arxiv_meta=True,
285
+ skip_pdf_noise=True
286
+ )
287
+
288
+ sample_texts = [
289
+ " O'Reilly",
290
+ "The population is 12,000 (approx.)."
291
+ "three",
292
+ "Two."
293
+ ]
294
+
295
+ results = splitter.get_referential_units_batch(sample_texts)
296
+
297
+ for i, claims in enumerate(results, 1):
298
+ print(f"\nText {i} claims:")
299
+ for claim in claims:
300
+ print(f" - {claim}")