mmif-python 1.0.8__tar.gz → 1.0.10__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 (38) hide show
  1. {mmif-python-1.0.8/mmif_python.egg-info → mmif-python-1.0.10}/PKG-INFO +1 -1
  2. mmif-python-1.0.10/VERSION +1 -0
  3. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/res/clams.vocabulary.yaml +61 -5
  4. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/res/mmif.json +5 -2
  5. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/serialize/annotation.py +88 -30
  6. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/serialize/mmif.py +42 -1
  7. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/serialize/view.py +5 -2
  8. mmif-python-1.0.10/mmif/utils/timeunit_helper.py +84 -0
  9. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/utils/video_document_helper.py +6 -61
  10. mmif-python-1.0.10/mmif/ver/__init__.py +2 -0
  11. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/vocabulary/annotation_types.py +23 -14
  12. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/vocabulary/base_types.py +2 -2
  13. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/vocabulary/document_types.py +2 -2
  14. mmif-python-1.0.10/mmif_docloc_http/__init__.py +12 -0
  15. {mmif-python-1.0.8 → mmif-python-1.0.10/mmif_python.egg-info}/PKG-INFO +1 -1
  16. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif_python.egg-info/SOURCES.txt +2 -0
  17. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif_python.egg-info/requires.txt +0 -1
  18. mmif-python-1.0.10/mmif_python.egg-info/top_level.txt +2 -0
  19. {mmif-python-1.0.8 → mmif-python-1.0.10}/setup.py +14 -11
  20. mmif-python-1.0.8/VERSION +0 -1
  21. mmif-python-1.0.8/mmif/ver/__init__.py +0 -2
  22. mmif-python-1.0.8/mmif_python.egg-info/top_level.txt +0 -1
  23. {mmif-python-1.0.8 → mmif-python-1.0.10}/LICENSE +0 -0
  24. {mmif-python-1.0.8 → mmif-python-1.0.10}/MANIFEST.in +0 -0
  25. {mmif-python-1.0.8 → mmif-python-1.0.10}/README.md +0 -0
  26. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/__init__.py +0 -0
  27. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/res/__init__.py +0 -0
  28. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/res/do-not-edit.txt +0 -0
  29. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/serialize/__init__.py +0 -0
  30. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/serialize/model.py +0 -0
  31. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/utils/__init__.py +0 -0
  32. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/ver/do-not-edit.txt +0 -0
  33. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/vocabulary/__init__.py +0 -0
  34. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif/vocabulary/do-not-edit.txt +0 -0
  35. {mmif-python-1.0.8 → mmif-python-1.0.10}/mmif_python.egg-info/dependency_links.txt +0 -0
  36. {mmif-python-1.0.8 → mmif-python-1.0.10}/requirements.cv +0 -0
  37. {mmif-python-1.0.8 → mmif-python-1.0.10}/requirements.txt +0 -0
  38. {mmif-python-1.0.8 → mmif-python-1.0.10}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mmif-python
3
- Version: 1.0.8
3
+ Version: 1.0.10
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.10
@@ -37,13 +37,57 @@ metadata:
37
37
  defined either at the metadata level, in which case it has scope over all
38
38
  annotations of the same type in a view, or at the instance level, in which
39
39
  it has scope over just the single annotation.
40
+ labelset:
41
+ type: List of Strings
42
+ description: >-
43
+ When an annotation object contains results of a classification task,
44
+ this metadata is used to specify the label values used in classification.
45
+ Individual annotations then must have <code>label</code> property that
46
+ is one of the values in this list.
47
+ <br><br>
48
+ [Note] Annotations from a classifier app must have this metadata or <code>
49
+ labelsetUri </code> metadata.
50
+ <br><br>
51
+ [Note] Not all of labels specified
52
+ in the <code>labelset</code> must occur in the output annotations. For
53
+ example, a <code>labelset</code> can contain a <i>catch-all</i> negative
54
+ label, but if the negative label can be not interesting enough to keep in
55
+ the output annotation.
56
+ labelsetUri:
57
+ type: String
58
+ description: >-
59
+ A URI to an externally defined labelset. Since the <code>labelset</code>
60
+ metadata is a list of simple strings, this URI can be used to point to a
61
+ more detailed definition of the labelset. This can be a JSON-LD document
62
+ or a SKOS concept scheme, for example.
63
+ <br><br>
64
+ [Note] Annotations from a classifier app must have this metadata or <code>
65
+ labelset </code> metadata.
40
66
 
41
67
  properties:
42
68
  document:
43
69
  type: ID
44
70
  description: >-
45
71
  The identifier of the document that the annotation is over.
46
-
72
+ label:
73
+ type: String
74
+ description: >-
75
+ A label given to this object by a classifier. The value must be a simple
76
+ string value of the label and must be one of the values defined in the
77
+ <code>labelset</code> or <code>labelsetUri</code> annotation metadata.
78
+ <br><br>
79
+ [Note] Annotations from a classifier app must have this property.
80
+ classifications:
81
+ type: Map from String to Number
82
+ description: >-
83
+ A map from label values to their "score" numbers provided by a classifier.
84
+ The score can be probability, similarity, confidence, or any other real
85
+ number that was used to determine the label value.
86
+ <br><br>
87
+ [Optional] on top of the <code>label</code> property. However when this
88
+ property is used, the <code>label</code> property must be one of the keys
89
+ and the keys must match to the values defined in the <code>labelset</code>
90
+ or <code>labelsetUri</code> annotation metadata.
47
91
 
