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.
Files changed (83) hide show
  1. indico_toolkit/__init__.py +24 -0
  2. indico_toolkit/association/__init__.py +15 -0
  3. indico_toolkit/association/association.py +65 -0
  4. indico_toolkit/association/extracted_tokens.py +106 -0
  5. indico_toolkit/association/line_items.py +189 -0
  6. indico_toolkit/association/positioning.py +301 -0
  7. indico_toolkit/association/splitting.py +41 -0
  8. indico_toolkit/auto_populate/__init__.py +3 -0
  9. indico_toolkit/auto_populate/populator.py +362 -0
  10. indico_toolkit/auto_populate/types.py +66 -0
  11. indico_toolkit/auto_review/__init__.py +6 -0
  12. indico_toolkit/auto_review/auto_review_functions.py +123 -0
  13. indico_toolkit/auto_review/auto_reviewer.py +62 -0
  14. indico_toolkit/client.py +35 -0
  15. indico_toolkit/errors.py +32 -0
  16. indico_toolkit/etloutput/__init__.py +210 -0
  17. indico_toolkit/etloutput/cell.py +47 -0
  18. indico_toolkit/etloutput/errors.py +16 -0
  19. indico_toolkit/etloutput/etloutput.py +110 -0
  20. indico_toolkit/etloutput/range.py +30 -0
  21. indico_toolkit/etloutput/table.py +44 -0
  22. indico_toolkit/etloutput/token.py +25 -0
  23. indico_toolkit/indico_wrapper/__init__.py +15 -0
  24. indico_toolkit/indico_wrapper/dataset.py +114 -0
  25. indico_toolkit/indico_wrapper/doc_extraction.py +79 -0
  26. indico_toolkit/indico_wrapper/download.py +193 -0
  27. indico_toolkit/indico_wrapper/indico_wrapper.py +119 -0
  28. indico_toolkit/indico_wrapper/modelop.py +200 -0
  29. indico_toolkit/indico_wrapper/reviewer.py +111 -0
  30. indico_toolkit/indico_wrapper/workflow.py +231 -0
  31. indico_toolkit/metrics/__init__.py +11 -0
  32. indico_toolkit/metrics/compare_ground_truth.py +123 -0
  33. indico_toolkit/metrics/compare_models.py +180 -0
  34. indico_toolkit/metrics/metrics.py +373 -0
  35. indico_toolkit/metrics/plotting.py +102 -0
  36. indico_toolkit/ocr/__init__.py +9 -0
  37. indico_toolkit/ocr/customocr_object.py +42 -0
  38. indico_toolkit/ocr/ondoc_object.py +93 -0
  39. indico_toolkit/ocr/standard_object.py +54 -0
  40. indico_toolkit/pipelines/__init__.py +3 -0
  41. indico_toolkit/pipelines/file_processing.py +157 -0
  42. indico_toolkit/polling/__init__.py +8 -0
  43. indico_toolkit/polling/autoreview.py +204 -0
  44. indico_toolkit/polling/downstream.py +145 -0
  45. indico_toolkit/polling/queries.py +68 -0
  46. indico_toolkit/py.typed +0 -0
  47. indico_toolkit/results/__init__.py +104 -0
  48. indico_toolkit/results/document.py +78 -0
  49. indico_toolkit/results/errors.py +4 -0
  50. indico_toolkit/results/model.py +60 -0
  51. indico_toolkit/results/normalization.py +159 -0
  52. indico_toolkit/results/predictionlist.py +395 -0
  53. indico_toolkit/results/predictions/__init__.py +88 -0
  54. indico_toolkit/results/predictions/box.py +58 -0
  55. indico_toolkit/results/predictions/citation.py +47 -0
  56. indico_toolkit/results/predictions/classification.py +50 -0
  57. indico_toolkit/results/predictions/documentextraction.py +160 -0
  58. indico_toolkit/results/predictions/extraction.py +35 -0
  59. indico_toolkit/results/predictions/formextraction.py +121 -0
  60. indico_toolkit/results/predictions/group.py +29 -0
  61. indico_toolkit/results/predictions/prediction.py +41 -0
  62. indico_toolkit/results/predictions/span.py +43 -0
  63. indico_toolkit/results/predictions/summarization.py +103 -0
  64. indico_toolkit/results/predictions/unbundling.py +51 -0
  65. indico_toolkit/results/result.py +152 -0
  66. indico_toolkit/results/review.py +32 -0
  67. indico_toolkit/results/utilities.py +73 -0
  68. indico_toolkit/retry.py +94 -0
  69. indico_toolkit/snapshots/__init__.py +3 -0
  70. indico_toolkit/snapshots/snapshot.py +340 -0
  71. indico_toolkit/structure/__init__.py +3 -0
  72. indico_toolkit/structure/create_structure.py +232 -0
  73. indico_toolkit/structure/queries.py +119 -0
  74. indico_toolkit/structure/utils.py +16 -0
  75. indico_toolkit/types/__init__.py +12 -0
  76. indico_toolkit/types/classification.py +92 -0
  77. indico_toolkit/types/extractions.py +227 -0
  78. indico_toolkit/types/predictions.py +28 -0
  79. indico_toolkit/types/workflow_object.py +133 -0
  80. indico_toolkit-6.14.0.dist-info/LICENSE +21 -0
  81. indico_toolkit-6.14.0.dist-info/METADATA +144 -0
  82. indico_toolkit-6.14.0.dist-info/RECORD +83 -0
  83. indico_toolkit-6.14.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,301 @@
