mmif-python 1.0.15__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.15 → mmif_python-1.0.17}/PKG-INFO +1 -1
- mmif_python-1.0.17/VERSION +1 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/serialize/annotation.py +46 -7
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/serialize/mmif.py +99 -17
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/serialize/model.py +6 -6
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/serialize/view.py +27 -17
- mmif_python-1.0.17/mmif/utils/text_document_helper.py +12 -0
- {mmif_python-1.0.15 → 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.15 → mmif_python-1.0.17}/mmif/vocabulary/annotation_types.py +2 -2
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/vocabulary/base_types.py +1 -1
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/vocabulary/document_types.py +2 -2
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif_python.egg-info/PKG-INFO +1 -1
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif_python.egg-info/SOURCES.txt +1 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/tests/test_serialize.py +82 -10
- {mmif_python-1.0.15 → mmif_python-1.0.17}/tests/test_utils.py +27 -7
- mmif_python-1.0.15/VERSION +0 -1
- mmif_python-1.0.15/mmif/ver/__init__.py +0 -2
- {mmif_python-1.0.15 → mmif_python-1.0.17}/LICENSE +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/MANIFEST.in +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/README.md +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/__init__.py +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/res/__init__.py +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/res/clams.vocabulary.yaml +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/res/do-not-edit.txt +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/res/mmif.json +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/serialize/__init__.py +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/utils/__init__.py +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/utils/sequence_helper.py +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/utils/timeunit_helper.py +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/ver/do-not-edit.txt +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/vocabulary/__init__.py +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif/vocabulary/do-not-edit.txt +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif_docloc_http/__init__.py +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif_python.egg-info/requires.txt +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/requirements.cv +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/requirements.seq +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/requirements.txt +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/setup.cfg +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/setup.py +0 -0
- {mmif_python-1.0.15 → mmif_python-1.0.17}/tests/test_versioncompat.py +0 -0
- {mmif_python-1.0.15 → 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
|
|
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
|
|
|
@@ -609,7 +642,56 @@ class Mmif(MmifObject):
|
|
|
609
642
|
if at_types in view.metadata.contains:
|
|
610
643
|
return view
|
|
611
644
|
return None
|
|
612
|
-
|
|
645
|
+
|
|
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.
|
|
663
|
+
|
|
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
|
+
|
|
674
|
+
from mmif.utils.timeunit_helper import convert
|
|
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
|
|
694
|
+
|
|
613
695
|
def _get_linear_anchor_point(self, ann: Annotation, targets_sorted=False, start: bool = True) -> Union[int, float]:
|
|
614
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)
|
|
615
697
|
"""
|
|
@@ -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)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import mmif
|
|
2
|
+
from mmif import Annotation
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def slice_text(mmif_obj, start: int, end: int, unit: str = "milliseconds") -> str:
|
|
6
|
+
token_type = "http://vocab.lappsgrid.org/Token"
|
|
7
|
+
anns_found = mmif_obj.get_annotations_between_time(start, end, unit)
|
|
8
|
+
tokens_sliced = []
|
|
9
|
+
for ann in anns_found:
|
|
10
|
+
if ann.is_type(token_type):
|
|
11
|
+
tokens_sliced.append(ann.get_property('word'))
|
|
12
|
+
return ' '.join(tokens_sliced)
|
|
@@ -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
|
|
@@ -342,6 +353,67 @@ class TestMmif(unittest.TestCase):
|
|
|
342
353
|
self.assertEqual(e_view.id, f'{p}4')
|
|
343
354
|
self.assertEqual(len(mmif_obj.views), 5)
|
|
344
355
|
|
|
356
|
+
def test_get_annotations_between_time(self):
|
|
357
|
+
token_type = "http://vocab.lappsgrid.org/Token"
|
|
358
|
+
# Below tokens are obtained by 'jq' in CLI using command:
|
|
359
|
+
# jq '[
|
|
360
|
+
# .views[3].annotations |
|
|
361
|
+
# .[] |
|
|
362
|
+
# select(."@type"=="http://vocab.lappsgrid.org/Token")] |
|
|
363
|
+
# sort_by(.properties.id | ltrimstr("t") | tonumber) |
|
|
364
|
+
# map(.properties.word)' <examples>.json
|
|
365
|
+
tokens_in_order = ["Hello",
|
|
366
|
+
",",
|
|
367
|
+
"this",
|
|
368
|
+
"is",
|
|
369
|
+
"Jim",
|
|
370
|
+
"Lehrer",
|
|
371
|
+
"with",
|
|
372
|
+
"the",
|
|
373
|
+
"NewsHour",
|
|
374
|
+
"on",
|
|
375
|
+
"PBS",
|
|
376
|
+
".",
|
|
377
|
+
"In",
|
|
378
|
+
"the",
|
|
379
|
+
"nineteen",
|
|
380
|
+
"eighties",
|
|
381
|
+
",",
|
|
382
|
+
"barking",
|
|
383
|
+
"dogs",
|
|
384
|
+
"have",
|
|
385
|
+
"increasingly",
|
|
386
|
+
"become",
|
|
387
|
+
"a",
|
|
388
|
+
"problem",
|
|
389
|
+
"in",
|
|
390
|
+
"urban",
|
|
391
|
+
"areas",
|
|
392
|
+
"."]
|
|
393
|
+
mmif_obj = Mmif(MMIF_EXAMPLES['everything'])
|
|
394
|
+
|
|
395
|
+
# Test case 1: All token annotations are selected
|
|
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))
|
|
398
|
+
for i, ann in enumerate(selected_token_anns):
|
|
399
|
+
self.assertEqual(tokens_in_order[i], ann.get_property("word"))
|
|
400
|
+
|
|
401
|
+
# Test case 2: No token annotation are selected
|
|
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))
|
|
406
|
+
|
|
407
|
+
# Test case 3(a): Partial tokens are selected (involve partial overlap)
|
|
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])
|
|
411
|
+
|
|
412
|
+
# Test case 3(b): Partial tokens are selected (only full overlap)
|
|
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])
|
|
416
|
+
|
|
345
417
|
def test_add_document(self):
|
|
346
418
|
mmif_obj = Mmif(MMIF_EXAMPLES['everything'])
|
|
347
419
|
med_obj = Document(FRACTIONAL_EXAMPLES['doc_only'])
|
|
@@ -604,6 +676,7 @@ class TestView(unittest.TestCase):
|
|
|
604
676
|
self.assertEqual(view_from_str, view_from_bytes)
|
|
605
677
|
self.assertEqual(json.loads(view_from_json.serialize()), json.loads(view_from_str.serialize()))
|
|
606
678
|
self.assertEqual(json.loads(view_from_bytes.serialize()), json.loads(view_from_str.serialize()))
|
|
679
|
+
|
|
607
680
|
|
|
608
681
|
def test_annotation_order_preserved(self):
|
|
609
682
|
view_serial = self.view_obj.serialize()
|
|
@@ -827,13 +900,12 @@ class TestAnnotation(unittest.TestCase):
|
|
|
827
900
|
# TODO (angus-lherrou @ 7/27/2020): testing should include validation for required attrs
|
|
828
901
|
# once that is implemented (issue #23)
|
|
829
902
|
def setUp(self) -> None:
|
|
830
|
-
self.data = {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
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']]}
|
|
837
909
|
|
|
838
910
|
def test_annotation_properties(self):
|
|
839
911
|
ann_json = self.data['everything']['annotations'][0]
|
|
@@ -4,14 +4,15 @@ 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
|
|
10
|
+
from tests.mmif_examples import *
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class TestTimeunitHelper(unittest.TestCase):
|
|
12
|
-
|
|
13
14
|
FPS = 30
|
|
14
|
-
|
|
15
|
+
|
|
15
16
|
def test_convert(self):
|
|
16
17
|
self.assertEqual(1000, tuh.convert(1, 's', 'ms', self.FPS))
|
|
17
18
|
self.assertEqual(1.1, tuh.convert(1100, 'ms', 's', self.FPS))
|
|
@@ -35,7 +36,7 @@ class TestVideoDocumentHelper(unittest.TestCase):
|
|
|
35
36
|
})
|
|
36
37
|
self.video_doc.add_property('fps', self.fps)
|
|
37
38
|
self.mmif_obj.add_document(self.video_doc)
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
def test_extract_mid_frame(self):
|
|
40
41
|
tf = self.a_view.new_annotation(AnnotationTypes.TimeFrame, start=100, end=200, timeUnit='frame', document='d1')
|
|
41
42
|
self.assertEqual(150, vdh.get_mid_framenum(self.mmif_obj, tf))
|
|
@@ -92,11 +93,12 @@ class TestVideoDocumentHelper(unittest.TestCase):
|
|
|
92
93
|
s_frame = vdh.second_to_framenum(self.video_doc, 3)
|
|
93
94
|
e_frame = vdh.second_to_framenum(self.video_doc, 5)
|
|
94
95
|
self.assertEqual(1, len(vdh.sample_frames(s_frame, e_frame, 60)))
|
|
95
|
-
|
|
96
|
+
|
|
96
97
|
def test_convert_timepoint(self):
|
|
97
|
-
timepoint_ann = self.a_view.new_annotation(AnnotationTypes.BoundingBox, timePoint=3, timeUnit='second',
|
|
98
|
+
timepoint_ann = self.a_view.new_annotation(AnnotationTypes.BoundingBox, timePoint=3, timeUnit='second',
|
|
99
|
+
document='d1')
|
|
98
100
|
self.assertEqual(vdh.convert(3, 's', 'f', self.fps), vdh.convert_timepoint(self.mmif_obj, timepoint_ann, 'f'))
|
|
99
|
-
|
|
101
|
+
|
|
100
102
|
def test_convert_timeframe(self):
|
|
101
103
|
self.a_view.metadata.new_contain(AnnotationTypes.TimeFrame, timeUnit='frame', document='d1')
|
|
102
104
|
timeframe_ann = self.a_view.new_annotation(AnnotationTypes.TimeFrame, start=100, end=200)
|
|
@@ -105,7 +107,7 @@ class TestVideoDocumentHelper(unittest.TestCase):
|
|
|
105
107
|
|
|
106
108
|
|
|
107
109
|
class TestSequenceHelper(unittest.TestCase):
|
|
108
|
-
|
|
110
|
+
|
|
109
111
|
def test_validate_labelset(self):
|
|
110
112
|
mmif_obj = Mmif(validate=False)
|
|
111
113
|
view = mmif_obj.new_view()
|
|
@@ -172,5 +174,23 @@ class TestSequenceHelper(unittest.TestCase):
|
|
|
172
174
|
sqh.smooth_outlying_short_intervals(scores, 1, 1))
|
|
173
175
|
|
|
174
176
|
|
|
177
|
+
class TestTextDocHelper(unittest.TestCase):
|
|
178
|
+
mmif_obj = Mmif(MMIF_EXAMPLES['everything'])
|
|
179
|
+
|
|
180
|
+
@pytest.mark.skip("The only valid test cases come from kaldi app which annotates wrong property")
|
|
181
|
+
def test_slice_text(self):
|
|
182
|
+
sliced_text_full_overlap = tdh.slice_text(self.mmif_obj, 11500, 14600)
|
|
183
|
+
sliced_text_partial_overlap = tdh.slice_text(self.mmif_obj, 7, 10, unit="seconds")
|
|
184
|
+
no_sliced_text = tdh.slice_text(self.mmif_obj, 0, 5000)
|
|
185
|
+
full_sliced_text = tdh.slice_text(self.mmif_obj, 0, 22, unit="seconds")
|
|
186
|
+
self.assertEqual("In the nineteen eighties ,", sliced_text_full_overlap)
|
|
187
|
+
self.assertEqual("is Jim Lehrer with the NewsHour", sliced_text_partial_overlap)
|
|
188
|
+
self.assertEqual("", no_sliced_text)
|
|
189
|
+
self.assertEqual(
|
|
190
|
+
"Hello , this is Jim Lehrer with the NewsHour on PBS . "
|
|
191
|
+
"In the nineteen eighties , barking dogs have increasingly become a problem in urban areas .",
|
|
192
|
+
full_sliced_text)
|
|
193
|
+
|
|
194
|
+
|
|
175
195
|
if __name__ == '__main__':
|
|
176
196
|
unittest.main()
|
mmif_python-1.0.15/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.0.15
|
|
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
|