48
92
  ---
49
93
 
@@ -117,7 +161,8 @@ properties:
117
161
  name: Span
118
162
  parent: Interval
119
163
 
120
- similarTo: http://vocab.lappsgrid.org/Region
164
+ similarTo:
165
+ - http://vocab.lappsgrid.org/Region
121
166
 
122
167
  description: >-
123
168
  An annotation over a region in primary text data. A Span may be defined by
@@ -134,10 +179,17 @@ description: >-
134
179
  segment used in audio processing, but that term has a different meaning in the
135
180
  image and video community.
136
181
 
182
+
137
183
  properties:
138
184
  frameType:
139
185
  type: String
140
- description: The type of TimeFrame. Could be bars-and-tones, speech, noise, music, other.
186
+ description: The type of TimeFrame. Possible values include, but are not
187
+ limited to, bars, tones, bars-and-tones, speech, noise, music, slate,
188
+ chyron, lower-third, credits, and other.
189
+ <br><br>
190
+ No longer encouraged to use, instead <code>label</code> property
191
+ should replace this property.
192
+
141
193
 
142
194
  ---
143
195
 
@@ -186,7 +238,11 @@ description: >-
186
238
  properties:
187
239
  boxType:
188
240
  type: String
189
- description: The type of BoundingBox. Mostly used for text boxes where we use the value text.
241
+ description: >-
242
+ The type of BoundingBox. Mostly used for text boxes where we use the value text.
243
+ <br><br>
244
+ No longer encouraged to use, and instead <code>label</code> property
245
+ should replace this property.
190
246
 
191
247
  ---
192
248
 
@@ -197,7 +253,7 @@ description: >-
197
253
  A sequence of Polygons, where each Polygon is associated with a
198
254
  TimePoint. So a VideoObject is in effect a sequence of image objects at
199
255
  certain time points.
200
-
256
+
201
257
  properties:
202
258
  polygons:
203
259
  type: List of IDs
@@ -32,7 +32,10 @@
32
32
  "type": "object",
33
33
  "patternProperties": {
34
34
  ".+": {
35
- "type": "string"
35
+ "anyOf": [
36
+ {"type": "string"},
37
+ {"type": "array", "items": { "type": "string" }}
38
+ ]
36
39
  }
37
40
  }
38
41
  },
@@ -66,7 +69,7 @@
66
69
  "additionalProperties": false,
67
70
  "patternProperties": {
68
71
  "^https?:\/\/": {
69
- "$ref": "#/definitions/strStrMap"
72
+ "type": "object"
70
73
  }
71
74
  }
72
75
  },
@@ -19,16 +19,26 @@ from urllib.parse import urlparse
19
19
 
20
20
  from mmif.vocabulary import ThingTypesBase, DocumentTypesBase
21
21
  from .model import MmifObject, JSON_PRMTV_TYPES
22
+ from .. import DocumentTypes, AnnotationTypes
23
+ import mmif_docloc_http
22
24
 
23
25
  __all__ = ['Annotation', 'AnnotationProperties', 'Document', 'DocumentProperties', 'Text']
24
26
 
25
27
  T = TypeVar('T')
28
+ LIST_PRMTV = typing.List[JSON_PRMTV_TYPES] # list of values (most cases for annotation props)
29
+ LIST_LIST_PRMTV = typing.List[LIST_PRMTV] # list of list of values (e.g. for coordinates)
30
+ DICT_PRMTV = typing.Dict[str, JSON_PRMTV_TYPES] # dict of values (`text` prop of `TextDocument` and other complex props)
31
+ DICT_LIST_PRMTV = typing.Dict[str, LIST_PRMTV] # dict of list of values (even more complex props)
26
32
 
27
- from .. import DocumentTypes, AnnotationTypes
28
33
 
34
+ # some built-in document location helpers
29
35
  discovered_docloc_plugins = {
30
- name[len('mmif_docloc_'):]: importlib.import_module(name) for _, name, _ in pkgutil.iter_modules() if re.match(r'mmif[-_]docloc[-_]', name)
36
+ 'http': mmif_docloc_http,
37
+ 'https': mmif_docloc_http
31
38
  }
39
+ discovered_docloc_plugins.update({
40
+ name[len('mmif_docloc_'):]: importlib.import_module(name) for _, name, _ in pkgutil.iter_modules() if re.match(r'mmif[-_]docloc[-_]', name)
41
+ })
32
42
 
33
43
 
34
44
  class Annotation(MmifObject):
@@ -56,7 +66,32 @@ class Annotation(MmifObject):
56
66
  # here is the place to parse formatted IDs and store prefixes in the parent mmif object.
57
67
  # (see https://github.com/clamsproject/mmif/issues/64#issuecomment-849241309 for discussion)
58
68
  super()._deserialize(input_dict)