1
+ from math import sqrt
2
+ from typing import List
3
+
4
+ from ..errors import ToolkitInputError
5
+
6
+
7
+ class Positioning:
8
+ """
9
+ Class to help identify relative positions in a document using bounding box data.
10
+
11
+ Positions are expected to contain, at a minimum, the following top-level
12
+ keys: "bbTop", "bbBot", "bbLeft", "bbRight", "page_num".
13
+ """
14
+
15
+ def __init__(self):
16
+ pass
17
+
18
+ def positioned_above(
19
+ self, above_pos: dict, below_pos: dict, must_be_same_page: bool = True
20
+ ) -> bool:
21
+ """
22
+ Check if the location of one box is above another
23
+ Args:
24
+ above_pos (dict): the position expected to be above
25
+ below_pos (dict): to position expected to be below
26
+ must_be_same_page (bool, optional): required to be on same page. Defaults to
27
+ True.
28
+
29
+ Returns:
30
+ bool: True if above_pos is above below_pos
31
+ """
32
+ if below_pos["page_num"] < above_pos["page_num"]:
33
+ is_above = False
34
+ elif below_pos["page_num"] != above_pos["page_num"] and must_be_same_page:
35
+ is_above = False
36
+ elif self.xaxis_overlap(above_pos, below_pos):
37
+ if (
38
+ below_pos["page_num"] == above_pos["page_num"]
39
+ ) and not self.yaxis_above(above_pos, below_pos):
40
+ is_above = False
41
+ else:
42
+ is_above = True
43
+ else:
44
+ is_above = False
45
+ return is_above
46
+
47
+ def positioned_above_overlap(
48
+ self, above_pos: dict, below_pos: dict, min_overlap_percent: float = None
49
+ ) -> bool:
50
+ """
51
+ Check if the location of one box is on the same page and above another and if
52
+ the lower box's overlap is at least the given percentage.
53
+
54
+ Args:
55
+ above_pos (dict): the position expected to be above
56
+ below_pos (dict): the position expected to be below
57
+ min_overlap_percent (float, optional): the minimum amount of overlap needed.
58
+ Defaults to None.
59
+
60
+ Returns:
61
+ bool: True if above_pos is above below_pos and below_pos' amount of overlap
62
+ is at least min_overlap_percent
63
+ """
64
+ is_above = False
65
+ is_min_overlap = True
66
+ if below_pos["page_num"] != above_pos["page_num"]:
67
+ raise ToolkitInputError("Predictions are not on the same page!")
68
+ if self.xaxis_overlap(above_pos, below_pos) and self.yaxis_above(
69
+ above_pos, below_pos
70
+ ):
71
+ is_above = True
72
+ overlap_amount = self.get_horizontal_overlap(above_pos, below_pos)
73
+ if min_overlap_percent and overlap_amount < min_overlap_percent:
74
+ is_min_overlap = False
75
+ return is_above and is_min_overlap
76
+
77
+ def positioned_on_same_level(
78
+ self, pos1: dict, pos2: dict, must_be_same_page: bool = True
79
+ ) -> bool:
80
+ """
81
+ Two box positions are located on the same level/row, i.e. yaxes overlap
82
+ Args:
83
+ pos1 (dict): first position
84
+ pos2 (dict): second position
85
+ must_be_same_page (bool, optional): required to be on same page. Defaults to
86
+ True.
87
+
88
+ Returns:
89
+ bool: True if positions on same level, else False
90
+ """
91
+ if must_be_same_page and not self.on_same_page(pos1, pos2):
92
+ same_level = False
93
+ elif self.yaxis_overlap(pos1, pos2):
94
+ same_level = True
95
+ else:
96
+ same_level = False
97
+ return same_level
98
+
99
+ def get_min_distance(
100
+ self, pos1: dict, pos2: dict, page_height: int = None
101
+ ) -> float:
102
+ """
103
+ Get the minimum distance between any two corners of two bounding boxes via the
104
+ pythagorean formula.
105
+
106
+ Args:
107
+ page_height (int, optional): If you want to measure distances across pages,
108
+ set the OCR page height otherwise locations on separate pages will raise
109
+ an exception. Defaults to None.
110
+
111
+ Returns:
112
+ float: minimum distance
113
+ """
114
+ add_page_height = False
115
+ page_difference = abs(pos1["page_num"] - pos2["page_num"])
116
+ if page_difference > 0:
117
+ if not page_height:
118
+ raise ToolkitInputError(
119
+ "Predictions are not on the same page! Must enter a page height"
120
+ )
121
+ else:
122
+ add_page_height = True
123
+ distances = []
124
+ corners = [
125
+ ("bbRight", "bbTop"),
126
+ ("bbRight", "bbBot"),
127
+ ("bbLeft", "bbTop"),
128
+ ("bbLeft", "bbBot"),
129
+ ]
130
+ for p1 in corners:
131
+ for p2 in corners:
132
+ distance = self._distance_between_points(
133
+ (pos1[p1[0]], pos1[p1[1]]), (pos2[p2[0]], pos2[p2[1]])
134
+ )
135
+ distances.append(distance)
136
+ min_distance = min(distances)
137
+ if add_page_height:
138
+ min_distance += page_height * page_difference
139
+ return min_distance
140
+
141
+ def get_horizontal_overlap(self, pos1: dict, pos2: dict) -> float:
142
+ """
143
+ Get the amount of horizontal overlap between two bounding boxes.
144
+ Returns:
145
+ float: percentage of pos2 that horizontally overlaps with pos1
146
+ """
147
+ page_difference = abs(pos1["page_num"] - pos2["page_num"])
148
+ if page_difference > 0:
149
+ raise ToolkitInputError("Predictions are not on the same page!")
150
+ if self.xaxis_overlap(pos1, pos2):
151
+ horizontal_overlap_distance = abs(
152
+ max(pos1["bbLeft"], pos2["bbLeft"])
153
+ - min(pos1["bbRight"], pos2["bbRight"])
154
+ )
155
+ position_width = abs(pos2["bbLeft"] - pos2["bbRight"])
156
+ return horizontal_overlap_distance / position_width
157
+ else:
158
+ return 0.0
159
+
160
+ def get_vertical_overlap(self, pos1: dict, pos2: dict) -> float:
161
+ """
162
+ Get the amount of vertical overlap between two bounding boxes.
163
+ Returns:
164
+ float: percentage of pos2 that vertically overlaps with pos1
165
+ """
166
+ page_difference = abs(pos1["page_num"] - pos2["page_num"])
167
+ if page_difference > 0:
168
+ raise ToolkitInputError("Predictions are not on the same page!")
169
+ if self.yaxis_overlap(pos1, pos2):
170
+ vertical_overlap_distance = abs(
171
+ max(pos1["bbTop"], pos2["bbTop"]) - min(pos1["bbBot"], pos2["bbBot"])
172
+ )
173
+ position_height = abs(pos2["bbTop"] - pos2["bbBot"])
174
+ return vertical_overlap_distance / position_height
175
+ else:
176
+ return 0.0
177
+
178
+ def get_tokens_within_bounds(
179
+ self, bbox: dict, ocr_tokens: List[dict], include_overlap: bool = False
180
+ ) -> List[dict]:
181
+ """
182
+ Args:
183
+ bbox (dict): dict with target box dimensions and page number
184
+ bbox = {
185
+ bbTop: int
186
+ bbBot: int
187
+ bbLeft: int
188
+ bbRight: int
189
+ page_num: int
190
+ }
191
+ ocr_tokens (List[dict]): on-doc OCR token output from raw or OnDoc class
192
+ include_overlap (bool, optional): Determines whether to include tokens
193
+ partially inside bbox. Defaults to False.
194
+
195
+ Returns:
196
+ List[dict]: list of OCR tokens that fall within the specified bounding box
197
+ """
198
+ if "position" not in ocr_tokens[0] or "page_num" not in ocr_tokens[0]:
199
+ raise ToolkitInputError(
200
+ "Token list argument is missing required key(s): "
201
+ "page_num and/or position"
202
+ )
203
+ if include_overlap:
204
+ return [
205
+ token
206
+ for token in ocr_tokens
207
+ if self.on_same_page(bbox, token)
208
+ and self.yaxis_overlap(bbox, token["position"])
209
+ and self.xaxis_overlap(bbox, token["position"])
210
+ ]
211
+ else:
212
+ return [
213
+ token
214
+ for token in ocr_tokens
215
+ if self.on_same_page(bbox, token)
216
+ and token["position"]["bbLeft"] > bbox["bbLeft"]
217
+ and token["position"]["bbRight"] < bbox["bbRight"]
218
+ and token["position"]["bbTop"] > bbox["bbTop"]
219
+ and token["position"]["bbBot"] < bbox["bbBot"]
220
+ ]
221
+
222
+ @staticmethod
223
+ def get_vertical_min_distance(
224
+ above_pos: dict, below_pos: dict, page_height: int = None
225
+ ) -> float:
226
+ """
227
+ Get the vertical minimum distance between two bounding boxes
228
+
229
+ Args:
230
+ above_pos (dict): the position expected to be above
231
+ below_pos (dict): to position expected to be below
232
+ page_height (int, optional): If you want to measure distances across pages,
233
+ set the OCR page height otherwise locations on separate pages will raise
234
+ an exception. Defaults to None.
235
+
236
+ Returns:
237
+ float: minimum distance
238
+ """
239
+ add_page_height = False
240
+ page_difference = abs(above_pos["page_num"] - below_pos["page_num"])
241
+ if page_difference > 0:
242
+ if not page_height:
243
+ raise ToolkitInputError(
244
+ "Predictions are not on the same page! Must enter a page height"
245
+ )
246
+ else:
247
+ add_page_height = True
248
+ min_distance = below_pos["bbTop"] - above_pos["bbBot"]
249
+
250
+ if add_page_height:
251
+ min_distance += page_height * page_difference
252
+ return min_distance
253
+
254
+ @staticmethod
255
+ def get_horizontal_min_distance(pos1: dict, pos2: dict) -> float:
256
+ """
257
+ Get the horizontal minimum distance between two bounding boxes
258
+
259
+ Returns:
260
+ float: minimum distance
261
+ """
262
+ page_difference = abs(pos1["page_num"] - pos2["page_num"])
263
+ if page_difference > 0:
264
+ raise ToolkitInputError(
265
+ "Predictions are not on the same page! Must enter a page height"
266
+ )
267
+
268
+ min_distance_1 = abs(pos1["bbLeft"] - pos2["bbRight"])
269
+ min_distance_2 = abs(pos1["bbRight"] - pos2["bbLeft"])
270
+ return min(min_distance_1, min_distance_2)
271
+
272
+ @staticmethod
273
+ def _distance_between_points(point1: tuple, point2: tuple) -> float:
274
+ x = (point1[0] - point2[0]) ** 2
275
+ y = (point1[1] - point2[1]) ** 2
276
+ return sqrt(x + y)
277
+
278
+ @staticmethod
279
+ def manhattan_distance_between_points(point1: tuple, point2: tuple) -> float:
280
+ x = abs(point1[0] - point2[0])
281
+ y = abs(point1[1] - point2[1])
282
+ return x + y
283
+
284
+ @staticmethod
285
+ def yaxis_overlap(x: dict, y: dict):
286
+ """
287
+ Two locations overlap on the yaxis, i.e. same "row"
288
+ """
289
+ return y["bbBot"] > x["bbTop"] and y["bbTop"] < x["bbBot"]
290
+
291
+ @staticmethod
292
+ def xaxis_overlap(x: dict, y: dict):
293
+ return x["bbLeft"] < y["bbRight"] and y["bbLeft"] < x["bbRight"]
294
+
295
+ @staticmethod
296
+ def yaxis_above(above_pos: dict, below_pos: dict):
297
+ return above_pos["bbBot"] < below_pos["bbTop"]
298
+
299
+ @staticmethod
300
+ def on_same_page(pos1: dict, pos2: dict):
301
+ return pos1["page_num"] == pos2["page_num"]
@@ -0,0 +1,41 @@
1
+ import re
2
+ from copy import deepcopy
3
+ from itertools import zip_longest
4
+ from typing import Any, Dict, Iterator
5
+
6
+
7
+ def split_prediction(
8
+ prediction: Dict[str, Any], separator: str = r"\s*\n\s*"
9
+ ) -> Iterator[Dict[str, Any]]:
10
+ """
11
+ Split a single `prediction` into mutiple predictions along a `separator` regex
12
+ and yield new predictions having updated text and spans. By default, split on
13
+ newlines (including adjacent whitespace).
14
+ """
15
+
16
+ # re.split (with group capture) returns a sequence of text values and delimiters:
17
+ # ["some", "\n", "separated", "\n", "text"]
18
+ # The text values are the even elements:
19
+ # ["some", "separated", "text"]
20
+ # The delimeters are the odd elements:
21
+ # ["\n", "\n"]
22
+ # zip_longest pairs them together without losing the last text value:
23
+ # [("some", "\n"), ("separated", "\n"), ("text", None)]
24
+ texts_and_delimiters = re.split(f"({separator})", prediction["text"])
25
+ texts = texts_and_delimiters[::2]
26
+ delimiters = texts_and_delimiters[1::2]
27
+ text_and_delimiter_pairs = zip_longest(texts, delimiters)
28
+ start = prediction["start"]
29
+
30
+ for text, delimiter in text_and_delimiter_pairs:
31
+ end = start + len(text)
32
+
33
+ yield {
34
+ **deepcopy(prediction),
35
+ "text": text,
36
+ "start": start,
37
+ "end": end,
38
+ }
39
+
40
+ if delimiter is not None:
41
+ start = end + len(delimiter)
@@ -0,0 +1,3 @@
1
+ from .populator import AutoPopulator
2
+
3
+ __all__ = ("AutoPopulator",)