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.
Files changed (46) hide show
  1. mmif_python-1.0.16/PKG-INFO +46 -0
  2. mmif_python-1.0.16/VERSION +1 -0
  3. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/serialize/mmif.py +189 -114
  4. mmif_python-1.0.16/mmif/utils/text_document_helper.py +12 -0
  5. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/utils/video_document_helper.py +40 -2
  6. mmif_python-1.0.16/mmif/ver/__init__.py +2 -0
  7. mmif_python-1.0.16/mmif_python.egg-info/PKG-INFO +46 -0
  8. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif_python.egg-info/SOURCES.txt +6 -1
  9. mmif_python-1.0.16/tests/test_serialize.py +1412 -0
  10. mmif_python-1.0.16/tests/test_utils.py +197 -0
  11. mmif_python-1.0.16/tests/test_versioncompat.py +56 -0
  12. mmif_python-1.0.16/tests/test_vocab.py +56 -0
  13. mmif-python-1.0.14/PKG-INFO +0 -37
  14. mmif-python-1.0.14/VERSION +0 -1
  15. mmif-python-1.0.14/mmif/ver/__init__.py +0 -2
  16. mmif-python-1.0.14/mmif_python.egg-info/PKG-INFO +0 -37
  17. {mmif-python-1.0.14 → mmif_python-1.0.16}/LICENSE +0 -0
  18. {mmif-python-1.0.14 → mmif_python-1.0.16}/MANIFEST.in +0 -0
  19. {mmif-python-1.0.14 → mmif_python-1.0.16}/README.md +0 -0
  20. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/__init__.py +0 -0
  21. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/res/__init__.py +0 -0
  22. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/res/clams.vocabulary.yaml +0 -0
  23. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/res/do-not-edit.txt +0 -0
  24. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/res/mmif.json +0 -0
  25. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/serialize/__init__.py +0 -0
  26. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/serialize/annotation.py +0 -0
  27. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/serialize/model.py +0 -0
  28. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/serialize/view.py +0 -0
  29. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/utils/__init__.py +0 -0
  30. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/utils/sequence_helper.py +0 -0
  31. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/utils/timeunit_helper.py +0 -0
  32. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/ver/do-not-edit.txt +0 -0
  33. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/vocabulary/__init__.py +0 -0
  34. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/vocabulary/annotation_types.py +0 -0
  35. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/vocabulary/base_types.py +0 -0
  36. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/vocabulary/do-not-edit.txt +0 -0
  37. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif/vocabulary/document_types.py +0 -0
  38. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif_docloc_http/__init__.py +0 -0
  39. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif_python.egg-info/dependency_links.txt +0 -0
  40. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif_python.egg-info/requires.txt +0 -0
  41. {mmif-python-1.0.14 → mmif_python-1.0.16}/mmif_python.egg-info/top_level.txt +0 -0
  42. {mmif-python-1.0.14 → mmif_python-1.0.16}/requirements.cv +0 -0
  43. {mmif-python-1.0.14 → mmif_python-1.0.16}/requirements.seq +0 -0
  44. {mmif-python-1.0.14 → mmif_python-1.0.16}/requirements.txt +0 -0
  45. {mmif-python-1.0.14 → mmif_python-1.0.16}/setup.cfg +0 -0
  46. {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
- # pytype: disable=bad-return-type
564
- def __getitem__(self, item: str) -> Union[Document, View, Annotation]:
728
+ def __getitem__(self, item: str) \
729
+ -> Union[Document, View, Annotation, MmifMetadata, DocumentsList, ViewsList]:
565
730
  """
566
- getitem implementation for Mmif. When nothing is found, this will raise an error
567
- rather than returning a None (although pytype doesn't think so...)
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: the search string, a document ID, a view ID, or a view-scoped annotation ID
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
- document_result = self.documents.get(split_attempt[0])
578
- view_result = self.views.get(split_attempt[0])
745
+ found = []
579
746
 
580
747
  if len(split_attempt) == 1:
581
- anno_result = None
582
- elif view_result:
583
- anno_result = view_result[split_attempt[1]]
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 view_result and document_result:
760
+ if len(found) > 1:
588
761
  raise KeyError("Ambiguous ID search result")
589
- if not (view_result or document_result):
762
+ elif len(found) == 0:
590
763
  raise KeyError("ID not found: %s" % item)
591
- return anno_result or view_result or document_result
592
- # pytype: enable=bad-return-type
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 > video_document.get_property(FRAMECOUNT_DOCPROP_KEY):
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,2 @@
1
+ __version__ = "1.0.16"
2
+ __specver__ = "1.0.4"
@@ -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