59
-
69
+ for k, v in self.properties.items():
70
+ self._add_prop_aliases(k, v)
71
+
72
+ def _add_prop_aliases(self, key_to_add, val_to_add):
73
+ """
74
+ Method to handle aliases of the same property.
75
+ Annotation property aliases were first introduced in MMIF 1.0.2,
76
+ with addition of general `label` property to all `Annotation`
77
+ subtypes, and effectively deprecated `frameType` and `boxType`
78
+ in `TimeFrame` and `BoundingBox` respectively.
79
+ """
80
+ prop_aliases = AnnotationTypes.prop_aliases.get(self._type.shortname, {})
81
+ for alias_reprep, alias_group in prop_aliases.items():
82
+ if key_to_add in alias_group:
83
+ for alias in alias_group:
84
+ if alias != key_to_add:
85
+ self._props_ephemeral[alias] = val_to_add
86
+ if alias in self.properties.keys():
87
+ warning_msg = f'Found both "{key_to_add}" and "{alias}" in the properties of "{self.id}" annotation in "{self.parent}" view. '
88
+ if alias == alias_reprep:
89
+ warning_msg += f'However "{key_to_add}" is an alias of "{alias_reprep}".'
90
+ else:
91
+ warning_msg += f'However "{key_to_add}" and "{alias}" are boath aliases of "{alias_reprep}".'
92
+ warning_msg += f'Having two synonyms in the same annotation can cause unexpected behavior. '
93
+ warnings.warn(warning_msg, UserWarning)
94
+
60
95
  def is_type(self, at_type: Union[str, ThingTypesBase]) -> bool:
61
96
  """
62
97
  Check if the @type of this object matches.
@@ -94,30 +129,40 @@ class Annotation(MmifObject):
94
129
 
95
130
  @staticmethod
96
131
  def check_prop_value_is_simple_enough(
97
- value: Union[JSON_PRMTV_TYPES, List[JSON_PRMTV_TYPES], List[List[JSON_PRMTV_TYPES]]]):
132
+ value: Union[JSON_PRMTV_TYPES, LIST_PRMTV, LIST_LIST_PRMTV, DICT_PRMTV, DICT_LIST_PRMTV]) -> bool:
133
+
98
134
  def json_primitives(x):
99
135
  return isinstance(x, typing.get_args(JSON_PRMTV_TYPES))
136
+
137
+ def json_primitives_list(x):
138
+ return isinstance(x, list) and all(map(json_primitives, x))
139
+
140
+ def json_primitives_list_of_list(x):
141
+ return all(map(lambda elem: isinstance(elem, list), x) and map(json_primitives, [subelem for elem in x for subelem in elem]))
142
+
100
143
  return json_primitives(value) \
101
- or (isinstance(value, list) and all(map(json_primitives, value))) \
102
- or (all(map(lambda elem: isinstance(elem, list), value)) and map(json_primitives, [subelem for elem in value for subelem in elem]))
144
+ or json_primitives_list(value) \
145
+ or json_primitives_list_of_list(value) \
146
+ or (isinstance(value, dict) and all(map(lambda x: isinstance(x[0], str) and (json_primitives(x[1]) or json_primitives_list(x[1])), value.items())))
103
147
 
104
148
  def add_property(self, name: str,
105
- value: Union[JSON_PRMTV_TYPES, List[JSON_PRMTV_TYPES], List[List[JSON_PRMTV_TYPES]]]
106
- ) -> None:
149
+ value: Union[JSON_PRMTV_TYPES, LIST_PRMTV, LIST_LIST_PRMTV, DICT_PRMTV, DICT_LIST_PRMTV]) -> None:
107
150
  """
108
151
  Adds a property to the annotation's properties.
109
152
  :param name: the name of the property
110
153
  :param value: the property's desired value
111
154
  :return: None
112
155
  """
113
- if self.check_prop_value_is_simple_enough(value):
114
- self.properties[name] = value
115
- else:
116
- raise ValueError("Property values cannot be a complex object. It must be "
117
- "either string, number, boolean, None, or a list of them."
118
- f"(\"{name}\": \"{str(value)}\"")
119
-
120
- def get(self, prop_name: str) -> Union['AnnotationProperties', JSON_PRMTV_TYPES, List[JSON_PRMTV_TYPES], List[List[JSON_PRMTV_TYPES]]]:
156
+ # if self.check_prop_value_is_simple_enough(value):
157
+ self.properties[name] = value
158
+ # else:
159
+ # raise ValueError("Property values cannot be a complex object. It must be "
160
+ # "either string, number, boolean, None, a JSON array of them, "
161
+ # "or a JSON object of them keyed by strings."
162
+ # f"(\"{name}\": \"{str(value)}\"")
163
+ self._add_prop_aliases(name, value)
164
+
165
+ def get(self, prop_name: str) -> Union['AnnotationProperties', JSON_PRMTV_TYPES, LIST_PRMTV, LIST_LIST_PRMTV, DICT_PRMTV, DICT_LIST_PRMTV]:
121
166
  """
122
167
  A special getter for Annotation properties. This is to allow for
123
168
  directly accessing properties without having to go through the
@@ -148,8 +193,25 @@ class Annotation(MmifObject):
148
193
  except KeyError:
