structured-data-extraction 0.1.0__tar.gz → 0.2.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.
- {structured_data_extraction-0.1.0/src/structured_data_extraction.egg-info → structured_data_extraction-0.2.0}/PKG-INFO +61 -2
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/README.md +60 -1
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/pyproject.toml +1 -1
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/extract.py +20 -11
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/main.py +24 -7
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0/src/structured_data_extraction.egg-info}/PKG-INFO +61 -2
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/LICENSE +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/setup.cfg +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/__init__.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/classify.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/config.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/convert.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/embed.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/features.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/filter_records.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/matching.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/models/section_classifier/docling_cats.joblib +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/models/section_classifier/rf_classifier.joblib +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/models/section_classifier/tfidf_char_vectorizer.joblib +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/models/section_classifier/tfidf_vectorizer.joblib +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/paths.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/rater.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/records.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/search.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/tables.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/templates/extraction.j2 +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/templates/schema_discovery.j2 +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction/vocab.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction.egg-info/SOURCES.txt +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction.egg-info/dependency_links.txt +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction.egg-info/entry_points.txt +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction.egg-info/requires.txt +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/src/structured_data_extraction.egg-info/top_level.txt +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/tests/test_convert.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/tests/test_embed.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/tests/test_extract.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/tests/test_features.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/tests/test_matching.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/tests/test_rater.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/tests/test_records.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/tests/test_search.py +0 -0
- {structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/tests/test_tables.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structured-data-extraction
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Pipeline for extracting structured data (tables, sections, key/value fields) from PDF documents using docling, embeddings, and LLMs.
|
|
5
5
|
Author-email: Dimitrios Avellas <dimitrisavellas8@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -140,6 +140,7 @@ Notes:
|
|
|
140
140
|
## Usage
|
|
141
141
|
|
|
142
142
|
```bash
|
|
143
|
+
CLI:
|
|
143
144
|
# one PDF
|
|
144
145
|
structured-extract pdf/1167fc.pdf --keys gain CMRR "supply voltage" #when key is more than one word then it should be inside ""
|
|
145
146
|
|
|
@@ -148,7 +149,65 @@ structured-extract pdf/ --keys revenue "net income"
|
|
|
148
149
|
|
|
149
150
|
```
|
|
150
151
|
|
|
151
|
-
|
|
152
|
+
```python file
|
|
153
|
+
Script:
|
|
154
|
+
from pathlib import Path
|
|
155
|
+
from structured_data_extraction.main import process_single_pdf, process_batch, finalize_outputs
|
|
156
|
+
|
|
157
|
+
path = Path("pdf/example.pdf")
|
|
158
|
+
# OR dir = Path("pdf_dir")
|
|
159
|
+
keys = ["key_1", "key_2", "key_3"]
|
|
160
|
+
|
|
161
|
+
results = process_single_pdf(path, keys)
|
|
162
|
+
# OR process_batch(pdf_dir, user_keys)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
finalize_outputs(levels=["all"])
|
|
167
|
+
|
|
168
|
+
print("Done. See output/fused.json and output/records.json")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Wrap multi-word keys in quotes. If you omit `--keys`, it prompts for a comma-separated list.
|
|
172
|
+
|
|
173
|
+
### Custom schema (skip schema discovery)
|
|
174
|
+
|
|
175
|
+
Pass `--schema` with a JSON file to bypass the schema-discovery LLM call entirely. The file must contain `document_type` and `schema` keys, using the same format as `intermediate/*_schema.json`. Keys are derived from the schema, so `--keys` is optional.
|
|
176
|
+
|
|
177
|
+
Use `"fixed"` on any sub-field to constrain the LLM to a single value — the field becomes a `Literal` type in the Pydantic model, so the model can only emit that exact value. This lets you slice a table down to specific variants, grades, or qualifiers:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
structured-extract pdf/AD840.pdf --schema my_schema.json
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"document_type": "operational amplifier datasheet",
|
|
186
|
+
"schema": {
|
|
187
|
+
"quiescent_current_s_typ": {
|
|
188
|
+
"type": "list",
|
|
189
|
+
"description": "Quiescent current typical value for the AD840S variant",
|
|
190
|
+
"fields": {
|
|
191
|
+
"differentiator": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"description": "Product variant",
|
|
194
|
+
"fixed": "AD840S"
|
|
195
|
+
},
|
|
196
|
+
"qualifier": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"description": "Column label: min, typ, or max",
|
|
199
|
+
"fixed": "Typ"
|
|
200
|
+
},
|
|
201
|
+
"condition": { "type": "string", "description": "Operating condition" },
|
|
202
|
+
"value": { "type": "number", "description": "Quiescent current value", "unit": "mA" },
|
|
203
|
+
"metadata": { "type": "object", "description": "Source and provenance" }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
A full run writes per-model extractions to `output/<stem>_extraction_<model>.json`, then fuses them into `fused.json` (every fact, sorted by confidence) plus `rating_report.json`, and wraps `fused.json` into the provenance-stamped `records.json`.
|
|
152
211
|
|
|
153
212
|
A multi-model run then ends by printing the confidence-tier histogram and offering to keep a subset of tiers — pass `--levels` (e.g. `--levels 1.0 0.75`, or `all`) to choose up front, or omit it for an interactive prompt. Single-model runs skip this step, since every record is confidence 1.0.
|
|
154
213
|
|
|
@@ -108,6 +108,7 @@ Notes:
|
|
|
108
108
|
## Usage
|
|
109
109
|
|
|
110
110
|
```bash
|
|
111
|
+
CLI:
|
|
111
112
|
# one PDF
|
|
112
113
|
structured-extract pdf/1167fc.pdf --keys gain CMRR "supply voltage" #when key is more than one word then it should be inside ""
|
|
113
114
|
|
|
@@ -116,7 +117,65 @@ structured-extract pdf/ --keys revenue "net income"
|
|
|
116
117
|
|
|
117
118
|
```
|
|
118
119
|
|
|
119
|
-
|
|
120
|
+
```python file
|
|
121
|
+
Script:
|
|
122
|
+
from pathlib import Path
|
|
123
|
+
from structured_data_extraction.main import process_single_pdf, process_batch, finalize_outputs
|
|
124
|
+
|
|
125
|
+
path = Path("pdf/example.pdf")
|
|
126
|
+
# OR dir = Path("pdf_dir")
|
|
127
|
+
keys = ["key_1", "key_2", "key_3"]
|
|
128
|
+
|
|
129
|
+
results = process_single_pdf(path, keys)
|
|
130
|
+
# OR process_batch(pdf_dir, user_keys)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
finalize_outputs(levels=["all"])
|
|
135
|
+
|
|
136
|
+
print("Done. See output/fused.json and output/records.json")
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Wrap multi-word keys in quotes. If you omit `--keys`, it prompts for a comma-separated list.
|
|
140
|
+
|
|
141
|
+
### Custom schema (skip schema discovery)
|
|
142
|
+
|
|
143
|
+
Pass `--schema` with a JSON file to bypass the schema-discovery LLM call entirely. The file must contain `document_type` and `schema` keys, using the same format as `intermediate/*_schema.json`. Keys are derived from the schema, so `--keys` is optional.
|
|
144
|
+
|
|
145
|
+
Use `"fixed"` on any sub-field to constrain the LLM to a single value — the field becomes a `Literal` type in the Pydantic model, so the model can only emit that exact value. This lets you slice a table down to specific variants, grades, or qualifiers:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
structured-extract pdf/AD840.pdf --schema my_schema.json
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"document_type": "operational amplifier datasheet",
|
|
154
|
+
"schema": {
|
|
155
|
+
"quiescent_current_s_typ": {
|
|
156
|
+
"type": "list",
|
|
157
|
+
"description": "Quiescent current typical value for the AD840S variant",
|
|
158
|
+
"fields": {
|
|
159
|
+
"differentiator": {
|
|
160
|
+
"type": "string",
|
|
161
|
+
"description": "Product variant",
|
|
162
|
+
"fixed": "AD840S"
|
|
163
|
+
},
|
|
164
|
+
"qualifier": {
|
|
165
|
+
"type": "string",
|
|
166
|
+
"description": "Column label: min, typ, or max",
|
|
167
|
+
"fixed": "Typ"
|
|
168
|
+
},
|
|
169
|
+
"condition": { "type": "string", "description": "Operating condition" },
|
|
170
|
+
"value": { "type": "number", "description": "Quiescent current value", "unit": "mA" },
|
|
171
|
+
"metadata": { "type": "object", "description": "Source and provenance" }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
A full run writes per-model extractions to `output/<stem>_extraction_<model>.json`, then fuses them into `fused.json` (every fact, sorted by confidence) plus `rating_report.json`, and wraps `fused.json` into the provenance-stamped `records.json`.
|
|
120
179
|
|
|
121
180
|
A multi-model run then ends by printing the confidence-tier histogram and offering to keep a subset of tiers — pass `--levels` (e.g. `--levels 1.0 0.75`, or `all`) to choose up front, or omit it for an interactive prompt. Single-model runs skip this step, since every record is confidence 1.0.
|
|
122
181
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "structured-data-extraction"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "Pipeline for extracting structured data (tables, sections, key/value fields) from PDF documents using docling, embeddings, and LLMs."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -23,13 +23,13 @@ import os
|
|
|
23
23
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
24
24
|
from datetime import datetime, timezone
|
|
25
25
|
from pathlib import Path
|
|
26
|
-
from typing import Any, List, Optional
|
|
26
|
+
from typing import Any, List, Literal, Optional
|
|
27
27
|
import instructor
|
|
28
28
|
from jinja2 import Environment, ChoiceLoader, FileSystemLoader, PackageLoader
|
|
29
29
|
from openai import OpenAI
|
|
30
30
|
from pydantic import BaseModel, Field, create_model
|
|
31
31
|
from . import config, matching
|
|
32
|
-
from .paths import inter, read_json, cached
|
|
32
|
+
from .paths import inter, read_json, write_json, cached
|
|
33
33
|
from .tables import strip_code_fence
|
|
34
34
|
|
|
35
35
|
logger = logging.getLogger(__name__)
|
|
@@ -110,10 +110,15 @@ def _build_extraction_model(schema, user_keys):
|
|
|
110
110
|
for sub_name, sub_def in fields_spec.items():
|
|
111
111
|
if sub_name == "metadata":
|
|
112
112
|
continue
|
|
113
|
-
field_type = type_map.get(sub_def.get("type", "string"), str)
|
|
114
113
|
sub_desc = sub_def.get("description", "")
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
fixed = sub_def.get("fixed")
|
|
115
|
+
if fixed is not None:
|
|
116
|
+
field_type = Literal[fixed]
|
|
117
|
+
sub_fields[sub_name] = (field_type, Field(default=fixed, description=sub_desc))
|
|
118
|
+
else:
|
|
119
|
+
field_type = type_map.get(sub_def.get("type", "string"), str)
|
|
120
|
+
default = "" if field_type is str else None
|
|
121
|
+
sub_fields[sub_name] = (field_type, Field(default=default, description=sub_desc))
|
|
117
122
|
if sub_def.get("unit") and not has_unit_field:
|
|
118
123
|
sub_fields[f"{sub_name}_unit"] = (str, Field(default="", description=f"Unit for {sub_name}"))
|
|
119
124
|
|
|
@@ -368,7 +373,7 @@ def _run_one_model(model_name, stem, out_dir, unique, vocab, user_keys, schema,
|
|
|
368
373
|
return extraction
|
|
369
374
|
|
|
370
375
|
|
|
371
|
-
def extract_parallel(stem, user_keys):
|
|
376
|
+
def extract_parallel(stem, user_keys, user_schema=None):
|
|
372
377
|
"""Run every configured extraction model over one document in parallel.
|
|
373
378
|
|
|
374
379
|
Stamps run provenance (run.json), discovers the schema once for all models, then fans
|
|
@@ -387,11 +392,15 @@ def extract_parallel(stem, user_keys):
|
|
|
387
392
|
|
|
388
393
|
md_text, vocab, unique = _prepare_sections(stem)
|
|
389
394
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
+
if user_schema is not None:
|
|
396
|
+
doc_type, schema = user_schema["document_type"], user_schema["schema"]
|
|
397
|
+
write_json(inter(stem, "schema.json"), user_schema)
|
|
398
|
+
logger.info("Using user-supplied schema (skipped schema discovery)")
|
|
399
|
+
else:
|
|
400
|
+
disc = cached(inter(stem, "schema.json"),
|
|
401
|
+
lambda: dict(zip(("document_type", "schema"),
|
|
402
|
+
discover_schema(md_text, unique, vocab, user_keys))))
|
|
403
|
+
doc_type, schema = disc["document_type"], disc["schema"]
|
|
395
404
|
logger.info(f"Schema: document_type='{doc_type}'")
|
|
396
405
|
|
|
397
406
|
results = []
|
|
@@ -49,7 +49,7 @@ def _invalidate_stale_key_caches(stem, user_keys):
|
|
|
49
49
|
p.unlink()
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
def process_single_pdf(pdf_path, user_keys):
|
|
52
|
+
def process_single_pdf(pdf_path, user_keys, user_schema=None):
|
|
53
53
|
"""Run the full extraction pipeline on one PDF.
|
|
54
54
|
|
|
55
55
|
Executes every stage in order: convert → vocab → classify → tables →
|
|
@@ -150,7 +150,7 @@ def process_single_pdf(pdf_path, user_keys):
|
|
|
150
150
|
refine_matched_tables(str(tables_file), matched_idxs, pdf_path=str(pdf_path))
|
|
151
151
|
|
|
152
152
|
with timed("extract"):
|
|
153
|
-
results = extract_parallel(stem, user_keys)
|
|
153
|
+
results = extract_parallel(stem, user_keys, user_schema=user_schema)
|
|
154
154
|
|
|
155
155
|
_log_run_summary(stem, timings, results, matches)
|
|
156
156
|
logger.info(f"=== Done with {pdf_path.name} ===")
|
|
@@ -233,7 +233,7 @@ def _offer_filter(records_path, levels):
|
|
|
233
233
|
filter_records.run(records_path, levels=levels)
|
|
234
234
|
|
|
235
235
|
|
|
236
|
-
def process_batch(pdf_dir, user_keys):
|
|
236
|
+
def process_batch(pdf_dir, user_keys, user_schema=None):
|
|
237
237
|
pdf_dir = Path(pdf_dir)
|
|
238
238
|
if not pdf_dir.exists():
|
|
239
239
|
logger.error(f"Directory not found: {pdf_dir}")
|
|
@@ -248,7 +248,7 @@ def process_batch(pdf_dir, user_keys):
|
|
|
248
248
|
results = []
|
|
249
249
|
for pdf in pdfs:
|
|
250
250
|
try:
|
|
251
|
-
result = process_single_pdf(str(pdf), user_keys)
|
|
251
|
+
result = process_single_pdf(str(pdf), user_keys, user_schema=user_schema)
|
|
252
252
|
if result:
|
|
253
253
|
results.append(result)
|
|
254
254
|
except Exception as e:
|
|
@@ -280,6 +280,9 @@ def main():
|
|
|
280
280
|
parser.add_argument("--levels", nargs="+", metavar="LVL",
|
|
281
281
|
help="confidence tiers to keep (e.g. 1.0 0.75, or 'all'); "
|
|
282
282
|
"omit to be prompted after extraction")
|
|
283
|
+
parser.add_argument("--schema", metavar="FILE",
|
|
284
|
+
help="Path to a JSON schema file (same format as intermediate/*_schema.json); "
|
|
285
|
+
"skips schema discovery. Keys are derived from the schema, so --keys is optional")
|
|
283
286
|
parser.add_argument("--dump-templates", metavar="DIR",
|
|
284
287
|
help="Write the built-in prompt templates to DIR for editing, then exit; "
|
|
285
288
|
"point TEMPLATES_DIR at that folder to use your edits")
|
|
@@ -293,8 +296,22 @@ def main():
|
|
|
293
296
|
parser.error("a PDF file or directory is required (unless using --dump-templates)")
|
|
294
297
|
|
|
295
298
|
config.FORCE = args.force
|
|
299
|
+
|
|
300
|
+
user_schema = None
|
|
301
|
+
if args.schema:
|
|
302
|
+
schema_path = Path(args.schema)
|
|
303
|
+
if not schema_path.exists():
|
|
304
|
+
print(f"Schema file not found: {schema_path}")
|
|
305
|
+
sys.exit(1)
|
|
306
|
+
user_schema = json.loads(schema_path.read_text(encoding="utf-8"))
|
|
307
|
+
if not isinstance(user_schema, dict) or "schema" not in user_schema or "document_type" not in user_schema:
|
|
308
|
+
print("Schema file must contain 'document_type' and 'schema' keys")
|
|
309
|
+
sys.exit(1)
|
|
310
|
+
|
|
296
311
|
user_keys = args.keys
|
|
297
|
-
if not user_keys:
|
|
312
|
+
if not user_keys and user_schema:
|
|
313
|
+
user_keys = list(user_schema["schema"].keys())
|
|
314
|
+
elif not user_keys:
|
|
298
315
|
print("Enter keys to extract (comma-separated):")
|
|
299
316
|
raw = input("> ").strip()
|
|
300
317
|
user_keys = [k.strip() for k in raw.split(",") if k.strip()]
|
|
@@ -308,7 +325,7 @@ def main():
|
|
|
308
325
|
config.OUTPUT_DIR = args.output
|
|
309
326
|
|
|
310
327
|
if Path(args.path).is_dir():
|
|
311
|
-
results = process_batch(args.path, user_keys)
|
|
328
|
+
results = process_batch(args.path, user_keys, user_schema=user_schema)
|
|
312
329
|
if results:
|
|
313
330
|
finalize_outputs(args.levels)
|
|
314
331
|
print(f"Extracted from {len(results)} files → {config.OUTPUT_DIR}")
|
|
@@ -316,7 +333,7 @@ def main():
|
|
|
316
333
|
print("No documents produced any extraction.")
|
|
317
334
|
sys.exit(1)
|
|
318
335
|
else:
|
|
319
|
-
result = process_single_pdf(args.path, user_keys)
|
|
336
|
+
result = process_single_pdf(args.path, user_keys, user_schema=user_schema)
|
|
320
337
|
if result:
|
|
321
338
|
finalize_outputs(args.levels)
|
|
322
339
|
print(json.dumps(result, indent=2, ensure_ascii=False)) #document them well
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: structured-data-extraction
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Pipeline for extracting structured data (tables, sections, key/value fields) from PDF documents using docling, embeddings, and LLMs.
|
|
5
5
|
Author-email: Dimitrios Avellas <dimitrisavellas8@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -140,6 +140,7 @@ Notes:
|
|
|
140
140
|
## Usage
|
|
141
141
|
|
|
142
142
|
```bash
|
|
143
|
+
CLI:
|
|
143
144
|
# one PDF
|
|
144
145
|
structured-extract pdf/1167fc.pdf --keys gain CMRR "supply voltage" #when key is more than one word then it should be inside ""
|
|
145
146
|
|
|
@@ -148,7 +149,65 @@ structured-extract pdf/ --keys revenue "net income"
|
|
|
148
149
|
|
|
149
150
|
```
|
|
150
151
|
|
|
151
|
-
|
|
152
|
+
```python file
|
|
153
|
+
Script:
|
|
154
|
+
from pathlib import Path
|
|
155
|
+
from structured_data_extraction.main import process_single_pdf, process_batch, finalize_outputs
|
|
156
|
+
|
|
157
|
+
path = Path("pdf/example.pdf")
|
|
158
|
+
# OR dir = Path("pdf_dir")
|
|
159
|
+
keys = ["key_1", "key_2", "key_3"]
|
|
160
|
+
|
|
161
|
+
results = process_single_pdf(path, keys)
|
|
162
|
+
# OR process_batch(pdf_dir, user_keys)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
finalize_outputs(levels=["all"])
|
|
167
|
+
|
|
168
|
+
print("Done. See output/fused.json and output/records.json")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Wrap multi-word keys in quotes. If you omit `--keys`, it prompts for a comma-separated list.
|
|
172
|
+
|
|
173
|
+
### Custom schema (skip schema discovery)
|
|
174
|
+
|
|
175
|
+
Pass `--schema` with a JSON file to bypass the schema-discovery LLM call entirely. The file must contain `document_type` and `schema` keys, using the same format as `intermediate/*_schema.json`. Keys are derived from the schema, so `--keys` is optional.
|
|
176
|
+
|
|
177
|
+
Use `"fixed"` on any sub-field to constrain the LLM to a single value — the field becomes a `Literal` type in the Pydantic model, so the model can only emit that exact value. This lets you slice a table down to specific variants, grades, or qualifiers:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
structured-extract pdf/AD840.pdf --schema my_schema.json
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"document_type": "operational amplifier datasheet",
|
|
186
|
+
"schema": {
|
|
187
|
+
"quiescent_current_s_typ": {
|
|
188
|
+
"type": "list",
|
|
189
|
+
"description": "Quiescent current typical value for the AD840S variant",
|
|
190
|
+
"fields": {
|
|
191
|
+
"differentiator": {
|
|
192
|
+
"type": "string",
|
|
193
|
+
"description": "Product variant",
|
|
194
|
+
"fixed": "AD840S"
|
|
195
|
+
},
|
|
196
|
+
"qualifier": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"description": "Column label: min, typ, or max",
|
|
199
|
+
"fixed": "Typ"
|
|
200
|
+
},
|
|
201
|
+
"condition": { "type": "string", "description": "Operating condition" },
|
|
202
|
+
"value": { "type": "number", "description": "Quiescent current value", "unit": "mA" },
|
|
203
|
+
"metadata": { "type": "object", "description": "Source and provenance" }
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
A full run writes per-model extractions to `output/<stem>_extraction_<model>.json`, then fuses them into `fused.json` (every fact, sorted by confidence) plus `rating_report.json`, and wraps `fused.json` into the provenance-stamped `records.json`.
|
|
152
211
|
|
|
153
212
|
A multi-model run then ends by printing the confidence-tier histogram and offering to keep a subset of tiers — pass `--levels` (e.g. `--levels 1.0 0.75`, or `all`) to choose up front, or omit it for an interactive prompt. Single-model runs skip this step, since every record is confidence 1.0.
|
|
154
213
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/tests/test_features.py
RENAMED
|
File without changes
|
{structured_data_extraction-0.1.0 → structured_data_extraction-0.2.0}/tests/test_matching.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|