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.
- {mmif_python-1.5.2 → mmif_python-1.5.3}/CHANGELOG.md +6 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/PKG-INFO +1 -1
- {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/target-versions.csv +1 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/serialize/mmif.py +81 -14
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/PKG-INFO +1 -1
- mmif_python-1.5.3/mmif_python.egg-info/scm_version.json +8 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/test_serialize.py +126 -3
- mmif_python-1.5.2/mmif_python.egg-info/scm_version.json +0 -8
- {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/codecov.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/issue-assign.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/issue-close.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/issue-infra-project.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/publish.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/.github/workflows/release-check.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/.gitignore +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/CONTRIBUTING.md +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/LICENSE +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/README.md +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/build.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/clean.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/docs.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/prep_release.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/publish.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/build-tools/test.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/_mmif_example_builder/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/_templates/versions.html +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/cli.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/conf.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/consumer-tutorial.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/index.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/introduction.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/plugins.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/summarizer.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/documentation/target-versions.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/res/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/res/mmif.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/serialize/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/serialize/annotation.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/serialize/model.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/serialize/view.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/cli/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/cli/describe.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/cli/rewind.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/cli/source.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/cli/summarize.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/sequence_helper.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/config.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/graph.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/nodes.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/summary.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/summarizer/utils.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/text_document_helper.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/timeunit_helper.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/video_document_helper.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/utils/workflow_helper.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/ver/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/vocabulary/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/vocabulary/annotation_types.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/vocabulary/base_types.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif/vocabulary/document_types.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_docloc_http/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/SOURCES.txt +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/entry_points.txt +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/requires.txt +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/scm_file_list.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/pyproject.toml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/setup.cfg +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/black-2997fps.mp4 +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/black-2997fps.webm +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/1.0.5-old-shortid.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/README.md +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/bars-tones-slates/raw.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/east-tesseract-typing/raw.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/everything/raw.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/examples.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/README.md +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/image-box-ocr-timex.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/image-box-ocr.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/image-box.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/image.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/slate-detect.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/slate-reco.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-demux.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript-demux-fa.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript-demux-fa.short.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript-demux.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript-sentencesplit.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/segmenter-kaldi-ner/raw.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/swt-1.0.mmif +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif_examples.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/test_specver.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/test_utils.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/test_utils_cli.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.3}/tests/test_vocab.py +0 -0
- {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,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,
|
|
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
|
|
225
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
@@ -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):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript-demux-fa.json
RENAMED
|
File without changes
|
|
File without changes
|
{mmif_python-1.5.2 → mmif_python-1.5.3}/tests/mmif-examples/others/video-transcript-demux.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|