149
194
  return False
150
195
 
196
+ def _get_label(self) -> str:
197
+ """
198
+ Another prototypical method to handle property aliases.
199
+ See :meth:`.Annotation._add_prop_aliases` for more details on
200
+ what property aliases are.
201
+ Not recommended to use this method as `_add_prop_aliases` method
202
+ is preferred.
203
+ """
204
+ if 'label' in self:
205
+ return str(self.get('label'))
206
+ elif self._type.shortname == 'TimeFrame' and 'frameType' in self:
207
+ return str(self.get('frameType'))
208
+ elif self._type.shortname == 'BoundingBox' and 'boxType' in self:
209
+ return str(self.get('boxType'))
210
+ else:
211
+ raise KeyError("No label found in this annotation.")
212
+
151
213
  def is_document(self):
152
- return isinstance(self.at_type, DocumentTypesBase)
214
+ return isinstance(self._type, DocumentTypesBase)
153
215
 
154
216
 
155
217
  class Document(Annotation):
@@ -180,7 +242,7 @@ class Document(Annotation):
180
242
  super().__init__(doc_obj)
181
243
 
182
244
  def add_property(self, name: str,
183
- value: Union[JSON_PRMTV_TYPES, List[JSON_PRMTV_TYPES]]
245
+ value: Union[JSON_PRMTV_TYPES, LIST_PRMTV]
184
246
  ) -> None:
185
247
  """
186
248
  Adds a property to the document's properties.
@@ -249,30 +311,26 @@ class Document(Annotation):
249
311
 
250
312
  @property
251
313
  def text_language(self) -> str:
252
- if self.at_type == DocumentTypes.TextDocument:
314
+ if self._type == DocumentTypes.TextDocument:
253
315
  return self.properties.text_language
254
316
  else:
255
317
  raise ValueError("Only TextDocument can have `text` field.")
256
318
 
257
319
  @text_language.setter
258
320
  def text_language(self, lang_code: str) -> None:
259
- if self.at_type == DocumentTypes.TextDocument:
321
+ if self._type == DocumentTypes.TextDocument:
260
322
  self.properties.text_language = lang_code
261
323
  else:
262
324
  raise ValueError("Only TextDocument can have `text` field.")
263
325
 
264
326
  @property
265
327
  def text_value(self) -> str:
266
- if self.at_type == DocumentTypes.TextDocument:
328
+ if self._type == DocumentTypes.TextDocument:
267
329
  if self.location:
268
- if self.location_scheme() == 'file':
269
- f = open(self.location_path(), 'r', encoding='utf8')
270
- textvalue = f.read()
271
- f.close()
272
- return textvalue
273
- else:
274
- # TODO (krim @ 7/11/21): add more handlers for other types of locations (e.g. s3, https, ...)
275
- return ''
330
+ f = open(self.location_path(nonexist_ok=False), 'r', encoding='utf8')
331
+ textvalue = f.read()
332
+ f.close()
333
+ return textvalue
276
334
  else:
277
335
  return self.properties.text_value
278
336
  else:
@@ -280,7 +338,7 @@ class Document(Annotation):
280
338
 
281
339
  @text_value.setter
282
340
  def text_value(self, text_value: str) -> None:
283
- if self.at_type == DocumentTypes.TextDocument:
341
+ if self._type == DocumentTypes.TextDocument:
284
342
  self.properties.text_value = text_value
285
343
  else:
286
344
  raise ValueError("Only TextDocument can have `text` field.")
@@ -12,10 +12,10 @@ from datetime import datetime
12
12
  from typing import List, Union, Optional, Dict, ClassVar, cast
13
13
 
14
14
  import jsonschema.validators
15
-
16
15
  import mmif
17
16
  from mmif import ThingTypesBase
18
17
  from mmif.vocabulary import AnnotationTypes, DocumentTypes
18
+
19
19
  from .annotation import Annotation, Document
20
20
  from .model import MmifObject, DataList
21
21
  from .view import View
@@ -478,6 +478,47 @@ class Mmif(MmifObject):
478
478
  if at_types in view.metadata.contains:
479
479
  return view
480
480
  return None
481
+
482
+ def _get_linear_anchor_point(self, ann: Annotation, start: bool = True) -> Union[int, float]:
483
+ # TODO (krim @ 2/5/24): Update the return type once timeunits are unified to `ms` as integers (https://github.com/clamsproject/mmif/issues/192)
484
+ """
485
+ Retrieves the anchor point of the annotation. Currently, this method only supports linear anchors,
486
+ namely time and text, hence does not work with spatial anchors (polygons or video-object).
487
+
488
+ :param ann: An Annotation object that has a linear anchor point. Namely, some subtypes of `Region` vocabulary type.
489
+ :param start: If True, returns the start anchor point. Otherwise, returns the end anchor point. N/A for `timePoint` anchors.
490
+ :return: the anchor point of the annotation. 1d for linear regions (time, text)
491
+ """
492
+ props = ann.properties
493
+ if 'timePoint' in props:
494
+ return ann.get_property('timePoint')
495
+ elif 'targets' in props:
496
+ target_id = ann.get_property('targets')[0 if start else -1]
497
+ # TODO (krim @ 2/5/24): not sure if this is the correct way to pick the "first" (or "last") target,
498
+ # since the targets list is not guaranteed to be sorted.
499
+ # However, due the recursive nature of this method, it is likely impossible
500
+ # to get all `start` (or `end`) values of the targets recursively and then pick the min (or max) value.
501
+ if Mmif.id_delimiter in target_id:
502
+ target = self.__getitem__(target_id)
503
+ else:
504
+ target = self.__getitem__(Mmif.id_delimiter.join((ann.parent, target_id)))
505
+ return self._get_linear_anchor_point(target, start=start)
506
+ elif (start and 'start' in props) or (not start and 'end' in props):
507
+ return ann.get_property('start' if start else 'end')
508
+ else:
509
+ raise ValueError(f"{ann.id} ({ann.at_type}) does not have a valid anchor point. Is it a valid 'Region' type?")
510
+
511
+ def get_start(self, annotation: Annotation) -> Union[int, float]:
512
+ """
513
+ An alias to `get_anchor_point` method with `start=True`.
514
+ """
515
+ return self._get_linear_anchor_point(annotation, start=True)
516
+
517
+ def get_end(self, annotation: Annotation) -> Union[int, float]:
518
+ """
519
+ An alias to `get_anchor_point` method with `start=False`.
520
+ """
521
+ return self._get_linear_anchor_point(annotation, start=False)
481
522
 
482
523
  # pytype: disable=bad-return-type
483
524
  def __getitem__(self, item: str) -> Union[Document, View, Annotation]:
@@ -186,7 +186,7 @@ class View(MmifObject):
186
186
  properties.items())):
187
187
  yield annotation
188
188
 
189
- def get_annotation_by_id(self, ann_id):
189
+ def get_annotation_by_id(self, ann_id) -> Annotation:
190
190
  ann_found = self.annotations.get(ann_id)
191
191
  if ann_found is None or not isinstance(ann_found, Annotation):
192
192
  raise KeyError(f"Annotation \"{ann_id}\" is not found in view {self.id}.")
@@ -367,7 +367,10 @@ class AnnotationsList(DataList[Union[Annotation, Document]]):
367
367
  class ContainsDict(DataDict[ThingTypesBase, Contain]):
368
368
 
369
369
  def _deserialize(self, input_dict: dict) -> None:
370
- self._items = {ThingTypesBase.from_str(key): Contain(value) for key, value in input_dict.items()}
370
+ for key, value in input_dict.items():
371
+ if isinstance(key, str):
372
+ key = ThingTypesBase.from_str(key)
373
+ self._items[key] = Contain(value)
371
374
 
372
375
  def update(self, other: Union[dict, 'ContainsDict'], overwrite=False):
373
376
  for k, v in other.items():
@@ -0,0 +1,84 @@
1
+ from datetime import datetime
2
+ from typing import Union
3
+
4
+
5
+ UNIT_NORMALIZATION = {
6
+ 'm': 'millisecond',
7
+ 'ms': 'millisecond',
8
+ 'msec': 'millisecond',
9
+ 'millisecond': 'millisecond',
10
+ 'milliseconds': 'millisecond',
11
+ 's': 'second',
12
+ 'se': 'second',
13
+ 'sec': 'second',
14
+ 'second': 'second',
15
+ 'seconds': 'second',
16
+ 'f': 'frame',
17
+ 'fr': 'frame',
18
+ 'frame': 'frame',
19
+ 'frames': 'frame',
20
+ 'i': 'isoformat',
21
+ 'iso': 'isoformat',
22
+ 'isoformat': 'isoformat',
23
+ }
24
+
25
+
26
+ def _isoformat_to_millisecond(isoformat: str) -> int:
27
+ t = datetime.strptime(isoformat, '%H:%M:%S.%f')
28
+ return int(1000 * (t.hour * 3600 + t.minute * 60 + t.second + t.microsecond / 1000000))
29
+
30
+
31
+ def _millisecond_to_isoformat(millisecond: int) -> str:
32
+ t = datetime.utcfromtimestamp(millisecond / 1000)
33
+ return t.strftime('%H:%M:%S.%f')[:-3]
34
+
35
+
36
+ def _second_to_isoformat(second: float) -> str:
37
+ t = datetime.utcfromtimestamp(second)
38
+ return t.strftime('%H:%M:%S.%f')[:-3] # python strftime will return "microsecond" with 6 digits
39
+
40
+
41
+ def convert(t: Union[int, float, str], in_unit: str, out_unit: str, fps: float) -> Union[int, float, str]:
42
+ """
43
+ Converts time from one unit to another. Works with ``frames``, ``seconds``, ``milliseconds``.
44
+
45
+ :param t: time value to convert
46
+ :param in_unit: input time unit, one of ``frames``, ``seconds``, ``milliseconds``
47
+ :param out_unit: output time unit, one of ``frames``, ``seconds``, ``milliseconds``
48
+ :param fps: frames per second
49
+ :return: converted time value
50
+ """
51
+ try:
52
+ in_unit = UNIT_NORMALIZATION[in_unit]
53
+ except KeyError:
54
+ raise ValueError(f"Not supported time unit: {in_unit}")
55
+ try:
56
+ out_unit = UNIT_NORMALIZATION[out_unit]
57
+ except KeyError:
58
+ raise ValueError(f"Not supported time unit: {out_unit}")
59
+ if in_unit == 'isoformat':
60
+ if isinstance(t, str):
61
+ t = _isoformat_to_millisecond(t)
62
+ in_unit = 'millisecond'
63
+ else:
64
+ raise ValueError(f"Invalid time format: ISO format string expected, but got {t} of type {type(t)}")
65
+ # s>s, ms>ms, f>f
66
+ if in_unit == out_unit:
67
+ return t
68
+ elif out_unit == 'frame':
69
+ # ms>f
70
+ if 'millisecond' == in_unit:
71
+ return int(t / 1000 * fps)
72
+ # s>f
73
+ elif 'second' == in_unit:
74
+ return int(t * fps)
75
+ # s>(ms or i)
76
+ elif in_unit == 'second':
77
+ return t * 1000 if out_unit == 'millisecond' else _second_to_isoformat(t)
78
+ # ms>(s or i)
79
+ elif in_unit == 'millisecond':
80
+ return t / 1000 if out_unit == 'second' else _millisecond_to_isoformat(t)
81
+ # f>ms, f>s
82
+ else:
83
+ return (t / fps) if out_unit == 'second' else (t / fps * 1000) # pytype: disable=bad-return-type
84
+
@@ -4,6 +4,7 @@ from typing import List, Union, Tuple
4
4
 
5
5
  import mmif
6
6
  from mmif import Annotation, Document, Mmif
7
+ from mmif.utils.timeunit_helper import convert
7
8
  from mmif.vocabulary import DocumentTypes
8
9
 
9
10
  for cv_dep in ('cv2', 'ffmpeg', 'PIL'):
@@ -19,23 +20,6 @@ FPS_DOCPROP_KEY = 'fps'
19
20
  FRAMECOUNT_DOCPROP_KEY = 'frameCount'
20
21
  DURATION_DOCPROP_KEY = 'duration'
21
22
  DURATIONUNIT_DOCPROP_KEY = 'durationTimeUnit'
22
-
23
- UNIT_NORMALIZATION = {
24
- 'm': 'millisecond',
25
- 'ms': 'millisecond',
26
- 'msec': 'millisecond',
27
- 'millisecond': 'millisecond',
28
- 'milliseconds': 'millisecond',
29
- 's': 'second',
30
- 'se': 'second',
31
- 'sec': 'second',
32
- 'second': 'second',
33
- 'seconds': 'second',
34
- 'f': 'frame',
35
- 'fr': 'frame',
36
- 'frame': 'frame',
37
- 'frames': 'frame',
38
- }
39
23
 
40
24
 
41
25
  def capture(video_document: Document):
@@ -120,7 +104,7 @@ def get_mid_framenum(mmif: Mmif, time_frame: Annotation):
120
104
  timeunit = time_frame.get_property('timeUnit')
121
105
  video_document = mmif[time_frame.get_property('document')]
122
106
  fps = get_framerate(video_document)
123
- return sum(convert(float(time_frame.get_property(timepoint_propkey)), timeunit, 'frame', fps) for timepoint_propkey in ('start', 'end')) // 2
107
+ return convert(mmif.get_start(time_frame) + mmif.get_end(time_frame), timeunit, 'frame', fps) // 2
124
108
 
125
109
 
126
110
  def extract_mid_frame(mmif: Mmif, time_frame: Annotation, as_PIL: bool = False):
@@ -155,45 +139,6 @@ def sample_frames(start_frame: int, end_frame: int, sample_ratio: int = 1) -> Li
155
139
  return frame_nums
156
140
 
157
141
 
158
- def convert(time: Union[int, float], in_unit: str, out_unit: str, fps: float) -> Union[int, float]:
159
- """
160
- Converts time from one unit to another. Works with ``frames``, ``seconds``, ``milliseconds``.
161
-
162
- :param time: time value to convert
163
- :param in_unit: input time unit, one of ``frames``, ``seconds``, ``milliseconds``
164
- :param out_unit: output time unit, one of ``frames``, ``seconds``, ``milliseconds``
165
- :param fps: frames per second
166
- :return: converted time value
167
- """
168
- try:
169
- in_unit = UNIT_NORMALIZATION[in_unit]
170
- except KeyError:
171
- raise ValueError(f"Not supported time unit: {in_unit}")
172
- try:
173
- out_unit = UNIT_NORMALIZATION[out_unit]
174
- except KeyError:
175
- raise ValueError(f"Not supported time unit: {out_unit}")
176
- # s>s, ms>ms, f>f
177
- if in_unit == out_unit:
178
- return time
179
- elif out_unit == 'frame':
180
- # ms>f
181
- if 'millisecond' == in_unit:
182
- return int(time / 1000 * fps)
183
- # s>f
184
- elif 'second' == in_unit:
185
- return int(time * fps)
186
- # s>ms
187
- elif in_unit == 'second':
188
- return time * 1000
189
- # ms>s
190
- elif in_unit == 'millisecond':
191
- return time // 1000
192
- # f>ms, f>s
193
- else:
194
- return (time / fps) if out_unit == 'second' else (time / fps * 1000) # pytype: disable=bad-return-type
195
-
196
-
197
142
  def get_annotation_property(mmif, annotation, prop_name):
198
143
  """
