granuscore 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Granuscore Authors
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.
@@ -0,0 +1,5 @@
1
+ prune assets
2
+ prune evaluation
3
+ prune training_scripts
4
+ prune notebooks
5
+ prune data
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.4
2
+ Name: granuscore
3
+ Version: 1.0.0
4
+ Summary: Granularity scoring for natural language
5
+ Author-email: Lukas Ellinger <lukas.ellinger@tum.de>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/lukasellinger/granuscore
8
+ Project-URL: Issues, https://github.com/lukasellinger/granuscore/issues
9
+ Keywords: granularity,natural language processing,nlp,text analysis,semantic abstraction,specificity
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.9
12
+ Classifier: Programming Language :: Python :: 3.10
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Topic :: Text Processing :: Linguistic
18
+ Requires-Python: <3.13,>=3.9
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: faiss-cpu>=1.13.2
22
+ Requires-Dist: hierarchy_transformers>=0.1.1
23
+ Requires-Dist: lightgbm>=4.6.0
24
+ Requires-Dist: spacy>=3.8.11
25
+ Requires-Dist: torch>=2.9.1
26
+ Requires-Dist: numpy>=1.26.4
27
+ Requires-Dist: platformdirs>=3.10.0
28
+ Requires-Dist: requests>=2.31.0
29
+ Requires-Dist: tqdm>=4.66.0
30
+ Requires-Dist: openai>=2.21.0
31
+ Provides-Extra: dev
32
+ Requires-Dist: optuna>=4.6.0; extra == "dev"
33
+ Requires-Dist: pygam>=0.12.0; extra == "dev"
34
+ Requires-Dist: python-dotenv>=1.2.1; extra == "dev"
35
+ Requires-Dist: ollama>=0.6.1; extra == "dev"
36
+ Requires-Dist: google-genai>=1.66.0; extra == "dev"
37
+ Requires-Dist: wget>=3.2; extra == "dev"
38
+ Dynamic: license-file
39
+
40
+ [![PyPI version](https://img.shields.io/pypi/v/granuscore.svg)](https://pypi.org/project/granuscore/)
41
+ [![GitHub](https://img.shields.io/badge/GitHub-Repository-black)](https://github.com/lukasellinger/granuscore)
42
+ # Granuscore
43
+
44
+ **Granuscore** is a Python library for measuring the *semantic granularity* of natural language text.
45
+
46
+ It provides an end-to-end pipeline that:
47
+
48
+ 1. splits text into referential units,
49
+ 2. assigns continuous granularity scores to each unit,
50
+ 3. aggregates these scores into document-level estimates.
51
+
52
+ Granuscore is designed for analyzing how *fine-grained* or *coarse-grained* textual expressions are in applications such as question answering, educational dialogue, summarization, and scientific writing.
53
+
54
+ ---
55
+
56
+ ## Installation
57
+
58
+ Install from PyPI:
59
+
60
+ ```bash
61
+ pip install granuscore
62
+ ```
63
+
64
+ Or install the latest development version locally:
65
+
66
+ ```bash
67
+ git clone https://github.com/lukasellinger/granuscore.git
68
+ cd granuscore
69
+ pip install -e .
70
+ ```
71
+
72
+ Optional development dependencies:
73
+
74
+ ```bash
75
+ pip install -e ".[dev]"
76
+ ```
77
+
78
+ ---
79
+
80
+ ## Quick Start
81
+
82
+ ```python
83
+ from granuscore import GranuScore
84
+
85
+ scorer = GranuScore()
86
+
87
+ text = """
88
+ Tony Hawk was born in San Diego.
89
+ """
90
+
91
+ score = scorer(text)
92
+
93
+ print(score)
94
+ ```
95
+
96
+ By default, Granuscore returns percentile scores, where higher values correspond to coarser-grained expressions.
97
+
98
+ ---
99
+
100
+ ## Default Configuration
101
+
102
+ The default configuration reproduces the setup used in the paper.
103
+
104
+ ```python
105
+ scorer = GranuScore()
106
+ ```
107
+
108
+ Equivalent to:
109
+
110
+ ```python
111
+ scorer = GranuScore(
112
+ predictor_type="hit",
113
+ )
114
+ ```
115
+
116
+ Default settings:
117
+
118
+ - `predictor_type="hit"`
119
+ - `model_name="Hierarchy-Transformers/HiT-MiniLM-L12-WordNetNoun"`
120
+ - `search_method="random_anchors"`
121
+ - `random_anchors_k=999`
122
+
123
+ Required artifacts such as:
124
+ - FAISS indices,
125
+ - anchor vectors,
126
+ - LightGBM models,
127
+ - and reference percentile distributions
128
+
129
+ are automatically downloaded and cached on first use.
130
+
131
+ ---
132
+
133
+ ## Important Compatibility Note
134
+
135
+ The default configuration works out of the box and is the recommended setup.
136
+
137
+ If you customize components such as:
138
+ - the embedding model,
139
+ - search method,
140
+ - FAISS index,
141
+ - anchor vectors,
142
+ - or LightGBM model,
143
+
144
+ you must ensure that all resources are compatible with each other.
145
+
146
+ For example, a LightGBM model trained using:
147
+
148
+ ```python
149
+ search_method="random_anchors"
150
+ ```
151
+
152
+ should not be combined with:
153
+
154
+ ```python
155
+ search_method="nearest_neighbor"
156
+ ```
157
+
158
+ Similarly, FAISS indices, anchor vectors, percentile reference distributions, and LightGBM models must originate from the same embedding space and training configuration.
159
+
160
+ Compatibility between custom resources is not validated automatically.
161
+
162
+ ---
163
+
164
+ ## Notebook Tutorial
165
+
166
+ An interactive introduction is available in:
167
+
168
+ ```text
169
+ notebooks/getting_started.ipynb
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Repository Structure
175
+
176
+ ```text
177
+ granuscore/
178
+ ├── src/
179
+ │ └── granuscore/
180
+ │ ├── pipeline.py
181
+ │ ├── granularity_predictor.py
182
+ │ ├── claim_splitter.py
183
+ │ ├── bucket_output.py
184
+ │ ├── cache.py
185
+ │ └── artifacts.py
186
+ ├── notebooks/
187
+ │ └── getting_started.ipynb
188
+ ├── training_scripts/
189
+ ├── evaluation/
190
+ ├── assets/
191
+ ├── data/ (needs to be externally downloaded)
192
+ ├── pyproject.toml
193
+ ├── LICENSE
194
+ └── README.md
195
+ ```
196
+
197
+ ---
198
+
199
+ ## Reproducing Paper Experiments
200
+
201
+ The datasets and precomputed resources required to reproduce the experiments from the paper are available here:
202
+
203
+ https://drive.google.com/drive/folders/1mJdUENOxHEiuYn-_f1KRQ1PZggXJDnb4?usp=sharing
204
+
205
+ Download the archive and extract it into the repository root:
206
+
207
+ ```bash
208
+ unzip data.zip
209
+ ```
210
+
211
+ This will create the expected directory structure used by the training and evaluation scripts.
212
+
213
+ ---
214
+
215
+ ## Training Pipeline
216
+
217
+ Training uses precomputed `.pkl` feature files.
218
+
219
+ 1. Generate precomputed datasets:
220
+
221
+ ```text
222
+ training_scripts/build_precalc_data/
223
+ ```
224
+
225
+ 2. Train LightGBM models:
226
+
227
+ ```bash
228
+ python training_scripts/train_lgb_models.py
229
+ ```
230
+
231
+ ---
232
+
233
+ ## Citation
234
+ Updated Citation information will be added after publication.
235
+
236
+ ```bibtex
237
+ @misc{ellinger2026granuscore,
238
+ title={Granuscore: A Reference-Free Measure of Granularity for Text Analysis and Question Answering},
239
+ author={Ellinger, Lukas and Fichtl, Alexander M. and Anschütz, Miriam and Groh, Georg},
240
+ year={2026}
241
+ }
242
+ ```
@@ -0,0 +1,203 @@
1
+ [![PyPI version](https://img.shields.io/pypi/v/granuscore.svg)](https://pypi.org/project/granuscore/)
2
+ [![GitHub](https://img.shields.io/badge/GitHub-Repository-black)](https://github.com/lukasellinger/granuscore)
3
+ # Granuscore
4
+
5
+ **Granuscore** is a Python library for measuring the *semantic granularity* of natural language text.
6
+
7
+ It provides an end-to-end pipeline that:
8
+
9
+ 1. splits text into referential units,
10
+ 2. assigns continuous granularity scores to each unit,
11
+ 3. aggregates these scores into document-level estimates.
12
+
13
+ Granuscore is designed for analyzing how *fine-grained* or *coarse-grained* textual expressions are in applications such as question answering, educational dialogue, summarization, and scientific writing.
14
+
15
+ ---
16
+
17
+ ## Installation
18
+
19
+ Install from PyPI:
20
+
21
+ ```bash
22
+ pip install granuscore
23
+ ```
24
+
25
+ Or install the latest development version locally:
26
+
27
+ ```bash
28
+ git clone https://github.com/lukasellinger/granuscore.git
29
+ cd granuscore
30
+ pip install -e .
31
+ ```
32
+
33
+ Optional development dependencies:
34
+
35
+ ```bash
36
+ pip install -e ".[dev]"
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Quick Start
42
+
43
+ ```python
44
+ from granuscore import GranuScore
45
+
46
+ scorer = GranuScore()
47
+
48
+ text = """
49
+ Tony Hawk was born in San Diego.
50
+ """
51
+
52
+ score = scorer(text)
53
+
54
+ print(score)
55
+ ```
56
+
57
+ By default, Granuscore returns percentile scores, where higher values correspond to coarser-grained expressions.
58
+
59
+ ---
60
+
61
+ ## Default Configuration
62
+
63
+ The default configuration reproduces the setup used in the paper.
64
+
65
+ ```python
66
+ scorer = GranuScore()
67
+ ```
68
+
69
+ Equivalent to:
70
+
71
+ ```python
72
+ scorer = GranuScore(
73
+ predictor_type="hit",
74
+ )
75
+ ```
76
+
77
+ Default settings:
78
+
79
+ - `predictor_type="hit"`
80
+ - `model_name="Hierarchy-Transformers/HiT-MiniLM-L12-WordNetNoun"`
81
+ - `search_method="random_anchors"`
82
+ - `random_anchors_k=999`
83
+
84
+ Required artifacts such as:
85
+ - FAISS indices,
86
+ - anchor vectors,
87
+ - LightGBM models,
88
+ - and reference percentile distributions
89
+
90
+ are automatically downloaded and cached on first use.
91
+
92
+ ---
93
+
94
+ ## Important Compatibility Note
95
+
96
+ The default configuration works out of the box and is the recommended setup.
97
+
98
+ If you customize components such as:
99
+ - the embedding model,
100
+ - search method,
101
+ - FAISS index,
102
+ - anchor vectors,
103
+ - or LightGBM model,
104
+
105
+ you must ensure that all resources are compatible with each other.
106
+
107
+ For example, a LightGBM model trained using:
108
+
109
+ ```python
110
+ search_method="random_anchors"
111
+ ```
112
+
113
+ should not be combined with:
114
+
115
+ ```python
116
+ search_method="nearest_neighbor"
117
+ ```
118
+
119
+ Similarly, FAISS indices, anchor vectors, percentile reference distributions, and LightGBM models must originate from the same embedding space and training configuration.
120
+
121
+ Compatibility between custom resources is not validated automatically.
122
+
123
+ ---
124
+
125
+ ## Notebook Tutorial
126
+
127
+ An interactive introduction is available in:
128
+
129
+ ```text
130
+ notebooks/getting_started.ipynb
131
+ ```
132
+
133
+ ---
134
+
135
+ ## Repository Structure
136
+
137
+ ```text
138
+ granuscore/
139
+ ├── src/
140
+ │ └── granuscore/
141
+ │ ├── pipeline.py
142
+ │ ├── granularity_predictor.py
143
+ │ ├── claim_splitter.py
144
+ │ ├── bucket_output.py
145
+ │ ├── cache.py
146
+ │ └── artifacts.py
147
+ ├── notebooks/
148
+ │ └── getting_started.ipynb
149
+ ├── training_scripts/
150
+ ├── evaluation/
151
+ ├── assets/
152
+ ├── data/ (needs to be externally downloaded)
153
+ ├── pyproject.toml
154
+ ├── LICENSE
155
+ └── README.md
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Reproducing Paper Experiments
161
+
162
+ The datasets and precomputed resources required to reproduce the experiments from the paper are available here:
163
+
164
+ https://drive.google.com/drive/folders/1mJdUENOxHEiuYn-_f1KRQ1PZggXJDnb4?usp=sharing
165
+
166
+ Download the archive and extract it into the repository root:
167
+
168
+ ```bash
169
+ unzip data.zip
170
+ ```
171
+
172
+ This will create the expected directory structure used by the training and evaluation scripts.
173
+
174
+ ---
175
+
176
+ ## Training Pipeline
177
+
178
+ Training uses precomputed `.pkl` feature files.
179
+
180
+ 1. Generate precomputed datasets:
181
+
182
+ ```text
183
+ training_scripts/build_precalc_data/
184
+ ```
185
+
186
+ 2. Train LightGBM models:
187
+
188
+ ```bash
189
+ python training_scripts/train_lgb_models.py
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Citation
195
+ Updated Citation information will be added after publication.
196
+
197
+ ```bibtex
198
+ @misc{ellinger2026granuscore,
199
+ title={Granuscore: A Reference-Free Measure of Granularity for Text Analysis and Question Answering},
200
+ author={Ellinger, Lukas and Fichtl, Alexander M. and Anschütz, Miriam and Groh, Georg},
201
+ year={2026}
202
+ }
203
+ ```
@@ -0,0 +1,66 @@
1
+ [build-system]
2
+ requires = ["setuptools>=69", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "granuscore"
7
+ version = "1.0.0"
8
+ description = "Granularity scoring for natural language"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9,<3.13"
11
+
12
+ authors = [
13
+ {name = "Lukas Ellinger", email = "lukas.ellinger@tum.de"},
14
+ ]
15
+
16
+ license = "MIT"
17
+
18
+ classifiers = [
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Intended Audience :: Science/Research",
25
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
26
+ "Topic :: Text Processing :: Linguistic",
27
+ ]
28
+
29
+ keywords = [
30
+ "granularity",
31
+ "natural language processing",
32
+ "nlp",
33
+ "text analysis",
34
+ "semantic abstraction",
35
+ "specificity"
36
+ ]
37
+
38
+ dependencies = [
39
+ "faiss-cpu>=1.13.2",
40
+ "hierarchy_transformers>=0.1.1",
41
+ "lightgbm>=4.6.0",
42
+ "spacy>=3.8.11",
43
+ "torch>=2.9.1",
44
+ "numpy>=1.26.4",
45
+ "platformdirs>=3.10.0",
46
+ "requests>=2.31.0",
47
+ "tqdm>=4.66.0",
48
+ "openai>=2.21.0"
49
+ ]
50
+
51
+ [project.urls]
52
+ Repository = "https://github.com/lukasellinger/granuscore"
53
+ Issues = "https://github.com/lukasellinger/granuscore/issues"
54
+
55
+ [tool.setuptools.packages.find]
56
+ where = ["src"]
57
+
58
+ [project.optional-dependencies]
59
+ dev = [
60
+ "optuna>=4.6.0",
61
+ "pygam>=0.12.0",
62
+ "python-dotenv>=1.2.1",
63
+ "ollama>=0.6.1",
64
+ "google-genai>=1.66.0",
65
+ "wget>=3.2"
66
+ ]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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)