mmif-python 1.0.16__tar.gz → 1.0.17__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {mmif_python-1.0.16 → mmif_python-1.0.17}/PKG-INFO +1 -1
- mmif_python-1.0.17/VERSION +1 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/serialize/annotation.py +46 -7
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/serialize/mmif.py +95 -80
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/serialize/model.py +6 -6
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/serialize/view.py +27 -17
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/utils/video_document_helper.py +6 -6
- mmif_python-1.0.17/mmif/ver/__init__.py +2 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/vocabulary/annotation_types.py +2 -2
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/vocabulary/base_types.py +1 -1
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/vocabulary/document_types.py +2 -2
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif_python.egg-info/PKG-INFO +1 -1
- {mmif_python-1.0.16 → mmif_python-1.0.17}/tests/test_serialize.py +35 -21
- {mmif_python-1.0.16 → mmif_python-1.0.17}/tests/test_utils.py +2 -3
- mmif_python-1.0.16/VERSION +0 -1
- mmif_python-1.0.16/mmif/ver/__init__.py +0 -2
- {mmif_python-1.0.16 → mmif_python-1.0.17}/LICENSE +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/MANIFEST.in +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/README.md +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/__init__.py +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/res/__init__.py +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/res/clams.vocabulary.yaml +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/res/do-not-edit.txt +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/res/mmif.json +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/serialize/__init__.py +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/utils/__init__.py +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/utils/sequence_helper.py +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/utils/text_document_helper.py +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/utils/timeunit_helper.py +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/ver/do-not-edit.txt +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/vocabulary/__init__.py +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif/vocabulary/do-not-edit.txt +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif_docloc_http/__init__.py +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif_python.egg-info/SOURCES.txt +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif_python.egg-info/requires.txt +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/requirements.cv +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/requirements.seq +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/requirements.txt +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/setup.cfg +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/setup.py +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/tests/test_versioncompat.py +0 -0
- {mmif_python-1.0.16 → mmif_python-1.0.17}/tests/test_vocab.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mmif-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.17
|
|
4
4
|
Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
|
|
5
5
|
Home-page: https://mmif.clams.ai
|
|
6
6
|
Author: Brandeis Lab for Linguistics and Computation
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.17
|
|
@@ -14,7 +14,7 @@ import pkgutil
|
|
|
14
14
|
import re
|
|
15
15
|
import typing
|
|
16
16
|
import warnings
|
|
17
|
-
from typing import Union, Dict,
|
|
17
|
+
from typing import Union, Dict, Optional, Iterator, MutableMapping, TypeVar
|
|
18
18
|
from urllib.parse import urlparse
|
|
19
19
|
|
|
20
20
|
from mmif.vocabulary import ThingTypesBase, DocumentTypesBase
|
|
@@ -46,12 +46,13 @@ class Annotation(MmifObject):
|
|
|
46
46
|
MmifObject that represents an annotation in a MMIF view.
|
|
47
47
|
"""
|
|
48
48
|
|
|
49
|
-
def __init__(self, anno_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
49
|
+
def __init__(self, anno_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
|
|
50
50
|
self._type: ThingTypesBase = ThingTypesBase('')
|
|
51
51
|
# to store the parent view ID
|
|
52
52
|
self._parent_view_id = ''
|
|
53
53
|
self._props_ephemeral: AnnotationProperties = AnnotationProperties()
|
|
54
|
-
self.
|
|
54
|
+
self._alignments = {} # to hold alignment information (Alignment anno long_id -> aligned anno long_id)
|
|
55
|
+
self.reserved_names.update(('_parent_view_id', '_props_ephemeral', '_alignments'))
|
|
55
56
|
if not hasattr(self, 'properties'): # don't overwrite DocumentProperties on super() call
|
|
56
57
|
self.properties: AnnotationProperties = AnnotationProperties()
|
|
57
58
|
self._attribute_classes = {'properties': AnnotationProperties}
|
|
@@ -59,6 +60,9 @@ class Annotation(MmifObject):
|
|
|
59
60
|
self._required_attributes = ["_type", "properties"]
|
|
60
61
|
super().__init__(anno_obj)
|
|
61
62
|
|
|
63
|
+
def __hash__(self):
|
|
64
|
+
return hash(self.serialize())
|
|
65
|
+
|
|
62
66
|
def _deserialize(self, input_dict: dict) -> None:
|
|
63
67
|
self.at_type = input_dict.pop('_type', '')
|
|
64
68
|
# TODO (krim @ 6/1/21): If annotation IDs must follow a certain string format,
|
|
@@ -69,6 +73,40 @@ class Annotation(MmifObject):
|
|
|
69
73
|
for k, v in self.properties.items():
|
|
70
74
|
self._add_prop_aliases(k, v)
|
|
71
75
|
|
|
76
|
+
def _cache_alignment(self, alignment_ann: 'Annotation', alignedto_ann: 'Annotation') -> None:
|
|
77
|
+
"""
|
|
78
|
+
Cache alignment information. This cache will not be serialized.
|
|
79
|
+
|
|
80
|
+
:param alignment_ann: the Alignment annotation that has this annotation on one side
|
|
81
|
+
:param alignedto_ann: the annotation that this annotation is aligned to (other side of Alignment)
|
|
82
|
+
"""
|
|
83
|
+
self._alignments[alignment_ann] = alignedto_ann
|
|
84
|
+
|
|
85
|
+
def aligned_to_by(self, alignment: 'Annotation') -> Optional['Annotation']:
|
|
86
|
+
"""
|
|
87
|
+
Retrieves the other side of ``Alignment`` annotation that has this annotation on one side.
|
|
88
|
+
|
|
89
|
+
:param alignment: ``Alignment`` annotation that has this annotation on one side
|
|
90
|
+
:return: the annotation that this annotation is aligned to (other side of ``Alignment``),
|
|
91
|
+
or None if this annotation is not used in the ``Alignment``.
|
|
92
|
+
"""
|
|
93
|
+
return self._alignments.get(alignment)
|
|
94
|
+
|
|
95
|
+
def get_all_aligned(self) -> Iterator['Annotation']:
|
|
96
|
+
"""
|
|
97
|
+
Generator to iterate through all alignments and aligned annotations. Note that this generator will yield
|
|
98
|
+
the `Alignment` annotations as well. Every odd-numbered yield will be an `Alignment` annotation, and every
|
|
99
|
+
even-numbered yield will be the aligned annotation. If there's a specific annotation type that you're looking
|
|
100
|
+
for, you need to filter the generated results outside.
|
|
101
|
+
|
|
102
|
+
:return: yields the alignment annotation and the aligned annotation.
|
|
103
|
+
The order is decided by the order of appearance of Alignment annotations in the MMIF
|
|
104
|
+
"""
|
|
105
|
+
for alignment, aligned in self._alignments.items():
|
|
106
|
+
yield alignment
|
|
107
|
+
yield aligned
|
|
108
|
+
|
|
109
|
+
|
|
72
110
|
def _add_prop_aliases(self, key_to_add, val_to_add):
|
|
73
111
|
"""
|
|
74
112
|
Method to handle aliases of the same property.
|
|
@@ -163,6 +201,7 @@ class Annotation(MmifObject):
|
|
|
163
201
|
value: Union[PRMTV_TYPES, LIST_PRMTV, LIST_LIST_PRMTV, DICT_PRMTV, DICT_LIST_PRMTV]) -> None:
|
|
164
202
|
"""
|
|
165
203
|
Adds a property to the annotation's properties.
|
|
204
|
+
|
|
166
205
|
:param name: the name of the property
|
|
167
206
|
:param value: the property's desired value
|
|
168
207
|
:return: None
|
|
@@ -241,7 +280,7 @@ class Document(Annotation):
|
|
|
241
280
|
|
|
242
281
|
:param document_obj: the JSON data that defines the document
|
|
243
282
|
"""
|
|
244
|
-
def __init__(self, doc_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
283
|
+
def __init__(self, doc_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
|
|
245
284
|
# see https://github.com/clamsproject/mmif-python/issues/226 for discussion
|
|
246
285
|
# around the use of these three dictionaries
|
|
247
286
|
# (names changed since, `existing` >> `ephemeral` and `temporary` >> `pending`)
|
|
@@ -434,7 +473,7 @@ class AnnotationProperties(MmifObject, MutableMapping[str, T]):
|
|
|
434
473
|
else:
|
|
435
474
|
return self._unnamed_attributes[key]
|
|
436
475
|
|
|
437
|
-
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
476
|
+
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
|
|
438
477
|
self.id: str = ''
|
|
439
478
|
# any individual at_type (subclassing this class) can have its own set of required attributes
|
|
440
479
|
self._required_attributes = ["id"]
|
|
@@ -451,7 +490,7 @@ class DocumentProperties(AnnotationProperties):
|
|
|
451
490
|
:param mmif_obj: the JSON data that defines the properties
|
|
452
491
|
"""
|
|
453
492
|
|
|
454
|
-
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
493
|
+
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
|
|
455
494
|
self.mime: str = ''
|
|
456
495
|
# note the trailing underscore here. I wanted to use the name `location`
|
|
457
496
|
# for @property in this class and `Document` class, so had to use a diff
|
|
@@ -572,7 +611,7 @@ class DocumentProperties(AnnotationProperties):
|
|
|
572
611
|
|
|
573
612
|
class Text(MmifObject):
|
|
574
613
|
|
|
575
|
-
def __init__(self, text_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
614
|
+
def __init__(self, text_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
|
|
576
615
|
self._value: str = ''
|
|
577
616
|
self._language: str = ''
|
|
578
617
|
self.disallow_additional_properties()
|
|
@@ -10,16 +10,16 @@ import math
|
|
|
10
10
|
import warnings
|
|
11
11
|
from collections import defaultdict
|
|
12
12
|
from datetime import datetime
|
|
13
|
-
from typing import List, Union, Optional, Dict, cast, Iterator
|
|
13
|
+
from typing import List, Union, Optional, Dict, cast, Iterator
|
|
14
14
|
|
|
15
15
|
import jsonschema.validators
|
|
16
16
|
|
|
17
17
|
import mmif
|
|
18
18
|
from mmif import ThingTypesBase
|
|
19
|
+
from mmif.serialize.annotation import Annotation, Document
|
|
20
|
+
from mmif.serialize.model import MmifObject, DataList
|
|
21
|
+
from mmif.serialize.view import View
|
|
19
22
|
from mmif.vocabulary import AnnotationTypes, DocumentTypes
|
|
20
|
-
from .annotation import Annotation, Document
|
|
21
|
-
from .model import MmifObject, DataList
|
|
22
|
-
from .view import View
|
|
23
23
|
|
|
24
24
|
__all__ = ['Mmif']
|
|
25
25
|
|
|
@@ -31,7 +31,7 @@ class MmifMetadata(MmifObject):
|
|
|
31
31
|
:param metadata_obj: the JSON data
|
|
32
32
|
"""
|
|
33
33
|
|
|
34
|
-
def __init__(self, metadata_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
34
|
+
def __init__(self, metadata_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
|
|
35
35
|
# TODO (krim @ 10/7/20): there could be a better name and a better way to give a value to this
|
|
36
36
|
self.mmif: str = f"http://mmif.clams.ai/{mmif.__specver__}"
|
|
37
37
|
self._required_attributes = ["mmif"]
|
|
@@ -81,7 +81,9 @@ class ViewsList(DataList[View]):
|
|
|
81
81
|
"""
|
|
82
82
|
_items: Dict[str, View]
|
|
83
83
|
|
|
84
|
-
def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None):
|
|
84
|
+
def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None, parent_mmif=None, *_):
|
|
85
|
+
self._parent_mmif = parent_mmif
|
|
86
|
+
self.reserved_names.update(("_parent_mmif", "_id_counts"))
|
|
85
87
|
super().__init__(mmif_obj)
|
|
86
88
|
|
|
87
89
|
def _deserialize(self, input_list: list) -> None: # pytype: disable=signature-mismatch
|
|
@@ -93,7 +95,7 @@ class ViewsList(DataList[View]):
|
|
|
93
95
|
:return: None
|
|
94
96
|
"""
|
|
95
97
|
if input_list:
|
|
96
|
-
self._items = {item['id']: View(item) for item in input_list}
|
|
98
|
+
self._items = {item['id']: View(item, self._parent_mmif) for item in input_list}
|
|
97
99
|
|
|
98
100
|
def append(self, value: View, overwrite=False) -> None:
|
|
99
101
|
"""
|
|
@@ -205,6 +207,7 @@ class Mmif(MmifObject):
|
|
|
205
207
|
"""
|
|
206
208
|
super()._deserialize(input_dict)
|
|
207
209
|
for view in self.views:
|
|
210
|
+
view._parent_mmif = self
|
|
208
211
|
# this dict will be populated with properties
|
|
209
212
|
# that are not encoded in individual annotations objects themselves
|
|
210
213
|
extrinsic_props = defaultdict(dict)
|
|
@@ -212,6 +215,7 @@ class Mmif(MmifObject):
|
|
|
212
215
|
for prop_key, prop_value in type_lv_props.items():
|
|
213
216
|
extrinsic_props[at_type][prop_key] = prop_value
|
|
214
217
|
for ann in view.get_annotations():
|
|
218
|
+
## for "capital" Annotation properties
|
|
215
219
|
# first add all extrinsic properties to the Annotation objects
|
|
216
220
|
# as "ephemeral" properties
|
|
217
221
|
for prop_key, prop_value in extrinsic_props[ann.at_type].items():
|
|
@@ -226,13 +230,44 @@ class Mmif(MmifObject):
|
|
|
226
230
|
except KeyError:
|
|
227
231
|
warnings.warn(f"Annotation {ann.id} (in view {view.id}) has a document ID {doc_id} that "
|
|
228
232
|
f"does not exist in the MMIF object. Skipping.", RuntimeWarning)
|
|
229
|
-
|
|
233
|
+
|
|
234
|
+
## caching start and end points for time-based annotations
|
|
235
|
+
# add quick access to `start` and `end` values if the annotation is using `targets` property
|
|
230
236
|
if 'targets' in ann.properties:
|
|
231
237
|
if 'start' in ann.properties or 'end' in ann.properties:
|
|
232
|
-
raise ValueError(f"Annotation {ann.id} (in view {view.id}) has `
|
|
238
|
+
raise ValueError(f"Annotation {ann.id} (in view {view.id}) has `targets` and `start`/`end/` "
|
|
233
239
|
f"properties at the same time. Annotation anchors are ambiguous.")
|
|
234
240
|
ann._props_ephemeral['start'] = self._get_linear_anchor_point(ann, start=True)
|
|
235
241
|
ann._props_ephemeral['end'] = self._get_linear_anchor_point(ann, start=False)
|
|
242
|
+
|
|
243
|
+
## caching alignments
|
|
244
|
+
if ann.at_type == AnnotationTypes.Alignment:
|
|
245
|
+
self._cache_alignment(ann)
|
|
246
|
+
|
|
247
|
+
def _cache_alignment(self, alignment_ann: Annotation):
|
|
248
|
+
view = self.views.get(alignment_ann.parent)
|
|
249
|
+
if view is None:
|
|
250
|
+
warnings.warn(f"Alignment {alignment_ann.long_id} doesn't have a parent view, but it should.", RuntimeWarning)
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
## caching alignments
|
|
254
|
+
def _desprately_search_annotation_object(ann_short_id):
|
|
255
|
+
ann_long_id = f"{view.id}{self.id_delimiter}{ann_short_id}"
|
|
256
|
+
try:
|
|
257
|
+
return self.__getitem__(ann_long_id)
|
|
258
|
+
except KeyError:
|
|
259
|
+
return self.__getitem__(ann_short_id)
|
|
260
|
+
|
|
261
|
+
if all(map(lambda x: x in alignment_ann.properties, ('source', 'target'))):
|
|
262
|
+
source_ann = _desprately_search_annotation_object(alignment_ann.get('source'))
|
|
263
|
+
target_ann = _desprately_search_annotation_object(alignment_ann.get('target'))
|
|
264
|
+
if isinstance(source_ann, Annotation) and isinstance(target_ann, Annotation):
|
|
265
|
+
source_ann._cache_alignment(alignment_ann, target_ann)
|
|
266
|
+
target_ann._cache_alignment(alignment_ann, source_ann)
|
|
267
|
+
else:
|
|
268
|
+
warnings.warn(
|
|
269
|
+
f"Alignment {alignment_ann.long_id} has `source` and `target` properties that do not point to Annotation objects.",
|
|
270
|
+
RuntimeWarning)
|
|
236
271
|
|
|
237
272
|
def generate_capital_annotations(self):
|
|
238
273
|
"""
|
|
@@ -339,7 +374,7 @@ class Mmif(MmifObject):
|
|
|
339
374
|
new_view = View()
|
|
340
375
|
new_view.id = self.new_view_id()
|
|
341
376
|
new_view.metadata.timestamp = datetime.now()
|
|
342
|
-
self.
|
|
377
|
+
self.add_view(new_view)
|
|
343
378
|
return new_view
|
|
344
379
|
|
|
345
380
|
def add_view(self, view: View, overwrite=False) -> None:
|
|
@@ -353,6 +388,7 @@ class Mmif(MmifObject):
|
|
|
353
388
|
an existing view with the same ID
|
|
354
389
|
:return: None
|
|
355
390
|
"""
|
|
391
|
+
view._parent_mmif = self
|
|
356
392
|
self.views.append(view, overwrite)
|
|
357
393
|
|
|
358
394
|
def add_document(self, document: Document, overwrite=False) -> None:
|
|
@@ -555,7 +591,7 @@ class Mmif(MmifObject):
|
|
|
555
591
|
|
|
556
592
|
get_views_with_error = get_all_views_with_error
|
|
557
593
|
|
|
558
|
-
def get_all_views_contain(self, at_types: Union[ThingTypesBase, str
|
|
594
|
+
def get_all_views_contain(self, *at_types: Union[ThingTypesBase, str]) -> List[View]:
|
|
559
595
|
"""
|
|
560
596
|
Returns the list of all views in the MMIF if given types
|
|
561
597
|
are present in that view's 'contains' metadata.
|
|
@@ -563,11 +599,8 @@ class Mmif(MmifObject):
|
|
|
563
599
|
:param at_types: a list of types or just a type to check for. When given more than one types, all types must be found.
|
|
564
600
|
:return: the list of views that contain the type
|
|
565
601
|
"""
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if all(map(lambda x: x in view.metadata.contains, at_types))]
|
|
569
|
-
else:
|
|
570
|
-
return [view for view in self.views if at_types in view.metadata.contains]
|
|
602
|
+
return [view for view in self.views
|
|
603
|
+
if all(map(lambda x: x in view.metadata.contains, at_types))]
|
|
571
604
|
|
|
572
605
|
get_views_contain = get_all_views_contain
|
|
573
606
|
|
|
@@ -610,72 +643,54 @@ class Mmif(MmifObject):
|
|
|
610
643
|
return view
|
|
611
644
|
return None
|
|
612
645
|
|
|
613
|
-
def
|
|
614
|
-
|
|
615
|
-
|
|
646
|
+
def _is_in_time_range(self, ann: Annotation, range_s: Union[int, float], range_e: Union[int, float]) -> bool:
|
|
647
|
+
"""
|
|
648
|
+
Checks if the annotation is anchored within the given time range. Any overlap is considered included.
|
|
649
|
+
|
|
650
|
+
:param ann: the Annotation object to check, must be time-based itself or anchored to time-based annotations
|
|
651
|
+
:param range_s: the start time point of the range (in milliseconds)
|
|
652
|
+
:param range_e: the end time point of the range (in milliseconds)
|
|
653
|
+
|
|
654
|
+
:return: True if the annotation is anchored within the time range, False otherwise
|
|
655
|
+
"""
|
|
656
|
+
ann_s, ann_e = self.get_start(ann), self.get_end(ann)
|
|
657
|
+
return (ann_s < range_s < ann_e) or (ann_s < range_e < ann_e) or (ann_s > range_s and ann_e < range_e)
|
|
658
|
+
|
|
659
|
+
def get_annotations_between_time(self, start: Union[int, float], end: Union[int, float], time_unit: str = "ms",
|
|
660
|
+
at_types: List[Union[ThingTypesBase, str]] = []) -> Iterator[Annotation]:
|
|
661
|
+
"""
|
|
662
|
+
Finds annotations that are anchored between the given time points.
|
|
616
663
|
|
|
617
|
-
|
|
618
|
-
|
|
664
|
+
:param start: the start time point in the unit of `input_unit`
|
|
665
|
+
:param end: the end time point in the unit of `input_unit`
|
|
666
|
+
:param time_unit: the unit of the input time points. Default is `ms`.
|
|
667
|
+
:param at_types: a list of annotation types to filter with. Any type in this list will be included in the return.
|
|
668
|
+
:return: an iterator of Annotation objects that are anchored between the given time points
|
|
669
|
+
"""
|
|
670
|
+
assert start < end, f"Start time point must be smaller than the end time point, given {start} and {end}"
|
|
671
|
+
assert start >= 0, f"Start time point must be non-negative, given {start}"
|
|
672
|
+
assert end >= 0, f"End time point must be non-negative, given {end}"
|
|
673
|
+
|
|
619
674
|
from mmif.utils.timeunit_helper import convert
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
valid_tf_anns = []
|
|
640
|
-
tf_to_anns = defaultdict(list)
|
|
641
|
-
|
|
642
|
-
# 1. find all views that contain the type of TF
|
|
643
|
-
views = self.get_all_views_contain([AnnotationTypes.TimeFrame, AnnotationTypes.Alignment])
|
|
644
|
-
|
|
645
|
-
# 2. For each view, extract annotations that satisfy conditions that are TF/TP and fall into time interval
|
|
646
|
-
for view in views:
|
|
647
|
-
# Make sure time unit stay at the same level
|
|
648
|
-
start_time, end_time = self._handle_time_unit(time_unit, view.metadata.contains.get(AnnotationTypes.TimeFrame)["timeUnit"],
|
|
649
|
-
start, end)
|
|
650
|
-
tf_anns = view.get_annotations(at_type=AnnotationTypes.TimeFrame)
|
|
651
|
-
al_anns = view.get_annotations(at_type=AnnotationTypes.Alignment)
|
|
652
|
-
|
|
653
|
-
# Select 'TimeFrame' annotations within given time interval
|
|
654
|
-
for tf in tf_anns:
|
|
655
|
-
if self._is_in_time_between(start_time, end_time, tf):
|
|
656
|
-
valid_tf_anns.append(tf)
|
|
657
|
-
|
|
658
|
-
# Map 'TimeFrame' annotation to its aligned annotation
|
|
659
|
-
for align in al_anns:
|
|
660
|
-
source_id, target_id = align.get_property('source'), align.get_property('target')
|
|
661
|
-
to_long_id = lambda x: x if self.id_delimiter in x else f'{view.id}{self.id_delimiter}{x}'
|
|
662
|
-
try:
|
|
663
|
-
source, target = view.get_annotation_by_id(source_id), view.get_annotation_by_id(target_id)
|
|
664
|
-
if source in valid_tf_anns:
|
|
665
|
-
tf_to_anns[to_long_id(source_id)].append(target)
|
|
666
|
-
elif target in valid_tf_anns:
|
|
667
|
-
tf_to_anns[to_long_id(target_id)].append(source)
|
|
668
|
-
except KeyError:
|
|
669
|
-
pass
|
|
670
|
-
|
|
671
|
-
# 3. For those extracted 'TimeFrame' annotations, sort them by their start time
|
|
672
|
-
sort_tf_anns = sorted(valid_tf_anns, key=lambda x: self.get_start(x))
|
|
673
|
-
|
|
674
|
-
# 4. Yield all annotations aligned with sorted 'TimeFrame' annotations
|
|
675
|
-
for tf_ann in sort_tf_anns:
|
|
676
|
-
anns = tf_to_anns[tf_ann.long_id]
|
|
677
|
-
for ann in anns:
|
|
678
|
-
yield ann
|
|
675
|
+
|
|
676
|
+
time_anchors_in_range = []
|
|
677
|
+
at_types = set(at_types)
|
|
678
|
+
|
|
679
|
+
for view in self.get_all_views_contain(AnnotationTypes.TimeFrame) + self.get_all_views_contain(AnnotationTypes.TimePoint):
|
|
680
|
+
time_unit_in_view = view.metadata.contains.get(AnnotationTypes.TimeFrame)["timeUnit"]
|
|
681
|
+
|
|
682
|
+
start_time = convert(start, time_unit, time_unit_in_view, 1)
|
|
683
|
+
end_time = convert(end, time_unit, time_unit_in_view, 1)
|
|
684
|
+
for ann in view.get_annotations():
|
|
685
|
+
if ann.at_type in (AnnotationTypes.TimeFrame, AnnotationTypes.TimePoint) and self._is_in_time_range(ann, start_time, end_time):
|
|
686
|
+
time_anchors_in_range.append(ann)
|
|
687
|
+
time_anchors_in_range.sort(key=lambda x: self.get_start(x))
|
|
688
|
+
for time_anchor in time_anchors_in_range:
|
|
689
|
+
if not at_types or time_anchor.at_type in at_types:
|
|
690
|
+
yield time_anchor
|
|
691
|
+
for aligned in time_anchor.get_all_aligned():
|
|
692
|
+
if not at_types or aligned.at_type in at_types:
|
|
693
|
+
yield aligned
|
|
679
694
|
|
|
680
695
|
def _get_linear_anchor_point(self, ann: Annotation, targets_sorted=False, start: bool = True) -> Union[int, float]:
|
|
681
696
|
# TODO (krim @ 2/5/24): Update the return type once timeunits are unified to `ms` as integers (https://github.com/clamsproject/mmif/issues/192)
|
|
@@ -99,7 +99,7 @@ class MmifObject(object):
|
|
|
99
99
|
_exclude_from_diff: Set[str]
|
|
100
100
|
_attribute_classes: Dict[str, Type] = {} # Mapping: str -> Type
|
|
101
101
|
|
|
102
|
-
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
102
|
+
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
|
|
103
103
|
if isinstance(mmif_obj, bytes):
|
|
104
104
|
mmif_obj = mmif_obj.decode('utf8')
|
|
105
105
|
if not hasattr(self, '_required_attributes'):
|
|
@@ -258,7 +258,7 @@ class MmifObject(object):
|
|
|
258
258
|
"""
|
|
259
259
|
for k, v in input_dict.items():
|
|
260
260
|
if self._attribute_classes and k in self._attribute_classes:
|
|
261
|
-
self[k] = self._attribute_classes[k](v)
|
|
261
|
+
self[k] = self._attribute_classes[k](v, self)
|
|
262
262
|
else:
|
|
263
263
|
self[k] = v
|
|
264
264
|
|
|
@@ -285,13 +285,13 @@ class MmifObject(object):
|
|
|
285
285
|
if key in self._named_attributes():
|
|
286
286
|
if self._attribute_classes and key in self._attribute_classes \
|
|
287
287
|
and not isinstance(value, (self._attribute_classes[key])):
|
|
288
|
-
self.__dict__[key] = self._attribute_classes[key](value)
|
|
288
|
+
self.__dict__[key] = self._attribute_classes[key](value, self)
|
|
289
289
|
else:
|
|
290
290
|
self.__dict__[key] = value
|
|
291
291
|
else:
|
|
292
292
|
if self._attribute_classes and key in self._attribute_classes \
|
|
293
293
|
and not isinstance(value, (self._attribute_classes[key])):
|
|
294
|
-
self.set_additional_property(key, self._attribute_classes[key](value))
|
|
294
|
+
self.set_additional_property(key, self._attribute_classes[key](value, self))
|
|
295
295
|
else:
|
|
296
296
|
self.set_additional_property(key, value)
|
|
297
297
|
|
|
@@ -342,7 +342,7 @@ class DataList(MmifObject, Generic[T]):
|
|
|
342
342
|
|
|
343
343
|
:param Union[str, list] mmif_obj: the data that the list contains
|
|
344
344
|
"""
|
|
345
|
-
def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None):
|
|
345
|
+
def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None, *_):
|
|
346
346
|
self.reserved_names.add('_items')
|
|
347
347
|
self._items: Dict[str, T] = dict()
|
|
348
348
|
self.disallow_additional_properties()
|
|
@@ -440,7 +440,7 @@ class DataList(MmifObject, Generic[T]):
|
|
|
440
440
|
|
|
441
441
|
|
|
442
442
|
class DataDict(MmifObject, Generic[T, S]):
|
|
443
|
-
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None):
|
|
443
|
+
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_):
|
|
444
444
|
self.reserved_names.add('_items')
|
|
445
445
|
self._items: Dict[T, S] = dict()
|
|
446
446
|
self.disallow_additional_properties()
|
|
@@ -9,15 +9,12 @@ import json
|
|
|
9
9
|
from datetime import datetime
|
|
10
10
|
from typing import Dict, Union, Optional, Generator, List, cast
|
|
11
11
|
|
|
12
|
-
from mmif
|
|
13
|
-
from mmif.
|
|
14
|
-
from .
|
|
15
|
-
from .model import MmifObject, DataList, DataDict
|
|
12
|
+
from mmif import DocumentTypes, AnnotationTypes, ThingTypesBase, ClamsTypesBase
|
|
13
|
+
from mmif.serialize.annotation import Annotation, Document
|
|
14
|
+
from mmif.serialize.model import PRMTV_TYPES, MmifObject, DataList, DataDict
|
|
16
15
|
|
|
17
16
|
__all__ = ['View', 'ViewMetadata', 'Contain']
|
|
18
17
|
|
|
19
|
-
from .. import DocumentTypes
|
|
20
|
-
|
|
21
18
|
|
|
22
19
|
class View(MmifObject):
|
|
23
20
|
"""
|
|
@@ -32,10 +29,12 @@ class View(MmifObject):
|
|
|
32
29
|
:param view_obj: the JSON data that defines the view
|
|
33
30
|
"""
|
|
34
31
|
|
|
35
|
-
def __init__(self, view_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
32
|
+
def __init__(self, view_obj: Optional[Union[bytes, str, dict]] = None, parent_mmif=None, *_) -> None:
|
|
36
33
|
# used to autogenerate annotation ids
|
|
37
34
|
self._id_counts = {}
|
|
38
|
-
|
|
35
|
+
# used to access the parent MMIF object
|
|
36
|
+
self._parent_mmif = parent_mmif
|
|
37
|
+
self.reserved_names.update(("_parent_mmif", "_id_counts"))
|
|
39
38
|
self._exclude_from_diff = {"_id_counts"}
|
|
40
39
|
|
|
41
40
|
self.id: str = ''
|
|
@@ -64,7 +63,7 @@ class View(MmifObject):
|
|
|
64
63
|
else:
|
|
65
64
|
return self.metadata.new_contain(at_type, **contains_metadata)
|
|
66
65
|
|
|
67
|
-
def
|
|
66
|
+
def _set_ann_id(self, annotation: Annotation, identifier):
|
|
68
67
|
if identifier is not None:
|
|
69
68
|
annotation.id = identifier
|
|
70
69
|
else:
|
|
@@ -73,7 +72,7 @@ class View(MmifObject):
|
|
|
73
72
|
new_id = f'{prefix}_{new_num}'
|
|
74
73
|
self._id_counts[prefix] = new_num
|
|
75
74
|
annotation.id = new_id
|
|
76
|
-
|
|
75
|
+
|
|
77
76
|
def new_annotation(self, at_type: Union[str, ThingTypesBase], aid: Optional[str] = None,
|
|
78
77
|
overwrite=False, **properties) -> 'Annotation':
|
|
79
78
|
"""
|
|
@@ -96,7 +95,7 @@ class View(MmifObject):
|
|
|
96
95
|
"""
|
|
97
96
|
new_annotation = Annotation()
|
|
98
97
|
new_annotation.at_type = at_type
|
|
99
|
-
self.
|
|
98
|
+
self._set_ann_id(new_annotation, aid)
|
|
100
99
|
for propk, propv in properties.items():
|
|
101
100
|
new_annotation.add_property(propk, propv)
|
|
102
101
|
for propk, propv in self.metadata.contains.get(at_type, {}).items():
|
|
@@ -122,8 +121,10 @@ class View(MmifObject):
|
|
|
122
121
|
annotation.parent = self.id
|
|
123
122
|
self.annotations.append(annotation, overwrite)
|
|
124
123
|
self.new_contain(annotation.at_type)
|
|
124
|
+
if annotation.at_type == AnnotationTypes.Alignment:
|
|
125
|
+
self._parent_mmif._cache_alignment(annotation)
|
|
125
126
|
return annotation
|
|
126
|
-
|
|
127
|
+
|
|
127
128
|
def new_textdocument(self, text: str, lang: str = "en", did: Optional[str] = None,
|
|
128
129
|
overwrite=False, **properties) -> 'Document':
|
|
129
130
|
"""
|
|
@@ -147,7 +148,7 @@ class View(MmifObject):
|
|
|
147
148
|
"""
|
|
148
149
|
new_document = Document()
|
|
149
150
|
new_document.at_type = DocumentTypes.TextDocument
|
|
150
|
-
self.
|
|
151
|
+
self._set_ann_id(new_document, did)
|
|
151
152
|
new_document.text_language = lang
|
|
152
153
|
new_document.text_value = text
|
|
153
154
|
for propk, propv in properties.items():
|
|
@@ -189,9 +190,18 @@ class View(MmifObject):
|
|
|
189
190
|
yield annotation
|
|
190
191
|
|
|
191
192
|
def get_annotation_by_id(self, ann_id) -> Annotation:
|
|
192
|
-
|
|
193
|
+
if self.id_delimiter in ann_id and not ann_id.startswith(self.id):
|
|
194
|
+
try:
|
|
195
|
+
ann_found = self._parent_mmif[ann_id]
|
|
196
|
+
except KeyError:
|
|
197
|
+
ann_found = None
|
|
198
|
+
else:
|
|
199
|
+
ann_found = self.annotations.get(ann_id.split(self.id_delimiter)[-1])
|
|
193
200
|
if ann_found is None or not isinstance(ann_found, Annotation):
|
|
194
|
-
|
|
201
|
+
if self.id_delimiter in ann_id:
|
|
202
|
+
raise KeyError(f"Annotation \"{ann_id}\" is not found in the MMIF.")
|
|
203
|
+
else:
|
|
204
|
+
raise KeyError(f"Annotation \"{ann_id}\" is not found in view {self.id}.")
|
|
195
205
|
else:
|
|
196
206
|
return ann_found
|
|
197
207
|
|
|
@@ -257,7 +267,7 @@ class ViewMetadata(MmifObject):
|
|
|
257
267
|
:param viewmetadata_obj: the JSON data that defines the metadata
|
|
258
268
|
"""
|
|
259
269
|
|
|
260
|
-
def __init__(self, viewmetadata_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
270
|
+
def __init__(self, viewmetadata_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
|
|
261
271
|
self.document: str = ''
|
|
262
272
|
self.timestamp: Optional[datetime] = None
|
|
263
273
|
self.app: str = ''
|
|
@@ -377,7 +387,7 @@ class ErrorDict(MmifObject):
|
|
|
377
387
|
"""
|
|
378
388
|
Error object that stores information about error occurred during processing.
|
|
379
389
|
"""
|
|
380
|
-
def __init__(self, error_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
390
|
+
def __init__(self, error_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
|
|
381
391
|
self.message: str = ''
|
|
382
392
|
self.stackTrace: str = ''
|
|
383
393
|
super().__init__(error_obj)
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import importlib
|
|
2
|
+
import math
|
|
2
3
|
import warnings
|
|
3
4
|
from typing import List, Union, Tuple
|
|
4
|
-
import math
|
|
5
5
|
|
|
6
6
|
import mmif
|
|
7
7
|
from mmif import Annotation, Document, Mmif
|
|
8
8
|
from mmif.utils.timeunit_helper import convert
|
|
9
|
-
from mmif.vocabulary import DocumentTypes
|
|
9
|
+
from mmif.vocabulary import DocumentTypes
|
|
10
10
|
|
|
11
11
|
for cv_dep in ('cv2', 'ffmpeg', 'PIL'):
|
|
12
12
|
try:
|
|
@@ -212,19 +212,19 @@ def convert_timepoint(mmif: Mmif, timepoint: Annotation, out_unit: str) -> Union
|
|
|
212
212
|
return convert(timepoint.get_property('timePoint'), in_unit, out_unit, get_framerate(vd))
|
|
213
213
|
|
|
214
214
|
|
|
215
|
-
def convert_timeframe(mmif: Mmif, time_frame: Annotation, out_unit: str) ->
|
|
215
|
+
def convert_timeframe(mmif: Mmif, time_frame: Annotation, out_unit: str) -> Tuple[Union[int, float, str], Union[int, float, str]]:
|
|
216
216
|
"""
|
|
217
217
|
Converts start and end points in a ``TimeFrame`` annotation a different time unit.
|
|
218
218
|
|
|
219
219
|
:param mmif: :py:class:`~mmif.serialize.mmif.Mmif` instance
|
|
220
220
|
:param time_frame: :py:class:`~mmif.serialize.annotation.Annotation` instance that holds a time interval annotation (``"@type": ".../TimeFrame/..."``)
|
|
221
221
|
:param out_unit: time unit to which the point is converted
|
|
222
|
-
:return: tuple of frame numbers
|
|
222
|
+
:return: tuple of frame numbers, seconds/milliseconds, or ISO notation of TimeFrame's start and end
|
|
223
223
|
"""
|
|
224
224
|
in_unit = time_frame.get_property('timeUnit')
|
|
225
225
|
vd = mmif[time_frame.get_property('document')]
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
fps = get_framerate(vd)
|
|
227
|
+
return convert(time_frame.get_property('start'), in_unit, out_unit, fps), convert(time_frame.get_property('end'), in_unit, out_unit, fps)
|
|
228
228
|
|
|
229
229
|
|
|
230
230
|
def framenum_to_second(video_doc: Document, frame: int):
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Spec version 1.0.
|
|
1
|
+
# Spec version 1.0.5
|
|
2
2
|
# This file is auto-generated by setup.py
|
|
3
3
|
|
|
4
4
|
from .base_types import AnnotationTypesBase
|
|
@@ -7,7 +7,7 @@ from .base_types import AnnotationTypesBase
|
|
|
7
7
|
class AnnotationTypes(AnnotationTypesBase):
|
|
8
8
|
"""
|
|
9
9
|
This class contains the CLAMS annotation types
|
|
10
|
-
defined in the spec version 1.0.
|
|
10
|
+
defined in the spec version 1.0.5 as class variables.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
# prpoerty aliases, first added in MMIF 1.0.2
|
|
@@ -196,7 +196,7 @@ class DocumentTypesBase(ClamsTypesBase):
|
|
|
196
196
|
class ThingType(ThingTypesBase):
|
|
197
197
|
"""
|
|
198
198
|
This class contains the topmost CLAMS thing type
|
|
199
|
-
defined in the spec version 1.0.
|
|
199
|
+
defined in the spec version 1.0.5 as a class variable.
|
|
200
200
|
"""
|
|
201
201
|
Thing = ClamsTypesBase('http://mmif.clams.ai/vocabulary/Thing/v1')
|
|
202
202
|
_typevers = {'Thing': 'v1'}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Spec version 1.0.
|
|
1
|
+
# Spec version 1.0.5
|
|
2
2
|
# This file is auto-generated by setup.py
|
|
3
3
|
|
|
4
4
|
from .base_types import DocumentTypesBase
|
|
@@ -7,7 +7,7 @@ from .base_types import DocumentTypesBase
|
|
|
7
7
|
class DocumentTypes(DocumentTypesBase):
|
|
8
8
|
"""
|
|
9
9
|
This class contains the CLAMS document types
|
|
10
|
-
defined in the spec version 1.0.
|
|
10
|
+
defined in the spec version 1.0.5 as class variables.
|
|
11
11
|
"""
|
|
12
12
|
Document = DocumentTypesBase('http://mmif.clams.ai/vocabulary/Document/v1')
|
|
13
13
|
VideoDocument = DocumentTypesBase('http://mmif.clams.ai/vocabulary/VideoDocument/v1')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: mmif-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.17
|
|
4
4
|
Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
|
|
5
5
|
Home-page: https://mmif.clams.ai
|
|
6
6
|
Author: Brandeis Lab for Linguistics and Computation
|
|
@@ -222,7 +222,7 @@ class TestMmif(unittest.TestCase):
|
|
|
222
222
|
def test_document_location_helpers_http(self):
|
|
223
223
|
new_doc = Document()
|
|
224
224
|
new_doc.id = "d1"
|
|
225
|
-
new_doc.location = f"https://
|
|
225
|
+
new_doc.location = f"https://example.com/"
|
|
226
226
|
self.assertEqual(new_doc.location_scheme(), 'https')
|
|
227
227
|
try:
|
|
228
228
|
path = new_doc.location_path()
|
|
@@ -277,11 +277,11 @@ class TestMmif(unittest.TestCase):
|
|
|
277
277
|
self.assertEqual(2, len(views))
|
|
278
278
|
views = mmif_obj.get_all_views_contain('http://vocab.lappsgrid.org/SemanticTag')
|
|
279
279
|
self.assertEqual(1, len(views))
|
|
280
|
-
views = mmif_obj.get_views_contain(
|
|
280
|
+
views = mmif_obj.get_views_contain(
|
|
281
281
|
AnnotationTypes.TimeFrame,
|
|
282
282
|
DocumentTypes.TextDocument,
|
|
283
283
|
AnnotationTypes.Alignment,
|
|
284
|
-
|
|
284
|
+
)
|
|
285
285
|
self.assertEqual(1, len(views))
|
|
286
286
|
views = mmif_obj.get_all_views_contain(not_existing_attype)
|
|
287
287
|
self.assertEqual(0, len(views))
|
|
@@ -323,6 +323,17 @@ class TestMmif(unittest.TestCase):
|
|
|
323
323
|
views_and_alignments = mmif_obj.get_alignments(DocumentTypes.TextDocument, AnnotationTypes.BoundingBox)
|
|
324
324
|
self.assertEqual(1, len(views_and_alignments))
|
|
325
325
|
self.assertTrue('v6' in views_and_alignments)
|
|
326
|
+
|
|
327
|
+
def test_cache_alignment(self):
|
|
328
|
+
mmif_obj = Mmif(MMIF_EXAMPLES['everything'])
|
|
329
|
+
views_and_alignments = mmif_obj.get_alignments(DocumentTypes.TextDocument, AnnotationTypes.TimeFrame)
|
|
330
|
+
for vid, alignments in views_and_alignments.items():
|
|
331
|
+
v = mmif_obj.get_view_by_id(vid)
|
|
332
|
+
for alignment in alignments:
|
|
333
|
+
s = v.get_annotation_by_id(alignment.get('source'))
|
|
334
|
+
t = v.get_annotation_by_id(alignment.get('target'))
|
|
335
|
+
self.assertTrue(s.aligned_to_by(alignment).long_id.endswith(t.long_id))
|
|
336
|
+
self.assertTrue(t.aligned_to_by(alignment).long_id.endswith(s.long_id))
|
|
326
337
|
|
|
327
338
|
def test_new_view_id(self):
|
|
328
339
|
p = Mmif.view_prefix
|
|
@@ -350,7 +361,7 @@ class TestMmif(unittest.TestCase):
|
|
|
350
361
|
# .[] |
|
|
351
362
|
# select(."@type"=="http://vocab.lappsgrid.org/Token")] |
|
|
352
363
|
# sort_by(.properties.id | ltrimstr("t") | tonumber) |
|
|
353
|
-
# map(.properties.
|
|
364
|
+
# map(.properties.word)' <examples>.json
|
|
354
365
|
tokens_in_order = ["Hello",
|
|
355
366
|
",",
|
|
356
367
|
"this",
|
|
@@ -382,23 +393,26 @@ class TestMmif(unittest.TestCase):
|
|
|
382
393
|
mmif_obj = Mmif(MMIF_EXAMPLES['everything'])
|
|
383
394
|
|
|
384
395
|
# Test case 1: All token annotations are selected
|
|
385
|
-
selected_token_anns = mmif_obj.get_annotations_between_time(0, 22000)
|
|
386
|
-
self.assertEqual(28, len(
|
|
396
|
+
selected_token_anns = [ann for ann in mmif_obj.get_annotations_between_time(0, 22000) if ann.is_type(token_type)]
|
|
397
|
+
self.assertEqual(28, len(selected_token_anns))
|
|
387
398
|
for i, ann in enumerate(selected_token_anns):
|
|
388
|
-
self.
|
|
389
|
-
self.assertEqual(tokens_in_order[i], ann.get_property("text"))
|
|
399
|
+
self.assertEqual(tokens_in_order[i], ann.get_property("word"))
|
|
390
400
|
|
|
391
401
|
# Test case 2: No token annotation are selected
|
|
392
|
-
selected_token_anns = mmif_obj.get_annotations_between_time(0, 5, time_unit="seconds")
|
|
393
|
-
self.assertEqual(
|
|
402
|
+
selected_token_anns = list(mmif_obj.get_annotations_between_time(0, 5, time_unit="seconds"))
|
|
403
|
+
self.assertEqual(4, len(list(selected_token_anns)))
|
|
404
|
+
for ann in selected_token_anns:
|
|
405
|
+
self.assertFalse(ann.is_type(token_type))
|
|
394
406
|
|
|
395
407
|
# Test case 3(a): Partial tokens are selected (involve partial overlap)
|
|
396
|
-
selected_token_anns = mmif_obj.get_annotations_between_time(7, 10, time_unit="seconds"
|
|
397
|
-
|
|
408
|
+
selected_token_anns = mmif_obj.get_annotations_between_time(7, 10, time_unit="seconds",
|
|
409
|
+
at_types=['http://vocab.lappsgrid.org/Token'])
|
|
410
|
+
self.assertEqual(tokens_in_order[3:9], [ann.get_property("word") for ann in selected_token_anns])
|
|
398
411
|
|
|
399
412
|
# Test case 3(b): Partial tokens are selected (only full overlap)
|
|
400
|
-
selected_token_anns = mmif_obj.get_annotations_between_time(11500, 14600
|
|
401
|
-
|
|
413
|
+
selected_token_anns = mmif_obj.get_annotations_between_time(11500, 14600,
|
|
414
|
+
at_types=['http://vocab.lappsgrid.org/Token'])
|
|
415
|
+
self.assertEqual(tokens_in_order[12:17], [ann.get_property("word") for ann in selected_token_anns])
|
|
402
416
|
|
|
403
417
|
def test_add_document(self):
|
|
404
418
|
mmif_obj = Mmif(MMIF_EXAMPLES['everything'])
|
|
@@ -662,6 +676,7 @@ class TestView(unittest.TestCase):
|
|
|
662
676
|
self.assertEqual(view_from_str, view_from_bytes)
|
|
663
677
|
self.assertEqual(json.loads(view_from_json.serialize()), json.loads(view_from_str.serialize()))
|
|
664
678
|
self.assertEqual(json.loads(view_from_bytes.serialize()), json.loads(view_from_str.serialize()))
|
|
679
|
+
|
|
665
680
|
|
|
666
681
|
def test_annotation_order_preserved(self):
|
|
667
682
|
view_serial = self.view_obj.serialize()
|
|
@@ -885,13 +900,12 @@ class TestAnnotation(unittest.TestCase):
|
|
|
885
900
|
# TODO (angus-lherrou @ 7/27/2020): testing should include validation for required attrs
|
|
886
901
|
# once that is implemented (issue #23)
|
|
887
902
|
def setUp(self) -> None:
|
|
888
|
-
self.data = {
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
for i, example in MMIF_EXAMPLES.items()}
|
|
903
|
+
self.data = {}
|
|
904
|
+
for i, example in MMIF_EXAMPLES.items():
|
|
905
|
+
self.data[i] = {'string': example,
|
|
906
|
+
'json': json.loads(example),
|
|
907
|
+
'mmif': Mmif(example),
|
|
908
|
+
'annotations': [annotation for view in json.loads(example)['views'] for annotation in view['annotations']]}
|
|
895
909
|
|
|
896
910
|
def test_annotation_properties(self):
|
|
897
911
|
ann_json = self.data['everything']['annotations'][0]
|
|
@@ -4,10 +4,9 @@ import pytest
|
|
|
4
4
|
|
|
5
5
|
from mmif import Mmif, Document, AnnotationTypes
|
|
6
6
|
from mmif.utils import sequence_helper as sqh
|
|
7
|
+
from mmif.utils import text_document_helper as tdh
|
|
7
8
|
from mmif.utils import timeunit_helper as tuh
|
|
8
9
|
from mmif.utils import video_document_helper as vdh
|
|
9
|
-
from mmif.utils import text_document_helper as tdh
|
|
10
|
-
from mmif.serialize import mmif
|
|
11
10
|
from tests.mmif_examples import *
|
|
12
11
|
|
|
13
12
|
|
|
@@ -178,7 +177,7 @@ class TestSequenceHelper(unittest.TestCase):
|
|
|
178
177
|
class TestTextDocHelper(unittest.TestCase):
|
|
179
178
|
mmif_obj = Mmif(MMIF_EXAMPLES['everything'])
|
|
180
179
|
|
|
181
|
-
@pytest.mark.skip("The only valid test cases come from
|
|
180
|
+
@pytest.mark.skip("The only valid test cases come from kaldi app which annotates wrong property")
|
|
182
181
|
def test_slice_text(self):
|
|
183
182
|
sliced_text_full_overlap = tdh.slice_text(self.mmif_obj, 11500, 14600)
|
|
184
183
|
sliced_text_partial_overlap = tdh.slice_text(self.mmif_obj, 7, 10, unit="seconds")
|
mmif_python-1.0.16/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.16
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|