199
144
  .. deprecated:: 1.0.8
@@ -208,7 +153,7 @@ def get_annotation_property(mmif, annotation, prop_name):
208
153
  return annotation.get_property(prop_name)
209
154
 
210
155
 
211
- def convert_timepoint(mmif: Mmif, timepoint: Annotation, out_unit: str) -> Union[int, float]:
156
+ def convert_timepoint(mmif: Mmif, timepoint: Annotation, out_unit: str) -> Union[int, float, str]:
212
157
  """
213
158
  Converts a time point included in an annotation to a different time unit.
214
159
  The input annotation must have ``timePoint`` property.
@@ -223,7 +168,7 @@ def convert_timepoint(mmif: Mmif, timepoint: Annotation, out_unit: str) -> Union
223
168
  return convert(timepoint.get_property('timePoint'), in_unit, out_unit, get_framerate(vd))
224
169
 
225
170
 
226
- def convert_timeframe(mmif: Mmif, time_frame: Annotation, out_unit: str) -> Union[Tuple[int, int], Tuple[float, float]]:
171
+ def convert_timeframe(mmif: Mmif, time_frame: Annotation, out_unit: str) -> Union[Tuple[Union[int, float, str], Union[int, float, str]]]:
227
172
  """
228
173
  Converts start and end points in a ``TimeFrame`` annotation a different time unit.
