mmif-python 1.5.2__tar.gz → 1.5.3__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 (102) hide show
  1. {mmif_python-1.5.2 → mmif_python-1.5.3}/CHANGELOG.md +6 -0
  2. {mmif_python-1.5.2 → mmif_python-1.5.3}/PKG-INFO +1 -1
  3. {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/target-versions.csv +1 -0
  4. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/serialize/mmif.py +81 -14
  5. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/PKG-INFO +1 -1
  6. mmif_python-1.5.3/mmif_python.egg-info/scm_version.json +8 -0
  7. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/test_serialize.py +126 -3
  8. mmif_python-1.5.2/mmif_python.egg-info/scm_version.json +0 -8
  9. {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/codecov.yml +0 -0
  10. {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/issue-assign.yml +0 -0
  11. {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/issue-close.yml +0 -0
  12. {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/issue-infra-project.yml +0 -0
  13. {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/publish.yml +0 -0
  14. {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/release-check.yml +0 -0
  15. {mmif_python-1.5.2 → mmif_python-1.5.3}/.gitignore +0 -0
  16. {mmif_python-1.5.2 → mmif_python-1.5.3}/CONTRIBUTING.md +0 -0
  17. {mmif_python-1.5.2 → mmif_python-1.5.3}/LICENSE +0 -0
  18. {mmif_python-1.5.2 → mmif_python-1.5.3}/README.md +0 -0
  19. {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/build.py +0 -0
  20. {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/clean.py +0 -0
  21. {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/docs.py +0 -0
  22. {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/prep_release.py +0 -0
  23. {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/publish.py +0 -0
  24. {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/test.py +0 -0
  25. {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/_mmif_example_builder/__init__.py +0 -0
  26. {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/_templates/versions.html +0 -0
  27. {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/cli.rst +0 -0
  28. {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/conf.py +0 -0
  29. {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/consumer-tutorial.rst +0 -0
  30. {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/index.rst +0 -0
  31. {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/introduction.rst +0 -0
  32. {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/plugins.rst +0 -0
  33. {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/summarizer.rst +0 -0
  34. {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/target-versions.rst +0 -0
  35. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/__init__.py +0 -0
  36. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/res/__init__.py +0 -0
  37. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/res/mmif.json +0 -0
  38. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/serialize/__init__.py +0 -0
  39. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/serialize/annotation.py +0 -0
  40. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/serialize/model.py +0 -0
  41. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/serialize/view.py +0 -0
  42. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/__init__.py +0 -0
  43. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/cli/__init__.py +0 -0
  44. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/cli/describe.py +0 -0
  45. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/cli/rewind.py +0 -0
  46. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/cli/source.py +0 -0
  47. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/cli/summarize.py +0 -0
  48. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/sequence_helper.py +0 -0
  49. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/__init__.py +0 -0
  50. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/config.py +0 -0
  51. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/graph.py +0 -0
  52. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/nodes.py +0 -0
  53. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/summary.py +0 -0
  54. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/utils.py +0 -0
  55. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/text_document_helper.py +0 -0
  56. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/timeunit_helper.py +0 -0
  57. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/video_document_helper.py +0 -0
  58. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/workflow_helper.py +0 -0
  59. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/ver/__init__.py +0 -0
  60. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/vocabulary/__init__.py +0 -0
  61. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/vocabulary/annotation_types.py +0 -0
  62. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/vocabulary/base_types.py +0 -0
  63. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/vocabulary/document_types.py +0 -0
  64. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_docloc_http/__init__.py +0 -0
  65. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/SOURCES.txt +0 -0
  66. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/dependency_links.txt +0 -0
  67. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/entry_points.txt +0 -0
  68. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/requires.txt +0 -0
  69. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/scm_file_list.json +0 -0
  70. {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/top_level.txt +0 -0
  71. {mmif_python-1.5.2 → mmif_python-1.5.3}/pyproject.toml +0 -0
  72. {mmif_python-1.5.2 → mmif_python-1.5.3}/setup.cfg +0 -0
  73. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/black-2997fps.mp4 +0 -0
  74. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/black-2997fps.webm +0 -0
  75. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/1.0.5-old-shortid.json +0 -0
  76. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/README.md +0 -0
  77. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/bars-tones-slates/raw.json +0 -0
  78. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/east-tesseract-typing/raw.json +0 -0
  79. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/everything/raw.json +0 -0
  80. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/examples.json +0 -0
  81. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/README.md +0 -0
  82. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/image-box-ocr-timex.json +0 -0
  83. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/image-box-ocr.json +0 -0
  84. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/image-box.json +0 -0
  85. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/image.json +0 -0
  86. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/slate-detect.json +0 -0
  87. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/slate-reco.json +0 -0
  88. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-demux.json +0 -0
  89. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript-demux-fa.json +0 -0
  90. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript-demux-fa.short.json +0 -0
  91. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript-demux.json +0 -0
  92. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript-sentencesplit.json +0 -0
  93. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript.json +0 -0
  94. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video.json +0 -0
  95. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/segmenter-kaldi-ner/raw.json +0 -0
  96. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/swt-1.0.mmif +0 -0
  97. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif_examples.py +0 -0
  98. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/test_specver.py +0 -0
  99. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/test_utils.py +0 -0
  100. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/test_utils_cli.py +0 -0
  101. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/test_vocab.py +0 -0
  102. {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/testsrc-2997fps-ptsoffset.mp4 +0 -0
@@ -1,4 +1,10 @@
1
1
 
2
+ ## releasing 1.5.2 (2026-07-07)
3
+ ### Overview
4
+ This is a quick patch on the bug that when MMIF is deserialized, the annotation ID counts in the views weren't properly initialized. This should not be a real bug since new annotations should always written to new views (of current processing app) but not to existing views (where annotation count > 0), however, bugs in app code can trigger this condition, and this patch preventively fixes such cases.
5
+
6
+
7
+
2
8
  ## releasing 1.5.1 (2026-06-19)
3
9
  ### Overview
4
10
  This patch contains a bug fix in VDH that couldn't handle some mkv-contained video files (https://github.com/clamsproject/mmif-python/issues/390) , and some other minor documentation generation bugs.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmif-python
3
- Version: 1.5.2
3
+ Version: 1.5.3
4
4
  Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: homepage, https://mmif.clams.ai
@@ -1,4 +1,5 @@
1
1
  "``mmif-python`` version","Target MMIF Specification"
2
+ `1.5.3 <https://pypi.org/project/mmif-python/1.5.3/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
2
3
  `1.5.2 <https://pypi.org/project/mmif-python/1.5.2/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
3
4
  `1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
4
5
  `1.5.0 <https://pypi.org/project/mmif-python/1.5.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
@@ -175,6 +175,14 @@ class Mmif(MmifObject):
175
175
  all_views = mmif.views[1:4] # Slice of views
176
176
  """
177
177
 
178
+ #: Hardcoded list of property names that must stay at the individual
179
+ #: annotation level, and never "factored" into the view's ``contains``
180
+ #: metadata, even when redundant across annotations.
181
+ _NON_FACTORABLE_PROPS = frozenset({
182
+ 'start', 'end', 'targets', 'source', 'target', 'representatives',
183
+ 'mime', 'location', 'location_', 'text',
184
+ })
185
+
178
186
  def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *, validate: bool = True) -> None:
179
187
  self.metadata: MmifMetadata = MmifMetadata()
180
188
  self.documents: DocumentsList = DocumentsList()
@@ -211,26 +219,42 @@ class Mmif(MmifObject):
211
219
  json_str = json.loads(json_str)
212
220
  jsonschema.validators.validate(json_str, schema)
213
221
 
214
- def serialize(self, sanitize: bool = False, autogenerate_capital_annotations: bool = True, **kwargs) -> str:
222
+ def serialize(self,
223
+ pretty: bool = False,
224
+ include_context: bool = True,
225
+ *,
226
+ sanitize: bool = False,
227
+ autogenerate_capital_annotations: bool = True,
228
+ factor_out_shared_properties: bool = True) -> str:
215
229
  """
216
230
  Serializes the MMIF object to a JSON string.
217
231
 
232
+ :param pretty: If True, returns the string with indentation.
233
+ :param include_context: If False, excludes contextual attributes (e.g.
234
+ runtime timestamps) from serialization.
218
235
  :param sanitize: If True, performs some sanitization of before returning
219
236
  the JSON string. See :meth:`sanitize` for details.
220
237
  :param autogenerate_capital_annotations: If True, automatically convert
221
238
  any "pending" temporary properties from `Document` objects to
222
239
  `Annotation` objects. See :meth:`generate_capital_annotations` for
223
240
  details.
224
- :param kwargs: Keyword arguments to pass to the parent's ``serialize``
225
- method (e.g., ``pretty=True``, ``include_context=False``).
241
+ :param factor_out_shared_properties: If True, lift properties shared
242
+ (same value) across all annotations of a type in a view into the
243
+ view's ``contains`` metadata. See
244
+ :meth:`factor_out_shared_properties` for details. Independent of
245
+ ``autogenerate_capital_annotations``, but runs after it (so
246
+ freshly-materialized pending properties are included) when both are
247
+ enabled.
226
248
  :return: JSON string of the MMIF object.
227
249
  """
228
250
  if autogenerate_capital_annotations:
229
251
  self.generate_capital_annotations()
252
+ if factor_out_shared_properties:
253
+ self.factor_out_shared_properties()
230
254
  # sanitization should be done after `Annotation` annotations are generated
231
255
  if sanitize:
232
256
  self.sanitize()
233
- return super().serialize(**kwargs)
257
+ return super().serialize(pretty=pretty, include_context=include_context)
234
258
 
235
259
  def _deserialize(self, input_dict: dict) -> None:
236
260
  """
@@ -261,9 +285,16 @@ class Mmif(MmifObject):
261
285
  for ann in view.get_annotations():
262
286
  ## for "capital" Annotation properties
263
287
  # first add all extrinsic properties to the Annotation objects
264
- # as "ephemeral" properties
288
+ # as "ephemeral" properties. A view-level `contains` default must
289
+ # not override an annotation-level value that is already present
290
+ # in `_props_ephemeral` -- namely an alias of a property set on the
291
+ # annotation itself (put there by `_add_prop_aliases` during the
292
+ # annotation's own deserialization, which runs earlier). Per the
293
+ # spec, the annotation-level value takes precedence over the
294
+ # view-level default.
265
295
  for prop_key, prop_value in extrinsic_props[ann.at_type].items():
266
- ann._props_ephemeral[prop_key] = prop_value
296
+ if prop_key not in ann._props_ephemeral:
297
+ ann._props_ephemeral[prop_key] = prop_value
267
298
  # then, do the same to associated Document objects. Note that,
268
299
  # in a view, it is guaranteed that all Annotation objects are not duplicates
269
300
  if ann.at_type == AnnotationTypes.Annotation:
@@ -297,7 +328,7 @@ class Mmif(MmifObject):
297
328
  f"Alignment {alignment_ann.id} has `source` and `target` properties that do not point to Annotation objects.",
298
329
  RuntimeWarning)
299
330
  ## caching alignments
300
- if all(map(lambda x: x in alignment_ann.properties, ('source', 'target'))):
331
+ if all(map(lambda x: x in alignment_ann, ('source', 'target'))):
301
332
  try:
302
333
  source_ann = self.__getitem__(alignment_ann.get('source'))
303
334
  target_ann = self.__getitem__(alignment_ann.get('target'))
@@ -383,15 +414,51 @@ class Mmif(MmifObject):
383
414
  if view_to_write.metadata.app == current_app and doc_id in view_to_write.annotations:
384
415
  view_to_write[doc_id].properties.update(props)
385
416
  else:
386
- if len(anns_to_write) == 1:
387
- # if there's only one document, we can record the doc_id in the contains metadata
388
- view_to_write.metadata.new_contain(AnnotationTypes.Annotation, document=doc_id)
389
- props.pop('document', None)
390
- else:
391
- # otherwise, doc_id needs to be recorded in the annotation property
392
- props['document'] = doc_id
417
+ props['document'] = doc_id
393
418
  view_to_write.new_annotation(AnnotationTypes.Annotation, **props)
394
419
 
420
+
421
+ def factor_out_shared_properties(self):
422
+ """
423
+ Factor properties that are shared (identical value) across all
424
+ annotations of a type within a view up into that view's ``contains``
425
+ metadata, per the MMIF spec recommendation to prefer view-level
426
+ defaults for properties shared among annotations of a type.
427
+
428
+ A property is factored when it is present, with the same value, on
429
+ **every** annotation of a given ``@type`` in the view, there are at
430
+ least two such annotations (so the value is genuinely shared), and the
431
+ key is not in the :attr:`_NON_FACTORABLE_PROPS` protection list.
432
+ Factored values are mirrored into each annotation's ephemeral props at
433
+ deserialization time, so in-memory access is unchanged, while the
434
+ serialized output carries the value once, in ``contains``.
435
+ """
436
+ for view in self.views:
437
+ by_type = defaultdict(list)
438
+ for ann in view.get_annotations():
439
+ by_type[ann.at_type].append(ann)
440
+ for at_type, anns in by_type.items():
441
+ if len(anns) < 2:
442
+ continue
443
+ shared = {}
444
+ for key in list(anns[0].properties.keys()):
445
+ if key in self._NON_FACTORABLE_PROPS:
446
+ continue
447
+ value = anns[0].properties[key]
448
+ if all(key in a.properties and a.properties[key] == value
449
+ for a in anns):
450
+ shared[key] = value
451
+ if not shared:
452
+ continue
453
+ if at_type not in view.metadata.contains:
454
+ view.metadata.new_contain(at_type)
455
+ contain = view.metadata.contains[at_type]
456
+ for key, value in shared.items():
457
+ contain[key] = value
458
+ for a in anns:
459
+ del a.properties[key]
460
+ a._props_ephemeral[key] = value
461
+
395
462
  def sanitize(self):
396
463
  """
397
464
  Sanitizes a Mmif object by running some safeguards.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmif-python
3
- Version: 1.5.2
3
+ Version: 1.5.3
4
4
  Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: homepage, https://mmif.clams.ai
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.5.3",
3
+ "distance": 0,
4
+ "node": "g6a1f4c6c1350de7a1fd9de42ededd26f771400ca",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-08"
8
+ }
@@ -76,7 +76,7 @@ class TestMmif(unittest.TestCase):
76
76
  mmif_obj = Mmif(example)
77
77
  except ValidationError:
78
78
  self.fail(f"example {i}")
79
- self.assertEqual(mmif_obj.serialize(True), Mmif(mmif_obj.serialize()).serialize(True), f'Failed on {i}')
79
+ self.assertEqual(mmif_obj.serialize(pretty=True), Mmif(mmif_obj.serialize()).serialize(pretty=True), f'Failed on {i}')
80
80
 
81
81
  def test_json_mmif_deserialize(self):
82
82
  for i, example in MMIF_EXAMPLES.items():
@@ -91,7 +91,7 @@ class TestMmif(unittest.TestCase):
91
91
  for annotation in view.annotations:
92
92
  self.assertIn('_type', annotation.__dict__)
93
93
  self.assertTrue('id' in list(mmif_obj.views._items.values())[0].__dict__)
94
- self.assertEqual(mmif_obj.serialize(True), Mmif(json.loads(mmif_obj.serialize())).serialize(True), f'Failed on {i}')
94
+ self.assertEqual(mmif_obj.serialize(pretty=True), Mmif(json.loads(mmif_obj.serialize())).serialize(pretty=True), f'Failed on {i}')
95
95
 
96
96
  def test_str_vs_json_deserialize(self):
97
97
  for i, example in MMIF_EXAMPLES.items():
@@ -99,7 +99,7 @@ class TestMmif(unittest.TestCase):
99
99
  continue
100
100
  str_mmif_obj = Mmif(example)
101
101
  json_mmif_obj = Mmif(json.loads(example))
102
- self.assertEqual(str_mmif_obj.serialize(True), json_mmif_obj.serialize(True), f'Failed on {i}')
102
+ self.assertEqual(str_mmif_obj.serialize(pretty=True), json_mmif_obj.serialize(pretty=True), f'Failed on {i}')
103
103
 
104
104
  def test_bad_mmif_deserialize_no_metadata(self):
105
105
  self.mmif_examples_json['everything'].pop('metadata')
@@ -1194,6 +1194,35 @@ class TestAnnotation(unittest.TestCase):
1194
1194
  self.assertTrue("nonspeech", tf3.get_property('label'))
1195
1195
  self.assertTrue("speech", tf3.get_property('frameLabel'))
1196
1196
 
1197
+ def test_alias_precedes_view_contains_default(self):
1198
+ # An annotation that sets a property via a deprecated alias (frameType
1199
+ # aliases label) provides an annotation-level value. A view-level
1200
+ # `contains` default for the canonical name must NOT override it -- the
1201
+ # annotation-level value takes precedence over the view-level default.
1202
+ tf = "http://mmif.clams.ai/vocabulary/TimeFrame/v6"
1203
+ raw = {
1204
+ "metadata": {"mmif": "http://mmif.clams.ai/1.2.0"},
1205
+ "documents": [{"@type": "http://mmif.clams.ai/vocabulary/VideoDocument/v1",
1206
+ "properties": {"id": "m1", "mime": "video/mp4",
1207
+ "location": "file:///v.mp4"}}],
1208
+ "views": [{"id": "v1",
1209
+ "metadata": {"app": "http://x/1", "contains": {tf: {"label": "X"}}},
1210
+ "annotations": [{"@type": tf,
1211
+ "properties": {"id": "v1:a1", "start": 0,
1212
+ "end": 5, "frameType": "bars"}}]}]
1213
+ }
1214
+ mmif_obj = Mmif(json.dumps(raw), validate=False)
1215
+ ann = mmif_obj['v1:a1']
1216
+ # the annotation's own frameType (alias of label) wins over contains
1217
+ self.assertEqual('bars', ann.get_property('label'))
1218
+ self.assertEqual('bars', ann.get_property('frameType'))
1219
+ # a genuine view-level default (not shadowed by any annotation value)
1220
+ # still applies
1221
+ raw['views'][0]['metadata']['contains'][tf] = {"timeUnit": "milliseconds"}
1222
+ raw['views'][0]['annotations'][0]['properties'] = {"id": "v1:a1", "start": 0, "end": 5}
1223
+ ann = Mmif(json.dumps(raw), validate=False)['v1:a1']
1224
+ self.assertEqual('milliseconds', ann.get_property('timeUnit'))
1225
+
1197
1226
  def test_change_id(self):
1198
1227
  anno_obj: Annotation = self.data['everything']['mmif']['v5:bb1']
1199
1228
 
@@ -1502,6 +1531,100 @@ class TestDocument(unittest.TestCase):
1502
1531
  self.assertEqual(2, len(caps))
1503
1532
  self.assertEqual({1709977.0, 1709975}, durations)
1504
1533
 
1534
+ def test_factor_out_shared_properties(self):
1535
+ # A view-level metadata property (document/timeUnit/labelset) shared
1536
+ # across all annotations of a type is factored up into the view's
1537
+ # `contains` on serialize, while instance-level properties (label) and
1538
+ # anchors stay on the annotations.
1539
+ m = Mmif(validate=False)
1540
+ v = m.new_view(); v.metadata.app = tester_appname
1541
+ v.new_annotation(AnnotationTypes.TimeFrame, document='m1', timeUnit='ms',
1542
+ start=0, end=5, label='bars')
1543
+ v.new_annotation(AnnotationTypes.TimeFrame, document='m1', timeUnit='ms',
1544
+ start=5, end=9, label='slate')
1545
+ out = json.loads(m.serialize())
1546
+ vout = out['views'][0]
1547
+ tf_key = next(k for k in vout['metadata']['contains'] if 'TimeFrame' in k)
1548
+ contains = vout['metadata']['contains'][tf_key]
1549
+ # shared `document`/`timeUnit` are factored up ...
1550
+ self.assertEqual('m1', contains.get('document'))
1551
+ self.assertEqual('ms', contains.get('timeUnit'))
1552
+ for a in vout['annotations']:
1553
+ # ... and removed from each annotation ...
1554
+ self.assertNotIn('document', a['properties'])
1555
+ self.assertNotIn('timeUnit', a['properties'])
1556
+ # ... while anchors and the differing label stay per-annotation
1557
+ self.assertIn('start', a['properties'])
1558
+ self.assertIn('label', a['properties'])
1559
+ # round-trip: the factored values still resolve per annotation
1560
+ m2 = Mmif(m.serialize())
1561
+ for a in m2.views[0].get_annotations(AnnotationTypes.TimeFrame):
1562
+ self.assertEqual('m1', a.get_property('document'))
1563
+ self.assertEqual('ms', a.get_property('timeUnit'))
1564
+
1565
+ def test_factor_out_protects_listed_props_and_skips_singletons(self):
1566
+ # Blocklisted props (anchors/links) are never factored even when
1567
+ # identical across annotations; a type with a single annotation is
1568
+ # skipped (needs > 1 to be genuinely shared).
1569
+ m = Mmif(validate=False)
1570
+ v = m.new_view(); v.metadata.app = tester_appname
1571
+ tf = v.new_annotation(AnnotationTypes.TimeFrame, start=0, end=5)
1572
+ td1 = v.new_textdocument(text='a'); td2 = v.new_textdocument(text='b')
1573
+ # two Alignments share the same `source` -- must NOT be factored out
1574
+ v.new_annotation(AnnotationTypes.Alignment, source=tf.id, target=td1.id)
1575
+ v.new_annotation(AnnotationTypes.Alignment, source=tf.id, target=td2.id)
1576
+ out = json.loads(m.serialize())
1577
+ aligns = [a for a in out['views'][0]['annotations'] if 'Alignment' in a['@type']]
1578
+ for a in aligns:
1579
+ self.assertIn('source', a['properties'])
1580
+ # a single-annotation type is not factored (threshold)
1581
+ m2 = Mmif(validate=False); v2 = m2.new_view(); v2.metadata.app = tester_appname
1582
+ v2.new_annotation(AnnotationTypes.TimeFrame, document='m1', start=0, end=5)
1583
+ out2 = json.loads(m2.serialize())
1584
+ self.assertIn('document', out2['views'][0]['annotations'][0]['properties'])
1585
+
1586
+ def test_alignment_cached_from_view_level_source_target(self):
1587
+ # regression for the `_cache_alignment` fix: `source`/`target` supplied
1588
+ # as view-level `contains` defaults (distributed to ephemeral, absent
1589
+ # from the annotation's own properties) must still be honored.
1590
+ al = "http://mmif.clams.ai/vocabulary/Alignment/v1"
1591
+ tf = "http://mmif.clams.ai/vocabulary/TimeFrame/v6"
1592
+ td = "http://mmif.clams.ai/vocabulary/TextDocument/v2"
1593
+ raw = {
1594
+ "metadata": {"mmif": "http://mmif.clams.ai/1.2.0"},
1595
+ "documents": [],
1596
+ "views": [{"id": "v1",
1597
+ "metadata": {"app": "http://x/1", "contains": {
1598
+ al: {"source": "v1:tf1", "target": "v1:td1"}}},
1599
+ "annotations": [
1600
+ {"@type": tf, "properties": {"id": "v1:tf1", "start": 0, "end": 5}},
1601
+ {"@type": td, "properties": {"id": "v1:td1", "text": {"@value": "x"}}},
1602
+ {"@type": al, "properties": {"id": "v1:a1"}}]}]
1603
+ }
1604
+ m = Mmif(json.dumps(raw), validate=False)
1605
+ self.assertTrue(list(m['v1:tf1'].get_all_aligned()))
1606
+
1607
+ def test_factor_out_shared_properties_flag(self):
1608
+ # `factor_out_shared_properties` gates factoring independently of
1609
+ # `autogenerate_capital_annotations`.
1610
+ m = Mmif(validate=False)
1611
+ v = m.new_view(); v.metadata.app = tester_appname
1612
+ v.new_annotation(AnnotationTypes.TimeFrame, document='m1', start=0, end=5)
1613
+ v.new_annotation(AnnotationTypes.TimeFrame, document='m1', start=5, end=9)
1614
+ # disabled -> shared `document` stays on each annotation
1615
+ off = json.loads(m.serialize(factor_out_shared_properties=False))
1616
+ for a in off['views'][0]['annotations']:
1617
+ self.assertIn('document', a['properties'])
1618
+ # independent of generation: a reloaded MMIF (props already in
1619
+ # `.properties`) is still factored with capital-annotation generation OFF
1620
+ reloaded = Mmif(m.serialize(factor_out_shared_properties=False))
1621
+ on = json.loads(reloaded.serialize(autogenerate_capital_annotations=False,
1622
+ factor_out_shared_properties=True))
1623
+ tf_key = next(k for k in on['views'][0]['metadata']['contains'] if 'TimeFrame' in k)
1624
+ self.assertEqual('m1', on['views'][0]['metadata']['contains'][tf_key].get('document'))
1625
+ for a in on['views'][0]['annotations']:
1626
+ self.assertNotIn('document', a['properties'])
1627
+
1505
1628
  def test_capital_annotation_generation_viewfinder(self):
1506
1629
  mmif = Mmif(validate=False)
1507
1630
  for i in range(1, 3):
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "1.5.2",
3
- "distance": 0,
4
- "node": "g1b794b4741b27b9999c5cd9fec00d46bef6d5d8e",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-07-07"
8
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes