womblex 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.
- womblex-0.2.0/CHANGELOG.md +1093 -0
- womblex-0.2.0/LICENSE +201 -0
- womblex-0.2.0/MANIFEST.in +29 -0
- womblex-0.2.0/PKG-INFO +434 -0
- womblex-0.2.0/README.md +379 -0
- womblex-0.2.0/pyproject.toml +154 -0
- womblex-0.2.0/setup.cfg +4 -0
- womblex-0.2.0/src/womblex/__init__.py +3 -0
- womblex-0.2.0/src/womblex/_models/README.md +42 -0
- womblex-0.2.0/src/womblex/_models/en_AU/LICENSE +347 -0
- womblex-0.2.0/src/womblex/_models/en_AU/index.aff +205 -0
- womblex-0.2.0/src/womblex/_models/en_AU/index.dic +49824 -0
- womblex-0.2.0/src/womblex/_models/kanon-2-tokenizer/merges.txt +50108 -0
- womblex-0.2.0/src/womblex/_models/kanon-2-tokenizer/tokenizer.json +250913 -0
- womblex-0.2.0/src/womblex/_models/kanon-2-tokenizer/tokenizer_config.json +57 -0
- womblex-0.2.0/src/womblex/_models/kanon-2-tokenizer/vocab.json +50370 -0
- womblex-0.2.0/src/womblex/analyse/__init__.py +1 -0
- womblex-0.2.0/src/womblex/analyse/embed.py +84 -0
- womblex-0.2.0/src/womblex/analyse/embed_stage.py +132 -0
- womblex-0.2.0/src/womblex/analyse/enrich.py +435 -0
- womblex-0.2.0/src/womblex/analyse/enrich_merge.py +184 -0
- womblex-0.2.0/src/womblex/analyse/enrich_stage.py +364 -0
- womblex-0.2.0/src/womblex/analyse/graph.py +504 -0
- womblex-0.2.0/src/womblex/analyse/graph_refresh.py +231 -0
- womblex-0.2.0/src/womblex/analyse/models.py +200 -0
- womblex-0.2.0/src/womblex/analyse/query.py +122 -0
- womblex-0.2.0/src/womblex/batch.py +77 -0
- womblex-0.2.0/src/womblex/cli/__init__.py +93 -0
- womblex-0.2.0/src/womblex/cli/__main__.py +10 -0
- womblex-0.2.0/src/womblex/cli/_shared.py +71 -0
- womblex-0.2.0/src/womblex/cli/cloud.py +271 -0
- womblex-0.2.0/src/womblex/cli/embed.py +93 -0
- womblex-0.2.0/src/womblex/cli/ingest.py +123 -0
- womblex-0.2.0/src/womblex/cli/link.py +248 -0
- womblex-0.2.0/src/womblex/cli/normalise.py +92 -0
- womblex-0.2.0/src/womblex/cli/pii.py +99 -0
- womblex-0.2.0/src/womblex/cli/pipeline.py +490 -0
- womblex-0.2.0/src/womblex/cli/profile.py +76 -0
- womblex-0.2.0/src/womblex/cli/quality.py +72 -0
- womblex-0.2.0/src/womblex/cli/redact.py +290 -0
- womblex-0.2.0/src/womblex/cli/score.py +83 -0
- womblex-0.2.0/src/womblex/cli/spellfix.py +90 -0
- womblex-0.2.0/src/womblex/cli/verify.py +100 -0
- womblex-0.2.0/src/womblex/cloud/__init__.py +21 -0
- womblex-0.2.0/src/womblex/cloud/queue.py +220 -0
- womblex-0.2.0/src/womblex/cloud/worker.py +116 -0
- womblex-0.2.0/src/womblex/config.py +778 -0
- womblex-0.2.0/src/womblex/ingest/__init__.py +1 -0
- womblex-0.2.0/src/womblex/ingest/abn_bulk.py +346 -0
- womblex-0.2.0/src/womblex/ingest/detect.py +599 -0
- womblex-0.2.0/src/womblex/ingest/elements.py +154 -0
- womblex-0.2.0/src/womblex/ingest/extract.py +555 -0
- womblex-0.2.0/src/womblex/ingest/forms.py +254 -0
- womblex-0.2.0/src/womblex/ingest/geospatial.py +169 -0
- womblex-0.2.0/src/womblex/ingest/gnaf.py +189 -0
- womblex-0.2.0/src/womblex/ingest/gnaf_schema.py +131 -0
- womblex-0.2.0/src/womblex/ingest/grid_projection.py +255 -0
- womblex-0.2.0/src/womblex/ingest/heuristics_cv2.py +282 -0
- womblex-0.2.0/src/womblex/ingest/heuristics_numpy.py +149 -0
- womblex-0.2.0/src/womblex/ingest/interfaces/__init__.py +23 -0
- womblex-0.2.0/src/womblex/ingest/interfaces/protocols.py +124 -0
- womblex-0.2.0/src/womblex/ingest/llm_ocr.py +175 -0
- womblex-0.2.0/src/womblex/ingest/morphology.py +244 -0
- womblex-0.2.0/src/womblex/ingest/orchestrator.py +493 -0
- womblex-0.2.0/src/womblex/ingest/paddle_ocr.py +492 -0
- womblex-0.2.0/src/womblex/ingest/page_profile.py +222 -0
- womblex-0.2.0/src/womblex/ingest/records.py +264 -0
- womblex-0.2.0/src/womblex/ingest/redaction.py +5 -0
- womblex-0.2.0/src/womblex/ingest/spreadsheet.py +354 -0
- womblex-0.2.0/src/womblex/ingest/spreadsheet_print.py +505 -0
- womblex-0.2.0/src/womblex/ingest/strategies.py +21 -0
- womblex-0.2.0/src/womblex/ingest/strategies_file.py +173 -0
- womblex-0.2.0/src/womblex/ingest/strategies_scanned.py +562 -0
- womblex-0.2.0/src/womblex/ingest/views.py +273 -0
- womblex-0.2.0/src/womblex/link/__init__.py +22 -0
- womblex-0.2.0/src/womblex/link/matcher.py +169 -0
- womblex-0.2.0/src/womblex/link/normalise.py +59 -0
- womblex-0.2.0/src/womblex/link/reference.py +100 -0
- womblex-0.2.0/src/womblex/link/stage.py +180 -0
- womblex-0.2.0/src/womblex/operations/__init__.py +44 -0
- womblex-0.2.0/src/womblex/operations/chunk.py +95 -0
- womblex-0.2.0/src/womblex/operations/enrich.py +89 -0
- womblex-0.2.0/src/womblex/operations/extract.py +86 -0
- womblex-0.2.0/src/womblex/operations/models.py +56 -0
- womblex-0.2.0/src/womblex/operations/persist.py +71 -0
- womblex-0.2.0/src/womblex/operations/pii.py +104 -0
- womblex-0.2.0/src/womblex/operations/redact.py +77 -0
- womblex-0.2.0/src/womblex/pii/__init__.py +13 -0
- womblex-0.2.0/src/womblex/pii/cleaner.py +419 -0
- womblex-0.2.0/src/womblex/pii/pii_stage.py +239 -0
- womblex-0.2.0/src/womblex/pii/stage.py +162 -0
- womblex-0.2.0/src/womblex/process/__init__.py +1 -0
- womblex-0.2.0/src/womblex/process/chunk_stage.py +398 -0
- womblex-0.2.0/src/womblex/process/chunker.py +528 -0
- womblex-0.2.0/src/womblex/process/normalise.py +196 -0
- womblex-0.2.0/src/womblex/process/normalise_stage.py +141 -0
- womblex-0.2.0/src/womblex/process/quality.py +135 -0
- womblex-0.2.0/src/womblex/process/quality_stage.py +112 -0
- womblex-0.2.0/src/womblex/process/spellfix.py +194 -0
- womblex-0.2.0/src/womblex/process/spellfix_stage.py +158 -0
- womblex-0.2.0/src/womblex/process/text_overlay.py +86 -0
- womblex-0.2.0/src/womblex/profile/__init__.py +19 -0
- womblex-0.2.0/src/womblex/profile/columns.py +325 -0
- womblex-0.2.0/src/womblex/py.typed +0 -0
- womblex-0.2.0/src/womblex/redact/__init__.py +9 -0
- womblex-0.2.0/src/womblex/redact/batch.py +316 -0
- womblex-0.2.0/src/womblex/redact/detector.py +142 -0
- womblex-0.2.0/src/womblex/redact/stage.py +303 -0
- womblex-0.2.0/src/womblex/redact/utils.py +28 -0
- womblex-0.2.0/src/womblex/score.py +281 -0
- womblex-0.2.0/src/womblex/store/__init__.py +6 -0
- womblex-0.2.0/src/womblex/store/checkpoint.py +130 -0
- womblex-0.2.0/src/womblex/store/enrichment_doc.py +102 -0
- womblex-0.2.0/src/womblex/store/enrichment_output.py +471 -0
- womblex-0.2.0/src/womblex/store/normalise_output.py +94 -0
- womblex-0.2.0/src/womblex/store/output.py +728 -0
- womblex-0.2.0/src/womblex/store/pii_output.py +162 -0
- womblex-0.2.0/src/womblex/store/provenance_output.py +133 -0
- womblex-0.2.0/src/womblex/store/quality_output.py +88 -0
- womblex-0.2.0/src/womblex/store/register_manifest.py +114 -0
- womblex-0.2.0/src/womblex/store/remote.py +144 -0
- womblex-0.2.0/src/womblex/store/retention.py +120 -0
- womblex-0.2.0/src/womblex/store/run_manifest.py +46 -0
- womblex-0.2.0/src/womblex/store/shard_audit.py +701 -0
- womblex-0.2.0/src/womblex/store/spellfix_output.py +131 -0
- womblex-0.2.0/src/womblex/utils/__init__.py +0 -0
- womblex-0.2.0/src/womblex/utils/availability.py +25 -0
- womblex-0.2.0/src/womblex/utils/checksum.py +17 -0
- womblex-0.2.0/src/womblex/utils/metrics.py +358 -0
- womblex-0.2.0/src/womblex/utils/models.py +85 -0
- womblex-0.2.0/src/womblex/utils/tabular_metrics.py +209 -0
- womblex-0.2.0/src/womblex/utils/token_packer.py +269 -0
- womblex-0.2.0/src/womblex/verify/__init__.py +5 -0
- womblex-0.2.0/src/womblex/verify/engine.py +230 -0
- womblex-0.2.0/src/womblex.egg-info/PKG-INFO +434 -0
- womblex-0.2.0/src/womblex.egg-info/SOURCES.txt +138 -0
- womblex-0.2.0/src/womblex.egg-info/dependency_links.txt +1 -0
- womblex-0.2.0/src/womblex.egg-info/entry_points.txt +2 -0
- womblex-0.2.0/src/womblex.egg-info/requires.txt +34 -0
- womblex-0.2.0/src/womblex.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-07-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Pre-extracted records ingest (`ingest/records.py`).** Turns already-clean
|
|
14
|
+
text records (a JSONL corpus; the Open Australian Legal Corpus) straight into
|
|
15
|
+
the standard element-shard layout (`*.elements.parquet` + sidecars +
|
|
16
|
+
`*._manifest.parquet`) so the `enrich → chunk → embed → graph-refresh`
|
|
17
|
+
pipeline runs over a pre-extracted corpus unchanged — unlike the register
|
|
18
|
+
ingests (`gnaf`/`abn`/`geo`) which *bypass* the NLP pipeline, this one *feeds*
|
|
19
|
+
it. `source_hash = sha256(record_id + text)` is content-addressed (unchanged
|
|
20
|
+
records are cache hits on re-ingest); text is split into paragraph blocks so
|
|
21
|
+
the reassembled narrative round-trips. Corpus-agnostic — a
|
|
22
|
+
`RecordFieldMapping` (declared by a thin `stories/<corpus>` config) names the
|
|
23
|
+
id / text / provenance fields. Record metadata lands in a
|
|
24
|
+
`*.provenance.parquet` sidecar (`store/provenance_output.py`) consolidated
|
|
25
|
+
into a run-root `manifest.parquet` (source_hash → provenance).
|
|
26
|
+
- **Token-budget request packer (`utils/token_packer.py`).** Isaacus rate
|
|
27
|
+
limits bind on *tokens per request/window*, not request count, so requests
|
|
28
|
+
are packed by exact local token counts from the kanon-2 tokenizer:
|
|
29
|
+
`pack_by_tokens` groups items to `min(max_items, token_budget)`; an
|
|
30
|
+
over-budget item is sent solo; `split_on_boundaries` splits an over-ceiling
|
|
31
|
+
document on blank-line boundaries into offset-tagged segments. `TokenCounter`
|
|
32
|
+
is a cached, offline wrapper over the tokenizer.
|
|
33
|
+
- **Enrichment — token-aware batching + long-doc split (`enrich_stage.py`).**
|
|
34
|
+
Replaces the one-doc-per-call loop with packer-driven requests of
|
|
35
|
+
`min(max_texts_per_request=8, token_budget)` (8× fewer requests for small
|
|
36
|
+
docs; token-aware so a batch of long judgments never overpacks a
|
|
37
|
+
429-triggering request). A doc over `split_ceiling` is split and its
|
|
38
|
+
per-segment results offset-merged (`analyse/enrich_merge.py`). `enrich.py`
|
|
39
|
+
honours a `Retry-After` header on 429. New `EnrichmentConfig` knobs:
|
|
40
|
+
`tokenizer`, `max_texts_per_request`, `token_budget`, `split_ceiling`.
|
|
41
|
+
- **Graph-edge refresh stage (`analyse/graph_refresh.py`, `womblex
|
|
42
|
+
graph-refresh`).** Offline, deterministic rebuild of mention→chunk edges from
|
|
43
|
+
the entity + chunk sidecars (both carry char offsets) — needed because AI
|
|
44
|
+
chunking runs *after* enrichment, so the enrich-time graph has no chunk edges
|
|
45
|
+
yet. Populates `enrichment_entities.chunk_index` and refreshes
|
|
46
|
+
`*.graph_edges.parquet`, preserving hierarchy/citation edges. Idempotent.
|
|
47
|
+
- **Offline kanon-2 tokenizer.** The tokenizer is vendored under
|
|
48
|
+
`_models/kanon-2-tokenizer` and resolved locally by both the token packer and
|
|
49
|
+
`create_chunker` — no Hugging Face round-trip per run, offline-safe.
|
|
50
|
+
- **Distributed / cloud execution (`womblex[cloud]`).** Optional scale-out for
|
|
51
|
+
long batch runs without changing the local CPU-first default. Three pieces:
|
|
52
|
+
(1) `store/remote.py` — an fsspec stage-in/stage-out object-storage adapter
|
|
53
|
+
(S3/MinIO/GCS/local) that confines all remote-storage knowledge to one place
|
|
54
|
+
so the `Path`-based stages stay untouched; (2) `cloud/queue.py` — a Postgres
|
|
55
|
+
`FOR UPDATE SKIP LOCKED` job queue over one `womblex_jobs` table where the row
|
|
56
|
+
`status` *is* the distributed checkpoint (idempotent re-enqueue on
|
|
57
|
+
`(run_id, batch_num)`, per-job retry, crashed-worker requeue); (3)
|
|
58
|
+
`cloud/worker.py` — a worker that claims a batch, stages its inputs, runs the
|
|
59
|
+
shared `batch.process_batch` body, and publishes `batch-NNNN.*.parquet` shards
|
|
60
|
+
back. New CLI: `womblex enqueue` / `worker` / `jobs` / `finalize` (the last
|
|
61
|
+
consolidates a distributed run's shard manifests into
|
|
62
|
+
`<run>/manifest.parquet` in the store — the explicit end-step `womblex run`
|
|
63
|
+
performs locally). Outputs are the ordinary shard layout, so `manifest` /
|
|
64
|
+
`chunk --shards` consume a distributed run exactly like a local one.
|
|
65
|
+
`process_batch` is also now the single shared body behind `womblex run`, so
|
|
66
|
+
local and distributed modes cannot diverge.
|
|
67
|
+
- **Container image + compose stack.** `Dockerfile` (extraction + `[cloud]`)
|
|
68
|
+
and `docker-compose.yml` bundling Postgres (queue), MinIO (object store), and
|
|
69
|
+
horizontally scalable workers (`docker compose up --scale worker=N`).
|
|
70
|
+
- **CI security job.** `ci.yml` gains a `security` job: Semgrep SAST over `src/`
|
|
71
|
+
with the Python + OWASP Top Ten rulesets (blocking) and `pip-audit`
|
|
72
|
+
dependency scanning (informational — the ML dep tree carries advisories we
|
|
73
|
+
can't action directly). The test job now also installs the `cloud` extra so
|
|
74
|
+
the object-storage tests run in CI.
|
|
75
|
+
- **ABN Lookup bulk extract ingest.** New `ingest/abn_bulk.py` stream-parses
|
|
76
|
+
the ABR bulk extract XML files (`yyyymmddPublicNN.xml`, ~6 GB uncompressed
|
|
77
|
+
across 20 files) with constant memory and writes two Parquet files per
|
|
78
|
+
input: `<stem>.parquet` (one row per ABR record — ABN/status, entity type,
|
|
79
|
+
main entity name or legal-entity name parts with given names as separate
|
|
80
|
+
`given_name_1` / `given_name_2` columns since a single given name may
|
|
81
|
+
itself contain a space, state/postcode, ACN, GST) and
|
|
82
|
+
`<stem>_names.parquet` (one row per registered name — main/legal, business,
|
|
83
|
+
trading and DGR fund names keyed by ABN, ready for `link/` register
|
|
84
|
+
consumption). Values are verbatim strings, absent optionals are `""`, and
|
|
85
|
+
provenance (schema version, source file, MD5, row counts) rides as parquet
|
|
86
|
+
metadata — the `ingest/gnaf.py` pattern. Failures are isolated per file:
|
|
87
|
+
any error (malformed XML, read/write failure) logs with the source name,
|
|
88
|
+
removes partial output, and lets the directory ingest continue. New
|
|
89
|
+
`womblex ingest-abn <file|dir>` CLI command; bypasses the NLP pipeline.
|
|
90
|
+
(`ingest/abn_bulk.py`, `cli/ingest.py`, `tests/test_abn_bulk.py` —
|
|
91
|
+
all-synthetic fixtures.) The shared MD5 helper moved to
|
|
92
|
+
`utils/checksum.py`, replacing the per-module copies in `ingest/gnaf.py`
|
|
93
|
+
and `ingest/geospatial.py`.
|
|
94
|
+
- **Spreadsheet preamble/header detection.** Export products that open with
|
|
95
|
+
title rows, generated-date lines or `key: value` metadata blocks above the
|
|
96
|
+
real header (e.g. AusTender contract-notice exports, agency stats
|
|
97
|
+
workbooks) previously had the first row parsed as the header, with pandas
|
|
98
|
+
fabricating `Unnamed: N` column names that landed verbatim-violating cell
|
|
99
|
+
values on the element stream — and ragged CSVs (one-field title row above
|
|
100
|
+
a wide header) failed outright. Sheets are now read with `header=None`
|
|
101
|
+
(CSVs via a new field-count-sniffing `read_csv_raw`, capped at `nrows`
|
|
102
|
+
when sampling) and split via `split_preamble`: the header is the
|
|
103
|
+
candidate row (≥2 non-empty cells in a 10-row window) that starts the
|
|
104
|
+
longest run of table-consistent rows below it — a blank separator or the
|
|
105
|
+
wider table below breaks a title/metadata row's run, ties prefer the
|
|
106
|
+
wider candidate, single-cell section rows are neutral, and a width-ratio
|
|
107
|
+
rule plus row-0 fallback covers header-only and single-column sheets.
|
|
108
|
+
Preamble rows land verbatim on the sheet_meta element
|
|
109
|
+
(`meta["preamble"]`) and the row-0-is-header contract of the cell grid is
|
|
110
|
+
preserved for downstream table views. Header-first, single-column and
|
|
111
|
+
uniformly narrow (key/value, glossary) sheets are unaffected. Detection
|
|
112
|
+
(`_detect_spreadsheet`) shares the same reader and split with headroom so
|
|
113
|
+
the 500-row classification sample is unchanged, and `SheetInfo.key_column`
|
|
114
|
+
resolves against the real header. (`ingest/spreadsheet.py`,
|
|
115
|
+
`ingest/detect.py`.)
|
|
116
|
+
- **Run-level document manifest.** `womblex run` now consolidates the per-batch
|
|
117
|
+
`batch-NNNN._manifest.parquet` sidecars into a single
|
|
118
|
+
`<run_root>/manifest.parquet` at the end of the run — the published
|
|
119
|
+
documents table mapping `source_hash` (the join key on every chunk/sidecar
|
|
120
|
+
row) back to `doc_id`, `filename`, extraction method, counts and status, so
|
|
121
|
+
shipped chunks are attributable to their source documents. A new
|
|
122
|
+
`womblex manifest --shards <dir> [-o PATH]` command regenerates it for
|
|
123
|
+
existing runs. (`store/run_manifest.py`, `cli/pipeline.py`.)
|
|
124
|
+
- **Shippable enrichment graph.** `enrich_shards` now writes a
|
|
125
|
+
`*.graph_edges.parquet` sibling per batch alongside the entities/meta
|
|
126
|
+
sidecars — the Kanon-2 document graph (containment, segment hierarchy,
|
|
127
|
+
person/location hierarchy, citations, cross-references, contact-info and
|
|
128
|
+
date relations) flattened to the existing `GRAPH_EDGE_SCHEMA`, with
|
|
129
|
+
`document_id` carrying the `source_hash` so it joins the other sidecars.
|
|
130
|
+
When the batch already has a `*.chunks.parquet` sibling, narrative chunks
|
|
131
|
+
are mapped in so the graph includes mention→chunk edges. On resume, a batch
|
|
132
|
+
missing its graph sidecar is re-enriched so prior runs gain it (the graph is
|
|
133
|
+
only buildable from the live enrichment result). New
|
|
134
|
+
`write_graph_edges_shard` / `read_graph_edges` / `graph_edges_path_for`.
|
|
135
|
+
(`analyse/enrich_stage.py`, `store/enrichment_output.py`.)
|
|
136
|
+
- **CLI fix — `womblex chunk --shards` + `--config` combinable.** The two
|
|
137
|
+
flags were in a `required=True` mutually exclusive argparse group, which made
|
|
138
|
+
the `--shards` branch's config handling (chunking settings, `chunking_model`
|
|
139
|
+
for AI chunking, `processing.text_source`) unreachable from the CLI —
|
|
140
|
+
per-stage AI chunking was dead-ended. They now combine: `--shards` with
|
|
141
|
+
`--config` sources chunking settings from the YAML; `--config` alone remains
|
|
142
|
+
the E2E composition mode. (`cli/pipeline.py`.)
|
|
143
|
+
- **Single-enrichment reuse for AI chunking.** When AI chunking
|
|
144
|
+
(`chunking.chunking_model`) and the `enrich` stage are both on, the enrich
|
|
145
|
+
stage now persists the raw ILGS Document per doc to a new
|
|
146
|
+
`*.enrichment_doc.parquet` sidecar (opt-in `enrichment.persist_document`,
|
|
147
|
+
auto-enabled by `WomblexConfig`), and the chunk stage reuses it for semchunk's
|
|
148
|
+
AI path instead of re-enriching — eliminating the double Kanon-2 call. Reuse is
|
|
149
|
+
gated by a **byte-identity guard**: a persisted `Document.text` is used only
|
|
150
|
+
when it equals the chunk stage's freshly reassembled narrative for that
|
|
151
|
+
`source_hash`; on any mismatch (different `text_source`, stale/corrupt blob,
|
|
152
|
+
absent sidecar) the doc falls back to self-enrich, so offsets can never desync
|
|
153
|
+
the PII mention↔chunk mapping. Requires running `enrich` before `chunk`; the
|
|
154
|
+
`WomblexConfig` validator now warns about that ordering rather than about
|
|
155
|
+
double-enrichment. New self-contained `store/enrichment_doc.py`;
|
|
156
|
+
`enrich_documents_raw` / `enrich_document_raw` expose the raw SDK Document;
|
|
157
|
+
`chunk_batch` gains `narrative_overrides`. Verified live against
|
|
158
|
+
`kanon-2-enricher` (gates in `docs/decisions.md`). (`store/enrichment_doc.py`,
|
|
159
|
+
`analyse/enrich.py`, `analyse/enrich_stage.py`, `process/chunker.py`,
|
|
160
|
+
`process/chunk_stage.py`, `config.py`, `cli/link.py`.)
|
|
161
|
+
- **AI chunking pass-through (semchunk 4).** `ChunkingConfig.chunking_model`
|
|
162
|
+
(default `null`) enables semchunk 4's AI chunking — chunk boundaries follow
|
|
163
|
+
the Isaacus enricher's (`kanon-2-enricher`) structure spans instead of the
|
|
164
|
+
offline token/recursive split. Opt-in and off by default, so callers using a
|
|
165
|
+
non-Kanon tokeniser keep purely offline chunking (composable). `create_chunker`
|
|
166
|
+
now forwards `chunking_model`, `isaacus_client`, and `tokenizer_kwargs`
|
|
167
|
+
straight to `semchunk.chunkerify` (thin-adapter doctrine — semchunk's params
|
|
168
|
+
are the feature surface); threaded through both the E2E `run_chunking` path and
|
|
169
|
+
the per-stage `chunk_shards`. Graph-reuse across the chunk + enrich stages (so
|
|
170
|
+
the narrative is enriched once, not twice) is now implemented — see
|
|
171
|
+
"Single-enrichment reuse for AI chunking" above. Bumps `semchunk>=3.0` →
|
|
172
|
+
`>=4.0`. (`process/chunker.py`, `config.py`, `process/chunk_stage.py`,
|
|
173
|
+
`operations/chunk.py`.)
|
|
174
|
+
- **`spellfix` stage — dictionary-gated OCR character-confusion repair
|
|
175
|
+
(`womblex spellfix`).** A separate, opt-in cleaning op (distinct from the
|
|
176
|
+
fidelity-neutral `normalise`) that fixes digit/letter glyph confusions
|
|
177
|
+
(`chi1d`→`child`). Validates candidates against the bundled en_AU Hunspell
|
|
178
|
+
dictionary (`spylls`, harvested from the Australian Writing MCP; MIT/SCOWL) and
|
|
179
|
+
rewrites a token only on three gates: out-of-dictionary trigger,
|
|
180
|
+
single-character in-dictionary candidate, and a *unique* such candidate.
|
|
181
|
+
Default Tier A swaps only OCR digit→letter homoglyphs (length-preserving);
|
|
182
|
+
Tier B general edit-distance-1 is opt-in (`--general` / `general_edits`,
|
|
183
|
+
carries proper-noun risk). Repairs at the **element layer** (reads
|
|
184
|
+
`*.elements.parquet`, chaining off the normalise overlay when present) and
|
|
185
|
+
writes a `*.spellfix_text.parquet` element-text overlay + a
|
|
186
|
+
`*.spellfix_corrections.parquet` audit — raw elements untouched. New deps:
|
|
187
|
+
`spylls`; bundled dict under `_models/en_AU`. (`process/spellfix.py`,
|
|
188
|
+
`process/spellfix_stage.py`, `store/spellfix_output.py`, `cli/spellfix.py`,
|
|
189
|
+
`SpellfixConfig`.)
|
|
190
|
+
- **Composable element-text overlays via one `processing.text_source`.** New
|
|
191
|
+
`process/text_overlay.py` resolves the normalise / spellfix element-text layer
|
|
192
|
+
selected by a single pipeline setting (`elements` | `normalised` | `spellfix`)
|
|
193
|
+
and applies it before reassembly at **both** the chunk and enrich sites, so
|
|
194
|
+
chunking, embeddings, Kanon-2 enrichment and PII all consume the same repaired
|
|
195
|
+
text in one offset coordinate space. Deliberately one knob (not per-stage):
|
|
196
|
+
enrichment runs on the whole document and PII maps mention offsets onto chunks
|
|
197
|
+
via `chunk.start_char`, so the enricher input and chunk source must match.
|
|
198
|
+
- **Enricher `overflow_strategy` (default `auto`).** `enrich_documents` /
|
|
199
|
+
`EnrichmentConfig` now pass `overflow_strategy` to `enrichments.create`,
|
|
200
|
+
defaulting to `auto` (vs upstream `null`, which errors on >16k-token inputs).
|
|
201
|
+
Kanon-2 chunks long documents internally and stitches the ILGS graph back into
|
|
202
|
+
a single prediction; returned span offsets still index the full source, so the
|
|
203
|
+
PII offset mapping is unaffected. Fixes long FOI bundles erroring on enrichment.
|
|
204
|
+
- **`score --text-source` — CER of extraction vs normalisation.** `womblex
|
|
205
|
+
score` (and `score_labels`) now accept `text_source={elements,normalised}`:
|
|
206
|
+
`normalised` reassembles the labelled page from the `*.normalised_text.parquet`
|
|
207
|
+
sidecar instead of the verbatim element stream, so a caller can measure how
|
|
208
|
+
the cleanup/normalisation stage changes CER against the same GT.
|
|
209
|
+
- **Benchmark: ACT-ECI labelled-pages raw-vs-normalised CER.** New
|
|
210
|
+
`TestActEciLabelledPages` (`-m benchmark`) extracts each labelled page, scores
|
|
211
|
+
raw extraction and normalise-stage output against the per-page GT, and reports
|
|
212
|
+
a per-strategy `Raw CER / Norm CER / Δ` table in `docs/accuracy/EXTRACTION.md`.
|
|
213
|
+
Degenerate GT (<20 chars) excluded; a regression guard asserts normalisation
|
|
214
|
+
never worsens CER. (Fixtures cohort expanded 7→19 labelled pages.)
|
|
215
|
+
- **`quality` stage — chunk-quality annotation sidecar (`womblex quality`).**
|
|
216
|
+
Reads `*.chunks.parquet` and writes a `*.chunk_quality.parquet` sibling
|
|
217
|
+
(joined on `(source_hash, chunk_index)`) with ML-readiness flags
|
|
218
|
+
(`char_len`, `alpha_frac`, `is_short`, `boilerplate_flag`) and cross-batch
|
|
219
|
+
duplicate cluster ids (`exact_dup_id`, `near_dup_id`). Duplicate clustering
|
|
220
|
+
is self-contained (no datasketch dep): `exact_dup_id` over
|
|
221
|
+
whitespace/case/punctuation-normalised text, `near_dup_id` via a fixed-seed
|
|
222
|
+
MinHash+LSH (default 64 perms / 4 bands ≈ Jaccard 0.92). Annotation only —
|
|
223
|
+
chunk text is never mutated; runs as a single global pass since dedup is
|
|
224
|
+
corpus-wide. `boilerplate_patterns` are corpus-driven config, never
|
|
225
|
+
hardcoded. New `QualityConfig`; 5 unit tests.
|
|
226
|
+
- **`normalise` stage — `unicode_hygiene` transform.** Folds unicode
|
|
227
|
+
whitespace (NBSP, en/em spaces, ideographic space, U+2028/9 separators) to
|
|
228
|
+
ASCII space/newline and strips zero-width marks, BOM and stray control
|
|
229
|
+
chars; smart quotes and em/en dashes are preserved. New `unicode_hygiene`
|
|
230
|
+
toggle on `NormaliseConfig` (default on), composed ahead of the existing
|
|
231
|
+
transforms. 4 new unit tests.
|
|
232
|
+
|
|
233
|
+
### Fixed
|
|
234
|
+
- **OpenCV 5 compatibility in skew detection.** `detect_skew_angle`
|
|
235
|
+
(`ingest/heuristics_cv2.py`) unpacked `HoughLinesP` segments as `line[0]`,
|
|
236
|
+
which assumes OpenCV 4's `(N, 1, 4)` layout; OpenCV 5 flattens to `(N, 4)`,
|
|
237
|
+
turning `line[0]` into a scalar and crashing every OCR-path extraction
|
|
238
|
+
(`TypeError: cannot unpack non-iterable numpy.int32`). The segments are now
|
|
239
|
+
reshaped to `(-1, 4)` before unpacking, which accepts both layouts. New
|
|
240
|
+
direct unit tests pin both shapes so the regression no longer needs the
|
|
241
|
+
full OCR fixture suite to surface.
|
|
242
|
+
- **mypy no longer pins `python_version = "3.11"`.** The pin forced the CI
|
|
243
|
+
3.12 matrix leg to re-check under 3.11 grammar — redundant with the 3.11
|
|
244
|
+
leg, and broken once numpy ≥ 2.5 (3.12-only) began shipping PEP 695 `type`
|
|
245
|
+
statements in its stubs, which mypy rejects under a 3.11 target. Each leg
|
|
246
|
+
now checks at its own interpreter version.
|
|
247
|
+
- **Register manifest now covers `ingest-geo` and derives roles from footer
|
|
248
|
+
metadata only.** `cmd_ingest_geo` never called `write_register_manifest`
|
|
249
|
+
despite the documented `abn`/`gnaf`/`geo` coverage, and the module's
|
|
250
|
+
namespace whitelist said `geo` while `ingest/geospatial.py` writes
|
|
251
|
+
`geospatial.*` footer keys — so geo outputs could not be indexed at all.
|
|
252
|
+
The namespace is now taken from whichever `<ns>.source_file` footer key is
|
|
253
|
+
present (no per-register registry to keep in sync), geo ingest writes the
|
|
254
|
+
manifest like ABN/G-NAF, and the ABN ingest tags each output with an
|
|
255
|
+
`abn.role` footer key (`records`/`names`) so the manifest's role column
|
|
256
|
+
comes from metadata rather than the `_names` filename suffix — the exact
|
|
257
|
+
glob-style fragility the manifest exists to remove. ABN outputs written
|
|
258
|
+
before this change lack the role key and re-index as `records`; re-run
|
|
259
|
+
`ingest-abn` to restore the distinction. Also renames the module's
|
|
260
|
+
`RUN_MANIFEST_FILENAME` constant to `REGISTER_MANIFEST_FILENAME` — it had
|
|
261
|
+
borrowed the *run* manifest's constant name from `store/run_manifest.py`
|
|
262
|
+
while naming a different artefact.
|
|
263
|
+
- **`RemoteStore` no longer leaks s3fs-shaped options into non-S3 backends.**
|
|
264
|
+
`storage_options_from_env()` built AWS-style kwargs (`key`, `secret`,
|
|
265
|
+
`client_kwargs.endpoint_url`) and `from_uri` applied them to *any* remote
|
|
266
|
+
protocol, so a `gs://`/`az://` store with AWS env vars set (common in mixed
|
|
267
|
+
environments) got misconfigured. The helper now takes the target URI and
|
|
268
|
+
returns options only for `s3://`; other backends authenticate via their own
|
|
269
|
+
native mechanisms. Also: `womblex enqueue`'s batch-size fallback now reads
|
|
270
|
+
`ProcessingConfig()` instead of restating the default, and the worker
|
|
271
|
+
derives its shard-upload glob from the `BatchOutcome.shard_path` the batch
|
|
272
|
+
reported, so the `batch-NNNN` naming scheme lives only in `womblex.batch`.
|
|
273
|
+
- **K9-fig — full-page scans no longer dropped from chunking as `figure`.**
|
|
274
|
+
The dominant-region fallback in `_layout_blocks_and_tables` collapsed a
|
|
275
|
+
whole page's OCR onto one block tagged with the largest layout region's
|
|
276
|
+
kind; when that was DocLayNet `Picture` → `figure`, a full-page scanned
|
|
277
|
+
document became a single `figure` element, which is excluded from the
|
|
278
|
+
chunk narrative (`figure` ∉ `TEXT_KINDS`) — silently losing the document.
|
|
279
|
+
New shared helper `_ocr_region_block_type(text, layout_kind)` promotes a
|
|
280
|
+
non-text fallback kind to `paragraph` when the OCR yields ≥5 words; sparse
|
|
281
|
+
output (page-number stamps, bare logos) keeps `figure`. (The original E4
|
|
282
|
+
audit mis-attributed this to `_ocr_image_regions`; that path now routes
|
|
283
|
+
through the same helper.) On the ACT-ECI corpus: `figure` 1,200→154,
|
|
284
|
+
`paragraph` +1,046, and all 16 previously zero-chunk complaint documents
|
|
285
|
+
now produce chunks (docs-with-chunks 2,610→2,626). 4 new unit tests.
|
|
286
|
+
|
|
287
|
+
### Added
|
|
288
|
+
- **I7 — entity-link sidecar: `womblex enrich` + `womblex link` per-stage CLIs.**
|
|
289
|
+
Two new per-stage stages mirroring `womblex chunk --shards`, each with an
|
|
290
|
+
independent `CheckpointManager` and per-batch sibling parquets.
|
|
291
|
+
`womblex enrich --shards <dir>` reassembles each doc's narrative
|
|
292
|
+
(`reassemble_narrative`), calls the Kanon-2 enricher one doc at a time
|
|
293
|
+
(per-doc failure isolation), and writes `*.enrichment_entities.parquet` +
|
|
294
|
+
`*.enrichment_meta.parquet` (reusing `store/enrichment_output.py` schemas,
|
|
295
|
+
keyed on `source_hash`). `womblex link --shards <dir> --config <yaml>`
|
|
296
|
+
resolves enrichment candidates (corporate persons + address locations) to a
|
|
297
|
+
reference register and writes `*.entity_links.parquet`. **Generic by design:**
|
|
298
|
+
the schema uses an `entity_type` discriminator (no domain columns), the
|
|
299
|
+
matcher (`link/matcher.py`) is generic record-linkage (alias → address-exact
|
|
300
|
+
→ token-set name-fuzzy, stdlib `difflib`, no new dependency), and the corpus
|
|
301
|
+
declares register column-roles via the new `linking`/`reference` config — the
|
|
302
|
+
library knows nothing about specific registers. Reference loading
|
|
303
|
+
(`link/reference.py`) is bundle-aware by interface (CSV implemented; the
|
|
304
|
+
multi-file/geospatial seam is reserved, not built). Doc-grain attribution is
|
|
305
|
+
a derived read view over the persisted mention-grain rows, not a second file.
|
|
306
|
+
New `isaacus` is the optional extra (`uv sync --extra isaacus`). Live smoke
|
|
307
|
+
over the 17-doc Artemis set attributed 15/17 to the correct canonical service
|
|
308
|
+
(`SE-40002132`); the 2 misses are an enrichment-recall gap and an
|
|
309
|
+
OCR-typo+no-address doc, not matcher faults. New `tests/test_link.py` (23) +
|
|
310
|
+
`tests/test_enrich_stage.py`; full fast suite green.
|
|
311
|
+
- **Matcher** uses stdlib `difflib` only (no rapidfuzz dependency): alias →
|
|
312
|
+
address-exact → name-fuzzy, where name-fuzzy combines a token-set ratio
|
|
313
|
+
(suburb-suffix recall, cross-brand precision) with OCR-tolerant per-token
|
|
314
|
+
char similarity (folds "Earty"→"Early" while still rejecting a different
|
|
315
|
+
brand). With OCR tolerance the Artemis smoke reaches **16/17**.
|
|
316
|
+
- **`enrich`** isolates per-doc failures and, critically, does **not**
|
|
317
|
+
checkpoint a doc whose enrichment errored — a transient/connection failure
|
|
318
|
+
stays unprocessed so a resume retries it (regression-tested).
|
|
319
|
+
- **`womblex embed --shards` — chunk embeddings stage (Kanon-2 embedder).**
|
|
320
|
+
`analyse/embed.py` (thin `embeddings.create` wrapper: 128-text batching, 429
|
|
321
|
+
retry, order-preserving, `retrieval/document`/`query` task-aware) +
|
|
322
|
+
`analyse/embed_stage.py` (`embed_shards` over `*.chunks.parquet` →
|
|
323
|
+
`*.embeddings.parquet`, one vector per chunk, per-stage `CheckpointManager`,
|
|
324
|
+
batch-level failure isolation) + `cli/embed.py` + `EmbeddingConfig` +
|
|
325
|
+
`EMBEDDINGS_SCHEMA`/IO. The substrate for downstream search/clustering and a
|
|
326
|
+
doc→entity attribution backstop for no-extraction docs. `tests/test_embed_stage.py`.
|
|
327
|
+
- **I8–I10 — `womblex pii --shards` per-stage CLI: graph-driven detection +
|
|
328
|
+
`<PERSON_n>` masking.** Reads `*.chunks.parquet` + `*.enrichment_entities.parquet`
|
|
329
|
+
and writes `*.pii_spans.parquet` (audit; one row per span with `entity_id` +
|
|
330
|
+
`replacement`) plus a masked `*.clean_text.parquet` (publishable text layer,
|
|
331
|
+
drop-in for chunks). The Kanon-2 graph is the primary entity source
|
|
332
|
+
(`natural`→PERSON, `address`→ADDRESS); graph spans map onto narrative chunks
|
|
333
|
+
via `chunk.start_char`. Masking is **terminal** — applied after enrich + embed,
|
|
334
|
+
never rewriting the raw chunks that feed Isaacus. Tags are typed + numbered per
|
|
335
|
+
document off the graph entity (`<PERSON_1>`…). `pii/cleaner.py` refactored to a
|
|
336
|
+
span-returning `detect_spans()` + shared `_anonymize()`; the regex/cosine
|
|
337
|
+
backstop is now opt-in (`PIIConfig.use_regex_backstop`, default off — low
|
|
338
|
+
precision). New `store/pii_output.py`, `pii/pii_stage.py`, `cli/pii.py`,
|
|
339
|
+
`PIIConfig.write_clean_text`. `tests/test_pii_stage.py`.
|
|
340
|
+
- **I3 — `womblex redact --shards` per-stage CLI.** `womblex redact` is
|
|
341
|
+
now dual-mode, mirroring `womblex chunk`: `--shards <dir> --pdfs <dir>`
|
|
342
|
+
runs per-stage redaction detection over an existing extraction shard
|
|
343
|
+
directory and writes `*.redactions.parquet` siblings; `--config <yaml>`
|
|
344
|
+
runs the E2E extract+redact path unchanged. The `--shards`/`--config`
|
|
345
|
+
group is mutually exclusive and required. `--pdfs` is mandatory in
|
|
346
|
+
`--shards` mode because detection rasterises the source pages (unlike
|
|
347
|
+
chunking, which works purely from the element stream). The per-stage
|
|
348
|
+
path calls the existing `redact.batch.annotate_redactions_for_shards`
|
|
349
|
+
engine via a shared `_run_redact_shards` helper. 8 new CLI tests.
|
|
350
|
+
|
|
351
|
+
### Changed
|
|
352
|
+
- **`operations.py` split into an `operations/` package.** The 902-line module
|
|
353
|
+
(over the 750-line cap) became one module per independent operation
|
|
354
|
+
(`models`/`extract`/`redact`/`chunk`/`pii`/`enrich`/`persist`), each ≤90
|
|
355
|
+
lines. The flat import surface (`from womblex.operations import run_extraction`,
|
|
356
|
+
…) is preserved by `operations/__init__` re-exports — no caller changes.
|
|
357
|
+
Behaviour-neutral; `test_integration.py` patch targets for `create_chunker`
|
|
358
|
+
moved to `womblex.operations.chunk`.
|
|
359
|
+
- **Resume-integrity self-heal generalised across stages.** `store/shard_audit.py`
|
|
360
|
+
gains `reconcile_stage_checkpoint_with_shards(mgr, dir, *, suffix)`; the chunk
|
|
361
|
+
reconcile now delegates to it, and `enrich`/`link`/`embed` wire it (+
|
|
362
|
+
`--no-verify-resume`) — so every `CheckpointManager`-backed stage drops +
|
|
363
|
+
re-does batches with corrupt sidecars on resume, identically.
|
|
364
|
+
- **I5 — SemChunk wrapper audit (P2).** Audited `process/chunker.py`
|
|
365
|
+
against semchunk 3.2.5: `create_chunker` exposes every `chunkerify`
|
|
366
|
+
creation parameter and `chunk_batch` passes every relevant
|
|
367
|
+
`Chunker.__call__` parameter through (`offsets=True` pinned because
|
|
368
|
+
Womblex needs char offsets for page mapping). No semchunk-native
|
|
369
|
+
surface is reimplemented or shadowed. **Removed the dead
|
|
370
|
+
`ChunkingConfig.batch` flag** — it mapped to no semchunk parameter,
|
|
371
|
+
was consumed by no code path, and its description referred to the
|
|
372
|
+
pre-I2 per-document-vs-batch behaviour that I2 deleted (chunk_batch
|
|
373
|
+
always batches the whole input list). **Widened `chunk_size` to
|
|
374
|
+
`int | None`** so semchunk's auto-derive path (`None` → size from the
|
|
375
|
+
tokeniser's `model_max_length`) passes through faithfully; the default
|
|
376
|
+
stays `480` (the Kanon-2 window), so behaviour is unchanged unless a
|
|
377
|
+
config explicitly sets `chunk_size: null`. Documented the adapter
|
|
378
|
+
boundary explicitly in the `chunker.py` module docstring, the
|
|
379
|
+
`ChunkingConfig` docstring, and `docs/extraction.md`; the three
|
|
380
|
+
default divergences from upstream (`tokenizer`, `chunk_size=480`,
|
|
381
|
+
`processes=1`) are each annotated with their corpus reason. Pure
|
|
382
|
+
thin-adapter cleanup — chunk output is byte-identical to I2 by
|
|
383
|
+
construction (the removed field was never read; the new `None` path is
|
|
384
|
+
opt-in). 98 chunker/config/pipeline/output tests pass; 79 integration
|
|
385
|
+
tests pass.
|
|
386
|
+
- **`annotate-redactions` is now a deprecated back-compat alias** for
|
|
387
|
+
`redact --shards <dir> --pdfs <dir>`. Its positional-argument surface
|
|
388
|
+
(`annotate-redactions <shards> <pdfs>`) is preserved verbatim and routes
|
|
389
|
+
through the same `_run_redact_shards` helper, so existing scripts keep
|
|
390
|
+
working with byte-identical output. New callers should prefer
|
|
391
|
+
`redact --shards`. The redact stage retains the engine's JSON
|
|
392
|
+
`--checkpoint` rather than the `CheckpointManager` used by `chunk`;
|
|
393
|
+
unifying the two is a deferred P1 follow-up.
|
|
394
|
+
|
|
395
|
+
### Added
|
|
396
|
+
- **I2 — `womblex chunk --shards` per-stage CLI + `chunks.parquet`
|
|
397
|
+
sidecar.** New `CHUNKS_SCHEMA` (source_hash, chunk_index, text,
|
|
398
|
+
start_char/end_char, content_type, has_redaction, page_start /
|
|
399
|
+
page_end) in `store/output.py` with `write_chunks` / `read_chunks` /
|
|
400
|
+
`verify_chunks_persistence`. New `process/chunk_stage.py` walks a
|
|
401
|
+
shard directory, reassembles narrative + tables from the element
|
|
402
|
+
stream per source_hash, calls the single `chunk_batch` engine, and
|
|
403
|
+
writes a `*.chunks.parquet` sibling per batch. Per-stage
|
|
404
|
+
`CheckpointManager` keyed `<dataset>_chunk_checkpoint.json`;
|
|
405
|
+
chunks-side resume integrity in `shard_audit.scan_chunks_directory`
|
|
406
|
+
/ `reconcile_chunk_checkpoint_with_shards` archives corrupt
|
|
407
|
+
`*.chunks.parquet` independently of the element-stream files. The
|
|
408
|
+
shared `chunk_batch` powers both per-stage `chunk_shards` and E2E
|
|
409
|
+
`operations.run_chunking`, so `--shards` and `--config` modes feed
|
|
410
|
+
semchunk identical inputs.
|
|
411
|
+
|
|
412
|
+
### Changed
|
|
413
|
+
- **`process/chunker.py` collapsed against semchunk v3+ surface.**
|
|
414
|
+
Deleted per-doc wrappers `chunk_text`, `chunk_texts_batch`,
|
|
415
|
+
`chunk_document` (+ `_chunk_document_sequential` /
|
|
416
|
+
`_chunk_document_batch` dispatchers) — semchunk already batches
|
|
417
|
+
across a list of texts and parallelises over `processes` workers
|
|
418
|
+
when handed one. The new single entry point `chunk_batch(inputs,
|
|
419
|
+
chunker, ...)` flattens every doc's narrative into one semchunk
|
|
420
|
+
call (with `overlap`) and every doc's table markdowns into another
|
|
421
|
+
(no overlap), so `processes` and the progress bar parallelise
|
|
422
|
+
across the entire batch instead of being thrown away per-document.
|
|
423
|
+
`TextChunk` gained `page_start` / `page_end` (nullable); the
|
|
424
|
+
redaction-split repair pass propagates page spans across a merge.
|
|
425
|
+
New helpers `reassemble_narrative`, `collect_tables_from_elements`,
|
|
426
|
+
`build_chunk_input` formalise the "element stream → ChunkInput"
|
|
427
|
+
projection shared by both invocation paths.
|
|
428
|
+
- **`operations.run_chunking` rewired through `chunk_batch`.** Builds
|
|
429
|
+
one `ChunkInput` per completed result from
|
|
430
|
+
`dr.extraction.elements` (canonical), not `dr.extraction.full_text`
|
|
431
|
+
(which is derived from `pages` and reflects in-memory mutations).
|
|
432
|
+
Behaviour change: in-memory PII / redact-blackout mutations to
|
|
433
|
+
`pages[i].text` no longer flow to chunks under `womblex run`.
|
|
434
|
+
Aligns the E2E path with the per-stage one (both consume the
|
|
435
|
+
element stream); future PII / redact stages will reattach via
|
|
436
|
+
their sidecars per P1.
|
|
437
|
+
|
|
438
|
+
### Added
|
|
439
|
+
- **Shard integrity scan on `--resume` (E1).** New
|
|
440
|
+
`womblex.store.shard_audit.reconcile_checkpoint_with_shards` runs at
|
|
441
|
+
the top of `cmd_run` when `--resume` is given. Walks every batch's
|
|
442
|
+
four sibling parquet files: confirms presence + non-empty + parquet-
|
|
443
|
+
readable, and that manifest `elements_count` / `table_cells_count` /
|
|
444
|
+
`form_fields_count` sums match the actual sidecar row counts. Any
|
|
445
|
+
batch failing a check has its `doc_id`s dropped from the checkpoint
|
|
446
|
+
and its files renamed with a `.corrupt` suffix so reader globs
|
|
447
|
+
(`*.elements.parquet` etc.) skip them; the dropped docs get re-
|
|
448
|
+
extracted into new batches past the high-water mark. Batches whose
|
|
449
|
+
manifest is itself unreadable can't be reconciled automatically (no
|
|
450
|
+
way to enumerate `doc_id`s) — they're logged loudly and the operator
|
|
451
|
+
is told to re-run without `--resume`. Defaults on; opt out with
|
|
452
|
+
`--no-verify-resume`. Closes the silent-failure class of post-write
|
|
453
|
+
filesystem corruption (drive glitch, partial sync, manual deletion)
|
|
454
|
+
that motivated the i1b batch-0087 0-byte incident.
|
|
455
|
+
|
|
456
|
+
- **`womblex verify-shards` CLI (E2).** Audits a run / shard directory
|
|
457
|
+
for corruption + cross-batch consistency; takes a shard dir or a run
|
|
458
|
+
root (auto-detects `documents/`). Reports per-batch integrity, total
|
|
459
|
+
elements / methods / kind counts, dupe and empty hashes. With
|
|
460
|
+
`--compare-to <other>` produces a side-by-side diff against another
|
|
461
|
+
run (useful for K-cluster-style "what changed between two
|
|
462
|
+
extractions" investigations — promotes the ad-hoc `i1b_audit.py`
|
|
463
|
+
pattern to first-class library + CLI). Optional `--input-dir <pdfs>`
|
|
464
|
+
surfaces source-vs-manifest count drift. Exits 2 when corruption is
|
|
465
|
+
detected so CI / cron pipelines can fail loudly. New module:
|
|
466
|
+
`womblex.store.shard_audit`. New tests in `tests/test_shard_audit.py`
|
|
467
|
+
(19).
|
|
468
|
+
|
|
469
|
+
### Changed
|
|
470
|
+
- **Manifest schema gains `doc_id` column.** `MANIFEST_SCHEMA` now
|
|
471
|
+
carries the extraction's `doc_id` directly, removing the implicit
|
|
472
|
+
`Path(filename).stem == checkpoint.doc_id` coincidence that previously
|
|
473
|
+
bound the resume reconcile join. The reader (`read_manifest`) is
|
|
474
|
+
backward-compatible: manifests written before the bump derive
|
|
475
|
+
`doc_id` from `Path(filename).stem` on read so existing runs reconcile
|
|
476
|
+
without re-extraction. Parser version bump is intentionally deferred
|
|
477
|
+
— the schema is additive and reads gracefully.
|
|
478
|
+
|
|
479
|
+
### Added
|
|
480
|
+
- **K7(b) — Document-layout YOLO model (DocLayNet).** New default layout
|
|
481
|
+
checkpoint `yolo11n_doc_layout.pt` (5.37 MB,
|
|
482
|
+
[Armaggheddon/yolo11-document-layout](https://huggingface.co/Armaggheddon/yolo11-document-layout),
|
|
483
|
+
MIT) replaces the COCO-trained `yolov8n.pt` as the primary layout
|
|
484
|
+
backend. `YOLOLayoutAnalyzer` detects the loaded model's taxonomy from
|
|
485
|
+
its class names: DocLayNet's 11 document classes (Caption, Footnote,
|
|
486
|
+
Formula, List-item, Page-footer, Page-header, Picture, Section-header,
|
|
487
|
+
Table, Text, Title) map directly into womblex `ElementKind` values
|
|
488
|
+
via the new `_YOLO_DOCLAYNET_LABEL_MAP`. COCO weights remain as a
|
|
489
|
+
best-effort fallback when the DocLayNet checkpoint isn't resolvable.
|
|
490
|
+
Inference imgsz follows a per-taxonomy default (DocLayNet: 832, COCO:
|
|
491
|
+
640) — empirically equivalent on this corpus to the model card's 1280
|
|
492
|
+
recommendation while matching COCO speed; override to 1280 when
|
|
493
|
+
small-class (Caption / Footnote) recall matters. Closes the
|
|
494
|
+
1,587-element `kind='figure'` mis-classification on scanned pages
|
|
495
|
+
tracked in docs/decisions.md; unlocks Caption / Footnote producers
|
|
496
|
+
(K6 closes as a side effect).
|
|
497
|
+
|
|
498
|
+
- **`footnote` ElementKind.** New text-bearing kind added to
|
|
499
|
+
`ElementKind`, `TEXT_KINDS`, and `_BLOCK_TYPE_TO_KIND`. Primary
|
|
500
|
+
producer is the DocLayNet `Footnote` class via the new label map.
|
|
501
|
+
Downstream stages (PII / redact / chunk) operate on text kinds and
|
|
502
|
+
pick up the new kind automatically through `TEXT_KINDS`. Future
|
|
503
|
+
iterations may refine signatory / footnote separation now that the
|
|
504
|
+
distinction is preserved.
|
|
505
|
+
|
|
506
|
+
- **K2′ — OCR form-pair bboxes.** New `_extract_form_pairs_from_regions`
|
|
507
|
+
in `ingest/forms.py` walks per-region OCR detections (PaddleOCR /
|
|
508
|
+
RapidOCR per-line bboxes) and produces `FormField` entries with real
|
|
509
|
+
positions. `_apply_ocr_page` prefers this path; the legacy
|
|
510
|
+
`_extract_form_pairs_from_lines` survives as a fallback for LLM-OCR
|
|
511
|
+
engines that resolve reading order natively and don't emit per-region
|
|
512
|
+
bboxes. Closes the K2′ silent-zero-bbox issue on 4,184 of 5,183 OCR
|
|
513
|
+
form elements (80.7%). Same plumbing unblocks inline-per-span
|
|
514
|
+
redaction markers on raster pages (P6 option (c); see docs/decisions.md).
|
|
515
|
+
|
|
516
|
+
### Changed
|
|
517
|
+
- **`@pytest.mark.slow` tests now run by default.** Removed the
|
|
518
|
+
`-m 'not slow'` default from `[tool.pytest.ini_options].addopts`. The
|
|
519
|
+
24 OCR-fixture tests in `tests/test_fixtures.py` were originally marked
|
|
520
|
+
slow because they invoked EasyOCR (30+ seconds each). The backend has
|
|
521
|
+
since moved to rapidocr-onnxruntime and the whole cohort completes in
|
|
522
|
+
~7 seconds, so excluding them was costing coverage without saving
|
|
523
|
+
meaningful time. The `slow` marker is retained (description updated)
|
|
524
|
+
so users can still pass `-m 'not slow'` or `-m slow` for ad-hoc
|
|
525
|
+
filtering.
|
|
526
|
+
|
|
527
|
+
### Added
|
|
528
|
+
- **`run_id` + retention plumbing (I1 of publishable-corpus track).**
|
|
529
|
+
Pipeline runs now write outputs to `<output_root>/<run_id>/documents/`
|
|
530
|
+
rather than `<output_root>/documents/`. Run id resolution order:
|
|
531
|
+
`--run-id` CLI flag → `dataset.run_id` in config → auto-generated
|
|
532
|
+
`run-YYYYMMDDTHHMMSSZ` timestamp. `--resume` without a run id picks
|
|
533
|
+
the most-recent existing run dir. Checkpoints follow under
|
|
534
|
+
`<checkpoint_dir>/<run_id>/`.
|
|
535
|
+
|
|
536
|
+
New `processing.retention` config block: `policy: rolling | keep_all`
|
|
537
|
+
(default `rolling`) and `keep: int` (default `2`). On fresh runs (not
|
|
538
|
+
`--resume`), old run dirs beyond the retention window are purged in
|
|
539
|
+
lockstep with their checkpoint dirs. The current run is always
|
|
540
|
+
preserved regardless of position.
|
|
541
|
+
|
|
542
|
+
Foundation for stage-aware sidecar persistence (I2+); no extraction
|
|
543
|
+
output content has changed.
|
|
544
|
+
|
|
545
|
+
New module: `womblex.store.retention` (`generate_run_id`, `list_runs`,
|
|
546
|
+
`most_recent_run`, `apply_retention`). New tests in
|
|
547
|
+
`tests/test_retention.py` (20) and `tests/test_config.py` (8).
|
|
548
|
+
|
|
549
|
+
Retention only considers subdirectories whose name starts with `run-`
|
|
550
|
+
— legacy / hand-named output dirs (e.g. `output/documents/` from a
|
|
551
|
+
pre-run_id layout) are preserved unconditionally and must be removed
|
|
552
|
+
manually if no longer wanted. To bring a hand-named run under the
|
|
553
|
+
policy, name it with a `run-` prefix.
|
|
554
|
+
|
|
555
|
+
### Changed — BREAKING
|
|
556
|
+
- **Extraction output reshaped to element-stream + typed sidecars.**
|
|
557
|
+
`ExtractionResult` now carries `elements: list[Element]` as the
|
|
558
|
+
canonical structural stream. Per-batch parquet output is split across
|
|
559
|
+
four sibling files: `batch-NNNN.elements.parquet` plus
|
|
560
|
+
`.table_cells.parquet`, `.form_fields.parquet`, `._manifest.parquet`.
|
|
561
|
+
Sidecars are joinable via `(source_hash, parent_elem_order)`. The
|
|
562
|
+
previous one-parquet-per-batch shape (`EXTRACTION_SCHEMA` with nested
|
|
563
|
+
struct lists for tables / forms / images / text_blocks) is removed.
|
|
564
|
+
See `docs/extraction.md` for the canonical reference.
|
|
565
|
+
- **Narrative ↔ table interleaving preserved.** DOCX bodies are walked
|
|
566
|
+
in OOXML order so paragraphs and tables emit in their true position;
|
|
567
|
+
PDF per-page outputs are sorted by `(y, x)` so within-page reading
|
|
568
|
+
order survives.
|
|
569
|
+
- **Spreadsheets: one ExtractionResult per workbook.** Cells are the
|
|
570
|
+
element grain (`kind='sheet_cell'`); sheets emit a leading
|
|
571
|
+
`sheet_meta` element. The previous one-result-per-row shape is
|
|
572
|
+
removed.
|
|
573
|
+
- **Verbatim text at the schema boundary.** `_normalise_text` no longer
|
|
574
|
+
runs in the extraction hot path. Page text and element text are
|
|
575
|
+
emitted verbatim from the producing extractor. Downstream stages may
|
|
576
|
+
still mutate `pages[i].text`; the on-disk parquet is unaffected.
|
|
577
|
+
- **Legacy view properties remain.** `ExtractionResult.text_blocks` /
|
|
578
|
+
`.tables` / `.forms` / `.images` are now read-only derived views over
|
|
579
|
+
`elements` so downstream PII / redact / chunk stages continue working
|
|
580
|
+
without change.
|
|
581
|
+
- **Sidecar integrity check.** `verify_shard_persistence` now confirms
|
|
582
|
+
every `(source_hash, parent_elem_order)` in `table_cells` /
|
|
583
|
+
`form_fields` resolves to an element with the matching kind. Drift
|
|
584
|
+
raises `ShardVerificationError`.
|
|
585
|
+
|
|
586
|
+
### Added
|
|
587
|
+
- **Per-page extraction plan + orchestrator.** PDFs now route via per-page
|
|
588
|
+
profiling (`ingest/page_profile.py`) and a doc-wide orchestrator
|
|
589
|
+
(`ingest/orchestrator.py`) that dispatches operations page-by-page,
|
|
590
|
+
rather than picking one document-level strategy. Correctly handles
|
|
591
|
+
heterogeneous FOI bundles (e.g. one image-only page in an otherwise-
|
|
592
|
+
native doc) that the previous sample-based detector missed.
|
|
593
|
+
- **Spreadsheet-print extractor** (`ingest/spreadsheet_print.py`) for
|
|
594
|
+
native PDFs printed from a spreadsheet (CSV/Excel) — common shape for
|
|
595
|
+
government FOI manifests, schedules, and registers. Extracts row-by-row
|
|
596
|
+
data plus a metadata block (the label-value fields above the first
|
|
597
|
+
data row). Handles 90°-rotated landscape pages, multi-line headers,
|
|
598
|
+
centred header text within wider cells, and frozen headers across
|
|
599
|
+
pages. Triggered by a cheap qualifier (filename hints + table signal)
|
|
600
|
+
before structural vetting. Config-driven via
|
|
601
|
+
`extraction.native.spreadsheet_print` (`metadata_location`,
|
|
602
|
+
`filename_hints`).
|
|
603
|
+
- **Form-pair text extractor** (`ingest/forms.py`) — picks up "Field:
|
|
604
|
+
value" form pairs in native PDFs (`_extract_form_pairs_from_text`)
|
|
605
|
+
and OCR'd text (`_extract_form_pairs_from_lines`). Forms-column
|
|
606
|
+
populated for ~94 % of docs in the ACT FOI corpus (was 0 %).
|
|
607
|
+
- **`block_type` classifier rewrite.** Native blocks now typed by
|
|
608
|
+
position + typography (footer, header, signature, heading, paragraph)
|
|
609
|
+
instead of length-only fallback. `caption` reserved for image-adjacent
|
|
610
|
+
blocks downstream.
|
|
611
|
+
- **Per-image OCR on native pages** with embedded image regions (e.g.
|
|
612
|
+
letterhead text). Generalised from hybrid-only via the existing
|
|
613
|
+
`_ocr_image_regions` helper; recovers text-in-images that previously
|
|
614
|
+
fell through extraction.
|
|
615
|
+
- **Multi-strategy table detection** — PyMuPDF `find_tables(strategy=
|
|
616
|
+
"lines")` falls back to `strategy="text"` when the document uses
|
|
617
|
+
whitespace-aligned columnar layout instead of ruled cells.
|
|
618
|
+
- **`TableData.context` and `ExtractionResult.document_metadata`** —
|
|
619
|
+
per-table and document-level metadata blocks for spreadsheet-print
|
|
620
|
+
layouts.
|
|
621
|
+
- `womblex profile` CLI command for per-column schema inference on
|
|
622
|
+
CSV / XLSX / XLS / Parquet / NDJSON files.
|
|
623
|
+
|
|
624
|
+
### Changed
|
|
625
|
+
- **PDF dispatch flips from doc-level to page-level.** PDF strategy
|
|
626
|
+
classes (`Native*Extractor`, `Scanned*Extractor`, `HybridExtractor`,
|
|
627
|
+
`StructuredExtractor`) are deleted; `get_extractor()` now only
|
|
628
|
+
dispatches non-PDF types (DOCX, spreadsheet, text, image). The
|
|
629
|
+
orchestrator is the canonical PDF path.
|
|
630
|
+
- **Source redaction and PII tokens unified on angle brackets.**
|
|
631
|
+
`redact/stage.py` blackout mode emits `<REDACTED>` (was `[REDACTED]`);
|
|
632
|
+
`pii/cleaner.py` emits `<ENTITY_TYPE>` (e.g. `<PERSON>`, `<ADDRESS>`,
|
|
633
|
+
`<EMAIL>`). Aligns with the labelling-packet ground-truth fixtures
|
|
634
|
+
(`_transcript-with-redacted-tags*.txt`). The BPE/SentencePiece
|
|
635
|
+
tokenisation argument that previously motivated square brackets didn't
|
|
636
|
+
hold up — neither bracket style is single-piece in standard pretrained
|
|
637
|
+
tokenisers without explicit special-token registration. Source
|
|
638
|
+
redactions remain page-prefix (not inline per span) — inline-per-span
|
|
639
|
+
requires bbox-to-text mapping not currently routed through to the
|
|
640
|
+
redact stage; tracked as a follow-up.
|
|
641
|
+
|
|
642
|
+
### Internal
|
|
643
|
+
- Moved page-image morphology helpers from `detect.py` into
|
|
644
|
+
`ingest/morphology.py` to keep `detect.py` under the 750-line cap.
|
|
645
|
+
- `_normalise_text` regex extended to catch fullwidth Unicode `Page`
|
|
646
|
+
variants in OCR'd footers (`5lpage`, `3lPage`).
|
|
647
|
+
|
|
648
|
+
### Packaging
|
|
649
|
+
- MANIFEST.in no longer packages all of `src/womblex/_models/` — only the
|
|
650
|
+
en_AU Hunspell dictionary ships, matching the declared package-data. The
|
|
651
|
+
previous rule pulled every local model artefact (~110 MB compressed) into
|
|
652
|
+
both the sdist and, via setuptools' `include-package-data` default, the
|
|
653
|
+
wheel — over PyPI's 100 MB per-file limit. Large models remain resolved
|
|
654
|
+
via `WOMBLEX_MODELS_DIR`.
|
|
655
|
+
- PyPI metadata completed: `readme`, project URLs; setuptools floor raised
|
|
656
|
+
to >=77 for PEP 639 SPDX licence string support.
|
|
657
|
+
- The kanon-2 tokenizer (~5 MB) is bundled in the wheel alongside the en_AU
|
|
658
|
+
dictionary, so pip-installed token packing and chunking resolve it locally
|
|
659
|
+
with no Hugging Face round-trip, as already documented.
|
|
660
|
+
|
|
661
|
+
## [0.1.0] - 2026-04-28
|
|
662
|
+
|
|
663
|
+
### Added
|
|
664
|
+
- Initial public release.
|
|
665
|
+
- PDF extraction with document-type routing (native, scanned, hybrid, forms, tables).
|
|
666
|
+
- DOCX and spreadsheet (CSV/XLSX/XLS) ingestion.
|
|
667
|
+
- Standalone G-NAF and geospatial (SHP) ingest paths.
|
|
668
|
+
- Post-extraction redaction stage with configurable insertion points.
|
|
669
|
+
- Optional PII cleaning (PERSON detection via regex + cosine-similarity validation).
|
|
670
|
+
- semchunk-based chunking with table-aware splitting.
|
|
671
|
+
- Optional Isaacus enrichment via the `[isaacus]` extra.
|
|
672
|
+
- Local model resolution via `WOMBLEX_MODELS_DIR` for offline / edge deployment.
|
|
673
|
+
|
|
674
|
+
## Historical engineering notes (migrated from STATUS.md 2026-05-28)
|
|
675
|
+
|
|
676
|
+
> These sections record the point-in-time engineering detail behind the
|
|
677
|
+
> structured `[Unreleased]` / `[0.1.0]` entries above: the extraction-quality
|
|
678
|
+
> session bundle (native-page tables, block-aware paragraphs, footer
|
|
679
|
+
> pipe-as-I, the reverted OCR-table relaxation), the Phase 1–6 roadmap
|
|
680
|
+
> history, the CLI restructure, the detector evolution (vector-first +
|
|
681
|
+
> raster-path layout filter), and the redaction/PII marker-convention
|
|
682
|
+
> unification. The durable decisions, dead-ends, limitations and deferred
|
|
683
|
+
> backlog now live in [docs/decisions.md](docs/decisions.md); corpus-specific
|
|
684
|
+
> run history is a corpus concern (see the corpus's own status notes).
|
|
685
|
+
> (These notes were originally migrated from a now-removed `STATUS.md`.)
|
|
686
|
+
|
|
687
|
+
## What changed
|
|
688
|
+
|
|
689
|
+
The session bundled four production-hardening fixes addressing
|
|
690
|
+
distinct quality gaps surfaced by per-page GT comparison against the
|
|
691
|
+
labels packet at `stories/ACT_EarlyChildhoodIncidents/womblex-extract/labels/`.
|
|
692
|
+
A fifth proposed fix was tried, validated against labels, and
|
|
693
|
+
reverted on evidence of irreducible regression elsewhere.
|
|
694
|
+
|
|
695
|
+
### 1. Native-page table column-major emission (`extract.py` + `orchestrator.py`)
|
|
696
|
+
|
|
697
|
+
**Problem.** Native PDFs with ruled tables (Compliance Notice rules-of-
|
|
698
|
+
the-Law pages, e.g. 00281 page 1) had their cells correctly detected
|
|
699
|
+
into `tables[]` by `_extract_tables_from_page` but the prose `text`
|
|
700
|
+
field still read the same cells row-major via
|
|
701
|
+
`page.get_text("text")`, producing garble like
|
|
702
|
+
`"Provision of the Description Steps to be taken Law Section The
|
|
703
|
+
approved provider of The Provider to is to submit evidence..."` —
|
|
704
|
+
the data was present in `tables[]` but the prose field that
|
|
705
|
+
downstream chunking consumes was scrambled.
|
|
706
|
+
|
|
707
|
+
**Fix.**
|
|
708
|
+
- New `_find_native_tables(page)` returning
|
|
709
|
+
`list[tuple[TableData, fitz.Rect, list[list]]]` — exposes per-table
|
|
710
|
+
bbox + raw cell grid alongside the existing `TableData`.
|
|
711
|
+
- New `_emit_table_column_major(cells)` — emits each column as its
|
|
712
|
+
own paragraph (cells joined by `\n` within a column, columns
|
|
713
|
+
separated by `\n\n`). Mirrors the OCR-side `_table_aware_text` shape.
|
|
714
|
+
- `_extract_tables_from_page` is now a thin wrapper around
|
|
715
|
+
`_find_native_tables` — preserves the legacy `list[TableData]`
|
|
716
|
+
signature for callers that don't need bboxes.
|
|
717
|
+
- `_apply_native_page` calls `_find_native_tables`, partitions table
|
|
718
|
+
rects out of the prose call via `extract_page_text(page,
|
|
719
|
+
exclude_rects=…)`, and appends the column-major emissions
|
|
720
|
+
in y-order.
|
|
721
|
+
|
|
722
|
+
**Gating.** Only `confidence ≥ 0.8` (lines-strategy hits, ruled cells)
|
|
723
|
+
drive prose-region exclusion. Text-strategy hits (whitespace-aligned
|
|
724
|
+
columns, conf ≈ 0.6) stay in `tables` but **do not** partition the
|
|
725
|
+
prose, because text-strategy false-positives on ordinary multi-column
|
|
726
|
+
prose (e.g. 2-column layouts with regular x-spacing) — caught by
|
|
727
|
+
`test_native_extractor_handles_two_column_page` in the test suite
|
|
728
|
+
before the gating was added.
|
|
729
|
+
|
|
730
|
+
The spreadsheet-print path (Phase 4) handles whitespace-aligned
|
|
731
|
+
manifests separately via `extract_spreadsheet_print`, so the gating
|
|
732
|
+
doesn't lose that case.
|
|
733
|
+
|
|
734
|
+
### 2. Block-aware paragraph emission (`grid_projection.py`)
|
|
735
|
+
|
|
736
|
+
**Problem.** For single-column native pages, `extract_page_text` fell
|
|
737
|
+
back to `page.get_text("text", flags=TEXT_DEHYPHENATE)`, which joins
|
|
738
|
+
adjacent blocks with a single `\n`. Paragraph breaks between numbered
|
|
739
|
+
list items, bullets, headings, and footers were lost — `01132 page 1`
|
|
740
|
+
extracted as one continuous paragraph with section breaks invisible to
|
|
741
|
+
downstream chunking.
|
|
742
|
+
|
|
743
|
+
**Fix.**
|
|
744
|
+
- `extract_page_text` accepts `exclude_rects: Sequence[fitz.Rect] |
|
|
745
|
+
None`. Words whose midpoint falls inside any rect are filtered
|
|
746
|
+
before column projection.
|
|
747
|
+
- Single-column path now routes through `_render_blocks_with_breaks`,
|
|
748
|
+
which iterates `page.get_text("blocks", flags=TEXT_DEHYPHENATE)`
|
|
749
|
+
and joins blocks with `\n\n`. Blocks whose centre falls inside an
|
|
750
|
+
exclude rect are dropped (used in concert with fix 1 to suppress
|
|
751
|
+
table-region prose).
|
|
752
|
+
- New `_word_in_any_rect(word, rects)` helper for the word-level
|
|
753
|
+
filter.
|
|
754
|
+
|
|
755
|
+
The multi-column path (≥2 columns from `project_to_columns`)
|
|
756
|
+
continues to use `render_spatial_text(columns)` unchanged.
|
|
757
|
+
|
|
758
|
+
### 3. Body-context pipe-as-I (`extract.py` `_normalise_text`)
|
|
759
|
+
|
|
760
|
+
**Problem.** ACT Gov letter footers (`GPO Box 158 Canberra ACT 2601 |
|
|
761
|
+
phone: 132281 | www.act.gov.au`) OCR the `|` separator as a capital
|
|
762
|
+
`I` when it sits between a space and a lowercase keyword:
|
|
763
|
+
`2601 I phone:`. The existing `_FOOTER_PIPE_RE` only catches the
|
|
764
|
+
page-marker shape `<digit>lPage`.
|
|
765
|
+
|
|
766
|
+
**Fix.** `_BODY_PIPE_RE = re.compile(r" I (?=(?:phone|email|fax|www|http)\b)",
|
|
767
|
+
re.IGNORECASE)` — restricted to a fixed keyword set to avoid false
|
|
768
|
+
positives on legitimate sentence-initial `I` + verb. Applied
|
|
769
|
+
alongside the existing footer rule in `_normalise_text` (RES-004b).
|
|
770
|
+
|
|
771
|
+
### 4. `format_labels.TITLE_PATTERNS` regex anchor bug *(stories-side
|
|
772
|
+
script, not Womblex code)*
|
|
773
|
+
|
|
774
|
+
**Problem.** In
|
|
775
|
+
`stories/.../womblex-extract/format_labels.py`, the title patterns
|
|
776
|
+
embed `^` at the start (`r"^(SHOW CAUSE NOTICE|COMPLIANCE NOTICE|…)"`).
|
|
777
|
+
That works for `TITLE_RE.search`, but the same patterns were also
|
|
778
|
+
substituted in-string via `re.sub(rf"\s*({pat})", …)`, where `^`
|
|
779
|
+
restricted the match to position 0 — silently no-op'd for the same
|
|
780
|
+
title appearing mid-string. So `Dear COMPLIANCE NOTICE Section 177…`
|
|
781
|
+
never got the `\n\n` break inserted before `COMPLIANCE NOTICE`.
|
|
782
|
+
|
|
783
|
+
**Fix.** Removed `^` from `TITLE_PATTERNS`; dropped `re.IGNORECASE`
|
|
784
|
+
from the title substitution loop so body-text mentions like *"this
|
|
785
|
+
compliance notice"* don't false-trigger paragraph breaks.
|
|
786
|
+
|
|
787
|
+
Drives the **hybrid mean CER 0.208 → 0.145** result in the labels
|
|
788
|
+
retest — affects every label page where a known title appears
|
|
789
|
+
mid-stream after a redacted name.
|
|
790
|
+
|
|
791
|
+
### 5. *(reverted)* OCR-side `_table_aware_text` relaxation
|
|
792
|
+
|
|
793
|
+
**Tried.** Relax start condition from `min_start_rows=2` consecutive
|
|
794
|
+
≥3-item rows to a single 3+-item row, and allow 1-item continuation
|
|
795
|
+
rows (which OCR produces for multi-line cell wraps).
|
|
796
|
+
|
|
797
|
+
**Why reverted.** Helped table-shaped pages (R-01313F 0.080 → 0.018,
|
|
798
|
+
00281 0.27 → 0.22) but caused matching regression on form-shaped
|
|
799
|
+
pages (R-04060 0.020 → 0.157) — CRM-style screenshots where many
|
|
800
|
+
1-item label/value rows look like a multi-line cell wrap, get
|
|
801
|
+
absorbed into a "table", emitted column-major as one blob.
|
|
802
|
+
|
|
803
|
+
Tried four progressively stricter discriminators (2-row start,
|
|
804
|
+
consecutive-singleton cap, single-column-only singleton absorption,
|
|
805
|
+
column-spread minimums). None recovered baseline for both classes
|
|
806
|
+
of page. The signature of a real multi-line cell wrap and a form
|
|
807
|
+
field stream is structurally similar at the per-region level; we
|
|
808
|
+
couldn't separate them without page-image context the OCR pipeline
|
|
809
|
+
doesn't carry.
|
|
810
|
+
|
|
811
|
+
**Decision.** Revert to baseline. The native-path fix above already
|
|
812
|
+
handles the ruled-table case on the production native code path;
|
|
813
|
+
OCR-side ruled-table detection on rendered images stays at original
|
|
814
|
+
conservative behaviour. Trade-off documented; do not retry without
|
|
815
|
+
new discriminating signal.
|
|
816
|
+
|
|
817
|
+
## Verification
|
|
818
|
+
|
|
819
|
+
### Unit tests
|
|
820
|
+
`tests/test_extract.py` (40), `tests/test_grid_projection.py` (17),
|
|
821
|
+
`tests/test_spreadsheet_print.py` (14); the full-suite snapshot lives
|
|
822
|
+
in the Snapshot table above (numbers shift as new tests land).
|
|
823
|
+
Two new behaviours not yet covered by tests:
|
|
824
|
+
- `_emit_table_column_major` (covered indirectly via the native
|
|
825
|
+
integration test)
|
|
826
|
+
- `_render_blocks_with_breaks` paragraph-separator output
|
|
827
|
+
(covered indirectly via fixture accuracy)
|
|
828
|
+
|
|
829
|
+
### Labels retest
|
|
830
|
+
`stories/.../womblex-extract/cer_results.md` (18 reviewed pages):
|
|
831
|
+
|
|
832
|
+
| strategy | n | baseline mean CER | bundle mean CER |
|
|
833
|
+
|---|---:|---:|---:|
|
|
834
|
+
| hybrid | 2 | 0.208 | **0.145** |
|
|
835
|
+
| native_with_structured | 3 → 6 | 0.020 | 0.051 (+3 new pages) |
|
|
836
|
+
| scanned_machinewritten | 7 | 0.044 | 0.044 |
|
|
837
|
+
| scanned_mixed | 3 | 0.104 | 0.104 |
|
|
838
|
+
|
|
839
|
+
Newly-reviewed pages (no prior baseline):
|
|
840
|
+
- `02424A p4`: 0.000 — true-blank page, predicted empty matches GT empty
|
|
841
|
+
- `01132 p0`: 0.014 — body pipe-as-I + TITLE_PATTERNS fix visible
|
|
842
|
+
- `00281 p0`: 0.233 — OCR-quality residue (`Govemment` typo not in
|
|
843
|
+
normaliser; mid-sentence drop in the "satisfied that" clause).
|
|
844
|
+
Documented as OCR-character-level limit, not pipeline gap.
|
|
845
|
+
|
|
846
|
+
### Native-path source-PDF validation
|
|
847
|
+
Real `00281.pdf` page 1 (native_with_structured): ruled rules-of-the-
|
|
848
|
+
Law table extracts as 1 detected table; prose section emits
|
|
849
|
+
block-by-block with `\n\n` between `Time for Compliance / You are
|
|
850
|
+
required… / Failure to comply / It is an offence…`; table content
|
|
851
|
+
follows column-major: column 1 (`Section / 167(1) / Section / 174(2)`),
|
|
852
|
+
column 2 (the approved-provider description), column 3 (steps).
|
|
853
|
+
Behaviour matches design intent.
|
|
854
|
+
|
|
855
|
+
## Roadmap
|
|
856
|
+
|
|
857
|
+
### Phases 1–4 (✅ complete, prior session)
|
|
858
|
+
Per-page profile + plan-driven orchestrator (Phase 2), form-pair
|
|
859
|
+
extraction & block-type classifier rewrite (Phase 1), per-image OCR
|
|
860
|
+
& PII token swap & fullwidth footer (Phase 3), spreadsheet-print
|
|
861
|
+
extractor (Phase 4). All metrics maintained in the bundle's
|
|
862
|
+
acceptance test against the same random-500 sample.
|
|
863
|
+
|
|
864
|
+
### Phase 5 — Production hardening (✅ complete, prior session)
|
|
865
|
+
Four bundled fixes above. Validated against the labels packet and
|
|
866
|
+
the full 2,626-doc corpus. See `stories/STATUS.md` for the
|
|
867
|
+
production-corpus run output and quality audit.
|
|
868
|
+
|
|
869
|
+
### CLI surface
|
|
870
|
+
|
|
871
|
+
`womblex score --labels <dir> --shards <dir> [--group-by FIELD] [--report PATH]`
|
|
872
|
+
— promoted from a corpus-local script to first-class CLI in
|
|
873
|
+
2026-05-17. Scores a per-page labels packet (`<stem>.gt.md` +
|
|
874
|
+
`<stem>.meta.json`) against per-page text reassembled from the
|
|
875
|
+
element-stream parquet. See `src/womblex/score.py` for the module API
|
|
876
|
+
(`load_labels`, `build_manifest_index`, `reassemble_page_text`,
|
|
877
|
+
`score_labels`, `format_report_markdown`).
|
|
878
|
+
|
|
879
|
+
### Library — `redact/batch.py` (2026-05-19, batch redaction operations)
|
|
880
|
+
|
|
881
|
+
Promoted from a corpus-local validation script. Two entry points:
|
|
882
|
+
|
|
883
|
+
- `annotate_redactions_for_shards(shard_dir, pdf_dir, config, output_dir,
|
|
884
|
+
checkpoint_path)` — batch-detect redactions across extracted parquet
|
|
885
|
+
shards; write a sparse `*.redactions.parquet` sidecar per batch with
|
|
886
|
+
`(source_hash, elem_order, has_redaction)` rows for elements on
|
|
887
|
+
affected pages. Resumable via the optional checkpoint JSON.
|
|
888
|
+
- `validate_redactions_against_labels(labels_dir, pdf_dir, config)` —
|
|
889
|
+
run detection over PDFs referenced in a labels packet; return per-doc
|
|
890
|
+
`ValidationSummary` objects. Used for detector tuning / sanity checks.
|
|
891
|
+
|
|
892
|
+
CLI wrappers landed 2026-05-20 — see CLI restructure below.
|
|
893
|
+
|
|
894
|
+
### CLI — `cli.py` → `cli/` subpackage (2026-05-20)
|
|
895
|
+
|
|
896
|
+
Single 728-line `cli.py` (at the 750-line cap) split into a per-topic
|
|
897
|
+
subpackage. Each topic module exposes a ``COMMANDS: list[Command]`` and
|
|
898
|
+
``cli/__init__.py`` aggregates them, wires up argparse subparsers, and
|
|
899
|
+
dispatches by name.
|
|
900
|
+
|
|
901
|
+
```
|
|
902
|
+
src/womblex/cli/
|
|
903
|
+
├── __init__.py main() + ALL_COMMANDS aggregation + dispatch
|
|
904
|
+
├── _shared.py Command NamedTuple, setup_logging, discover_files, format_eta
|
|
905
|
+
├── pipeline.py run, extract, chunk
|
|
906
|
+
├── redact.py redact, annotate-redactions, validate-redactions
|
|
907
|
+
├── ingest.py ingest-gnaf, ingest-geo
|
|
908
|
+
├── score.py score
|
|
909
|
+
└── profile.py profile
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
Two new CLI subcommands landed in `cli/redact.py`:
|
|
913
|
+
|
|
914
|
+
- `womblex annotate-redactions <shards> <pdfs> [--output DIR] [--checkpoint PATH]`
|
|
915
|
+
— invokes `redact.batch.annotate_redactions_for_shards`. Resumable.
|
|
916
|
+
- `womblex validate-redactions --labels DIR --pdfs DIR [--report PATH]`
|
|
917
|
+
— invokes `redact.batch.validate_redactions_against_labels`. JSON or
|
|
918
|
+
markdown output.
|
|
919
|
+
|
|
920
|
+
Entry point `womblex = "womblex.cli:main"` unchanged (cli/__init__.py
|
|
921
|
+
exports `main`). All existing subcommand surfaces preserved verbatim
|
|
922
|
+
(args, help text, behaviour). Heavy double-line spacing from the old
|
|
923
|
+
file normalised during the move.
|
|
924
|
+
|
|
925
|
+
Largest file post-split: `cli/pipeline.py` at 283 lines (38% of cap).
|
|
926
|
+
|
|
927
|
+
### Detector — vector-first detection (2026-05-19)
|
|
928
|
+
|
|
929
|
+
`redact/stage.py:detect_redactions` was extended to try
|
|
930
|
+
`page.get_drawings()` first for filled near-black rectangles; falls back
|
|
931
|
+
to the existing CV2 contour detector on rasterised pages when the
|
|
932
|
+
vector path finds nothing. Both paths return `RedactionInfo` with
|
|
933
|
+
bboxes in pixel coordinates at the configured DPI.
|
|
934
|
+
|
|
935
|
+
Filters (each surfaced by a measured false-positive class during Phase
|
|
936
|
+
2 validation):
|
|
937
|
+
|
|
938
|
+
- Near-black fill (max channel ≤ 0.1 RGB; CMYK K ≥ 0.9 + others ≤ 0.1) —
|
|
939
|
+
baseline filter.
|
|
940
|
+
- `min_width ≥ 3pt` — excludes narrow vertical separator lines that
|
|
941
|
+
appear in manifest-style tables (FOI master regression on 37 pages).
|
|
942
|
+
- `min_height ≥ 8pt` — excludes glyph-rendering small filled rects on
|
|
943
|
+
PDFs that draw text as filled-path glyphs rather than vector text
|
|
944
|
+
(01125-class regression: 14,184 false positives → 144 actual).
|
|
945
|
+
|
|
946
|
+
Closes Outstanding §4(a) in `stories/STATUS.md`. Validated against the
|
|
947
|
+
labels packet: §1 residual recall improved 6→14, 7→13, 3→68 without
|
|
948
|
+
regressing the FOI master manifest (0 regions preserved) or any
|
|
949
|
+
plain-scanned doc. The 02737-class scanned_mixed false-positive cohort
|
|
950
|
+
falls back to the raster path and is unchanged (documented limitation,
|
|
951
|
+
see `stories/STATUS.md` §11).
|
|
952
|
+
|
|
953
|
+
### Phase 6 — `kind='table'` over-firing (2026-05-17, ✅ resolved)
|
|
954
|
+
|
|
955
|
+
Audit of the pre-refactor corpus run found 62% of `structured`-strategy
|
|
956
|
+
docs shipping pseudo-tables built from form layouts and shredded prose;
|
|
957
|
+
the same primitive over-fired more broadly across the 2,791 PDF table
|
|
958
|
+
records corpus-wide. Two fixes landed, in source code rather than
|
|
959
|
+
config because Womblex didn't expose the right knobs:
|
|
960
|
+
|
|
961
|
+
- **§1 — `_find_native_tables` block-count gate** (`ingest/extract.py`).
|
|
962
|
+
Reject any PyMuPDF table candidate where the count of natural
|
|
963
|
+
`get_text("dict")` blocks inside the table bbox is less than the row
|
|
964
|
+
count the table claims. Real tables decompose into ≥1 block per row;
|
|
965
|
+
prose-as-table over-claims rows by carving sub-block whitespace into
|
|
966
|
+
pseudo-rows. ~15 LOC + the `_count_blocks_in_bbox` helper.
|
|
967
|
+
- **§2 — `_has_manifest_table` + `PageProfile.has_manifest_signal`**
|
|
968
|
+
(`ingest/detect.py`, `ingest/page_profile.py`). Stricter signal than
|
|
969
|
+
`has_table_signal`: only fires when a page contains a table with ≥300
|
|
970
|
+
non-empty cells — the discriminator between real manifests (FOI
|
|
971
|
+
master 1,713 non-empty cells per page; Schedule-2ai–2av 503 per page)
|
|
972
|
+
and prose-as-table over-fires (170–280 per page). The
|
|
973
|
+
`qualify_for_spreadsheet_print` qualifier now gates on this stricter
|
|
974
|
+
signal, so regulatory letters with embedded rules-of-law tables stop
|
|
975
|
+
routing through the manifest extractor.
|
|
976
|
+
|
|
977
|
+
Validated by re-running extraction across the full 2,626-doc corpus
|
|
978
|
+
(2026-05-17) and auditing `kind='table'` elements + cells:
|
|
979
|
+
|
|
980
|
+
| stratum | pre-fix | post-fix | residual |
|
|
981
|
+
|---|---:|---:|---|
|
|
982
|
+
| conf=0.60 (`native_text` text-strategy) | 2,362 | 3 | 3 known prose-as-table on heavily-redacted pages (added to labels packet for follow-up) |
|
|
983
|
+
| conf=0.70 (`spreadsheet_print`) | 83 | 3 | All real manifests (FOI master, Schedule-2ai–2av, Schedule-2b); regulatory-letter misroutes eliminated |
|
|
984
|
+
| conf=0.80 (`native_text` lines-strategy) | 346 | 166 | ~90% clean rules-of-law tables; ~10% borderline (single-row / mostly-empty) |
|
|
985
|
+
| **total** | **2,791** | **172** | **~3-5% residual** |
|
|
986
|
+
|
|
987
|
+
The 3 residual conf=0.60 fabrications survive because PyMuPDF's
|
|
988
|
+
text-strategy `find_tables` clusters the text fragments left over
|
|
989
|
+
after large redaction blocks fragment the surrounding prose into
|
|
990
|
+
whitespace-aligned columns. The bars themselves do not register as
|
|
991
|
+
cells (measured cell-vs-fill overlap ≤ 1% across all three pages);
|
|
992
|
+
they cause the misread indirectly via the gap pattern they create.
|
|
993
|
+
The §1 gate's `n_blocks ≥ n_rows` premise is satisfied because the
|
|
994
|
+
count of surviving paragraph dict-blocks happens to match the count
|
|
995
|
+
of synthesised rows. Content impact is contained: text-bearing
|
|
996
|
+
elements on these pages capture the prose verbatim (the
|
|
997
|
+
`kind='table'` element is additive noise, not corruption). A
|
|
998
|
+
region-level black-fill signal could close the strong-signal page
|
|
999
|
+
(01349, 28% bbox coverage) but is on a threshold tightrope for
|
|
1000
|
+
01093/01094 (~3.5% coverage) where false-positives on real native
|
|
1001
|
+
tables become a real risk. Accepted as documented limitation; see
|
|
1002
|
+
`stories/STATUS.md` Outstanding §2. Material impact ~0.11% of source
|
|
1003
|
+
docs.
|
|
1004
|
+
|
|
1005
|
+
### Redaction & PII marker conventions (✅ unified 2026-05-21, bracket-only)
|
|
1006
|
+
|
|
1007
|
+
Two distinct concerns, two distinct markers:
|
|
1008
|
+
|
|
1009
|
+
| concern | source | marker | inline per span | metadata home |
|
|
1010
|
+
|---|---|---|---|---|
|
|
1011
|
+
| Source redaction | rendered black bar in PDF (FOI / publisher) | `<REDACTED>` | not yet — see below | `RedactionReport` per-span (bbox, page, method, confidence) |
|
|
1012
|
+
| PII redaction | detected in extracted text (regex + cosine + enrichment graph) | `<PERSON>`, `<EMAIL>`, `<ADDRESS>`, … (typed) | yes | enrichment graph + chunk `has_redaction` flag |
|
|
1013
|
+
|
|
1014
|
+
Bracket-style unification on angle brackets landed across:
|
|
1015
|
+
|
|
1016
|
+
- [pii/cleaner.py:331,406](src/womblex/pii/cleaner.py#L331) — emits `<ENTITY_TYPE>` (PII spans). Module docstring rationale rewritten; the prior BPE/SentencePiece tokenisation argument for square brackets didn't survive scrutiny (neither bracket style is single-piece in standard pretrained tokenisers without explicit special-token registration).
|
|
1017
|
+
- [redact/stage.py:187](src/womblex/redact/stage.py#L187) — `blackout` mode emits `<REDACTED>` (still page-prefix, not inline per span — see below).
|
|
1018
|
+
- [process/chunker.py:227](src/womblex/process/chunker.py#L227) — `_repair_redaction_splits` marker constant flipped to `<REDACTED>`; cross-file coupling comment preserved.
|
|
1019
|
+
- [operations.py:298](src/womblex/operations.py#L298), [config.py:86](src/womblex/config.py#L86) — docstrings.
|
|
1020
|
+
- [docs/architecture.md](docs/architecture.md), [docs/dataflow.md](docs/dataflow.md) — manual references aligned. [docs/accuracy/REDACTION_HANDLING.md](docs/accuracy/REDACTION_HANDLING.md) is generated by `test_fixture_accuracy.py` and will regenerate on next accuracy-benchmark run.
|
|
1021
|
+
- Tests across `test_pii.py`, `test_pii_enrichment.py`, `test_chunker.py`, `test_redaction.py`, `test_womblex_collection_accuracy.py`, `accuracy_reports.py` — all assertions migrated. The 3 previously-failing ADDRESS angle-bracket tests now pass.
|
|
1022
|
+
|
|
1023
|
+
**Inline-per-span for source redactions is deferred** — flipping the bracket style alone is bracket-only behaviour-preserving. Going inline-per-span requires a bbox-to-text-position mapping that doesn't exist for raster-path redactions (pixel-only bboxes with no character index). Native-path (PDF vector) detection has PDF coords and could be mapped; OCR/raster path needs word-bbox routing wired through to the redact stage. Tracked as a follow-up.
|
|
1024
|
+
|
|
1025
|
+
No on-disk data migration was needed — production runs to date are flag-mode, no `blackout` text mutation has been applied, and PII cleaning hasn't been applied to the corpus.
|
|
1026
|
+
|
|
1027
|
+
### Detector — raster-path layout filter (2026-05-22)
|
|
1028
|
+
|
|
1029
|
+
`RedactionDetector.detect()` accepts a new `exclude_rects` parameter; candidates whose bbox centre falls inside any rect are dropped. `redact/stage.py:detect_redactions()` runs YOLO layout analysis on raster-fallback pages and passes regions of `_LAYOUT_EXCLUSION_CLASSES` (`tv`, `laptop`, `monitor`, `cell phone`, `keyboard`, `mouse`, `book`, `dining table` — the COCO classes that heuristically land on form-field backgrounds and chart regions) as exclusion zones to the contour detector. Best-effort: try/except, falls back to raw raster pass on any error (missing ultralytics, model weights absent).
|
|
1030
|
+
|
|
1031
|
+
Gated by `RedactionConfig.use_layout_filter: bool = True` (default on). Threaded through `operations.py:run_redaction`, `redact/batch.py:annotate_redactions_for_shards`, `redact/batch.py:validate_redactions_against_labels`. CLI / `configs/example.yaml` exposes the flag.
|
|
1032
|
+
|
|
1033
|
+
**Cost:** Vector-path detection (native PDFs) is unchanged — YOLO never runs there. Raster-fallback path triggers YOLO inference per page, which materially slowed the accuracy benchmark (3 min → 22 min for `test_womblex_collection_accuracy.py`). For production batch runs on scanned_mixed cohorts the trade is correct (precision over speed).
|
|
1034
|
+
|
|
1035
|
+
**Cohort measurement (2026-05-22).** Ran `detect_redactions` on the 11 scanned_mixed docs from the corpus with the filter off vs on (config: `RedactionConfig(max_area_ratio=0.05)`, the corpus tune):
|
|
1036
|
+
|
|
1037
|
+
| metric | off | on | Δ |
|
|
1038
|
+
|---|---:|---:|---:|
|
|
1039
|
+
| total regions across 11 docs | 159 | 151 | **−8 (−5.0%)** |
|
|
1040
|
+
| docs with any region | 10 | 10 | 0 |
|
|
1041
|
+
| 02737-213A (the signature case) | 10 | 10 | **0** |
|
|
1042
|
+
| runtime | 9.5s | 12.6s | +3.1s (1.3×) |
|
|
1043
|
+
|
|
1044
|
+
**Interpretation.** The COCO YOLO model produces useful exclusion zones on 6 of 11 docs but at very small magnitudes (−1 to −3 per doc). It does **not** touch the worst case — 02737's 10 regions across 2 pages, the cohort's most egregious false positives, are entirely missed by the COCO classes the filter listens for. The hypothesis that `tv` / `laptop` / `monitor` etc. would heuristically land on dark form-field backgrounds was too weak: YOLO either doesn't detect those classes on rendered CRM-form pages, or detects them but not on the regions where contour detection misfires.
|
|
1045
|
+
|
|
1046
|
+
The filter is net-positive (more precise without regressing any doc) but the magnitude is too small to close §11's `scanned_mixed` false-positive gap on its own. A document-layout-trained checkpoint (DocLayNet / PubLayNet, with `Figure` / `Table` / `Form` classes) would be a better fit (the DocLayNet swap later landed — see CHANGELOG `[Unreleased]`).
|
|
1047
|
+
|
|
1048
|
+
**Default decision.** Keeping `use_layout_filter=True` is defensible (no doc regressed; some precision gain) but the value is modest and the test-suite runtime cost is real. Best path forward is the YOLO swap — see "Open follow-ups" K7(b) / track #A.
|
|
1049
|
+
|
|
1050
|
+
### Non-`table` element kind audit (2026-05-22)
|
|
1051
|
+
|
|
1052
|
+
Corpus-wide audit of element kinds beyond the resolved `kind='table'` work. Counts (90,843 elements across 2,626 docs) and per-strategy distribution live in `stories/STATUS.md` "Non-`table` element kind audit". Top findings, in summary form (full data + per-fix code-site references in the stories STATUS):
|
|
1053
|
+
|
|
1054
|
+
- **`kind='signature'` is 100% semantically wrong.** `_SIGNATURE_RE` matches `Yours sincerely` / `faithfully` / `truly` — i.e. the closing phrase, not the signatory block. All 442 signature elements in the corpus are closing-phrase matches. Actual signatory blocks (name + title + redaction bar) are filed as `paragraph`.
|
|
1055
|
+
- **`kind='figure'` is 65% mis-classified on scanned pages** (1,044 of 1,600). Root cause: `_YOLO_COCO_LABEL_MAP` defaults unknown COCO classes to `figure`, and on rendered scanned-page images YOLO finds plenty of unknown classes. These "figures" contain OCR text — they're text-bearing elements filed under a non-text kind.
|
|
1056
|
+
- **Three element kinds declared but never produced.** `list_item`, `caption`, `page_break` — schema enum + mapping table both present, no producer. Lists are extremely common in regulatory documents; the capability is silently absent.
|
|
1057
|
+
- **`header` block_type falls through to `paragraph`.** `_classify_native_block` returns `"header"` for `y_norm < 0.08`, but `"header"` isn't in `ElementKind` — it gets demoted into `meta['block_type']`, downstream consumers reading `kind` miss it.
|
|
1058
|
+
- **`kind='form'` over-fires on regulatory letters.** `Penalty: $10 000, in the case of an individual` (regulation citation) and `OFFICIAL: Sensitive - Legislative Secrecy` (document banner) get matched as form pairs. ~250-500 spurious in hybrid + structured cohorts.
|
|
1059
|
+
- **Bboxes for native-text elements serialise as `(0, 0, 0×0)`** — silent data-loss bug. Visible in every native sample but separate from kind classification. Tracked as K2.
|
|
1060
|
+
|
|
1061
|
+
Eight concrete fixes K1-K8 written up in "Open follow-ups" above. K1 / K3 / K4 / K5 / K7(a) / K8 land naturally as one low-effort change set. K2 and K7(b) are separate larger tracks.
|
|
1062
|
+
|
|
1063
|
+
### Deferred / cut
|
|
1064
|
+
|
|
1065
|
+
- ~~**Refresh accuracy-report generator + regenerate accuracy docs**~~ (resolved 2026-05-22). `tests/accuracy_reports.py` generator strings refreshed: `generate_redaction_report` now describes the vector-first detector + YOLO exclusion zones + `run_redaction` operation; `generate_extraction_report` now describes the per-page orchestrator + element-stream kinds + four sibling parquet shards; the strategy-matrix column reflects per-page dispatch. The four generated accuracy docs (`EXTRACTION.md`, `REDACTION_HANDLING.md`, `PII_CLEANING.md`) were regenerated cleanly. `docs/accuracy/CHUNKING.md` framings updated by hand for the post-refactor spreadsheet shape; a generator for this doc is still to-be-written (numbers still date from 2026-03-22).
|
|
1066
|
+
- **OCR-side ruled-table column-major emission** — irreducible
|
|
1067
|
+
trade-off documented under fix 5 above. Re-evaluate only with a
|
|
1068
|
+
new discriminating signal (e.g. layout-from-image-vision pass)
|
|
1069
|
+
that separates real table cells from form-field streams.
|
|
1070
|
+
- **Parquet schema for `TableData.context` + `document_metadata`** —
|
|
1071
|
+
Largely resolved by the element-stream refactor (2026-05-16):
|
|
1072
|
+
per-element ``meta`` map carries arbitrary key-value overflow on
|
|
1073
|
+
the elements shard; ``document_metadata`` rides on the manifest.
|
|
1074
|
+
``TableData.context`` is preserved on a kind='table' element via
|
|
1075
|
+
``meta`` keys (``context_*``). Original deferral note kept for
|
|
1076
|
+
history.
|
|
1077
|
+
- **Letterhead-typo normalisations for `Govemment` / standalone
|
|
1078
|
+
`AcT`** — the existing `_LETTERHEAD_FIXES` covered the parenthesised
|
|
1079
|
+
`(AcT)` and double-m `Govermment` shapes. Under the post-refactor
|
|
1080
|
+
verbatim-text policy, `_normalise_text` no longer runs in the
|
|
1081
|
+
extraction hot path, so adding entries to `_LETTERHEAD_FIXES` would
|
|
1082
|
+
have no effect on the on-disk parquet. If letterhead-typo correction
|
|
1083
|
+
is required, it now belongs to a downstream cleaning stage that
|
|
1084
|
+
rewrites `pages[i].text`, not the extractor.
|
|
1085
|
+
- **`02737`-style cross-cell handwritten forms** — paddle's
|
|
1086
|
+
row-major reading is architecturally mismatched with forms that
|
|
1087
|
+
humans read by cell, AND the handwriting itself crosses cell
|
|
1088
|
+
boundaries. Structural OCR-engine limit; not addressable in
|
|
1089
|
+
Womblex without a layout-aware OCR backend.
|
|
1090
|
+
|
|
1091
|
+
[Unreleased]: https://github.com/DeepCivic/womblex/compare/v0.2.0...HEAD
|
|
1092
|
+
[0.2.0]: https://github.com/DeepCivic/womblex/compare/v0.1.0...v0.2.0
|
|
1093
|
+
[0.1.0]: https://github.com/DeepCivic/womblex/releases/tag/v0.1.0
|