229
174
 
@@ -234,8 +179,8 @@ def convert_timeframe(mmif: Mmif, time_frame: Annotation, out_unit: str) -> Unio
234
179
  """
235
180
  in_unit = time_frame.get_property('timeUnit')
236
181
  vd = mmif[time_frame.get_property('document')]
237
- return convert(time_frame.get_property('start'), in_unit, out_unit, get_framerate(vd)), \
238
- convert(time_frame.get_property('end'), in_unit, out_unit, get_framerate(vd))
182
+ return convert(mmif.get_start(time_frame), in_unit, out_unit, get_framerate(vd)), \
183
+ convert(mmif.get_end(time_frame), in_unit, out_unit, get_framerate(vd))
239
184
 
240
185
 
241
186
  def framenum_to_second(video_doc: Document, frame: int):
@@ -0,0 +1,2 @@
1
+ __version__ = "1.0.10"
2
+ __specver__ = "1.0.2"
@@ -1,4 +1,4 @@
1
- # Spec version 1.0.0
1
+ # Spec version 1.0.2
2
2
  # This file is auto-generated by setup.py
3
3
 
4
4
  from .base_types import AnnotationTypesBase
@@ -7,18 +7,27 @@ 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.0 as class variables.
10
+ defined in the spec version 1.0.2 as class variables.
11
11
  """
12
- Annotation = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Annotation/v2')
13
- Region = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Region/v1')
14
- TimePoint = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/TimePoint/v1')
15
- Interval = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Interval/v1')
16
- Span = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Span/v1')
17
- TimeFrame = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/TimeFrame/v1')
18
- Chapter = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Chapter/v1')
19
- Polygon = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Polygon/v1')
20
- BoundingBox = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/BoundingBox/v1')
21
- VideoObject = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/VideoObject/v1')
22
- Relation = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Relation/v1')
12
+
13
+ # prpoerty aliases, first added in MMIF 1.0.2
14
+ # map from type short name to a dictionary of property aliases
15
+ # where nested dictionary maps from representative name to its aliases
16
+ prop_aliases = {
17
+ 'TimeFrame': {'label': {'frameType', 'label'}},
18
+ 'BoundingBox': {'label': {'boxType', 'label'}},
19
+ }
20
+
21
+ Annotation = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Annotation/v3')
22
+ Region = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Region/v2')
23
+ TimePoint = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/TimePoint/v2')
24
+ Interval = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Interval/v2')
25
+ Span = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Span/v2')
26
+ TimeFrame = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/TimeFrame/v3')
27
+ Chapter = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Chapter/v3')
28
+ Polygon = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Polygon/v2')
29
+ BoundingBox = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/BoundingBox/v2')
30
+ VideoObject = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/VideoObject/v2')
31
+ Relation = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Relation/v2')
23
32
  Alignment = AnnotationTypesBase('http://mmif.clams.ai/vocabulary/Alignment/v1')
24
- typevers = {'Annotation': 'v2', 'Region': 'v1', 'TimePoint': 'v1', 'Interval': 'v1', 'Span': 'v1', 'TimeFrame': 'v1', 'Chapter': 'v1', 'Polygon': 'v1', 'BoundingBox': 'v1', 'VideoObject': 'v1', 'Relation': 'v1', 'Alignment': 'v1'}
33
+ typevers = {'Annotation': 'v3', 'Region': 'v2', 'TimePoint': 'v2', 'Interval': 'v2', 'Span': 'v2', 'TimeFrame': 'v3', 'Chapter': 'v3', 'Polygon': 'v2', 'BoundingBox': 'v2', 'VideoObject': 'v2', 'Relation': 'v2', 'Alignment': 'v1'}
@@ -137,7 +137,7 @@ class ClamsTypesBase(TypesBase):
137
137
  return True
