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.
- granuscore-1.0.0/LICENSE +21 -0
- granuscore-1.0.0/MANIFEST.in +5 -0
- granuscore-1.0.0/PKG-INFO +242 -0
- granuscore-1.0.0/README.md +203 -0
- granuscore-1.0.0/pyproject.toml +66 -0
- granuscore-1.0.0/setup.cfg +4 -0
- granuscore-1.0.0/src/granuscore/__init__.py +13 -0
- granuscore-1.0.0/src/granuscore/artifcats.py +116 -0
- granuscore-1.0.0/src/granuscore/bucket_output.py +53 -0
- granuscore-1.0.0/src/granuscore/cache.py +189 -0
- granuscore-1.0.0/src/granuscore/claim_splitter.py +300 -0
- granuscore-1.0.0/src/granuscore/default_assets.py +38 -0
- granuscore-1.0.0/src/granuscore/granularity_predictor.py +796 -0
- granuscore-1.0.0/src/granuscore/loader.py +118 -0
- granuscore-1.0.0/src/granuscore/pipeline.py +562 -0
- granuscore-1.0.0/src/granuscore/text_cleaner.py +61 -0
- granuscore-1.0.0/src/granuscore/utils.py +48 -0
- granuscore-1.0.0/src/granuscore.egg-info/PKG-INFO +242 -0
- granuscore-1.0.0/src/granuscore.egg-info/SOURCES.txt +20 -0
- granuscore-1.0.0/src/granuscore.egg-info/dependency_links.txt +1 -0
- granuscore-1.0.0/src/granuscore.egg-info/requires.txt +18 -0
- granuscore-1.0.0/src/granuscore.egg-info/top_level.txt +1 -0
granuscore-1.0.0/LICENSE
ADDED
|
@@ -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,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
|
+
[](https://pypi.org/project/granuscore/)
|
|
41
|
+
[](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
|
+
[](https://pypi.org/project/granuscore/)
|
|
2
|
+
[](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,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)
|