spybyscript commited on
Commit
ff95cf8
·
verified ·
1 Parent(s): cb91722

Every trained head: explicit spans, relations and records; reference runtime; phone results

Browse files
NOTICE.txt CHANGED
@@ -1,5 +1,5 @@
1
  GLiNER2.5 base (gliner2.5-base-v1, fastino/gliner2.5-base-v1 @ 1a8bc24e00dc7300b9017c81d63e3dcdabb26596), by Fastino, licensed under the Apache License 2.0 (LICENSE). Its encoder is DeBERTa-v3-base (Microsoft, MIT License).
2
 
3
- Changes: converted to LiteRT by spybyscript (encoder wfp16, fp16hw; head and scorer FP32). The head and scorer wrappers and trace patches are adapted from FluidInference's Core ML conversion (https://huggingface.co/FluidInference/gliner2-5-base-coreml @ ca8ddf6c, Apache-2.0); the encoder rewrite is ported from litert-community/GLiNER2.5-Decide-LiteRT @ c687e899 (Apache-2.0). The token embedding table ships as a float16 file outside the graphs. Weights are the original apart from the precision each variant states. Only the encoder, the boundary head and the candidate scorer are exported; the relation-pair, explicit-span and record heads are not.
4
 
5
  model-card.md is the original model card; README.md describes this conversion.
 
1
  GLiNER2.5 base (gliner2.5-base-v1, fastino/gliner2.5-base-v1 @ 1a8bc24e00dc7300b9017c81d63e3dcdabb26596), by Fastino, licensed under the Apache License 2.0 (LICENSE). Its encoder is DeBERTa-v3-base (Microsoft, MIT License).
2
 
3
+ Changes: converted to LiteRT by spybyscript (encoder wfp16, fp16hw; head and scorer FP32). The head and scorer wrappers and trace patches are adapted from FluidInference's Core ML conversion (https://huggingface.co/FluidInference/gliner2-5-base-coreml @ ca8ddf6c, Apache-2.0); the encoder rewrite is ported from litert-community/GLiNER2.5-Decide-LiteRT @ c687e899 (Apache-2.0). The token embedding table ships as a float16 file outside the graphs. Weights are the original apart from the precision each variant states. Every trained module is exported: the encoder, the boundary head with the classifier, the candidate scorer, the explicit-span scorer, the relation scorer and the record decoder; the wrappers for the last three are adapted from FluidInference's extraction_export.py. runtime/litert_boundary_runtime.py is ported from FluidInference's extraction_runtime.py, preprocessing.py and extraction_pool.py (Apache-2.0).
4
 
5
  model-card.md is the original model card; README.md describes this conversion.
README.md CHANGED
@@ -14,44 +14,98 @@ tags:
14
 
15
  # GLiNER2.5 base (gliner2.5-base-v1) — LiteRT (Android)
16
 
17
- An on-device conversion of gliner2.5-base-v1 for Android's LiteRT, validated against the gliner2 library and measured on a phone. We know of no other LiteRT conversion of gliner2.5-base-v1.
 
 
18
 
19
  Converted from [fastino/gliner2.5-base-v1](https://huggingface.co/fastino/gliner2.5-base-v1) at revision `1a8bc24e00dc7300b9017c81d63e3dcdabb26596` (Apache-2.0, by Fastino).
20
- It keeps the contract of FluidInference's Core ML port, with the token table on the
21
- host: an encoder graph (`inputs_embeds`, `attention_mask` → `hidden_states`), a head graph (the features graph's other inputs and
22
- its thirteen outputs) and the candidate scorer; the host's pool and decoders between and after them. `config.json` is the full contract.
23
 
24
  | File | Size | Contents |
25
  |---|---|---|
26
- | `gliner2_base_encoder_wfp16.tflite` | 189 MB | encoder: encoder FULLY_CONNECTED weights stored float16 behind DEQUANTIZE (ai-edge-quantizer FLOAT_CASTING); FP32 activations, position tables and norms; finite -1e4 mask; head and scorer FP32 |
27
- | `gliner2_base_encoder_fp16hw.tflite` | 189 MB | encoder: wfp16's storage over the FP16-hardware encoder: LayerNorm power-of-two pre-scale (x*2^-k, eps*4^-k, k from the corpus' FP32 ranges), first feed-forward FULLY_CONNECTED split in two halves; head and scorer FP32 |
28
- | `gliner2_base_head_fp32.tflite` | 8.5 MB | head, FP32 |
29
- | `gliner2_base_scorer_fp32.tflite` | 2.1 MB | scorer, FP32 |
 
 
 
 
30
  | `gliner2_base_embeddings_f16.bin` | 197 MB | float16 [128011, 768], little-endian, row-major |