138
138
 
139
139
  def __hash__(self):
140
- return hash(str(self))
140
+ return hash(f'{self.base_uri}::{self.shortname}')
141
141
 
142
142
  def normalize_attype_vers(self):
143
143
  if '.' in self.version and self._check_legacy_version(self.version):
@@ -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.0 as a class variable.
199
+ defined in the spec version 1.0.2 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.0
1
+ # Spec version 1.0.2
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.0 as class variables.
10
+ defined in the spec version 1.0.2 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')
@@ -0,0 +1,12 @@
1
+ import urllib.request
2
+ import urllib.error
3
+
4
+
5
+ def resolve(docloc):
6
+ try:
7
+ if docloc.startswith('http://') or docloc.startswith('https://'):
8
+ return urllib.request.urlretrieve(docloc)[0]
9
+ else:
10
+ raise ValueError(f'cannot handle document location scheme: {docloc}')
11
+ except urllib.error.URLError as e:
12
+ raise e
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mmif-python
3
- Version: 1.0.8
3
+ Version: 1.0.10
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
@@ -16,6 +16,7 @@ mmif/serialize/mmif.py
16
16
  mmif/serialize/model.py
17
17
  mmif/serialize/view.py
18
18
  mmif/utils/__init__.py
19
+ mmif/utils/timeunit_helper.py
19
20
  mmif/utils/video_document_helper.py
20
21
  mmif/ver/__init__.py
21
22
  mmif/ver/do-not-edit.txt
@@ -24,6 +25,7 @@ mmif/vocabulary/annotation_types.py
24
25
  mmif/vocabulary/base_types.py
25
26
  mmif/vocabulary/do-not-edit.txt
26
27
  mmif/vocabulary/document_types.py
28
+ mmif_docloc_http/__init__.py
27
29
  mmif_python.egg-info/PKG-INFO
28
30
  mmif_python.egg-info/SOURCES.txt
29
31
  mmif_python.egg-info/dependency_links.txt
@@ -11,4 +11,3 @@ pytest
11
11
  pytest-pep8
12
12
  pytest-cov
13
13
  pytype
14
- sphinx
@@ -0,0 +1,2 @@
1
+ mmif
2
+ mmif_docloc_http
@@ -162,15 +162,19 @@ def generate_vocabulary(spec_version, clams_types_vers):
162
162
 
163
163
 
164
164
  def get_latest_mmif_gittag():
165
- cur_p = 1
166
- body = [None]
167
- tags = []
168
- while len(body) > 0:
169
- # for when we have more than 30 (default pagination size) tags
170
- res = request.urlopen(f'https://api.github.com/repos/clamsproject/mmif/tags?per_page=100&page={cur_p}')
171
- body = json.loads(res.read())
172
- tags.extend([tag['name'] for tag in body])
173
- cur_p += 1
165
+ if LOCALMMIF is not None:
166
+ tags = subprocess.run(f'git --git-dir {LOCALMMIF}/.git --no-pager tag'.split(),
167
+ capture_output=True).stdout.decode('utf-8').split('\n')
168
+ else:
169
+ cur_p = 1
170
+ body = [None]
171
+ tags = []
172
+ while len(body) > 0:
173
+ # for when we have more than 30 (default pagination size) tags
174
+ res = request.urlopen(f'https://api.github.com/repos/clamsproject/mmif/tags?per_page=100&page={cur_p}')
175
+ body = json.loads(res.read())
176
+ tags.extend([tag['name'] for tag in body])
177
+ cur_p += 1
174
178
  # sort and return highest version
175
179
  print(tags)
176
180
  mmif_ver_format = lambda x: re.match(r'\d+\.\d+\.\d$', x)
@@ -209,7 +213,7 @@ def prep_ext_files(setuptools_cmd):
209
213
  latest_mmif_gittag = get_latest_mmif_gittag()
210
214
  spec_file_gitref = latest_mmif_gittag if '.dev' not in version else 'develop'
211
215
  # legacy version tags were formatted as xx-a.b.c (e.g., vocab-0.0.1)
212
- spec_version = latest_mmif_gittag.split('-')[-1]
216
+ spec_version = latest_mmif_gittag.split('-')[-1].strip()
213
217
  # making resources into a python package so that `pkg_resources` can access resource files
214
218
  res_dir = generate_subpack(mmif_name, mmif_res_pkg)
215
219
 
@@ -296,7 +300,6 @@ setuptools.setup(
296
300
  'pytest-pep8',
297
301
  'pytest-cov',
298
302
  'pytype',
299
- 'sphinx'
300
303
  ]
301
304
  },
302
305
  python_requires='>=3.8',
mmif-python-1.0.8/VERSION DELETED
@@ -1 +0,0 @@
1
- 1.0.8
@@ -1,2 +0,0 @@
1
- __version__ = "1.0.8"
2
- __specver__ = "1.0.0"
@@ -1 +0,0 @@
1
- mmif
File without changes
File without changes
File without changes
File without changes