mmif-python 1.0.14__tar.gz → 1.0.16__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/PKG-INFO +46 -0
- mmif_python-1.0.16/VERSION +1 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/serialize/mmif.py +189 -114
- mmif_python-1.0.16/mmif/utils/text_document_helper.py +12 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/utils/video_document_helper.py +40 -2
- mmif_python-1.0.16/mmif/ver/__init__.py +2 -0
- mmif_python-1.0.16/mmif_python.egg-info/PKG-INFO +46 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif_python.egg-info/SOURCES.txt +6 -1
- mmif_python-1.0.16/tests/test_serialize.py +1412 -0
- mmif_python-1.0.16/tests/test_utils.py +197 -0
- mmif_python-1.0.16/tests/test_versioncompat.py +56 -0
- mmif_python-1.0.16/tests/test_vocab.py +56 -0
- mmif-python-1.0.14/PKG-INFO +0 -37
- mmif-python-1.0.14/VERSION +0 -1
- mmif-python-1.0.14/mmif/ver/__init__.py +0 -2
- mmif-python-1.0.14/mmif_python.egg-info/PKG-INFO +0 -37
- {mmif-python-1.0.14 → mmif_python-1.0.16}/LICENSE +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/MANIFEST.in +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/README.md +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/__init__.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/res/__init__.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/res/clams.vocabulary.yaml +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/res/do-not-edit.txt +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/res/mmif.json +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/serialize/__init__.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/serialize/annotation.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/serialize/model.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/serialize/view.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/utils/__init__.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/utils/sequence_helper.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/utils/timeunit_helper.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/ver/do-not-edit.txt +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/vocabulary/__init__.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/vocabulary/annotation_types.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/vocabulary/base_types.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/vocabulary/do-not-edit.txt +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/vocabulary/document_types.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif_docloc_http/__init__.py +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif_python.egg-info/requires.txt +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/requirements.cv +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/requirements.seq +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/requirements.txt +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/setup.cfg +0 -0
- {mmif-python-1.0.14 → mmif_python-1.0.16}/setup.py +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mmif-python
|
|
3
|
+
Version: 1.0.16
|
|
4
|
+
Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
|
|
5
|
+
Home-page: https://mmif.clams.ai
|
|
6
|
+
Author: Brandeis Lab for Linguistics and Computation
|
|
7
|
+
Author-email: admin@clams.ai
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: deepdiff>5
|
|
16
|
+
Requires-Dist: jsonschema
|
|
17
|
+
Provides-Extra: seq
|
|
18
|
+
Requires-Dist: numpy; extra == "seq"
|
|
19
|
+
Provides-Extra: cv
|
|
20
|
+
Requires-Dist: pillow; extra == "cv"
|
|
21
|
+
Requires-Dist: opencv-python; extra == "cv"
|
|
22
|
+
Requires-Dist: ffmpeg-python; extra == "cv"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-pep8; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
27
|
+
Requires-Dist: pytype; extra == "dev"
|
|
28
|
+
|
|
29
|
+
## MultiMedia Interchange Format
|
|
30
|
+
[MMIF](https://mmif.clams.ai) is a JSON(-LD)-based data format designed for transferring annotation data between computational analysis applications in [CLAMS project](https://clams.ai).
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## mmif-python
|
|
34
|
+
`mmif-python` is a Python implementation of the MMIF data format.
|
|
35
|
+
`mmif-python` provides various helper classes and functions to handle MMIF JSON in Python,
|
|
36
|
+
including ;
|
|
37
|
+
|
|
38
|
+
1. de-/serialization of MMIF internal data structures to/from JSON
|
|
39
|
+
2. validation of MMIF JSON
|
|
40
|
+
3. handling of CLAMS vocabulary types
|
|
41
|
+
4. navigation of MMIF object via various "search" methods (e.g. `mmif.get_all_views_contain(vocab_type))`)
|
|
42
|
+
|
|
43
|
+
## For more ...
|
|
44
|
+
* [Version history and patch notes](https://github.com/clamsproject/mmif-python/blob/main/CHANGELOG.md)
|
|
45
|
+
* [MMIF Python API documentation](https://clamsproject.github.io/mmif-python)
|
|
46
|
+
* [MMIF JSON specification and schema](https://clamsproject.github.io/mmif)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.0.16
|
|
@@ -10,7 +10,7 @@ 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, Tuple
|
|
14
14
|
|
|
15
15
|
import jsonschema.validators
|
|
16
16
|
|
|
@@ -24,6 +24,104 @@ from .view import View
|
|
|
24
24
|
__all__ = ['Mmif']
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class MmifMetadata(MmifObject):
|
|
28
|
+
"""
|
|
29
|
+
Basic MmifObject class to contain the top-level metadata of a MMIF file.
|
|
30
|
+
|
|
31
|
+
:param metadata_obj: the JSON data
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, metadata_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
35
|
+
# TODO (krim @ 10/7/20): there could be a better name and a better way to give a value to this
|
|
36
|
+
self.mmif: str = f"http://mmif.clams.ai/{mmif.__specver__}"
|
|
37
|
+
self._required_attributes = ["mmif"]
|
|
38
|
+
super().__init__(metadata_obj)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class DocumentsList(DataList[Document]):
|
|
42
|
+
"""
|
|
43
|
+
DocumentsList object that implements :class:`mmif.serialize.model.DataList`
|
|
44
|
+
for :class:`mmif.serialize.document.Document`.
|
|
45
|
+
"""
|
|
46
|
+
_items: Dict[str, Document]
|
|
47
|
+
|
|
48
|
+
def _deserialize(self, input_list: list) -> None: # pytype: disable=signature-mismatch
|
|
49
|
+
"""
|
|
50
|
+
Extends base ``_deserialize`` method to initialize ``items`` as a dict from
|
|
51
|
+
document IDs to :class:`mmif.serialize.document.Document` objects.
|
|
52
|
+
|
|
53
|
+
:param input_list: the JSON data that defines the list of documents
|
|
54
|
+
:return: None
|
|
55
|
+
"""
|
|
56
|
+
self._items = {item['properties']['id']: Document(item) for item in input_list}
|
|
57
|
+
|
|
58
|
+
def append(self, value: Document, overwrite=False) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Appends a document to the list.
|
|
61
|
+
|
|
62
|
+
Fails if there is already a document with the same ID
|
|
63
|
+
in the list, unless ``overwrite`` is set to True.
|
|
64
|
+
|
|
65
|
+
:param value: the :class:`mmif.serialize.document.Document`
|
|
66
|
+
object to add
|
|
67
|
+
:param overwrite: if set to True, will overwrite an
|
|
68
|
+
existing document with the same ID
|
|
69
|
+
:raises KeyError: if ``overwrite`` is set to False and
|
|
70
|
+
a document with the same ID exists
|
|
71
|
+
in the list
|
|
72
|
+
:return: None
|
|
73
|
+
"""
|
|
74
|
+
super()._append_with_key(value.id, value, overwrite)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ViewsList(DataList[View]):
|
|
78
|
+
"""
|
|
79
|
+
ViewsList object that implements :class:`mmif.serialize.model.DataList`
|
|
80
|
+
for :class:`mmif.serialize.view.View`.
|
|
81
|
+
"""
|
|
82
|
+
_items: Dict[str, View]
|
|
83
|
+
|
|
84
|
+
def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None):
|
|
85
|
+
super().__init__(mmif_obj)
|
|
86
|
+
|
|
87
|
+
def _deserialize(self, input_list: list) -> None: # pytype: disable=signature-mismatch
|
|
88
|
+
"""
|
|
89
|
+
Extends base ``_deserialize`` method to initialize ``items`` as a dict from
|
|
90
|
+
view IDs to :class:`mmif.serialize.view.View` objects.
|
|
91
|
+
|
|
92
|
+
:param input_list: the JSON data that defines the list of views
|
|
93
|
+
:return: None
|
|
94
|
+
"""
|
|
95
|
+
if input_list:
|
|
96
|
+
self._items = {item['id']: View(item) for item in input_list}
|
|
97
|
+
|
|
98
|
+
def append(self, value: View, overwrite=False) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Appends a view to the list.
|
|
101
|
+
|
|
102
|
+
Fails if there is already a view with the same ID
|
|
103
|
+
in the list, unless ``overwrite`` is set to True.
|
|
104
|
+
|
|
105
|
+
:param value: the :class:`mmif.serialize.view.View`
|
|
106
|
+
object to add
|
|
107
|
+
:param overwrite: if set to True, will overwrite an
|
|
108
|
+
existing view with the same ID
|
|
109
|
+
:raises KeyError: if ``overwrite`` is set to False and
|
|
110
|
+
a view with the same ID exists
|
|
111
|
+
in the list
|
|
112
|
+
:return: None
|
|
113
|
+
"""
|
|
114
|
+
super()._append_with_key(value.id, value, overwrite)
|
|
115
|
+
|
|
116
|
+
def get_last(self) -> Optional[View]:
|
|
117
|
+
"""
|
|
118
|
+
Returns the last view appended to the list.
|
|
119
|
+
"""
|
|
120
|
+
for view in reversed(self._items.values()):
|
|
121
|
+
if 'error' not in view.metadata and 'warning' not in view.metadata:
|
|
122
|
+
return view
|
|
123
|
+
|
|
124
|
+
|
|
27
125
|
class Mmif(MmifObject):
|
|
28
126
|
"""
|
|
29
127
|
MmifObject that represents a full MMIF file.
|
|
@@ -511,7 +609,74 @@ class Mmif(MmifObject):
|
|
|
511
609
|
if at_types in view.metadata.contains:
|
|
512
610
|
return view
|
|
513
611
|
return None
|
|
514
|
-
|
|
612
|
+
|
|
613
|
+
def _is_in_time_between(self, start: Union[int, float], end: Union[int, float], annotation: Annotation) -> bool:
|
|
614
|
+
s, e = self.get_start(annotation), self.get_end(annotation)
|
|
615
|
+
return (s < start < e) or (s > start and e < end) or (s < end < e)
|
|
616
|
+
|
|
617
|
+
def _handle_time_unit(self, input_unit: str, ann_unit: str,
|
|
618
|
+
start: int, end: int) -> Tuple[Union[int, float, str], Union[int, float, str]]:
|
|
619
|
+
from mmif.utils.timeunit_helper import convert
|
|
620
|
+
start = convert(start, input_unit, ann_unit, 1)
|
|
621
|
+
end = convert(end, input_unit, ann_unit, 1)
|
|
622
|
+
return start, end
|
|
623
|
+
|
|
624
|
+
def get_annotations_between_time(self, start: int, end: int, time_unit: str = "milliseconds") -> Iterator[Annotation]:
|
|
625
|
+
"""
|
|
626
|
+
Version: 1.0
|
|
627
|
+
Returns all 'Token' annotations aligned with 'TimeFrame' annotations sorted by start time within start and end time
|
|
628
|
+
Note: this function only works for mmif object obtained from Whisper-wrapper
|
|
629
|
+
|
|
630
|
+
:param start: the start time
|
|
631
|
+
:param end: the end time
|
|
632
|
+
:param time_unit: the time unit, either string "milliseconds" or "seconds", defaults to "milliseconds"
|
|
633
|
+
:return: a generator of 'Token' annotations
|
|
634
|
+
"""
|
|
635
|
+
assert start <= end, "Start time must be less than end time"
|
|
636
|
+
assert start >= 0, "Start time must be greater than or equal to zero"
|
|
637
|
+
assert end >= 0, "End time must be greater than or equal to zero"
|
|
638
|
+
# 0. Initialize container and helper method
|
|
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
|
|
679
|
+
|
|
515
680
|
def _get_linear_anchor_point(self, ann: Annotation, targets_sorted=False, start: bool = True) -> Union[int, float]:
|
|
516
681
|
# TODO (krim @ 2/5/24): Update the return type once timeunits are unified to `ms` as integers (https://github.com/clamsproject/mmif/issues/192)
|
|
517
682
|
"""
|
|
@@ -560,131 +725,41 @@ class Mmif(MmifObject):
|
|
|
560
725
|
"""
|
|
561
726
|
return self._get_linear_anchor_point(annotation, start=False)
|
|
562
727
|
|
|
563
|
-
|
|
564
|
-
|
|
728
|
+
def __getitem__(self, item: str) \
|
|
729
|
+
-> Union[Document, View, Annotation, MmifMetadata, DocumentsList, ViewsList]:
|
|
565
730
|
"""
|
|
566
|
-
getitem implementation for Mmif.
|
|
567
|
-
rather than returning a None
|
|
731
|
+
getitem implementation for Mmif. This will try to find any object, given an identifier or an immediate
|
|
732
|
+
attribute name. When nothing is found, this will raise an error rather than returning a None
|
|
568
733
|
|
|
569
734
|
:raises KeyError: if the item is not found or if the search results are ambiguous
|
|
570
|
-
:param item:
|
|
735
|
+
:param item: an attribute name or an object identifier (a document ID, a view ID, or an annotation ID). When
|
|
736
|
+
annotation ID is given as a "short" ID (without view ID prefix), the method will try to find a
|
|
737
|
+
match from the first view, and return immediately if found.
|
|
571
738
|
:return: the object searched for
|
|
739
|
+
:raise KeyError: if the item is not found or multiple objects are found with the same ID
|
|
572
740
|
"""
|
|
573
741
|
if item in self._named_attributes():
|
|
574
742
|
return self.__dict__[item]
|
|
575
743
|
split_attempt = item.split(self.id_delimiter)
|
|
576
744
|
|
|
577
|
-
|
|
578
|
-
view_result = self.views.get(split_attempt[0])
|
|
745
|
+
found = []
|
|
579
746
|
|
|
580
747
|
if len(split_attempt) == 1:
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
748
|
+
found.append(self.documents.get(split_attempt[0]))
|
|
749
|
+
found.append(self.views.get(split_attempt[0]))
|
|
750
|
+
for view in self.views:
|
|
751
|
+
found.append(view.annotations.get(split_attempt[0]))
|
|
752
|
+
elif len(split_attempt) == 2:
|
|
753
|
+
v = self.get_view_by_id(split_attempt[0])
|
|
754
|
+
if v is not None:
|
|
755
|
+
found.append(v.annotations.get(split_attempt[1]))
|
|
584
756
|
else:
|
|
585
757
|
raise KeyError("Tried to subscript into a view that doesn't exist")
|
|
758
|
+
found = [x for x in found if x is not None]
|
|
586
759
|
|
|
587
|
-
if
|
|
760
|
+
if len(found) > 1:
|
|
588
761
|
raise KeyError("Ambiguous ID search result")
|
|
589
|
-
|
|
762
|
+
elif len(found) == 0:
|
|
590
763
|
raise KeyError("ID not found: %s" % item)
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
class MmifMetadata(MmifObject):
|
|
596
|
-
"""
|
|
597
|
-
Basic MmifObject class to contain the top-level metadata of a MMIF file.
|
|
598
|
-
|
|
599
|
-
:param metadata_obj: the JSON data
|
|
600
|
-
"""
|
|
601
|
-
|
|
602
|
-
def __init__(self, metadata_obj: Optional[Union[bytes, str, dict]] = None) -> None:
|
|
603
|
-
# TODO (krim @ 10/7/20): there could be a better name and a better way to give a value to this
|
|
604
|
-
self.mmif: str = f"http://mmif.clams.ai/{mmif.__specver__}"
|
|
605
|
-
self._required_attributes = ["mmif"]
|
|
606
|
-
super().__init__(metadata_obj)
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
class DocumentsList(DataList[Document]):
|
|
610
|
-
"""
|
|
611
|
-
DocumentsList object that implements :class:`mmif.serialize.model.DataList`
|
|
612
|
-
for :class:`mmif.serialize.document.Document`.
|
|
613
|
-
"""
|
|
614
|
-
_items: Dict[str, Document]
|
|
615
|
-
|
|
616
|
-
def _deserialize(self, input_list: list) -> None: # pytype: disable=signature-mismatch
|
|
617
|
-
"""
|
|
618
|
-
Extends base ``_deserialize`` method to initialize ``items`` as a dict from
|
|
619
|
-
document IDs to :class:`mmif.serialize.document.Document` objects.
|
|
620
|
-
|
|
621
|
-
:param input_list: the JSON data that defines the list of documents
|
|
622
|
-
:return: None
|
|
623
|
-
"""
|
|
624
|
-
self._items = {item['properties']['id']: Document(item) for item in input_list}
|
|
625
|
-
|
|
626
|
-
def append(self, value: Document, overwrite=False) -> None:
|
|
627
|
-
"""
|
|
628
|
-
Appends a document to the list.
|
|
629
|
-
|
|
630
|
-
Fails if there is already a document with the same ID
|
|
631
|
-
in the list, unless ``overwrite`` is set to True.
|
|
632
|
-
|
|
633
|
-
:param value: the :class:`mmif.serialize.document.Document`
|
|
634
|
-
object to add
|
|
635
|
-
:param overwrite: if set to True, will overwrite an
|
|
636
|
-
existing document with the same ID
|
|
637
|
-
:raises KeyError: if ``overwrite`` is set to False and
|
|
638
|
-
a document with the same ID exists
|
|
639
|
-
in the list
|
|
640
|
-
:return: None
|
|
641
|
-
"""
|
|
642
|
-
super()._append_with_key(value.id, value, overwrite)
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
class ViewsList(DataList[View]):
|
|
646
|
-
"""
|
|
647
|
-
ViewsList object that implements :class:`mmif.serialize.model.DataList`
|
|
648
|
-
for :class:`mmif.serialize.view.View`.
|
|
649
|
-
"""
|
|
650
|
-
_items: Dict[str, View]
|
|
651
|
-
|
|
652
|
-
def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None):
|
|
653
|
-
super().__init__(mmif_obj)
|
|
654
|
-
|
|
655
|
-
def _deserialize(self, input_list: list) -> None: # pytype: disable=signature-mismatch
|
|
656
|
-
"""
|
|
657
|
-
Extends base ``_deserialize`` method to initialize ``items`` as a dict from
|
|
658
|
-
view IDs to :class:`mmif.serialize.view.View` objects.
|
|
659
|
-
|
|
660
|
-
:param input_list: the JSON data that defines the list of views
|
|
661
|
-
:return: None
|
|
662
|
-
"""
|
|
663
|
-
if input_list:
|
|
664
|
-
self._items = {item['id']: View(item) for item in input_list}
|
|
665
|
-
|
|
666
|
-
def append(self, value: View, overwrite=False) -> None:
|
|
667
|
-
"""
|
|
668
|
-
Appends a view to the list.
|
|
669
|
-
|
|
670
|
-
Fails if there is already a view with the same ID
|
|
671
|
-
in the list, unless ``overwrite`` is set to True.
|
|
672
|
-
|
|
673
|
-
:param value: the :class:`mmif.serialize.view.View`
|
|
674
|
-
object to add
|
|
675
|
-
:param overwrite: if set to True, will overwrite an
|
|
676
|
-
existing view with the same ID
|
|
677
|
-
:raises KeyError: if ``overwrite`` is set to False and
|
|
678
|
-
a view with the same ID exists
|
|
679
|
-
in the list
|
|
680
|
-
:return: None
|
|
681
|
-
"""
|
|
682
|
-
super()._append_with_key(value.id, value, overwrite)
|
|
683
|
-
|
|
684
|
-
def get_last(self) -> Optional[View]:
|
|
685
|
-
"""
|
|
686
|
-
Returns the last view appended to the list.
|
|
687
|
-
"""
|
|
688
|
-
for view in reversed(self._items.values()):
|
|
689
|
-
if 'error' not in view.metadata and 'warning' not in view.metadata:
|
|
690
|
-
return view
|
|
764
|
+
else:
|
|
765
|
+
return found[-1]
|
|
@@ -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)
|
|
@@ -6,7 +6,7 @@ import math
|
|
|
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, AnnotationTypes
|
|
10
10
|
|
|
11
11
|
for cv_dep in ('cv2', 'ffmpeg', 'PIL'):
|
|
12
12
|
try:
|
|
@@ -83,14 +83,16 @@ def extract_frames_as_images(video_document: Document, framenums: List[int], as_
|
|
|
83
83
|
frames = []
|
|
84
84
|
video = capture(video_document)
|
|
85
85
|
cur_f = 0
|
|
86
|
+
tot_fcount = video_document.get_property(FRAMECOUNT_DOCPROP_KEY)
|
|
86
87
|
while True:
|
|
87
|
-
if not framenums or cur_f >
|
|
88
|
+
if not framenums or cur_f > tot_fcount:
|
|
88
89
|
break
|
|
89
90
|
ret, frame = video.read()
|
|
90
91
|
if cur_f == framenums[0]:
|
|
91
92
|
if not ret:
|
|
92
93
|
sec = convert(cur_f, 'f', 's', video_document.get_property(FPS_DOCPROP_KEY))
|
|
93
94
|
warnings.warn(f'Frame #{cur_f} ({sec}s) could not be read from the video {video_document.id}.')
|
|
95
|
+
cur_f += 1
|
|
94
96
|
continue
|
|
95
97
|
frames.append(Image.fromarray(frame[:, :, ::-1]) if as_PIL else frame)
|
|
96
98
|
framenums.pop(0)
|
|
@@ -125,6 +127,42 @@ def extract_mid_frame(mmif: Mmif, time_frame: Annotation, as_PIL: bool = False):
|
|
|
125
127
|
return extract_frames_as_images(vd, [get_mid_framenum(mmif, time_frame)], as_PIL=as_PIL)[0]
|
|
126
128
|
|
|
127
129
|
|
|
130
|
+
def get_representative_framenum(mmif: Mmif, time_frame: Annotation):
|
|
131
|
+
"""
|
|
132
|
+
Calculates the representative frame number from an annotation.
|
|
133
|
+
|
|
134
|
+
:param mmif: :py:class:`~mmif.serialize.mmif.Mmif` instance
|
|
135
|
+
:param time_frame: :py:class:`~mmif.serialize.annotation.Annotation` instance that holds a time interval annotation containing a `representatives` property (``"@type": ".../TimeFrame/..."``)
|
|
136
|
+
:return: representative frame number as an integer
|
|
137
|
+
"""
|
|
138
|
+
if 'representatives' not in time_frame.properties:
|
|
139
|
+
raise ValueError(f'The time frame {time_frame.id} does not have a representative.')
|
|
140
|
+
timeunit = time_frame.get_property('timeUnit')
|
|
141
|
+
video_document = mmif[time_frame.get_property('document')]
|
|
142
|
+
fps = get_framerate(video_document)
|
|
143
|
+
representatives = time_frame.get_property('representatives')
|
|
144
|
+
top_representative_id = representatives[0]
|
|
145
|
+
try:
|
|
146
|
+
representative_timepoint_anno = mmif[time_frame._parent_view_id+time_frame.id_delimiter+top_representative_id]
|
|
147
|
+
except KeyError:
|
|
148
|
+
raise ValueError(f'Representative timepoint {top_representative_id} not found in any view.')
|
|
149
|
+
return convert(representative_timepoint_anno.get_property('timePoint'), timeunit, 'frame', fps)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def extract_representative_frame(mmif: Mmif, time_frame: Annotation, as_PIL: bool = False):
|
|
153
|
+
"""
|
|
154
|
+
Extracts the representative frame of an annotation as a numpy ndarray or PIL Image.
|
|
155
|
+
|
|
156
|
+
:param mmif: :py:class:`~mmif.serialize.mmif.Mmif` instance
|
|
157
|
+
:param time_frame: :py:class:`~mmif.serialize.annotation.Annotation` instance that holds a time interval annotation (``"@type": ".../TimeFrame/..."``)
|
|
158
|
+
:param as_PIL: return :py:class:`~PIL.Image.Image` instead of :py:class:`~numpy.ndarray`
|
|
159
|
+
:return: frame as a :py:class:`numpy.ndarray` or :py:class:`PIL.Image.Image`
|
|
160
|
+
"""
|
|
161
|
+
video_document = mmif[time_frame.get_property('document')]
|
|
162
|
+
rep_frame_num = get_representative_framenum(mmif, time_frame)
|
|
163
|
+
return extract_frames_as_images(video_document, [rep_frame_num], as_PIL=as_PIL)[0]
|
|
164
|
+
|
|
165
|
+
|
|
128
166
|
def sample_frames(start_frame: int, end_frame: int, sample_rate: float = 1) -> List[int]:
|
|
129
167
|
"""
|
|
130
168
|
Helper function to sample frames from a time interval.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: mmif-python
|
|
3
|
+
Version: 1.0.16
|
|
4
|
+
Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
|
|
5
|
+
Home-page: https://mmif.clams.ai
|
|
6
|
+
Author: Brandeis Lab for Linguistics and Computation
|
|
7
|
+
Author-email: admin@clams.ai
|
|
8
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: deepdiff>5
|
|
16
|
+
Requires-Dist: jsonschema
|
|
17
|
+
Provides-Extra: seq
|
|
18
|
+
Requires-Dist: numpy; extra == "seq"
|
|
19
|
+
Provides-Extra: cv
|
|
20
|
+
Requires-Dist: pillow; extra == "cv"
|
|
21
|
+
Requires-Dist: opencv-python; extra == "cv"
|
|
22
|
+
Requires-Dist: ffmpeg-python; extra == "cv"
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest; extra == "dev"
|
|
25
|
+
Requires-Dist: pytest-pep8; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
27
|
+
Requires-Dist: pytype; extra == "dev"
|
|
28
|
+
|
|
29
|
+
## MultiMedia Interchange Format
|
|
30
|
+
[MMIF](https://mmif.clams.ai) is a JSON(-LD)-based data format designed for transferring annotation data between computational analysis applications in [CLAMS project](https://clams.ai).
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
## mmif-python
|
|
34
|
+
`mmif-python` is a Python implementation of the MMIF data format.
|
|
35
|
+
`mmif-python` provides various helper classes and functions to handle MMIF JSON in Python,
|
|
36
|
+
including ;
|
|
37
|
+
|
|
38
|
+
1. de-/serialization of MMIF internal data structures to/from JSON
|
|
39
|
+
2. validation of MMIF JSON
|
|
40
|
+
3. handling of CLAMS vocabulary types
|
|
41
|
+
4. navigation of MMIF object via various "search" methods (e.g. `mmif.get_all_views_contain(vocab_type))`)
|
|
42
|
+
|
|
43
|
+
## For more ...
|
|
44
|
+
* [Version history and patch notes](https://github.com/clamsproject/mmif-python/blob/main/CHANGELOG.md)
|
|
45
|
+
* [MMIF Python API documentation](https://clamsproject.github.io/mmif-python)
|
|
46
|
+
* [MMIF JSON specification and schema](https://clamsproject.github.io/mmif)
|
|
@@ -18,6 +18,7 @@ mmif/serialize/model.py
|
|
|
18
18
|
mmif/serialize/view.py
|
|
19
19
|
mmif/utils/__init__.py
|
|
20
20
|
mmif/utils/sequence_helper.py
|
|
21
|
+
mmif/utils/text_document_helper.py
|
|
21
22
|
mmif/utils/timeunit_helper.py
|
|
22
23
|
mmif/utils/video_document_helper.py
|
|
23
24
|
mmif/ver/__init__.py
|
|
@@ -32,4 +33,8 @@ mmif_python.egg-info/PKG-INFO
|
|
|
32
33
|
mmif_python.egg-info/SOURCES.txt
|
|
33
34
|
mmif_python.egg-info/dependency_links.txt
|
|
34
35
|
mmif_python.egg-info/requires.txt
|
|
35
|
-
mmif_python.egg-info/top_level.txt
|
|
36
|
+
mmif_python.egg-info/top_level.txt
|
|
37
|
+
tests/test_serialize.py
|
|
38
|
+
tests/test_utils.py
|
|
39
|
+
tests/test_versioncompat.py
|
|
40
|
+
tests/test_vocab.py
|