31
-
32
- ## What this port does, and what it leaves out
33
-
34
- The original checkpoint has an encoder, a boundary head, a classifier, a record decoder and a relation scorer. This
35
- port exports the encoder, a head graph (the boundary head's extraction outputs, the null and count outputs and the
36
- classifier's choice logits) and the candidate scorer. The host runs the library's candidate pool and decoding between
37
- and after them.
38
-
39
- - **Entities**, as `extract_entities` returns them: labels with descriptions, thresholds, and the null head's
40
- abstention.
41
- - **Classification**, as `classify_text`: up to 8 choices.
42
- - **Structures whose fields are plain spans**, as `extract_json` returns them through the library's structure decoder
43
- without record mode.
44
- - These three were validated on 2,521 recorded calls (see Validation).
45
- - **Not available:** relation extraction (`extract_relations`; the original's `relation_scorer`) and record-mode
46
- structures (the `record_decoder`, which groups fields into several records), nor the explicit-span head of
47
- FluidInference's Core ML port. Requests that need them cannot run on this port.
48
- - **Fixed sizes:** one call holds at most 128 subword tokens (prompt and text), 64 text words, 8 queries (entity labels
49
- or structure fields) and 8 classification choices, with a candidate pool of 192. The host refuses larger requests
50
- rather than truncating them. The original runs with dynamic shapes, so long texts and large label sets need several
51
- calls here.
52
- - **Weights:** only their storage and compute precision change (see the file table); nothing is retrained or pruned.
53
-
54
- ## Validation (Mac, not a phone)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  2,521 base-v1 calls recorded from the gliner2 2.0.0 library on a synthetic English corpus of short conversational statements
57
  (`facts`: structure extraction; `keywords.a` and `keywords.b`: entity extraction with two label sets; `relation`:
@@ -71,7 +125,34 @@ accelerator the phone uses (Metal here); head and scorer on the CPU. Mac measure
71
 
72
  The acceptance gate: per call type, same answers ≥ 95 %, span F1 ≥ 0.95, confidence drift p99 ≤ 0.05. `validation.json` holds every number.
73
 
74
- ## On a phone (Galaxy Z Fold 8, 2026-10-29)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
  Snapdragon 8 Elite Gen 5 (SM8850), Android 17, LiteRT 2.2.0. The encoder runs wfp16 on 4 CPU threads (XNNPACK weight cache); the head and the scorer run on one thread each:
77
 
@@ -81,5 +162,6 @@ Snapdragon 8 Elite Gen 5 (SM8850), Android 17, LiteRT 2.2.0. The encoder runs wf
81
 
82
  ## Files
83
 
84
- `LICENSE` (Apache-2.0), `NOTICE.txt`, `model-card.md` (the original card), `config.json` (contract), `validation.json`,
85
- `SHA256SUMS`, `tokenizer/`, `source/` (the checkpoint's configs).
 
 
14
 
15
  # GLiNER2.5 base (gliner2.5-base-v1) — LiteRT (Android)
16
 
17
+ An on-device conversion of gliner2.5-base-v1 for Android's LiteRT with every trained head: entities, classification,
18
+ structures, relations and records, decoded by gliner2's own code, validated against the library and measured on phones.
19
+ We know of no other LiteRT conversion of gliner2.5-base-v1.
20
 
21
  Converted from [fastino/gliner2.5-base-v1](https://huggingface.co/fastino/gliner2.5-base-v1) at revision `1a8bc24e00dc7300b9017c81d63e3dcdabb26596` (Apache-2.0, by Fastino).
22
+ The split into graphs follows FluidInference's Core ML port ([gliner2-5-base-coreml](https://huggingface.co/FluidInference/gliner2-5-base-coreml), Apache-2.0),
23
+ with the token table on the host. `config.json` is the full contract.
 
24
 
25
  | File | Size | Contents |
26
  |---|---|---|
27
+ | `gliner2_base_encoder_wfp16.tflite` | 189 MB | encoder (`inputs_embeds`, `attention_mask` → `hidden_states`): FULLY_CONNECTED weights stored float16 behind DEQUANTIZE; FP32 activations, position tables and norms; finite -1e4 mask |
28
+ | `gliner2_base_encoder_fp16hw.tflite` | 189 MB | the same encoder rewritten for FP16 hardware: LayerNorm power-of-two pre-scale (x*2^-k, eps*4^-k), first feed-forward FULLY_CONNECTED split in two halves |
29
+ | `gliner2_base_head_fp32.tflite` | 8.5 MB | boundary head: routed text and query states, boundary marginals, pool projections, null and count heads, classification logits |
30
+ | `gliner2_base_scorer_fp32.tflite` | 2.1 MB | candidate scorer and the record candidate states |
31
+ | `gliner2_base_explicit_fp32.tflite` | 2.1 MB | explicit-span scorer: the boundary proposer's prior and the pair scorer over forced spans (entity attributes, choice fields) |
32
+ | `gliner2_base_relation_fp32.tflite` | 23.7 MB | relation scorer over head/tail pairs |
33
+ | `gliner2_base_record_assignment_fp32.tflite` | 1.2 MB | record decoder: instance-to-field assignment, object and latent-seed scores |
34
+ | `gliner2_base_record_anchorless_fp32.tflite` | 2.9 MB | record decoder: learned instances attending over the field candidates (anchorless records) |
35
  | `gliner2_base_embeddings_f16.bin` | 197 MB | float16 [128011, 768], little-endian, row-major |
36
+ | `runtime/` | | `litert_boundary_runtime.py` (the reference runtime), `verify_parity.py`, `parity_corpus.json` |
37
+
38
+ ## What it runs
39
+
40
+ `runtime/litert_boundary_runtime.py` is gliner2's BoundaryExtractor with every trained module on LiteRT: the schema
41
+ handling, the candidate pool and the decoders are the library's own code, and no PyTorch weights are loaded. Every
42
+ public call works: `extract`, `extract_entities`, `classify_text`, `extract_json`, `extract_relations`, and their
43
+ `batch_*` and `*_long` forms.
44
+
45
+ - Entities, with label descriptions, thresholds and the null head's abstention; entity attributes.
46
+ - Classification, single- and multi-label.
47
+ - Structures (`extract_json`), with choice fields and the natural, latent and anchorless record modes.
48
+ - Relations.
49
+
50
+ **Fixed sizes.** One request holds at most 128 subword tokens (schema and text), 64 text words, 8 queries (entity
51
+ labels, structure fields, relation roles) and 8 classification choices, with a candidate pool of 192 (the checkpoint's
52
+ own pool size). Relations take up to 4 types and 256 head/tail pairs per request; explicit spans up to 64 per query;
53
+ records up to 8 fields with 192 candidates each. These are FluidInference's bucket sizes. A larger request is refused
54
+ with a ValueError, never truncated: run long text through the `*_long` calls with a chunk that fits, e.g.
55
+ `extract_entities_long(text, labels, chunk_size=40, chunk_overlap=8)`. The original runs with dynamic shapes.
56
+
57
+ **Weights:** only their storage and compute precision change; nothing is retrained or pruned.
58
+
59
+ ## Use
60
+
61
+ ```python
62
+ # pip install gliner2==2.0.0 torch numpy ai-edge-litert
63
+ import sys; sys.path.insert(0, "runtime")
64
+ from litert_boundary_runtime import LiteRTBoundaryExtractor
65
+ from gliner2 import Schema
66
+
67
+ model = LiteRTBoundaryExtractor(".") # this repository's folder
68
+ model.extract_entities("Alice founded Acme in Toronto in 2020.", ["person", "organization", "city"])
69
+ model.extract_relations("Alice founded Acme in Toronto.", ["founded", "located_in"])
70
+ schema = Schema().entities(["person", "organization"]).relations(["works_for"])
71
+ model.extract("Ravi Shah is an engineer at Tidewater Systems.", schema, include_confidence=True)
72
+ ```
73
+
74
+ On a phone, run the same graphs with LiteRT's `CompiledModel` and port the library's decoding steps the runtime uses
75
+ (`config.json` lists every graph's signature).
76
+
77
+ ## Validation
78
+
79
+ ### Every call type against the library
80
+
81
+ `runtime/verify_parity.py` runs each request through gliner2 with the original weights and through the reference runtime
82
+ (LiteRT 2.2.0 on the CPU, `wfp16` encoder), and compares the structured results. The corpus
83
+ (`runtime/parity_corpus.json`) is 48 short synthetic texts with schema templates for every call type, 296 requests.
84
+
85
+ | Call type | Requests | Identical results | Span F1 (lowest) | Largest confidence difference |
86
+ |---|---|---|---|---|
87
+ | `entities` | 40 | 40 | 1.0 | 0.000228 |
88
+ | `entities_single` | 8 | 8 | 1.0 | 5e-06 |
89
+ | `relations_work` | 24 | 24 | 1.0 | 0.000883 |
90
+ | `record_natural` | 16 | 16 | 1.0 | 0.00013 |
91
+ | `record_latent` | 24 | 24 | 1.0 | 0.000641 |
92
+ | `attributes` | 16 | 16 | 1.0 | 0.000298 |
93
+ | `classification_topic` | 40 | 40 | 1.0 | 0.001305 |
94
+ | `mixed` | 16 | 16 | 1.0 | 0.001358 |
95
+ | `relations_family` | 8 | 8 | 1.0 | 0.00033 |
96
+ | `classification_described` | 24 | 24 | 1.0 | 0.001187 |
97
+ | `record_anchorless` | 8 | 8 | 1.0 | 0.000258 |
98
+ | `structure_event` | 24 | 24 | 1.0 | 0.000897 |
99
+ | `classification_multi` | 16 | 16 | 1.0 | 0.000356 |
100
+ | `entities_described` | 16 | 16 | 1.0 | 0.000411 |
101
+ | `structure_order` | 8 | 8 | 1.0 | 0.001252 |
102
+ | `relations_event` | 8 | 8 | 1.0 | 0.0002 |
103
+
104
+ All 296 of 296 requests give identical results. FluidInference's eleven fixtures (one per schema path: entities, relations,
105
+ the three record modes, attributes, mixed and plain classification, choice fields) also match, 11 of 11, with
106
+ confidences within 0.0004. `validation-full.json` holds the numbers.
107
+
108
+ ### The recorded calls (Mac, not a phone)
109
 
110
  2,521 base-v1 calls recorded from the gliner2 2.0.0 library on a synthetic English corpus of short conversational statements
111
  (`facts`: structure extraction; `keywords.a` and `keywords.b`: entity extraction with two label sets; `relation`:
 
125
 
126
  The acceptance gate: per call type, same answers ≥ 95 %, span F1 ≥ 0.95, confidence drift p99 ≤ 0.05. `validation.json` holds every number.
127
 
128
+
129
+ ## On phones
130
+
131
+ ### Galaxy S23 Ultra (SM8550, 2026-11-02)
132
+
133
+ Android 16, LiteRT 2.3.2 with `qnn-runtime` 2.50.0; each graph on a request recorded from the corpus, 10 calls after a
134
+ 30 s rest, thermal status 0 throughout; 4 CPU threads (XNNPACK). For the GPU the time is the whole call (it runs
135
+ asynchronously).
136
+
137
+ | Graph | CPU | GPU (FP32, low priority) | NPU (compiled on the phone) |
138
+ |---|---|---|---|
139
+ | encoder, 128 tokens | 143 ms | 156 ms, 0.85 GB dirty | 13 ms (`fp16hw`, 5.7 s compile) |
140
+ | head | 1.2 ms | does not compile | 1.2 ms |
141
+ | scorer | 2.9 ms | does not compile | 3.4 ms |
142
+ | explicit spans | 5.6 ms | does not compile | fails to run |
143
+ | relations | 16 ms | does not compile | 2.8 ms |
144
+ | record assignment | 8.9 ms | 21.5 ms | 4.0 ms |
145
+ | anchorless records | 10.8 ms | does not compile | 1.3 ms |
146
+
147
+ - **The encoder is the cost:** a request on the CPU takes about 150 ms (relations add 16 ms); with the encoder on the NPU,
148
+ about 20 ms.
149
+ - **Answers on the phone's accelerators** (32 recorded requests, two per call type; the phone's encoder outputs run
150
+ through the rest of the pipeline): the GPU at FP32 changed none; the NPU changed two, both latent-record requests (span
151
+ F1 0.98, confidence up to 0.28), and none of the other call types.
152
+ - **`fp16hw` on this phone's GPU at FP16 gives non-finite outputs** (LiteRT 2.3.2, Adreno 740): use the CPU, the GPU at
153
+ FP32 or the NPU there.
154
+
155
+ ### Galaxy Z Fold 8 (SM8850, 2026-10-29)
156
 
157
  Snapdragon 8 Elite Gen 5 (SM8850), Android 17, LiteRT 2.2.0. The encoder runs wfp16 on 4 CPU threads (XNNPACK weight cache); the head and the scorer run on one thread each:
158
 
 
162
 
163
  ## Files
164
 
165
+ `LICENSE` (Apache-2.0), `NOTICE.txt`, `model-card.md` (the original card), `config.json` (contract), `validation.json`
166
+ (the recorded-call validation), `validation-full.json` (every call type), `SHA256SUMS`, `tokenizer/`, `source/` (the
167
+ checkpoint's configs), `runtime/`.
SHA256SUMS CHANGED
@@ -1,15 +1,24 @@
1
  cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 LICENSE
2
- 5d22fdeddc1ceccba0576afa514f74b43411afe769562ad1ac480c529d7cf0a1 NOTICE.txt
3
- c61268a2b9b76631c2c630a2cf626242a753983211f2bd305aa7300955a6253c README.md
4
- 4c7cc23f44739e7a2633861f820df1576ffe9456d0613916f602ffbcf44f2fda config.json
5
  22c2e937cfc17e1ee9c8f3ec6f1449d44dc9230cfcbb8737c9bae8492628a3e8 gliner2_base_embeddings_f16.bin
6
  6f5241586176d6b123f75392bedd66a24c63f4105f8a45181dbc02385e95e5bb gliner2_base_encoder_fp16hw.tflite
7
  053cc6376843ee052a01f2c78e813e1f273ad73e71b95558a4b29804d6250e3c gliner2_base_encoder_wfp16.tflite
 
8
  1dd500e1c321acd7e84c1bec7fba5afb4211f41081f8f6dc0f8a4a78bffc3bd9 gliner2_base_head_fp32.tflite
 
 
 
9
  b828fed73d474ef01f4608f6f6a892cb9a1619c1ca3899f86f6ce33c2ebbc4ee gliner2_base_scorer_fp32.tflite
10
  b498995df84d4b6780cc25b2be763dc554eab173fed80134e738b2105be2bce6 model-card.md
 
 
 
 
11
  0eb92d00584d613aab32b2178f84a85176b62c87ae3689ce9084e83f6eba64d1 source/config.json
12
  d36a845b9f25dcaf1ec45a1c4bdf65ea4ac20596537e14530ec9f660a63aeca4 source/encoder_config/config.json
13
  cbc8ae6037812709c9c26f2a160f8dc48b0440bcb79c8141804259ae2d6adac3 tokenizer/tokenizer.json
14
  0bf3ea0873234bd9bfdd3853c440395009ac6365a925b91654daed5396d655e1 tokenizer/tokenizer_config.json
 
15
  fcdbb9325f3a84f338156b04e0def258fd16efe02195ec12bdb96c185cfc253b validation.json
 
1
  cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30 LICENSE
2
+ a4134d01e6b89380d84cfcde424328aaa717edadef0a6958c3129f206d91dc37 NOTICE.txt
3
+ f3d539b48e161356bae4ee2b7acf9e0126a0bce850743cf3179bc84130f6fe13 README.md
4
+ 251e71c11dd57b4106d0d5eb102a984abebc5415f4e0eb392a4966e3293b189f config.json
5
  22c2e937cfc17e1ee9c8f3ec6f1449d44dc9230cfcbb8737c9bae8492628a3e8 gliner2_base_embeddings_f16.bin
6
  6f5241586176d6b123f75392bedd66a24c63f4105f8a45181dbc02385e95e5bb gliner2_base_encoder_fp16hw.tflite
7
  053cc6376843ee052a01f2c78e813e1f273ad73e71b95558a4b29804d6250e3c gliner2_base_encoder_wfp16.tflite
8
+ 03a08c79fdd6ffc6de3e881af5cea7a369d7b3205017915396b9203d7a982051 gliner2_base_explicit_fp32.tflite
9
  1dd500e1c321acd7e84c1bec7fba5afb4211f41081f8f6dc0f8a4a78bffc3bd9 gliner2_base_head_fp32.tflite
10
+ dcc066dce8b721f39f4870459efb0abd3c5366f58498a9ff062c4065f9ee24d3 gliner2_base_record_anchorless_fp32.tflite
11
+ e6169de37236a4d056edefcaabfb1e9bba4df1ee34d45f8d6436dc94e22195f5 gliner2_base_record_assignment_fp32.tflite
12
+ dcf3528768b742dc6328c4441ae58694606a3fe15b3500ab429e13bf1caf4ed3 gliner2_base_relation_fp32.tflite
13
  b828fed73d474ef01f4608f6f6a892cb9a1619c1ca3899f86f6ce33c2ebbc4ee gliner2_base_scorer_fp32.tflite
14
  b498995df84d4b6780cc25b2be763dc554eab173fed80134e738b2105be2bce6 model-card.md
15
+ 14811c155fd1fe6caf235276c83952ec4934225cfcf9eb3765ecf7fa2cef29ab runtime/litert_boundary_runtime.py
16
+ a683dc6408f8ea3ec8fbe7a925f1c4bd61ee387944a5ee4793153bedbaeba057 runtime/parity_corpus.json
17
+ f84dbbdf154d3dbcfdb6e979a4ed8c6d2ab2cded4ed9d6f6990df0828c60d9a5 runtime/requirements.txt
18
+ b530aaa4b5dc9fdddcae4549457b8d0b2eb534044f10a1e2600515e475b9ac52 runtime/verify_parity.py
19
  0eb92d00584d613aab32b2178f84a85176b62c87ae3689ce9084e83f6eba64d1 source/config.json
20
  d36a845b9f25dcaf1ec45a1c4bdf65ea4ac20596537e14530ec9f660a63aeca4 source/encoder_config/config.json
21
  cbc8ae6037812709c9c26f2a160f8dc48b0440bcb79c8141804259ae2d6adac3 tokenizer/tokenizer.json
22
  0bf3ea0873234bd9bfdd3853c440395009ac6365a925b91654daed5396d655e1 tokenizer/tokenizer_config.json
23
+ 81973c2e176ab0e6e3bd283185f46a2daf73bd83b7dff75aaca9dc1581ee58df validation-full.json
24
  fcdbb9325f3a84f338156b04e0def258fd16efe02195ec12bdb96c185cfc253b validation.json
config.json CHANGED
@@ -106,7 +106,7 @@
106
  {
107
  "path": "gliner2_base_head_fp32.tflite",
108
  "role": "head",
109
- "about": "the ONNX features graph after its encoder; FP32; CPU",
110
  "bytes": 8517024,
111
  "signatures": [
112
  {
@@ -291,7 +291,7 @@
291
  {
292
  "path": "gliner2_base_scorer_fp32.tflite",
293
  "role": "scorer",
294
- "about": "the ONNX scorer unchanged; FP32; CPU",
295
  "bytes": 2126928,
296
  "signatures": [
297
  {
@@ -424,6 +424,325 @@
424
  ]
425
  }
426
  ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
427
  }
428
  ],
429
  "table": {
@@ -446,16 +765,16 @@
446
  "choices": 8,
447
  "pool": 192,
448
  "boundaries": 65,
449
- "about": "the host pads to T=128 tokens, W=64 words, Q=8 queries, C=8 choices, P=192 candidates, as BaseExtractor does, and refuses larger requests"
450
  },
451
  "host": {
452
  "steps": "as the gliner2 library does: pack and pad the request, then encoder -> head -> the shared candidate pool on the host -> scorer -> thresholds, flat overlap resolution and spans; entities abstain when sigmoid(null_logits) > 0.5",
453
  "changes": [
454
  "input_ids no longer enter a graph: the host looks them up in the float16 table into inputs_embeds",
455
  "attention_mask is float32 (1.0 / 0.0) instead of int32",
456
- "the ONNX features graph is two graphs: encoder (inputs_embeds, attention_mask -> hidden_states) and head (hidden_states plus the six other ONNX feature inputs -> the same thirteen outputs)",
457
- "the scorer is the ONNX scorer (same inputs and outputs), FP32",
458
- "bind inputs by name; signatures: encoder `t128`, head `t128`, scorer `p192`"
459
  ]
460
  },
461
  "accelerators": {
@@ -484,5 +803,6 @@
484
  },
485
  "provenance": {
486
  "graphs": "encoder graph code ported from litert-community/GLiNER2.5-Decide-LiteRT (Apache-2.0); head and scorer wrappers adapted from FluidInference/gliner2-5-base-coreml (Apache-2.0)"
487
- }
 
488
  }
 
106
  {
107
  "path": "gliner2_base_head_fp32.tflite",
108
  "role": "head",
109
+ "about": "the boundary head after the encoder (FluidInference's Core ML features graph without its encoder): FP32, CPU",
110
  "bytes": 8517024,
111
  "signatures": [
112
  {
 
291
  {
292
  "path": "gliner2_base_scorer_fp32.tflite",
293
  "role": "scorer",
294
+ "about": "the candidate scorer and the record candidate states (FluidInference's Core ML scorer): FP32, CPU",
295
  "bytes": 2126928,
296
  "signatures": [
297
  {
 
424
  ]
425
  }
426
  ]
427
+ },
428
+ {
429
+ "path": "gliner2_base_explicit_fp32.tflite",
430
+ "role": "explicit",
431
+ "about": "explicit-span scorer: the boundary proposer's prior and the pair scorer over up to 64 forced spans per query (attributes, choice fields); FP32, CPU",
432
+ "bytes": 2051844,
433
+ "signatures": [
434
+ {
435
+ "key": "w64_q8_s64",
436
+ "inputs": [
437
+ {
438
+ "name": "text_states",
439
+ "dtype": "float32",
440
+ "shape": [
441
+ 1,
442
+ 64,
443
+ 768
444
+ ]
445
+ },
446
+ {
447
+ "name": "text_mask",
448
+ "dtype": "float32",
449
+ "shape": [
450
+ 1,
451
+ 64
452
+ ]
453
+ },
454
+ {
455
+ "name": "query_states",
456
+ "dtype": "float32",
457
+ "shape": [
458
+ 1,
459
+ 8,
460
+ 768
461
+ ]
462
+ },
463
+ {
464
+ "name": "query_mask",
465
+ "dtype": "float32",
466
+ "shape": [
467
+ 1,
468
+ 8
469
+ ]
470
+ },
471
+ {
472
+ "name": "boundary_states",
473
+ "dtype": "float32",
474
+ "shape": [
475
+ 1,
476
+ 65,
477
+ 128
478
+ ]
479
+ },
480
+ {
481
+ "name": "start_logits",
482
+ "dtype": "float32",
483
+ "shape": [
484
+ 1,
485
+ 8,
486
+ 65
487
+ ]
488
+ },
489
+ {
490
+ "name": "end_logits",
491
+ "dtype": "float32",
492
+ "shape": [
493
+ 1,
494
+ 8,
495
+ 65
496
+ ]
497
+ },
498
+ {
499
+ "name": "inside_prefix",
500
+ "dtype": "float32",
501
+ "shape": [
502
+ 1,
503
+ 8,
504
+ 65
505
+ ]
506
+ },
507
+ {
508
+ "name": "inside_prefix_mean",
509
+ "dtype": "float32",
510
+ "shape": [
511
+ 1,
512
+ 8,
513
+ 1
514
+ ]
515
+ },
516
+ {
517
+ "name": "span_indices",
518
+ "dtype": "int32",
519
+ "shape": [
520
+ 1,
521
+ 8,
522
+ 64,
523
+ 2
524
+ ]
525
+ },
526
+ {
527
+ "name": "span_mask",
528
+ "dtype": "float32",
529
+ "shape": [
530
+ 1,
531
+ 8,
532
+ 64
533
+ ]
534
+ }
535
+ ],
536
+ "outputs": [
537
+ {
538
+ "name": "span_logits",
539
+ "dtype": "float32",
540
+ "shape": [
541
+ 1,
542
+ 8,
543
+ 64
544
+ ]
545
+ }
546
+ ]
547
+ }
548
+ ]
549
+ },
550
+ {
551
+ "path": "gliner2_base_relation_fp32.tflite",
552
+ "role": "relation",
553
+ "about": "relation scorer over up to 256 head/tail pairs of up to 4 relation types (pairs chosen by gliner2's typed pair generator); FP32, CPU",
554
+ "bytes": 23658524,
555
+ "signatures": [
556
+ {
557
+ "key": "w64_r4_p256",
558
+ "inputs": [
559
+ {
560
+ "name": "text_states",
561
+ "dtype": "float32",
562
+ "shape": [
563
+ 1,
564
+ 64,
565
+ 768
566
+ ]
567
+ },
568
+ {
569
+ "name": "text_length",
570
+ "dtype": "int32",
571
+ "shape": [
572
+ 1
573
+ ]
574
+ },
575
+ {
576
+ "name": "relation_states",
577
+ "dtype": "float32",
578
+ "shape": [
579
+ 1,
580
+ 4,
581
+ 1536
582
+ ]
583
+ },
584
+ {
585
+ "name": "batch_index",
586
+ "dtype": "int32",
587
+ "shape": [
588
+ 256
589
+ ]
590
+ },
591
+ {
592
+ "name": "relation_index",
593
+ "dtype": "int32",
594
+ "shape": [
595
+ 256
596
+ ]
597
+ },
598
+ {
599
+ "name": "head_start",
600
+ "dtype": "int32",
601
+ "shape": [
602
+ 256
603
+ ]
604
+ },
605
+ {
606
+ "name": "head_end",
607
+ "dtype": "int32",
608
+ "shape": [
609
+ 256
610
+ ]
611
+ },
612
+ {
613
+ "name": "tail_start",
614
+ "dtype": "int32",
615
+ "shape": [
616
+ 256
617
+ ]
618
+ },
619
+ {
620
+ "name": "tail_end",
621
+ "dtype": "int32",
622
+ "shape": [
623
+ 256
624
+ ]
625
+ },
626
+ {
627
+ "name": "pair_mask",
628
+ "dtype": "float32",
629
+ "shape": [
630
+ 256
631
+ ]
632
+ }
633
+ ],
634
+ "outputs": [
635
+ {
636
+ "name": "relation_logits",
637
+ "dtype": "float32",
638
+ "shape": [
639
+ 256
640
+ ]
641
+ }
642
+ ]
643
+ }
644
+ ]
645
+ },
646
+ {
647
+ "path": "gliner2_base_record_assignment_fp32.tflite",
648
+ "role": "record_assignment",
649
+ "about": "record decoder: instance-to-field assignment over 8 fields with up to 192 candidates each, for up to 1536 instances, plus object and latent-seed scores; FP32, CPU",
650
+ "bytes": 1193288,
651
+ "signatures": [
652
+ {
653
+ "key": "f8_c192_i1536",
654
+ "inputs": [
655
+ {
656
+ "name": "instance_states",
657
+ "dtype": "float32",
658
+ "shape": [
659
+ 1536,
660
+ 768
661
+ ]
662
+ },
663
+ {
664
+ "name": "field_queries",
665
+ "dtype": "float32",
666
+ "shape": [
667
+ 8,
668
+ 768
669
+ ]
670
+ },
671
+ {
672
+ "name": "field_candidate_states",
673
+ "dtype": "float32",
674
+ "shape": [
675
+ 8,
676
+ 192,
677
+ 768
678
+ ]
679
+ }
680
+ ],
681
+ "outputs": [
682
+ {
683
+ "name": "assignment_logits",
684
+ "dtype": "float32",
685
+ "shape": [
686
+ 1536,
687
+ 8,
688
+ 193
689
+ ]
690
+ },
691
+ {
692
+ "name": "object_logits",
693
+ "dtype": "float32",
694
+ "shape": [
695
+ 1536
696
+ ]
697
+ },
698
+ {
699
+ "name": "latent_seed_logits",
700
+ "dtype": "float32",
701
+ "shape": [
702
+ 1536
703
+ ]
704
+ }
705
+ ]
706
+ }
707
+ ]
708
+ },
709
+ {
710
+ "path": "gliner2_base_record_anchorless_fp32.tflite",
711
+ "role": "record_anchorless",
712
+ "about": "record decoder: its 32 learned instances attending over up to 1536 field candidates (anchorless records); FP32, CPU",
713
+ "bytes": 2875604,
714
+ "signatures": [
715
+ {
716
+ "key": "c1536",
717
+ "inputs": [
718
+ {
719
+ "name": "context_states",
720
+ "dtype": "float32",
721
+ "shape": [
722
+ 1536,
723
+ 768
724
+ ]
725
+ },
726
+ {
727
+ "name": "context_mask",
728
+ "dtype": "float32",
729
+ "shape": [
730
+ 1536
731
+ ]
732
+ }
733
+ ],
734
+ "outputs": [
735
+ {
736
+ "name": "instance_states",
737
+ "dtype": "float32",
738
+ "shape": [
739
+ 32,
740
+ 768
741
+ ]
742
+ }
743
+ ]
744
+ }
745
+ ]
746
  }
747
  ],
748
  "table": {
 
765
  "choices": 8,
766
  "pool": 192,
767
  "boundaries": 65,
768
+ "about": "the host pads to T=128 tokens, W=64 words, Q=8 queries, C=8 choices, P=192 candidates and refuses larger requests"
769
  },
770
  "host": {
771
  "steps": "as the gliner2 library does: pack and pad the request, then encoder -> head -> the shared candidate pool on the host -> scorer -> thresholds, flat overlap resolution and spans; entities abstain when sigmoid(null_logits) > 0.5",
772
  "changes": [
773
  "input_ids no longer enter a graph: the host looks them up in the float16 table into inputs_embeds",
774
  "attention_mask is float32 (1.0 / 0.0) instead of int32",
775
+ "FluidInference's features graph is two graphs: encoder (inputs_embeds, attention_mask -> hidden_states) and head (hidden_states plus the six other feature inputs -> the same thirteen outputs)",
776
+ "the scorer, explicit-span, relation and record graphs take FluidInference's inputs and return its outputs, FP32",
777
+ "bind inputs by name; signatures: encoder `t128`, head `t128`, scorer `p192`, explicit `w64_q8_s64`, relation `w64_r4_p256`, record assignment `f8_c192_i1536`, anchorless records `c1536`"
778
  ]
779
  },
780
  "accelerators": {
 
803
  },
804
  "provenance": {
805
  "graphs": "encoder graph code ported from litert-community/GLiNER2.5-Decide-LiteRT (Apache-2.0); head and scorer wrappers adapted from FluidInference/gliner2-5-base-coreml (Apache-2.0)"
806
+ },
807
+ "referenceRuntime": "runtime/litert_boundary_runtime.py (gliner2 2.0.0's BoundaryExtractor over these graphs)"
808
  }
gliner2_base_explicit_fp32.tflite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:03a08c79fdd6ffc6de3e881af5cea7a369d7b3205017915396b9203d7a982051
3
+ size 2051844
gliner2_base_record_anchorless_fp32.tflite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dcc066dce8b721f39f4870459efb0abd3c5366f58498a9ff062c4065f9ee24d3
3
+ size 2875604
gliner2_base_record_assignment_fp32.tflite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e6169de37236a4d056edefcaabfb1e9bba4df1ee34d45f8d6436dc94e22195f5
3
+ size 1193288
gliner2_base_relation_fp32.tflite ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:dcf3528768b742dc6328c4441ae58694606a3fe15b3500ab429e13bf1caf4ed3
3
+ size 23658524
runtime/litert_boundary_runtime.py ADDED
@@ -0,0 +1,490 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """gliner2.5-base-v1 on LiteRT with every trained head: gliner2's own schema handling and decoders over seven graphs.
2
+
3
+ Ported from FluidInference/gliner2-5-base-coreml@ca8ddf6c (Apache-2.0): extraction_runtime.py (the Core ML runtime),
4
+ preprocessing.py (prepare_extraction) and extraction_pool.py (select_candidates). Changes: LiteRT CompiledModel graphs in
5
+ place of Core ML packages; the features stage is two graphs (the encoder over the host-mapped float16 token table, then
6
+ the head); the bucket sizes come from the graphs.
7
+
8
+ from litert_boundary_runtime import LiteRTBoundaryExtractor
9
+ from gliner2 import Schema
10
+ model = LiteRTBoundaryExtractor("path/to/bundle")
11
+ model.extract("Alice founded Acme in Toronto.", Schema().entities(["person", "organization"]).relations(["founded"]))
12
+ model.extract_relations("Alice founded Acme in Toronto.", ["founded", "located_in"])
13
+
14
+ Every public call of gliner2's extractor works: extract, batch_extract, extract_entities, classify_text, extract_json,
15
+ extract_relations and their batch_* and *_long forms. The graphs have fixed sizes (128 subword tokens, 64 text words,
16
+ 8 queries and 8 classification choices per request); a larger request is refused with a ValueError, never truncated.
17
+ For long text use the *_long calls with a chunk_size that fits, e.g. extract_entities_long(text, labels, chunk_size=40,
18
+ chunk_overlap=8).
19
+
20
+ Needs gliner2==2.0.0 (schema, collator and decoders; no PyTorch weights are loaded), torch (the library's tensor
21
+ types), numpy and ai-edge-litert. The bundle folder holds the .tflite graphs, the float16 table, tokenizer/ and
22
+ source/config.json (the checkpoint's configuration).
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import math
28
+ from contextvars import ContextVar
29
+ from pathlib import Path
30
+
31
+ import numpy as np
32
+ import torch
33
+ from gliner2.configuration import BoundaryHeadSettings
34
+ from gliner2.models.boundary.constants import MASK_LOGIT
35
+ from gliner2.models.boundary.engine import BoundaryExtractor
36
+ from gliner2.models.boundary.indexing import gather_rows
37
+ from gliner2.models.boundary.pool import PooledCandidates, _deduplicate_pool
38
+ from gliner2.models.boundary.proposal import select_top_boundaries
39
+ from gliner2.models.boundary.records import RecordGroupOutput
40
+ from gliner2.models.boundary.relations import RelationProposalSettings, RelationTypeSpec, TypedRelationPairGenerator
41
+ from gliner2.models.base import load_extractor_tokenizer
42
+ from gliner2.models.outputs import CandidateTensorBatch, ExtractorOutput
43
+ from gliner2.processor import SchemaTransformer
44
+ from gliner2.training.trainer import ExtractorCollator
45
+
46
+ FEATURE_NAMES = ("text_states", "query_states", "boundary_states", "boundary_mask", "start_logits", "end_logits", "inside_prefix",
47
+ "inside_prefix_mean", "pool_start_projection", "pool_end_projection", "null_logits", "count_log_rates",
48
+ "classification_logits")
49
+ HEAD_INPUTS = ("hidden_states", "text_indices", "text_mask", "query_indices", "query_mask", "cls_indices", "cls_mask")
50
+ SCORER_INPUTS = ("text_states", "text_mask", "query_states", "query_mask", "boundary_states", "start_logits", "end_logits",
51
+ "inside_prefix", "inside_prefix_mean", "candidate_indices", "candidate_mask", "candidate_compatibility")
52
+ TABLE = "gliner2_base_embeddings_f16.bin"
53
+ FILES = {
54
+ "head": "gliner2_base_head_fp32.tflite", "scorer": "gliner2_base_scorer_fp32.tflite",
55
+ "explicit": "gliner2_base_explicit_fp32.tflite", "relation": "gliner2_base_relation_fp32.tflite",
56
+ "record_assignment": "gliner2_base_record_assignment_fp32.tflite", "record_anchorless": "gliner2_base_record_anchorless_fp32.tflite",
57
+ }
58
+
59
+
60
+ class Graph:
61
+ """One signature of a LiteRT CompiledModel: run(feed) -> {output name: float32 array}."""
62
+
63
+ def __init__(self, path: Path, signature: str | None = None, threads: int = 4, accelerator: str = "cpu", cache: str = ""):
64
+ from ai_edge_litert.compiled_model import CompiledModel, CpuOptions, GpuOptions, HardwareAccelerator, Options
65
+ if accelerator == "cpu":
66
+ options = Options(HardwareAccelerator.CPU, cpu_options=CpuOptions(num_threads=threads, xnnpack_weight_cache_path=cache))
67
+ else: # "gpu" (FP32) or "gpu-fp16"
68
+ options = Options(HardwareAccelerator.GPU,
69
+ gpu_options=GpuOptions(enforce_f32=accelerator == "gpu", constant_tensor_sharing=True))
70
+ self.model = CompiledModel.from_file(str(path), options=options)
71
+ if signature is None:
72
+ signature = self.model.get_signature_list()[0] if hasattr(self.model, "get_signature_list") else None
73
+ self.index = self.model.get_signature_index(signature) if signature else 0
74
+ sig = self.model.get_signature_by_index(self.index)
75
+ self.signature = signature or sig.get("key")
76
+ self.inputs, self.outputs = sig["inputs"], sig["outputs"]
77
+ self.ins, self.outs = self.model.create_input_buffers(self.index), self.model.create_output_buffers(self.index)
78
+ details = self.model.get_input_tensor_details(self.signature)
79
+ self.dtypes = {k: (np.int32 if "int" in str(v.get("dtype", "")).lower() else np.float32) for k, v in details.items()}
80
+ self.input_shapes = {k: tuple(int(x) for x in v["shape"]) for k, v in details.items()}
81
+ self.shapes = {k: tuple(int(x) for x in v["shape"]) for k, v in self.model.get_output_tensor_details(self.signature).items()}
82
+
83
+ def __call__(self, feed: dict) -> dict:
84
+ for buffer, name in zip(self.ins, self.inputs):
85
+ buffer.write(np.ascontiguousarray(np.asarray(feed[name]), self.dtypes[name]))
86
+ self.model.run_by_index(self.index, self.ins, self.outs)
87
+ return {name: buffer.read(int(np.prod(self.shapes[name])), np.float32).reshape(self.shapes[name])
88
+ for buffer, name in zip(self.outs, self.outputs)}
89
+
90
+
91
+ def signature_of(path: Path) -> str:
92
+ from ai_edge_litert.compiled_model import CompiledModel
93
+ model = CompiledModel.from_file(str(path))
94
+ key = model.get_signature_by_index(0)["key"]
95
+ model.close()
96
+ return key
97
+
98
+
99
+ def to_tensor(value):
100
+ return torch.from_numpy(np.asarray(value).copy())
101
+
102
+
103
+ def padded(value, size):
104
+ if value.shape[0] > size:
105
+ raise ValueError(f"Request exceeds the bucket capacity {size}")
106
+ result = value.new_zeros((size, *value.shape[1:]))
107
+ result[: value.shape[0]] = value
108
+ return result
109
+
110
+
111
+ def prepare_extraction(processor, text: str, schema, length: int, max_words: int, max_queries: int, max_choices: int):
112
+ """gliner2's collator on one request, padded to the bucket; refuses requests that do not fit (never truncates)."""
113
+ built = schema.build() if hasattr(schema, "build") else schema
114
+ batch = ExtractorCollator(processor, is_training=False, max_len=None, architecture="boundary")([(text, built)])
115
+ if batch.input_ids.shape[1] > length:
116
+ raise ValueError(f"Schema and text require {batch.input_ids.shape[1]} subwords; bucket holds {length}")
117
+ if batch.text_word_indices.shape[1] > max_words:
118
+ raise ValueError(f"Text requires {batch.text_word_indices.shape[1]} words; bucket holds {max_words}")
119
+ if batch.query_marker_indices.shape[1] > max_queries:
120
+ raise ValueError(f"Schema requires {batch.query_marker_indices.shape[1]} queries; bucket holds {max_queries}")
121
+ if batch.cls_marker_indices.shape[1] > max_choices:
122
+ raise ValueError(f"Schema requires {batch.cls_marker_indices.shape[1]} choices; bucket holds {max_choices}")
123
+ if batch.query_marker_indices.shape[1] == 0 and batch.cls_marker_indices.shape[1] == 0:
124
+ raise ValueError("Schema has no extraction or classification queries")
125
+
126
+ def pad(values, width, fill=0):
127
+ array = values.numpy()
128
+ return np.pad(array, ((0, 0), (0, width - array.shape[1])), constant_values=fill)
129
+
130
+ arrays = {
131
+ "input_ids": pad(batch.input_ids, length, processor.tokenizer.pad_token_id).astype(np.int32),
132
+ "attention_mask": pad(batch.attention_mask, length).astype(np.int32),
133
+ "text_indices": pad(batch.text_word_indices, max_words).astype(np.int32),
134
+ "text_mask": pad(batch.text_word_mask, max_words).astype(np.float32),
135
+ "query_indices": pad(batch.query_marker_indices, max_queries).astype(np.int32),
136
+ "query_mask": pad(batch.query_marker_mask, max_queries).astype(np.float32),
137
+ "cls_indices": pad(batch.cls_marker_indices, max_choices).astype(np.int32),
138
+ "cls_mask": pad(batch.cls_marker_mask, max_choices).astype(np.float32),
139
+ }
140
+ return arrays, batch
141
+
142
+
143
+ def select_candidates(start_projection, end_projection, boundary_mask, query_mask, start_logits, end_logits, *, boundary_top_k: int,
144
+ pool_size: int, min_pool_per_query: int) -> PooledCandidates:
145
+ """gliner2 2.0.0's shared candidate pool (stable ranking and deduplication) from the head graph's projections."""
146
+ batch, n_boundaries, dim = start_projection.shape
147
+ n_queries = query_mask.shape[1]
148
+ floor = torch.full_like(start_logits, MASK_LOGIT)
149
+ valid_boundary = boundary_mask.unsqueeze(1) & query_mask.unsqueeze(-1)
150
+ union_start = torch.where(valid_boundary, start_logits, floor).amax(1)
151
+ union_end = torch.where(valid_boundary, end_logits, floor).amax(1)
152
+ union_valid = boundary_mask & query_mask.any(-1, keepdim=True)
153
+ _, starts, starts_valid = select_top_boundaries(union_start.unsqueeze(1), union_valid.unsqueeze(1), boundary_top_k)
154
+ _, ends, ends_valid = select_top_boundaries(union_end.unsqueeze(1), union_valid.unsqueeze(1), boundary_top_k)
155
+ starts, ends = starts[:, 0], ends[:, 0]
156
+ starts_valid, ends_valid = starts_valid[:, 0], ends_valid[:, 0]
157
+ n_starts, n_ends = starts.shape[1], ends.shape[1]
158
+ pair_start = starts.unsqueeze(-1).expand(batch, n_starts, n_ends).reshape(batch, -1)
159
+ pair_end = ends.unsqueeze(1).expand(batch, n_starts, n_ends).reshape(batch, -1)
160
+ pair_valid = (starts_valid.unsqueeze(-1) & ends_valid.unsqueeze(1) & (ends.unsqueeze(1) > starts.unsqueeze(-1))).reshape(batch, -1)
161
+ compatibility = (gather_rows(start_projection, pair_start) * gather_rows(end_projection, pair_end)).sum(-1) / math.sqrt(dim)
162
+ union_pair_score = (compatibility + union_start.gather(1, pair_start.clamp(0, n_boundaries - 1))
163
+ + union_end.gather(1, pair_end.clamp(0, n_boundaries - 1)))
164
+ quota = min(min_pool_per_query, pair_start.shape[-1])
165
+ quota_keys = pair_start.new_zeros((batch, 0))
166
+ quota_scores = union_pair_score.new_zeros((batch, 0))
167
+ quota_valid = pair_valid.new_zeros((batch, 0))
168
+ if quota:
169
+ start_idx = pair_start.clamp(0, start_logits.shape[2] - 1).unsqueeze(1).expand(batch, n_queries, -1)
170
+ end_idx = pair_end.clamp(0, end_logits.shape[2] - 1).unsqueeze(1).expand(batch, n_queries, -1)
171
+ per_query = start_logits.gather(2, start_idx) + end_logits.gather(2, end_idx) + compatibility.unsqueeze(1)
172
+ per_query_valid = pair_valid.unsqueeze(1) & query_mask.unsqueeze(-1)
173
+ ranked = torch.argsort(per_query.masked_fill(~per_query_valid, MASK_LOGIT), dim=-1, descending=True, stable=True)[..., :quota]
174
+ quota_start, quota_end = start_idx.gather(-1, ranked), end_idx.gather(-1, ranked)
175
+ quota_valid = per_query_valid.gather(-1, ranked).reshape(batch, -1)
176
+ quota_keys = (quota_start * n_boundaries + quota_end).reshape(batch, -1)
177
+ rank_bonus = torch.arange(quota, 0, -1, device=start_projection.device, dtype=union_pair_score.dtype)
178
+ quota_scores = (union_pair_score.new_full((batch, n_queries, quota), -MASK_LOGIT * 0.5) + rank_bonus.view(1, 1, quota)).reshape(batch, -1)
179
+ all_keys = torch.cat((quota_keys, pair_start * n_boundaries + pair_end), -1)
180
+ all_scores = torch.cat((quota_scores, union_pair_score.detach()), -1)
181
+ all_valid = torch.cat((quota_valid, pair_valid), -1)
182
+ selected_keys, selected_valid = _deduplicate_pool(all_keys, all_scores, all_valid, pool_size, n_boundaries)
183
+ selected_keys = torch.where(selected_valid, selected_keys, torch.zeros_like(selected_keys))
184
+ selected_s = torch.div(selected_keys, n_boundaries, rounding_mode="floor")
185
+ selected_e = selected_keys - selected_s * n_boundaries
186
+ indices = torch.where(selected_valid.unsqueeze(-1), torch.stack((selected_s, selected_e), -1), torch.zeros_like(torch.stack((selected_s, selected_e), -1)))
187
+ selected_compat = (gather_rows(start_projection, selected_s) * gather_rows(end_projection, selected_e)).sum(-1) / math.sqrt(dim)
188
+ selected_score = (selected_compat + union_start.gather(1, selected_s.clamp(0, n_boundaries - 1))
189
+ + union_end.gather(1, selected_e.clamp(0, n_boundaries - 1))).masked_fill(~selected_valid, MASK_LOGIT)
190
+ selected_compat = torch.where(selected_valid, selected_compat, torch.zeros_like(selected_compat))
191
+ return PooledCandidates(indices=indices, mask=selected_valid, proposal_logits=selected_score, gold_mask=None,
192
+ compat_logits=selected_compat, stats=None)
193
+
194
+
195
+ class LiteRTBoundaryHead(torch.nn.Module):
196
+ """Gives gliner2's decoder the tensors the boundary head would have produced, from the LiteRT stages."""
197
+
198
+ def __init__(self, context: ContextVar, explicit: Graph):
199
+ super().__init__()
200
+ self.context, self.explicit = context, explicit
201
+ self.max_queries, self.max_spans = explicit.input_shapes["span_indices"][1], explicit.input_shapes["span_indices"][2]
202
+
203
+ def forward(self, text, text_mask, query, query_mask, return_candidates=True):
204
+ state = self.context.get()
205
+ features, full, count = state["features"], state["candidates"], query.shape[1]
206
+ selected = CandidateTensorBatch(indices=full.indices[:, :count], proposal_logits=full.proposal_logits[:, :count],
207
+ pair_logits=full.pair_logits[:, :count], valid_mask=full.valid_mask[:, :count],
208
+ query_mask=full.query_mask[:, :count], candidate_states=full.candidate_states[:, :count])
209
+ return ExtractorOutput(candidates=selected if return_candidates else None, start_logits=features["start_logits"][:, :count],
210
+ end_logits=features["end_logits"][:, :count], null_logits=features["null_logits"][:, :count],
211
+ count_log_rates=features["count_log_rates"][:, :count], batch_size=1)
212
+
213
+ def score_explicit_spans(self, text, text_mask, query, query_mask, indices, valid_mask=None):
214
+ state = self.context.get()
215
+ features = state["features"]
216
+ full_queries = features["query_states"][0]
217
+ active = int(state["arrays"]["query_mask"].sum())
218
+ query_count, span_count = query.shape[1], indices.shape[2]
219
+ if query_count > self.max_queries or span_count > self.max_spans:
220
+ raise ValueError("Explicit-span request exceeds the bucket capacity")
221
+ selected = []
222
+ for row in query[0]:
223
+ matches = torch.isclose(full_queries[:active], row, atol=1e-6, rtol=0).all(-1).nonzero(as_tuple=False).flatten()
224
+ if matches.numel() != 1:
225
+ raise ValueError("Explicit-span query cannot be mapped to the encoded schema")
226
+ selected.append(int(matches[0]))
227
+ selection = torch.tensor(selected, dtype=torch.long)
228
+
229
+ def feature(name):
230
+ return padded(features[name][0].index_select(0, selection), self.max_queries).unsqueeze(0)
231
+
232
+ span_indices = torch.zeros(1, self.max_queries, self.max_spans, 2, dtype=torch.int32)
233
+ span_mask = torch.zeros(1, self.max_queries, self.max_spans, dtype=torch.float32)
234
+ span_indices[:, :query_count, :span_count] = indices.int()
235
+ span_mask[:, :query_count, :span_count] = valid_mask.float() if valid_mask is not None else 1.0
236
+ feed = {"text_states": text, "text_mask": text_mask.float(), "query_states": padded(query[0], self.max_queries).unsqueeze(0),
237
+ "query_mask": padded(query_mask[0].float(), self.max_queries).unsqueeze(0), "boundary_states": features["boundary_states"],
238
+ "start_logits": feature("start_logits"), "end_logits": feature("end_logits"), "inside_prefix": feature("inside_prefix"),
239
+ "inside_prefix_mean": feature("inside_prefix_mean"), "span_indices": span_indices, "span_mask": span_mask}
240
+ output = self.explicit({k: v.numpy() for k, v in feed.items()})["span_logits"]
241
+ return to_tensor(output)[:, :query_count, :span_count]
242
+
243
+
244
+ class LiteRTRelationScorer(torch.nn.Module):
245
+ """Runs the relation graph after gliner2's own typed pair selection."""
246
+
247
+ def __init__(self, context: ContextVar, graph: Graph):
248
+ super().__init__()
249
+ self.context, self.graph = context, graph
250
+ self.max_words = graph.input_shapes["text_states"][1]
251
+ self.max_relations = graph.input_shapes["relation_states"][1]
252
+ self.pair_cap = graph.input_shapes["pair_mask"][0]
253
+
254
+ def forward(self, text, relation, candidates, pairs):
255
+ count = len(pairs)
256
+ if text.shape[1] != self.max_words or relation.shape[1] > self.max_relations or count > self.pair_cap:
257
+ raise ValueError("Relation request exceeds the bucket capacity")
258
+ relation_states = torch.zeros(1, self.max_relations, relation.shape[-1], dtype=relation.dtype)
259
+ relation_states[:, : relation.shape[1]] = relation
260
+ text_length = int(self.context.get()["arrays"]["text_mask"].sum())
261
+ feed = {"text_states": text, "text_length": torch.tensor([text_length], dtype=torch.int32), "relation_states": relation_states,
262
+ "batch_index": padded(pairs.batch_index.int(), self.pair_cap), "relation_index": padded(pairs.relation_index.int(), self.pair_cap),
263
+ "head_start": padded(pairs.head_start.int(), self.pair_cap), "head_end": padded(pairs.head_end.int(), self.pair_cap),
264
+ "tail_start": padded(pairs.tail_start.int(), self.pair_cap), "tail_end": padded(pairs.tail_end.int(), self.pair_cap),
265
+ "pair_mask": padded(pairs.pair_mask.float(), self.pair_cap)}
266
+ return to_tensor(self.graph({k: v.numpy() for k, v in feed.items()})["relation_logits"])[:count]
267
+
268
+
269
+ class LiteRTRecordHead(torch.nn.Module):
270
+ """Keeps gliner2's instance ordering while the record decoder's learned layers run on LiteRT."""
271
+
272
+ def __init__(self, assignment: Graph, anchorless: Graph):
273
+ super().__init__()
274
+ self.assignment, self.anchorless = assignment, anchorless
275
+ self.max_instances = assignment.input_shapes["instance_states"][0]
276
+ self.max_fields, self.max_candidates = assignment.input_shapes["field_candidate_states"][:2]
277
+
278
+ def forward_group(self, spec, query_states, candidates, sample_index):
279
+ field_specs = list(spec.fields)
280
+ field_query_ids = [field.query_id for field in field_specs]
281
+ if len(field_specs) > self.max_fields:
282
+ raise ValueError("Record has more fields than the bucket holds")
283
+ if any(q < 0 or q >= query_states.shape[0] for q in field_query_ids):
284
+ raise ValueError("Record field query is outside the encoded schema")
285
+ field_states, field_spans, field_logits, field_masks = [], [], [], []
286
+ for query_id in field_query_ids:
287
+ mask = candidates.valid_mask[sample_index, query_id]
288
+ if int(mask.sum()) > self.max_candidates:
289
+ raise ValueError("Record candidate count exceeds the bucket")
290
+ field_states.append(candidates.candidate_states[sample_index, query_id][mask])
291
+ field_spans.append(candidates.indices[sample_index, query_id][mask])
292
+ field_logits.append(candidates.pair_logits[sample_index, query_id][mask])
293
+ field_masks.append(torch.ones(int(mask.sum()), dtype=torch.bool))
294
+ instance_seed, instance_spans = [], []
295
+ if spec.mode == "natural":
296
+ anchor = field_query_ids.index(spec.anchor_query_id)
297
+ instances = field_states[anchor]
298
+ for index, span in enumerate(field_spans[anchor]):
299
+ instance_seed.append((anchor, index))
300
+ instance_spans.append((int(span[0]), int(span[1])))
301
+ elif spec.mode == "latent":
302
+ instances = torch.cat(field_states, 0) if field_states else query_states.new_zeros((0, query_states.shape[-1]))
303
+ for field_index, spans in enumerate(field_spans):
304
+ for index, span in enumerate(spans):
305
+ instance_seed.append((field_index, index))
306
+ instance_spans.append((int(span[0]), int(span[1])))
307
+ else:
308
+ context = torch.cat(field_states, 0) if field_states else query_states.new_zeros((0, query_states.shape[-1]))
309
+ capacity = self.anchorless.input_shapes["context_mask"][0]
310
+ context_mask = torch.zeros(capacity, dtype=torch.float32)
311
+ context_mask[: context.shape[0]] = 1.0
312
+ out = self.anchorless({"context_states": padded(context, capacity).numpy(), "context_mask": context_mask.numpy()})
313
+ instances = to_tensor(out["instance_states"])
314
+ instance_seed = [None] * instances.shape[0]
315
+ instance_spans = [None] * instances.shape[0]
316
+ count = instances.shape[0]
317
+ if count > self.max_instances:
318
+ raise ValueError("Record instance count exceeds the bucket")
319
+ candidate_states = torch.zeros(self.max_fields, self.max_candidates, query_states.shape[-1])
320
+ for index, states in enumerate(field_states):
321
+ candidate_states[index, : states.shape[0]] = states
322
+ predicted = self.assignment({"instance_states": padded(instances, self.max_instances).numpy(),
323
+ "field_queries": padded(query_states[field_query_ids], self.max_fields).numpy(),
324
+ "field_candidate_states": candidate_states.numpy()})
325
+ assignment = to_tensor(predicted["assignment_logits"])
326
+ assignment_by_field = [assignment[:count, i, : 1 + states.shape[0]] for i, states in enumerate(field_states)]
327
+ if spec.mode == "natural":
328
+ object_logits = field_logits[field_query_ids.index(spec.anchor_query_id)]
329
+ elif spec.mode == "latent":
330
+ object_logits = to_tensor(predicted["latent_seed_logits"])[:count]
331
+ else:
332
+ object_logits = to_tensor(predicted["object_logits"])[:count]
333
+ return RecordGroupOutput(spec=spec, object_logits=object_logits, assign_logits=assignment_by_field, field_query_ids=field_query_ids,
334
+ field_specs=field_specs, field_spans=field_spans, field_cand_mask=field_masks, field_cand_logits=field_logits,
335
+ instance_seed=instance_seed, instance_spans=instance_spans)
336
+
337
+
338
+ class LiteRTBoundaryExtractor(BoundaryExtractor):
339
+ """gliner2's BoundaryExtractor (schemas, candidate selection, decoders) with every trained module on LiteRT."""
340
+
341
+ def __init__(self, bundle: str, encoder: str = "gliner2_base_encoder_wfp16.tflite", accelerator: str = "cpu", threads: int = 4,
342
+ cache: str = ""):
343
+ torch.nn.Module.__init__(self)
344
+ folder = Path(bundle)
345
+ config = json.loads((folder / "source" / "config.json").read_text())
346
+ self.boundary_settings = BoundaryHeadSettings(**config["boundary_head"])
347
+ self.processor = SchemaTransformer(tokenizer=load_extractor_tokenizer(str(folder / "tokenizer")), token_pooling="first")
348
+ self.enable_records = self.boundary_settings.enable_records
349
+ self.enable_relations = self.boundary_settings.enable_relations
350
+ self.strict_extraction = True
351
+ self._device_anchor = torch.nn.Parameter(torch.zeros(1), requires_grad=False) # the runtime reads device and dtype from parameters()
352
+ self._context = ContextVar("gliner2_litert_extraction_context")
353
+ self.encoder_graph = Graph(folder / encoder, signature_of(folder / encoder), threads, accelerator, cache)
354
+ self.head_graph = Graph(folder / FILES["head"], signature_of(folder / FILES["head"]), 1)
355
+ self.scorer_graph = Graph(folder / FILES["scorer"], signature_of(folder / FILES["scorer"]), 1)
356
+ graphs = {k: Graph(folder / FILES[k], signature_of(folder / FILES[k]), 1)
357
+ for k in ("explicit", "relation", "record_assignment", "record_anchorless")}
358
+ self.length = self.encoder_graph.input_shapes["attention_mask"][1]
359
+ self.max_words = self.head_graph.input_shapes["text_indices"][1]
360
+ self.max_queries = self.head_graph.input_shapes["query_indices"][1]
361
+ self.max_choices = self.head_graph.input_shapes["cls_indices"][1]
362
+ hidden = self.encoder_graph.input_shapes["inputs_embeds"][2]
363
+ rows = (folder / TABLE).stat().st_size // (2 * hidden)
364
+ self.table = np.memmap(folder / TABLE, dtype="<f2", mode="r", shape=(rows, hidden))
365
+ self.boundary_head = LiteRTBoundaryHead(self._context, graphs["explicit"])
366
+ self.relation_scorer = LiteRTRelationScorer(self._context, graphs["relation"])
367
+ self.record_decoder = LiteRTRecordHead(graphs["record_assignment"], graphs["record_anchorless"])
368
+ self.relation_pair_generator = TypedRelationPairGenerator(RelationProposalSettings(
369
+ heads_per_relation=self.boundary_settings.relation_heads_per_type, tails_per_relation=self.boundary_settings.relation_tails_per_type,
370
+ pair_cap=self.boundary_settings.relation_pair_cap, argument_threshold=self.boundary_settings.relation_argument_proposal_threshold))
371
+
372
+ def features(self, arrays: dict) -> dict:
373
+ ids = arrays["input_ids"].reshape(-1).astype(np.int64)
374
+ hidden = self.encoder_graph({"inputs_embeds": self.table[ids].astype(np.float32)[None],
375
+ "attention_mask": arrays["attention_mask"].astype(np.float32).reshape(1, -1)})["hidden_states"]
376
+ feed = {k: arrays[k] for k in HEAD_INPUTS if k != "hidden_states"}
377
+ feed["hidden_states"] = hidden
378
+ return {name: to_tensor(value) for name, value in self.head_graph(feed).items()}
379
+
380
+ def _encode_core(self, batch):
381
+ state = self._context.get()
382
+ features = state["features"]
383
+ query_mask = to_tensor(state["arrays"]["query_mask"]).bool()
384
+ query_count = int(query_mask.sum())
385
+ query_states = features["query_states"][:, :query_count]
386
+ query_mask = query_mask[:, :query_count]
387
+ text_mask = to_tensor(state["arrays"]["text_mask"]).bool()
388
+ ext_specs, cls_specs, rel_specs, word_offsets = [], [], [], []
389
+ for sample in range(len(batch)):
390
+ specs = [{"group_index": q.task_index, "field_index": q.role_index, "task_type": q.task_type, "task_name": q.task_name,
391
+ "field_name": q.role_name} for q in batch.query_layouts[sample].queries]
392
+ ext_specs.append(specs)
393
+ classifications, choice_offset = [], 0
394
+ for group in range(batch.schema_counts[sample]):
395
+ if batch.task_types[sample][group] != "classifications":
396
+ continue
397
+ count = max(len(batch.schema_special_indices[sample][group]) - 1, 0)
398
+ tokens = batch.schema_tokens_list[sample][group]
399
+ if count:
400
+ classifications.append({"group_index": group, "task_name": tokens[2], "schema_tokens": tokens,
401
+ "group_embs": features["classification_logits"][sample, choice_offset: choice_offset + count]})
402
+ choice_offset += count
403
+ cls_specs.append(classifications)
404
+ word_offsets.append(max(int(batch.text_word_counts[sample]) - len(batch.start_mappings[sample]), 0))
405
+ groups = {}
406
+ for query_id, spec in enumerate(specs):
407
+ groups.setdefault(spec["group_index"], []).append(query_id)
408
+ relations = []
409
+ for group, role_ids in groups.items():
410
+ if batch.task_types[sample][group] != "relations" or len(role_ids) < 2:
411
+ continue
412
+ head_id, tail_id = role_ids[:2]
413
+ roles = query_states[sample, [head_id, tail_id]]
414
+ state_vec = torch.cat((roles[0], roles[1]), -1) if self.boundary_settings.directional_relation_states else roles.mean(0)
415
+ relations.append({"group_index": group, "relation_type": specs[head_id]["task_name"],
416
+ "spec": RelationTypeSpec(specs[head_id]["task_name"], head_query_ids=(head_id,), tail_query_ids=(tail_id,)),
417
+ "query_state": state_vec})
418
+ rel_specs.append(relations)
419
+ return {"text_states": features["text_states"], "text_mask": text_mask, "text_lengths": text_mask.sum(-1).long(),
420
+ "query_states": query_states, "query_mask": query_mask, "ext_specs": ext_specs, "cls_specs": cls_specs,
421
+ "rel_specs": rel_specs, "word_offsets": word_offsets}
422
+
423
+ def _extract_classification_result(self, results, schema_name, schema, embs, schema_tokens, temperature=1.0):
424
+ cls_config = self._resolve_classification_config(schema_tokens[2], schema.get("classifications", []))
425
+ if cls_config is None:
426
+ return
427
+ if temperature <= 0:
428
+ raise ValueError("Classification temperature must be positive")
429
+ logits = embs / temperature
430
+ activation, multi_label = cls_config.get("class_act", "auto"), cls_config.get("multi_label", False)
431
+ probabilities = torch.sigmoid(logits) if activation == "sigmoid" or (activation != "softmax" and multi_label) else torch.softmax(logits, dim=-1)
432
+ labels = cls_config["labels"]
433
+ if multi_label:
434
+ threshold = cls_config.get("cls_threshold", 0.5)
435
+ chosen = [(labels[i], float(probabilities[i])) for i in range(len(labels)) if float(probabilities[i]) >= threshold]
436
+ if not chosen:
437
+ best = int(probabilities.argmax())
438
+ chosen = [(labels[best], float(probabilities[best]))]
439
+ results[cls_config["task"]] = chosen
440
+ return
441
+ best = int(probabilities.argmax())
442
+ results[cls_config["task"]] = (labels[best], float(probabilities[best]))
443
+
444
+ def batch_extract(self, texts, schemas, batch_size: int = 8, threshold: float = 0.5, num_workers: int = 0, format_results: bool = True,
445
+ include_confidence: bool = False, include_spans: bool = False, max_len=None, overlap_policy=None):
446
+ """One request at a time through the fixed-shape graphs (gliner2's batch API, so the *_long calls work too)."""
447
+ if isinstance(schemas, list):
448
+ if len(schemas) != len(texts):
449
+ raise ValueError(f"Schema count ({len(schemas)}) != text count ({len(texts)})")
450
+ schema_list = schemas
451
+ else:
452
+ schema_list = [schemas] * len(texts)
453
+ return [self.extract(text, schema, threshold=threshold, format_results=format_results, include_confidence=include_confidence,
454
+ include_spans=include_spans, overlap_policy=overlap_policy) for text, schema in zip(texts, schema_list)]
455
+
456
+ def extract(self, text: str, schema, threshold: float = 0.5, format_results: bool = True, include_confidence: bool = False,
457
+ include_spans: bool = False, max_len=None, overlap_policy=None):
458
+ schema_dicts, metadata_list = self._build_schema_dicts_and_metadata([schema])
459
+ if overlap_policy is not None:
460
+ metadata_list[0]["_overlap_policy"] = self._resolved_overlap_policy(overlap_policy)
461
+ arrays, batch = prepare_extraction(self.processor, text, schema_dicts[0], self.length, self.max_words, self.max_queries, self.max_choices)
462
+ features = self.features(arrays)
463
+ query_mask = to_tensor(arrays["query_mask"]).bool()
464
+ candidates = None
465
+ if bool(query_mask.any()):
466
+ head = self.boundary_settings
467
+ pool = select_candidates(features["pool_start_projection"], features["pool_end_projection"], features["boundary_mask"].bool(),
468
+ query_mask, features["start_logits"], features["end_logits"], boundary_top_k=head.pool_boundary_top_k,
469
+ pool_size=head.pool_size, min_pool_per_query=head.min_pool_per_query)
470
+ feed = {"text_states": features["text_states"], "text_mask": to_tensor(arrays["text_mask"]), "query_states": features["query_states"],
471
+ "query_mask": to_tensor(arrays["query_mask"]), "boundary_states": features["boundary_states"],
472
+ "start_logits": features["start_logits"], "end_logits": features["end_logits"], "inside_prefix": features["inside_prefix"],
473
+ "inside_prefix_mean": features["inside_prefix_mean"], "candidate_indices": pool.indices.int(),
474
+ "candidate_mask": pool.mask.float(), "candidate_compatibility": pool.compat_logits}
475
+ scored = self.scorer_graph({k: v.numpy() for k, v in feed.items()})
476
+ q = self.max_queries
477
+ candidates = CandidateTensorBatch(indices=pool.indices.unsqueeze(1).expand(1, q, -1, 2),
478
+ proposal_logits=pool.proposal_logits.unsqueeze(1).expand(1, q, -1),
479
+ pair_logits=to_tensor(scored["pair_logits"]), valid_mask=pool.mask.unsqueeze(1).expand(1, q, -1),
480
+ query_mask=query_mask,
481
+ candidate_states=to_tensor(scored["candidate_states"]).unsqueeze(1).expand(1, q, -1, -1))
482
+ token = self._context.set({"arrays": arrays, "features": features, "candidates": candidates})
483
+ try:
484
+ raw = self._extract_from_batch(batch, threshold, metadata_list, include_confidence, include_spans)[0]
485
+ if format_results:
486
+ return self.format_results(raw, include_confidence, metadata_list[0].get("relation_order", []),
487
+ metadata_list[0].get("classification_tasks", []))
488
+ return raw
489
+ finally:
490
+ self._context.reset(token)
runtime/parity_corpus.json ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "about": "Parity corpus for LiteRT ports of GLiNER2 checkpoints: short general-domain texts with invented people and companies, and schema templates covering every call type gliner2 2.0.0 offers (entities with and without descriptions, relations, plain structures, choice fields, natural/latent/anchorless records, entity attributes, single- and multi-label classification, mixed schemas). Only agreement with the library is measured; there are no gold labels. Each text stays under 40 words so a call fits a 128-token bucket with its schema.",
3
+ "templates": {
4
+ "entities": {"entities": ["person", "organization", "location", "date"]},
5
+ "entities_described": {"entities": {"person": "a named individual", "product": "a named product, device or service", "money": "an amount of money"}},
6
+ "entities_single": {"entities": ["person", "organization"], "dtype": "str"},
7
+ "relations_work": {"relations": ["works_for", "founded", "located_in"]},
8
+ "relations_family": {"relations": {"parent_of": "the head is a parent of the tail", "married_to": "the two people are married"}},
9
+ "relations_event": {"relations": ["won", "held_in"]},
10
+ "structure_event": {"structures": [{"name": "event", "fields": [{"name": "name", "dtype": "str"}, {"name": "date", "dtype": "str"}, {"name": "place", "dtype": "str"}]}]},
11
+ "structure_order": {"structures": [{"name": "order", "fields": [{"name": "item", "dtype": "str"}, {"name": "category", "dtype": "str", "choices": ["electronics", "clothing", "furniture", "food"]}, {"name": "quantity", "dtype": "str"}]}]},
12
+ "record_natural": {"structures": [{"name": "employment", "mode": "natural", "anchor": "person", "fields": [{"name": "person", "dtype": "str"}, {"name": "company", "dtype": "str"}, {"name": "role", "dtype": "str"}]}]},
13
+ "record_latent": {"structures": [{"name": "employment", "mode": "latent", "fields": [{"name": "person", "dtype": "str"}, {"name": "company", "dtype": "str"}]}]},
14
+ "record_anchorless": {"structures": [{"name": "trip", "mode": "anchorless", "fields": [{"name": "traveler", "dtype": "str"}, {"name": "destination", "dtype": "str"}]}]},
15
+ "attributes": {"entities": ["person", "organization"], "attributes": {"role": {"labels": ["founder", "employee", "customer"]}}},
16
+ "classification_topic": {"classifications": [{"task": "topic", "labels": ["business", "family", "travel", "shopping", "science", "sports"]}]},
17
+ "classification_multi": {"classifications": [{"task": "tags", "labels": ["money", "people", "places", "dates", "products"], "multi_label": true}]},
18
+ "classification_described": {"classifications": [{"task": "tone", "labels": {"positive": "good news or praise", "negative": "bad news or complaints", "neutral": "plain facts"}}]},
19
+ "mixed": {"entities": ["person", "organization"], "relations": ["works_for"], "classifications": [{"task": "sentiment", "labels": ["positive", "negative", "neutral"]}]}
20
+ },
21
+ "groups": {
22
+ "business": {
23
+ "templates": ["entities", "entities_single", "relations_work", "record_natural", "record_latent", "attributes", "classification_topic", "mixed"],
24
+ "texts": [
25
+ "Nora Lindqvist founded Brightwell Labs in Oslo in 2019 and still runs it as chief executive.",
26
+ "Tomas Ferreira joined Halcyon Freight in March as head of logistics, moving from a rival in Lisbon.",
27
+ "Priya Raman works at Cobalt Analytics in Pune, where she leads a team of twelve engineers.",
28
+ "Marco Bellini and Ines Duarte started Verde Foods together; Marco handles sales while Ines manages the kitchen in Porto.",
29
+ "Westgate Mills hired Daniel Okafor as an accountant last week, and Sara Klein left the company after ten years.",
30
+ "Kwame Mensah is the founder of Sunline Solar, a startup based in Accra that installs rooftop panels.",
31
+ "After the merger, Lena Hoffmann became chief financial officer of Northpeak Group in Munich.",
32
+ "Ravi Shah, an engineer at Tidewater Systems, presented the new billing platform to customers in Boston on Tuesday."
33
+ ]
34
+ },
35
+ "family": {
36
+ "templates": ["entities", "relations_family", "record_latent", "classification_topic", "classification_described"],
37
+ "texts": [
38
+ "Helen Ward married Peter Ward in 1998, and their daughter Emma was born two years later in Leeds.",
39
+ "Carlos Mendes is the father of Lucia and Mateo, who both moved to Madrid for university.",
40
+ "My grandmother Rosa raised four children on a farm near Cork before moving to Dublin.",
41
+ "Aiko Tanaka and her husband Kenji celebrated their anniversary in Kyoto with their son Haruto.",
42
+ "Fatima Diallo is the mother of twins, Amina and Yusuf, who turned nine on Sunday.",
43
+ "James Porter's sister, Claire, lives in Toronto with her partner Mark and their dog.",
44
+ "Olga Petrova married Ivan Sokolov last spring; Ivan's brother Dmitri was the best man.",
45
+ "The Nguyen family, Linh, Bao and their son Minh, spent the holidays with relatives in Hanoi."
46
+ ]
47
+ },
48
+ "travel": {
49
+ "templates": ["entities", "relations_work", "record_anchorless", "structure_event", "classification_topic", "classification_multi"],
50
+ "texts": [
51
+ "Last July, Elena flew from Rome to Reykjavik and spent a week hiking near Vik.",
52
+ "Ben and Chloe drove from Denver to Santa Fe, stopping overnight in Taos on the way.",
53
+ "Our tour group arrived in Marrakech on Friday and leaves for Fez on Monday morning.",
54
+ "Hugo booked a train from Paris to Geneva for the conference on 14 May.",
55
+ "Mia visited her cousin in Cape Town and then took a ferry to Robben Island.",
56
+ "The museum in Bilbao reopened in April after a two-year renovation led by the city council.",
57
+ "Sam and Priya plan to backpack through Peru, starting in Cusco and ending in Lima.",
58
+ "Grace missed her connection in Frankfurt, so she reached Warsaw a day late."
59
+ ]
60
+ },
61
+ "shopping": {
62
+ "templates": ["entities_described", "structure_order", "record_natural", "classification_multi", "classification_described", "attributes"],
63
+ "texts": [
64
+ "I ordered two oak chairs from Hearthwood for 180 dollars, but only one arrived.",
65
+ "Lucas bought a Zenith X2 laptop and a wireless mouse at Volt Electronics for 1,240 euros.",
66
+ "Please send three boxes of green tea and a ceramic teapot to the Seattle store.",
67
+ "The customer returned a wool coat to Northline Apparel and asked for a refund of 95 pounds.",
68
+ "Amara paid 12 dollars for a loaf of sourdough and a jar of honey at the Saturday market.",
69
+ "Our office ordered forty desk lamps from Brightspace, and the invoice came to 2,000 dollars.",
70
+ "Jonah's new Aurora headphones broke after a week, and the shop offered him a replacement.",
71
+ "Ten kilograms of rice and five bags of lentils were delivered to the community kitchen."
72
+ ]
73
+ },
74
+ "science": {
75
+ "templates": ["entities", "entities_described", "relations_work", "structure_event", "classification_topic", "classification_described"],
76
+ "texts": [
77
+ "Dr. Yusuf Kaya and his team at the Halden Institute published a study on coral bleaching in June.",
78
+ "The Meridian telescope in Chile recorded a supernova on 3 February, according to astronomer Lucia Ortiz.",
79
+ "Researchers at Brookfield University tested a new battery that charges in six minutes.",
80
+ "Marta Silva, a biologist at the Lisbon Aquarium, tagged forty sea turtles last summer.",
81
+ "The probe Helios-4 entered orbit around Mars after a seven-month journey from Earth.",
82
+ "Engineers at Polaris Robotics in Seoul unveiled a robot that sorts recycling by material.",
83
+ "A trial run by Kestrel Health in Nairobi found the vaccine safe for children over five.",
84
+ "Professor Anil Mehta left Crestview College to lead the climate lab at Northgate University."
85
+ ]
86
+ },
87
+ "sports": {
88
+ "templates": ["entities", "relations_event", "structure_event", "record_latent", "classification_topic", "mixed"],
89
+ "texts": [
90
+ "Sofia Marin won the Vienna Marathon in April, finishing ahead of last year's champion Ruth Abebe.",
91
+ "The Harbor City Hawks beat the Ridgeview Bears 3 to 1 in the final held in Glasgow.",
92
+ "Coach Andre Lopes signed a new contract with Atletico Verde until 2028.",
93
+ "Kenji Watanabe took gold in the 200 metres at the championships in Nagoya on Saturday.",
94
+ "The tennis open in Melbourne ended with Clara Weiss lifting her first trophy.",
95
+ "Liam O'Brien scored twice as Dunmore Rovers won the cup at their home ground.",
96
+ "The cycling race from Lyon to Annecy was won by Pieter de Vries after a late sprint.",
97
+ "Amira Haddad set a national record in the long jump at the meet in Doha."
98
+ ]
99
+ }
100
+ }
101
+ }
runtime/requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ gliner2==2.0.0
2
+ torch
3
+ numpy
4
+ ai-edge-litert>=2.2.0
5
+ huggingface_hub # verify_parity.py
runtime/verify_parity.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Check a LiteRT GLiNER2 port against the original checkpoint on the bundled parity corpus.
3
+
4
+ pip install gliner2==2.0.0 torch numpy ai-edge-litert
5
+ python runtime/verify_parity.py --bundle . # from the repository folder
6
+
7
+ Runs every corpus request twice, through gliner2 with the original weights (downloaded from Fastino at the pinned
8
+ revision) and through this repository's LiteRT runtime, and prints per call type how many structured results are
9
+ identical, the lowest span F1 and the largest confidence difference. The corpus (runtime/parity_corpus.json) is short
10
+ synthetic texts with schema templates for every call type gliner2 2.0.0 offers; it has no gold labels.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import collections
16
+ import json
17
+ import pathlib
18
+ import sys
19
+
20
+ HERE = pathlib.Path(__file__).resolve().parent
21
+ SOURCES = {"boundary": ("fastino/gliner2.5-base-v1", "1a8bc24e00dc7300b9017c81d63e3dcdabb26596"),
22
+ "span": ("fastino/GLiNER2.5-Decide", "bbe10ff77ebb238777c17d3a8ac9260e30929057")}
23
+
24
+
25
+ def build_schema(t: dict):
26
+ from gliner2 import AttributeGroup, Schema
27
+ s = Schema()
28
+ if "entities" in t:
29
+ s.entities(t["entities"], dtype=t.get("dtype", "list"))
30
+ if "attributes" in t:
31
+ s.entity_attributes({k: AttributeGroup(labels=v["labels"], multi_label=v.get("multi_label", False)) for k, v in t["attributes"].items()})
32
+ if "relations" in t:
33
+ s.relations(t["relations"])
34
+ for c in t.get("classifications", []):
35
+ s.classification(c["task"], c["labels"], multi_label=c.get("multi_label", False))
36
+ for st in t.get("structures", []):
37
+ builder = s.structure(st["name"], mode=st.get("mode"), anchor=st.get("anchor"))
38
+ for f in st["fields"]:
39
+ builder.field(f["name"], dtype=f.get("dtype", "list"), choices=f.get("choices"))
40
+ return s
41
+
42
+
43
+ def without_confidence(value):
44
+ if isinstance(value, dict):
45
+ return {k: without_confidence(v) for k, v in value.items() if k != "confidence"}
46
+ if isinstance(value, list):
47
+ return [without_confidence(v) for v in value]
48
+ return value
49
+
50
+
51
+ def confidence_errors(reference, actual):
52
+ if isinstance(reference, dict) and isinstance(actual, dict):
53
+ out = []
54
+ for key, value in reference.items():
55
+ if key == "confidence" and key in actual:
56
+ out.append(abs(float(value) - float(actual[key])))
57
+ elif key in actual:
58
+ out.extend(confidence_errors(value, actual[key]))
59
+ return out
60
+ if isinstance(reference, (list, tuple)) and isinstance(actual, (list, tuple)):
61
+ return [e for a, b in zip(reference, actual) for e in confidence_errors(a, b)]
62
+ return []
63
+
64
+
65
+ def answers(value, path: str = "") -> list[str]:
66
+ out = []
67
+ if isinstance(value, dict):
68
+ if {"text", "start", "end"} <= set(value):
69
+ return [f"{path}|{value['text']}|{value['start']}|{value['end']}"]
70
+ if "head" in value and "tail" in value:
71
+ return [f"{path}|H:{'/'.join(answers(value['head']))}|T:{'/'.join(answers(value['tail']))}"]
72
+ if "label" in value:
73
+ return [f"{path}|{value['label']}"]
74
+ for key, item in value.items():
75
+ if key != "confidence":
76
+ out += answers(item, f"{path}.{key}" if path else key)
77
+ elif isinstance(value, (list, tuple)):
78
+ if len(value) == 2 and isinstance(value[0], str) and isinstance(value[1], float):
79
+ return [f"{path}|{value[0]}"]
80
+ for item in value:
81
+ out += answers(item, path)
82
+ elif value is not None:
83
+ out.append(f"{path}|{value}")
84
+ return out
85
+
86
+
87
+ def f1(reference: list[str], actual: list[str]) -> float:
88
+ a, b = collections.Counter(reference), collections.Counter(actual)
89
+ if not a and not b:
90
+ return 1.0
91
+ return 2 * sum((a & b).values()) / (sum(a.values()) + sum(b.values()))
92
+
93
+
94
+ def main() -> int:
95
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
96
+ parser.add_argument("--bundle", default=str(HERE.parent))
97
+ parser.add_argument("--corpus", default=str(HERE / "parity_corpus.json"))
98
+ args = parser.parse_args()
99
+ sys.path.insert(0, str(HERE))
100
+ import torch
101
+ from gliner2 import AutoExtractor
102
+ from huggingface_hub import snapshot_download
103
+ config = json.loads((pathlib.Path(args.bundle) / "source" / "config.json").read_text())
104
+ architecture = config.get("architecture", "span")
105
+ if architecture == "boundary":
106
+ from litert_boundary_runtime import LiteRTBoundaryExtractor as Runtime
107
+ else:
108
+ from litert_span_runtime import LiteRTSpanExtractor as Runtime
109
+ repo, revision = SOURCES[architecture]
110
+ native = AutoExtractor.from_pretrained(snapshot_download(repo, revision=revision), map_location="cpu").eval()
111
+ torch.set_num_threads(4)
112
+ runtime = Runtime(args.bundle)
113
+ corpus = json.loads(pathlib.Path(args.corpus).read_text())
114
+ by_kind = {}
115
+ for group in corpus["groups"].values():
116
+ for text in group["texts"]:
117
+ for kind in group["templates"]:
118
+ schema = build_schema(corpus["templates"][kind])
119
+ expected = native.extract(text, schema, include_confidence=True, include_spans=True)
120
+ k = by_kind.setdefault(kind, {"calls": 0, "identical": 0, "refused": 0, "minSpanF1": 1.0, "maxConfidenceDifference": 0.0})
121
+ k["calls"] += 1
122
+ try:
123
+ actual = runtime.extract(text, schema, include_confidence=True, include_spans=True)
124
+ except ValueError: # larger than the graphs' fixed sizes: refused, never truncated
125
+ k["refused"] += 1
126
+ continue
127
+ k["identical"] += without_confidence(expected) == without_confidence(actual)
128
+ k["minSpanF1"] = round(min(k["minSpanF1"], f1(answers(expected), answers(actual))), 4)
129
+ k["maxConfidenceDifference"] = round(max([k["maxConfidenceDifference"], *confidence_errors(expected, actual)]), 6)
130
+ for kind, k in by_kind.items():
131
+ print(f"{kind:26s} {json.dumps(k)}")
132
+ total = sum(k["calls"] - k["refused"] for k in by_kind.values())
133
+ same = sum(k["identical"] for k in by_kind.values())
134
+ print(f"identical structured results: {same} / {total}")
135
+ return 0 if same == total else 1
136
+
137
+
138
+ if __name__ == "__main__":
139
+ sys.exit(main())
validation-full.json ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "about": "Every call type against gliner2 2.0.0 with the original weights (runtime/verify_parity.py): LiteRT 2.2.0 on the CPU, wfp16 encoder.",
3
+ "source": "fastino/gliner2.5-base-v1@1a8bc24e00dc7300b9017c81d63e3dcdabb26596",
4
+ "corpus": {
5
+ "file": "runtime/parity_corpus.json",
6
+ "requests": 296,
7
+ "identical": 296,
8
+ "byCallType": {
9
+ "entities": {
10
+ "calls": 40,
11
+ "refused": 0,
12
+ "matched": 40,
13
+ "items": 148,
14
+ "minSpanF1": 1.0,
15
+ "maxConfidenceDifference": 0.000228
16
+ },
17
+ "entities_single": {
18
+ "calls": 8,
19
+ "refused": 0,
20
+ "matched": 8,
21
+ "items": 16,
22
+ "minSpanF1": 1.0,
23
+ "maxConfidenceDifference": 5e-06
24
+ },
25
+ "relations_work": {
26
+ "calls": 24,
27
+ "refused": 0,
28
+ "matched": 24,
29
+ "items": 62,
30
+ "minSpanF1": 1.0,
31
+ "maxConfidenceDifference": 0.000883
32
+ },
33
+ "record_natural": {
34
+ "calls": 16,
35
+ "refused": 0,
36
+ "matched": 16,
37
+ "items": 28,
38
+ "minSpanF1": 1.0,
39
+ "maxConfidenceDifference": 0.00013
40
+ },
41
+ "record_latent": {
42
+ "calls": 24,
43
+ "refused": 0,
44
+ "matched": 24,
45
+ "items": 429,
46
+ "minSpanF1": 1.0,
47
+ "maxConfidenceDifference": 0.000641
48
+ },
49
+ "attributes": {
50
+ "calls": 16,
51
+ "refused": 0,
52
+ "matched": 16,
53
+ "items": 31,
54
+ "minSpanF1": 1.0,
55
+ "maxConfidenceDifference": 0.000298
56
+ },
57
+ "classification_topic": {
58
+ "calls": 40,
59
+ "refused": 0,
60
+ "matched": 40,
61
+ "items": 40,
62
+ "minSpanF1": 1.0,
63
+ "maxConfidenceDifference": 0.001305
64
+ },
65
+ "mixed": {
66
+ "calls": 16,
67
+ "refused": 0,
68
+ "matched": 16,
69
+ "items": 68,
70
+ "minSpanF1": 1.0,
71
+ "maxConfidenceDifference": 0.001358
72
+ },
73
+ "relations_family": {
74
+ "calls": 8,
75
+ "refused": 0,
76
+ "matched": 8,
77
+ "items": 15,
78
+ "minSpanF1": 1.0,
79
+ "maxConfidenceDifference": 0.00033
80
+ },
81
+ "classification_described": {
82
+ "calls": 24,
83
+ "refused": 0,
84
+ "matched": 24,
85
+ "items": 24,
86
+ "minSpanF1": 1.0,
87
+ "maxConfidenceDifference": 0.001187
88
+ },
89
+ "record_anchorless": {
90
+ "calls": 8,
91
+ "refused": 0,
92
+ "matched": 8,
93
+ "items": 2,
94
+ "minSpanF1": 1.0,
95
+ "maxConfidenceDifference": 0.000258
96
+ },
97
+ "structure_event": {
98
+ "calls": 24,
99
+ "refused": 0,
100
+ "matched": 24,
101
+ "items": 45,
102
+ "minSpanF1": 1.0,
103
+ "maxConfidenceDifference": 0.000897
104
+ },
105
+ "classification_multi": {
106
+ "calls": 16,
107
+ "refused": 0,
108
+ "matched": 16,
109
+ "items": 22,
110
+ "minSpanF1": 1.0,
111
+ "maxConfidenceDifference": 0.000356
112
+ },
113
+ "entities_described": {
114
+ "calls": 16,
115
+ "refused": 0,
116
+ "matched": 16,
117
+ "items": 40,
118
+ "minSpanF1": 1.0,
119
+ "maxConfidenceDifference": 0.000411
120
+ },
121
+ "structure_order": {
122
+ "calls": 8,
123
+ "refused": 0,
124
+ "matched": 8,
125
+ "items": 22,
126
+ "minSpanF1": 1.0,
127
+ "maxConfidenceDifference": 0.001252
128
+ },
129
+ "relations_event": {
130
+ "calls": 8,
131
+ "refused": 0,
132
+ "matched": 8,
133
+ "items": 15,
134
+ "minSpanF1": 1.0,
135
+ "maxConfidenceDifference": 0.0002
136
+ }
137
+ }
138
+ },
139
+ "fixtures": {
140
+ "about": "FluidInference's eleven verify-full-extraction fixtures",
141
+ "identical": 11,
142
+ "total": 11,
143
+ "rows": [
144
+ {
145
+ "case": "entities",
146
+ "structureMatch": true,
147
+ "maxConfidenceDifference": 1e-06
148
+ },
149
+ {
150
+ "case": "entities_multi",
151
+ "structureMatch": true,
152
+ "maxConfidenceDifference": 3e-06
153
+ },
154
+ {
155
+ "case": "relations",
156
+ "structureMatch": true,
157
+ "maxConfidenceDifference": 2.2e-05
158
+ },
159
+ {
160
+ "case": "relations_two",
161
+ "structureMatch": true,
162
+ "maxConfidenceDifference": 6e-06
163
+ },
164
+ {
165
+ "case": "record_natural",
166
+ "structureMatch": true,
167
+ "maxConfidenceDifference": 0.000134
168
+ },
169
+ {
170
+ "case": "record_latent",
171
+ "structureMatch": true,
172
+ "maxConfidenceDifference": 0.000352
173
+ },
174
+ {
175
+ "case": "record_anchorless",
176
+ "structureMatch": true,
177
+ "maxConfidenceDifference": null
178
+ },
179
+ {
180
+ "case": "attributes",
181
+ "structureMatch": true,
182
+ "maxConfidenceDifference": 2.8e-05
183
+ },
184
+ {
185
+ "case": "mixed_classification",
186
+ "structureMatch": true,
187
+ "maxConfidenceDifference": 1e-06
188
+ },
189
+ {
190
+ "case": "classification_only",
191
+ "structureMatch": true,
192
+ "maxConfidenceDifference": 6.4e-05
193
+ },
194
+ {
195
+ "case": "enum_choice",
196
+ "structureMatch": true,
197
+ "maxConfidenceDifference": 0.0
198
+ }
199
+ ]
200
+ }
201
+ }