indico_toolkit 6.14.0__py3-none-any.whl
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.
- indico_toolkit/__init__.py +24 -0
- indico_toolkit/association/__init__.py +15 -0
- indico_toolkit/association/association.py +65 -0
- indico_toolkit/association/extracted_tokens.py +106 -0
- indico_toolkit/association/line_items.py +189 -0
- indico_toolkit/association/positioning.py +301 -0
- indico_toolkit/association/splitting.py +41 -0
- indico_toolkit/auto_populate/__init__.py +3 -0
- indico_toolkit/auto_populate/populator.py +362 -0
- indico_toolkit/auto_populate/types.py +66 -0
- indico_toolkit/auto_review/__init__.py +6 -0
- indico_toolkit/auto_review/auto_review_functions.py +123 -0
- indico_toolkit/auto_review/auto_reviewer.py +62 -0
- indico_toolkit/client.py +35 -0
- indico_toolkit/errors.py +32 -0
- indico_toolkit/etloutput/__init__.py +210 -0
- indico_toolkit/etloutput/cell.py +47 -0
- indico_toolkit/etloutput/errors.py +16 -0
- indico_toolkit/etloutput/etloutput.py +110 -0
- indico_toolkit/etloutput/range.py +30 -0
- indico_toolkit/etloutput/table.py +44 -0
- indico_toolkit/etloutput/token.py +25 -0
- indico_toolkit/indico_wrapper/__init__.py +15 -0
- indico_toolkit/indico_wrapper/dataset.py +114 -0
- indico_toolkit/indico_wrapper/doc_extraction.py +79 -0
- indico_toolkit/indico_wrapper/download.py +193 -0
- indico_toolkit/indico_wrapper/indico_wrapper.py +119 -0
- indico_toolkit/indico_wrapper/modelop.py +200 -0
- indico_toolkit/indico_wrapper/reviewer.py +111 -0
- indico_toolkit/indico_wrapper/workflow.py +231 -0
- indico_toolkit/metrics/__init__.py +11 -0
- indico_toolkit/metrics/compare_ground_truth.py +123 -0
- indico_toolkit/metrics/compare_models.py +180 -0
- indico_toolkit/metrics/metrics.py +373 -0
- indico_toolkit/metrics/plotting.py +102 -0
- indico_toolkit/ocr/__init__.py +9 -0
- indico_toolkit/ocr/customocr_object.py +42 -0
- indico_toolkit/ocr/ondoc_object.py +93 -0
- indico_toolkit/ocr/standard_object.py +54 -0
- indico_toolkit/pipelines/__init__.py +3 -0
- indico_toolkit/pipelines/file_processing.py +157 -0
- indico_toolkit/polling/__init__.py +8 -0
- indico_toolkit/polling/autoreview.py +204 -0
- indico_toolkit/polling/downstream.py +145 -0
- indico_toolkit/polling/queries.py +68 -0
- indico_toolkit/py.typed +0 -0
- indico_toolkit/results/__init__.py +104 -0
- indico_toolkit/results/document.py +78 -0
- indico_toolkit/results/errors.py +4 -0
- indico_toolkit/results/model.py +60 -0
- indico_toolkit/results/normalization.py +159 -0
- indico_toolkit/results/predictionlist.py +395 -0
- indico_toolkit/results/predictions/__init__.py +88 -0
- indico_toolkit/results/predictions/box.py +58 -0
- indico_toolkit/results/predictions/citation.py +47 -0
- indico_toolkit/results/predictions/classification.py +50 -0
- indico_toolkit/results/predictions/documentextraction.py +160 -0
- indico_toolkit/results/predictions/extraction.py +35 -0
- indico_toolkit/results/predictions/formextraction.py +121 -0
- indico_toolkit/results/predictions/group.py +29 -0
- indico_toolkit/results/predictions/prediction.py +41 -0
- indico_toolkit/results/predictions/span.py +43 -0
- indico_toolkit/results/predictions/summarization.py +103 -0
- indico_toolkit/results/predictions/unbundling.py +51 -0
- indico_toolkit/results/result.py +152 -0
- indico_toolkit/results/review.py +32 -0
- indico_toolkit/results/utilities.py +73 -0
- indico_toolkit/retry.py +94 -0
- indico_toolkit/snapshots/__init__.py +3 -0
- indico_toolkit/snapshots/snapshot.py +340 -0
- indico_toolkit/structure/__init__.py +3 -0
- indico_toolkit/structure/create_structure.py +232 -0
- indico_toolkit/structure/queries.py +119 -0
- indico_toolkit/structure/utils.py +16 -0
- indico_toolkit/types/__init__.py +12 -0
- indico_toolkit/types/classification.py +92 -0
- indico_toolkit/types/extractions.py +227 -0
- indico_toolkit/types/predictions.py +28 -0
- indico_toolkit/types/workflow_object.py +133 -0
- indico_toolkit-6.14.0.dist-info/LICENSE +21 -0
- indico_toolkit-6.14.0.dist-info/METADATA +144 -0
- indico_toolkit-6.14.0.dist-info/RECORD +83 -0
- indico_toolkit-6.14.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from ..model import ModelGroupType
|
|
4
|
+
from .box import NULL_BOX, Box
|
|
5
|
+
from .citation import NULL_CITATION, Citation
|
|
6
|
+
from .classification import Classification
|
|
7
|
+
from .documentextraction import DocumentExtraction
|
|
8
|
+
from .extraction import Extraction
|
|
9
|
+
from .formextraction import FormExtraction, FormExtractionType
|
|
10
|
+
from .group import Group
|
|
11
|
+
from .prediction import Prediction
|
|
12
|
+
from .span import NULL_SPAN, Span
|
|
13
|
+
from .summarization import Summarization
|
|
14
|
+
from .unbundling import Unbundling
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..document import Document
|
|
18
|
+
from ..errors import ResultError
|
|
19
|
+
from ..model import ModelGroup
|
|
20
|
+
from ..review import Review
|
|
21
|
+
|
|
22
|
+
__all__ = (
|
|
23
|
+
"Box",
|
|
24
|
+
"Citation",
|
|
25
|
+
"Classification",
|
|
26
|
+
"DocumentExtraction",
|
|
27
|
+
"Extraction",
|
|
28
|
+
"FormExtraction",
|
|
29
|
+
"FormExtractionType",
|
|
30
|
+
"Group",
|
|
31
|
+
"NULL_BOX",
|
|
32
|
+
"NULL_CITATION",
|
|
33
|
+
"NULL_SPAN",
|
|
34
|
+
"Prediction",
|
|
35
|
+
"Span",
|
|
36
|
+
"Summarization",
|
|
37
|
+
"Unbundling",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
CLASSIFICATION = ModelGroupType.CLASSIFICATION
|
|
41
|
+
DOCUMENT_EXTRACTION = ModelGroupType.DOCUMENT_EXTRACTION
|
|
42
|
+
FORM_EXTRACTION = ModelGroupType.FORM_EXTRACTION
|
|
43
|
+
GENAI_CLASSIFICATION = ModelGroupType.GENAI_CLASSIFICATION
|
|
44
|
+
GENAI_EXTRACTION = ModelGroupType.GENAI_EXTRACTION
|
|
45
|
+
GENAI_SUMMARIZATION = ModelGroupType.GENAI_SUMMARIZATION
|
|
46
|
+
UNBUNDLING = ModelGroupType.UNBUNDLING
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def from_v1_dict(
|
|
50
|
+
document: "Document",
|
|
51
|
+
model: "ModelGroup",
|
|
52
|
+
review: "Review | None",
|
|
53
|
+
prediction: object,
|
|
54
|
+
) -> "Prediction":
|
|
55
|
+
"""
|
|
56
|
+
Create a `Prediction` subclass from a v1 prediction dictionary.
|
|
57
|
+
"""
|
|
58
|
+
if model.type == CLASSIFICATION:
|
|
59
|
+
return Classification.from_v1_dict(document, model, review, prediction)
|
|
60
|
+
elif model.type == DOCUMENT_EXTRACTION:
|
|
61
|
+
return DocumentExtraction.from_v1_dict(document, model, review, prediction)
|
|
62
|
+
elif model.type == FORM_EXTRACTION:
|
|
63
|
+
return FormExtraction.from_v1_dict(document, model, review, prediction)
|
|
64
|
+
else:
|
|
65
|
+
raise ResultError(f"unsupported v1 model type `{model.type!r}`")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def from_v3_dict(
|
|
69
|
+
document: "Document",
|
|
70
|
+
model: "ModelGroup",
|
|
71
|
+
review: "Review | None",
|
|
72
|
+
prediction: object,
|
|
73
|
+
) -> "Prediction":
|
|
74
|
+
"""
|
|
75
|
+
Create a `Prediction` subclass from a v3 prediction dictionary.
|
|
76
|
+
"""
|
|
77
|
+
if model.type in (CLASSIFICATION, GENAI_CLASSIFICATION):
|
|
78
|
+
return Classification.from_v3_dict(document, model, review, prediction)
|
|
79
|
+
elif model.type in (DOCUMENT_EXTRACTION, GENAI_EXTRACTION):
|
|
80
|
+
return DocumentExtraction.from_v3_dict(document, model, review, prediction)
|
|
81
|
+
elif model.type == FORM_EXTRACTION:
|
|
82
|
+
return FormExtraction.from_v3_dict(document, model, review, prediction)
|
|
83
|
+
elif model.type == GENAI_SUMMARIZATION:
|
|
84
|
+
return Summarization.from_v3_dict(document, model, review, prediction)
|
|
85
|
+
elif model.type == UNBUNDLING:
|
|
86
|
+
return Unbundling.from_v3_dict(document, model, review, prediction)
|
|
87
|
+
else:
|
|
88
|
+
raise ResultError(f"unsupported v3 model type `{model.type!r}`")
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from ..utilities import get
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from typing import Final
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class Box:
|
|
12
|
+
page: int
|
|
13
|
+
top: int
|
|
14
|
+
left: int
|
|
15
|
+
right: int
|
|
16
|
+
bottom: int
|
|
17
|
+
|
|
18
|
+
def __lt__(self, other: "Box") -> bool:
|
|
19
|
+
"""
|
|
20
|
+
Bounding boxes are sorted with vertical hysteresis. Those on the same line are
|
|
21
|
+
sorted left-to-right, even when later tokens are higher than earlier ones,
|
|
22
|
+
as long as they overlap vertically.
|
|
23
|
+
|
|
24
|
+
┌──────────────────┐ ┌───────────────────┐
|
|
25
|
+
│ 1 │ │ 2 │
|
|
26
|
+
└──────────────────┘ │ │
|
|
27
|
+
└───────────────────┘
|
|
28
|
+
┌────────────────┐
|
|
29
|
+
┌─────────────┐ │ 4 │ ┌─────┐
|
|
30
|
+
│ 3 │ └────────────────┘ │ 5 │
|
|
31
|
+
└─────────────┘ └─────┘
|
|
32
|
+
"""
|
|
33
|
+
return (
|
|
34
|
+
self.page < other.page
|
|
35
|
+
or (self.page == other.page and self.bottom < other.top)
|
|
36
|
+
or (
|
|
37
|
+
self.page == other.page
|
|
38
|
+
and self.top < other.bottom
|
|
39
|
+
and self.left < other.left
|
|
40
|
+
)
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def from_dict(box: object) -> "Box":
|
|
45
|
+
return Box(
|
|
46
|
+
page=get(box, int, "page_num"),
|
|
47
|
+
top=get(box, int, "top"),
|
|
48
|
+
left=get(box, int, "left"),
|
|
49
|
+
right=get(box, int, "right"),
|
|
50
|
+
bottom=get(box, int, "bottom"),
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# It's more ergonomic to represent the lack of a bounding box with a special null box
|
|
55
|
+
# object rather than using `None` or raising an error. This lets you e.g. sort by the
|
|
56
|
+
# `box` attribute without having to constantly check for `None`, while still allowing
|
|
57
|
+
# you do a "None check" with `extraction.box == NULL_BOX`.
|
|
58
|
+
NULL_BOX: "Final" = Box(page=0, top=0, left=0, right=0, bottom=0)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from ..utilities import get
|
|
5
|
+
from .span import NULL_SPAN, Span
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Any, Final
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(order=True, frozen=True)
|
|
12
|
+
class Citation:
|
|
13
|
+
start: int
|
|
14
|
+
end: int
|
|
15
|
+
span: Span
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def slice(self) -> slice:
|
|
19
|
+
return slice(self.start, self.end)
|
|
20
|
+
|
|
21
|
+
def __bool__(self) -> bool:
|
|
22
|
+
return self != NULL_CITATION
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def from_dict(span: object) -> "Citation":
|
|
26
|
+
return Citation(
|
|
27
|
+
start=get(span, int, "response", "start"),
|
|
28
|
+
end=get(span, int, "response", "end"),
|
|
29
|
+
span=Span.from_dict(get(span, dict, "document")),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def to_dict(self) -> "dict[str, Any]":
|
|
33
|
+
return {
|
|
34
|
+
"document": self.span.to_dict(),
|
|
35
|
+
"response": {
|
|
36
|
+
"start": self.start,
|
|
37
|
+
"end": self.end,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# It's more ergonomic to represent the lack of citations with a special null citation
|
|
43
|
+
# object rather than using `None` or raising an error. This lets you e.g. sort by the
|
|
44
|
+
# `citation` attribute without having to constantly check for `None`, while still
|
|
45
|
+
# allowing you do a "None check" with `summarization.citation == NULL_CITATION` or
|
|
46
|
+
# `bool(summarization.citation)`.
|
|
47
|
+
NULL_CITATION: "Final" = Citation(span=NULL_SPAN, start=0, end=0)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from ..review import Review
|
|
5
|
+
from ..utilities import get, omit
|
|
6
|
+
from .prediction import Prediction
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ..document import Document
|
|
12
|
+
from ..model import ModelGroup
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Classification(Prediction):
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _from_dict(
|
|
19
|
+
document: "Document",
|
|
20
|
+
model: "ModelGroup",
|
|
21
|
+
review: "Review | None",
|
|
22
|
+
prediction: object,
|
|
23
|
+
) -> "Classification":
|
|
24
|
+
"""
|
|
25
|
+
Create a `Classification` from a prediction dictionary.
|
|
26
|
+
"""
|
|
27
|
+
return Classification(
|
|
28
|
+
document=document,
|
|
29
|
+
model=model,
|
|
30
|
+
review=review,
|
|
31
|
+
label=get(prediction, str, "label"),
|
|
32
|
+
confidences=get(prediction, dict, "confidence"),
|
|
33
|
+
extras=omit(prediction, "label", "confidence"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from_v1_dict = _from_dict
|
|
37
|
+
from_v3_dict = _from_dict
|
|
38
|
+
|
|
39
|
+
def _to_dict(self) -> "dict[str, Any]":
|
|
40
|
+
"""
|
|
41
|
+
Create a prediction dictionary for auto review changes.
|
|
42
|
+
"""
|
|
43
|
+
return {
|
|
44
|
+
**self.extras,
|
|
45
|
+
"label": self.label,
|
|
46
|
+
"confidence": self.confidences,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
to_v1_dict = _to_dict
|
|
50
|
+
to_v3_dict = _to_dict
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from ..review import Review
|
|
5
|
+
from ..utilities import get, has, omit
|
|
6
|
+
from .extraction import Extraction
|
|
7
|
+
from .group import Group
|
|
8
|
+
from .span import NULL_SPAN, Span
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..document import Document
|
|
14
|
+
from ..model import ModelGroup
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class DocumentExtraction(Extraction):
|
|
19
|
+
groups: "set[Group]"
|
|
20
|
+
spans: "list[Span]"
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def span(self) -> Span:
|
|
24
|
+
"""
|
|
25
|
+
Return the first `Span` the document extraction covers else `NULL_SPAN`.
|
|
26
|
+
|
|
27
|
+
Post-review, document extractions have no spans.
|
|
28
|
+
"""
|
|
29
|
+
return self.spans[0] if self.spans else NULL_SPAN
|
|
30
|
+
|
|
31
|
+
@span.setter
|
|
32
|
+
def span(self, span: Span) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Overwrite all `spans` with the one provided.
|
|
35
|
+
|
|
36
|
+
This is implemented under the assumption that if you're setting the single span,
|
|
37
|
+
you want it to be the only one. And if you're working in a context that's
|
|
38
|
+
multiple-span sensetive, you'll set `extraction.spans` instead.
|
|
39
|
+
"""
|
|
40
|
+
self.spans = [span]
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def from_v1_dict(
|
|
44
|
+
document: "Document",
|
|
45
|
+
model: "ModelGroup",
|
|
46
|
+
review: "Review | None",
|
|
47
|
+
prediction: object,
|
|
48
|
+
) -> "DocumentExtraction":
|
|
49
|
+
"""
|
|
50
|
+
Create a `DocumentExtraction` from a v1 prediction dictionary.
|
|
51
|
+
"""
|
|
52
|
+
return DocumentExtraction(
|
|
53
|
+
document=document,
|
|
54
|
+
model=model,
|
|
55
|
+
review=review,
|
|
56
|
+
label=get(prediction, str, "label"),
|
|
57
|
+
confidences=get(prediction, dict, "confidence"),
|
|
58
|
+
text=get(prediction, str, "normalized", "formatted"),
|
|
59
|
+
accepted=(
|
|
60
|
+
has(prediction, bool, "accepted") and get(prediction, bool, "accepted")
|
|
61
|
+
),
|
|
62
|
+
rejected=(
|
|
63
|
+
has(prediction, bool, "rejected") and get(prediction, bool, "rejected")
|
|
64
|
+
),
|
|
65
|
+
groups=set(map(Group.from_dict, get(prediction, list, "groupings"))),
|
|
66
|
+
spans=[Span.from_dict(prediction)] if has(prediction, int, "start") else [],
|
|
67
|
+
extras=omit(
|
|
68
|
+
prediction,
|
|
69
|
+
"label",
|
|
70
|
+
"confidence",
|
|
71
|
+
"accepted",
|
|
72
|
+
"rejected",
|
|
73
|
+
"groupings",
|
|
74
|
+
"page_num",
|
|
75
|
+
"start",
|
|
76
|
+
"end",
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@staticmethod
|
|
81
|
+
def from_v3_dict(
|
|
82
|
+
document: "Document",
|
|
83
|
+
model: "ModelGroup",
|
|
84
|
+
review: "Review | None",
|
|
85
|
+
prediction: object,
|
|
86
|
+
) -> "DocumentExtraction":
|
|
87
|
+
"""
|
|
88
|
+
Create a `DocumentExtraction` from a v3 prediction dictionary.
|
|
89
|
+
"""
|
|
90
|
+
return DocumentExtraction(
|
|
91
|
+
document=document,
|
|
92
|
+
model=model,
|
|
93
|
+
review=review,
|
|
94
|
+
label=get(prediction, str, "label"),
|
|
95
|
+
confidences=get(prediction, dict, "confidence"),
|
|
96
|
+
text=get(prediction, str, "normalized", "formatted"),
|
|
97
|
+
accepted=(
|
|
98
|
+
has(prediction, bool, "accepted") and get(prediction, bool, "accepted")
|
|
99
|
+
),
|
|
100
|
+
rejected=(
|
|
101
|
+
has(prediction, bool, "rejected") and get(prediction, bool, "rejected")
|
|
102
|
+
),
|
|
103
|
+
groups=set(map(Group.from_dict, get(prediction, list, "groupings"))),
|
|
104
|
+
spans=sorted(map(Span.from_dict, get(prediction, list, "spans"))),
|
|
105
|
+
extras=omit(
|
|
106
|
+
prediction,
|
|
107
|
+
"label",
|
|
108
|
+
"confidence",
|
|
109
|
+
"accepted",
|
|
110
|
+
"rejected",
|
|
111
|
+
"groupings",
|
|
112
|
+
"spans",
|
|
113
|
+
),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def to_v1_dict(self) -> "dict[str, Any]":
|
|
117
|
+
"""
|
|
118
|
+
Create a prediction dictionary for v1 auto review changes.
|
|
119
|
+
"""
|
|
120
|
+
prediction = {
|
|
121
|
+
**self.extras,
|
|
122
|
+
"label": self.label,
|
|
123
|
+
"confidence": self.confidences,
|
|
124
|
+
"groupings": [group.to_dict() for group in self.groups],
|
|
125
|
+
"page_num": self.span.page,
|
|
126
|
+
"start": self.span.start,
|
|
127
|
+
"end": self.span.end,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
prediction["normalized"]["formatted"] = self.text
|
|
131
|
+
prediction["text"] = self.text # 6.10 sometimes reverts to raw text in review.
|
|
132
|
+
|
|
133
|
+
if self.accepted:
|
|
134
|
+
prediction["accepted"] = True
|
|
135
|
+
elif self.rejected:
|
|
136
|
+
prediction["rejected"] = True
|
|
137
|
+
|
|
138
|
+
return prediction
|
|
139
|
+
|
|
140
|
+
def to_v3_dict(self) -> "dict[str, Any]":
|
|
141
|
+
"""
|
|
142
|
+
Create a prediction dictionary for v3 auto review changes.
|
|
143
|
+
"""
|
|
144
|
+
prediction = {
|
|
145
|
+
**self.extras,
|
|
146
|
+
"label": self.label,
|
|
147
|
+
"confidence": self.confidences,
|
|
148
|
+
"groupings": [group.to_dict() for group in self.groups],
|
|
149
|
+
"spans": [span.to_dict() for span in self.spans],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
prediction["normalized"]["formatted"] = self.text
|
|
153
|
+
prediction["text"] = self.text # 6.10 sometimes reverts to raw text in review.
|
|
154
|
+
|
|
155
|
+
if self.accepted:
|
|
156
|
+
prediction["accepted"] = True
|
|
157
|
+
elif self.rejected:
|
|
158
|
+
prediction["rejected"] = True
|
|
159
|
+
|
|
160
|
+
return prediction
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
from .prediction import Prediction
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Extraction(Prediction):
|
|
8
|
+
text: str
|
|
9
|
+
accepted: bool
|
|
10
|
+
rejected: bool
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def page(self) -> int:
|
|
14
|
+
"""
|
|
15
|
+
Convenience property to get an extraction's page without knowing its subclass.
|
|
16
|
+
Allows you to do `predictions.extractions.groupby(attrgetter("page"))` et al.
|
|
17
|
+
"""
|
|
18
|
+
if hasattr(self, "box"):
|
|
19
|
+
return self.box.page # type: ignore
|
|
20
|
+
else:
|
|
21
|
+
return self.span.page # type: ignore
|
|
22
|
+
|
|
23
|
+
def accept(self) -> None:
|
|
24
|
+
self.accepted = True
|
|
25
|
+
self.rejected = False
|
|
26
|
+
|
|
27
|
+
def unaccept(self) -> None:
|
|
28
|
+
self.accepted = False
|
|
29
|
+
|
|
30
|
+
def reject(self) -> None:
|
|
31
|
+
self.accepted = False
|
|
32
|
+
self.rejected = True
|
|
33
|
+
|
|
34
|
+
def unreject(self) -> None:
|
|
35
|
+
self.rejected = False
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from ..review import Review
|
|
6
|
+
from ..utilities import get, has, omit
|
|
7
|
+
from .box import Box
|
|
8
|
+
from .extraction import Extraction
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..document import Document
|
|
14
|
+
from ..model import ModelGroup
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FormExtractionType(Enum):
|
|
18
|
+
CHECKBOX = "checkbox"
|
|
19
|
+
SIGNATURE = "signature"
|
|
20
|
+
TEXT = "text"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class FormExtraction(Extraction):
|
|
25
|
+
type: FormExtractionType
|
|
26
|
+
box: Box
|
|
27
|
+
checked: bool
|
|
28
|
+
signed: bool
|
|
29
|
+
|
|
30
|
+
@staticmethod
|
|
31
|
+
def _from_dict(
|
|
32
|
+
document: "Document",
|
|
33
|
+
model: "ModelGroup",
|
|
34
|
+
review: "Review | None",
|
|
35
|
+
prediction: object,
|
|
36
|
+
) -> "FormExtraction":
|
|
37
|
+
"""
|
|
38
|
+
Create a `FormExtraction` from a prediction dictionary.
|
|
39
|
+
"""
|
|
40
|
+
return FormExtraction(
|
|
41
|
+
document=document,
|
|
42
|
+
model=model,
|
|
43
|
+
review=review,
|
|
44
|
+
label=get(prediction, str, "label"),
|
|
45
|
+
confidences=get(prediction, dict, "confidence"),
|
|
46
|
+
text=get(prediction, str, "normalized", "formatted"),
|
|
47
|
+
accepted=(
|
|
48
|
+
has(prediction, bool, "accepted") and get(prediction, bool, "accepted")
|
|
49
|
+
),
|
|
50
|
+
rejected=(
|
|
51
|
+
has(prediction, bool, "rejected") and get(prediction, bool, "rejected")
|
|
52
|
+
),
|
|
53
|
+
type=FormExtractionType(get(prediction, str, "type")),
|
|
54
|
+
box=Box.from_dict(prediction),
|
|
55
|
+
checked=(
|
|
56
|
+
has(prediction, bool, "normalized", "structured", "checked")
|
|
57
|
+
and get(prediction, bool, "normalized", "structured", "checked")
|
|
58
|
+
),
|
|
59
|
+
signed=(
|
|
60
|
+
has(prediction, bool, "normalized", "structured", "signed")
|
|
61
|
+
and get(prediction, bool, "normalized", "structured", "signed")
|
|
62
|
+
),
|
|
63
|
+
extras=omit(
|
|
64
|
+
prediction,
|
|
65
|
+
"label",
|
|
66
|
+
"confidence",
|
|
67
|
+
"accepted",
|
|
68
|
+
"rejected",
|
|
69
|
+
"type",
|
|
70
|
+
"page_num",
|
|
71
|
+
"top",
|
|
72
|
+
"left",
|
|
73
|
+
"right",
|
|
74
|
+
"bottom",
|
|
75
|
+
"checked",
|
|
76
|
+
"signed",
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
from_v1_dict = _from_dict
|
|
81
|
+
from_v3_dict = _from_dict
|
|
82
|
+
|
|
83
|
+
def _to_dict(self) -> "dict[str, Any]":
|
|
84
|
+
"""
|
|
85
|
+
Create a prediction dictionary for auto review changes.
|
|
86
|
+
"""
|
|
87
|
+
prediction = {
|
|
88
|
+
**self.extras,
|
|
89
|
+
"label": self.label,
|
|
90
|
+
"confidence": self.confidences,
|
|
91
|
+
"type": self.type.value,
|
|
92
|
+
"page_num": self.box.page,
|
|
93
|
+
"top": self.box.top,
|
|
94
|
+
"left": self.box.left,
|
|
95
|
+
"right": self.box.right,
|
|
96
|
+
"bottom": self.box.bottom,
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if self.type == FormExtractionType.CHECKBOX:
|
|
100
|
+
prediction["normalized"]["structured"]["checked"] = self.checked
|
|
101
|
+
prediction["normalized"]["formatted"] = (
|
|
102
|
+
"Checked" if self.checked else "Unchecked"
|
|
103
|
+
)
|
|
104
|
+
elif self.type == FormExtractionType.SIGNATURE:
|
|
105
|
+
prediction["normalized"]["structured"]["signed"] = self.signed
|
|
106
|
+
prediction["normalized"]["formatted"] = (
|
|
107
|
+
"Signed" if self.signed else "Unsigned"
|
|
108
|
+
)
|
|
109
|
+
elif self.type == FormExtractionType.TEXT:
|
|
110
|
+
prediction["normalized"]["formatted"] = self.text
|
|
111
|
+
prediction["text"] = self.text # 6.10 sometimes reverts to text in review.
|
|
112
|
+
|
|
113
|
+
if self.accepted:
|
|
114
|
+
prediction["accepted"] = True
|
|
115
|
+
elif self.rejected:
|
|
116
|
+
prediction["rejected"] = True
|
|
117
|
+
|
|
118
|
+
return prediction
|
|
119
|
+
|
|
120
|
+
to_v1_dict = _to_dict
|
|
121
|
+
to_v3_dict = _to_dict
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from ..utilities import get
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, order=True)
|
|
11
|
+
class Group:
|
|
12
|
+
id: int
|
|
13
|
+
name: str
|
|
14
|
+
index: int
|
|
15
|
+
|
|
16
|
+
@staticmethod
|
|
17
|
+
def from_dict(group: object) -> "Group":
|
|
18
|
+
return Group(
|
|
19
|
+
id=int(get(group, str, "group_id").split(":")[0]),
|
|
20
|
+
name=get(group, str, "group_name"),
|
|
21
|
+
index=get(group, int, "group_index"),
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def to_dict(self) -> "dict[str, Any]":
|
|
25
|
+
return {
|
|
26
|
+
"group_id": f"{self.id}:{self.name}",
|
|
27
|
+
"group_name": self.name,
|
|
28
|
+
"group_index": self.index,
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from ..review import Review
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..document import Document
|
|
10
|
+
from ..model import ModelGroup
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Prediction:
|
|
15
|
+
document: "Document"
|
|
16
|
+
model: "ModelGroup"
|
|
17
|
+
review: "Review | None"
|
|
18
|
+
|
|
19
|
+
label: str
|
|
20
|
+
confidences: "dict[str, float]"
|
|
21
|
+
extras: "dict[str, Any]"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def confidence(self) -> float:
|
|
25
|
+
return self.confidences[self.label]
|
|
26
|
+
|
|
27
|
+
@confidence.setter
|
|
28
|
+
def confidence(self, value: float) -> None:
|
|
29
|
+
self.confidences[self.label] = value
|
|
30
|
+
|
|
31
|
+
def to_v1_dict(self) -> "dict[str, Any]":
|
|
32
|
+
"""
|
|
33
|
+
Create a prediction dictionary for v1 auto review changes.
|
|
34
|
+
"""
|
|
35
|
+
raise NotImplementedError()
|
|
36
|
+
|
|
37
|
+
def to_v3_dict(self) -> "dict[str, Any]":
|
|
38
|
+
"""
|
|
39
|
+
Create a prediction dictionary for v3 auto review changes.
|
|
40
|
+
"""
|
|
41
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from ..utilities import get
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from typing import Any, Final
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(order=True, frozen=True)
|
|
11
|
+
class Span:
|
|
12
|
+
page: int
|
|
13
|
+
start: int
|
|
14
|
+
end: int
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def slice(self) -> slice:
|
|
18
|
+
return slice(self.start, self.end)
|
|
19
|
+
|
|
20
|
+
def __bool__(self) -> bool:
|
|
21
|
+
return self != NULL_SPAN
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def from_dict(span: object) -> "Span":
|
|
25
|
+
return Span(
|
|
26
|
+
page=get(span, int, "page_num"),
|
|
27
|
+
start=get(span, int, "start"),
|
|
28
|
+
end=get(span, int, "end"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> "dict[str, Any]":
|
|
32
|
+
return {
|
|
33
|
+
"page_num": self.page,
|
|
34
|
+
"start": self.start,
|
|
35
|
+
"end": self.end,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# It's more ergonomic to represent the lack of spans with a special null span object
|
|
40
|
+
# rather than using `None` or raising an error. This lets you e.g. sort by the `span`
|
|
41
|
+
# attribute without having to constantly check for `None`, while still allowing you do
|
|
42
|
+
# a "None check" with `extraction.span == NULL_SPAN` or `bool(extraction.span)`.
|
|
43
|
+
NULL_SPAN: "Final" = Span(page=0, start=0, end=0)
|