mmif-python 1.5.4__tar.gz → 1.6.0__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.4 → mmif_python-1.6.0}/CHANGELOG.md +15 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/PKG-INFO +1 -1
- {mmif_python-1.5.4 → mmif_python-1.6.0}/documentation/introduction.rst +2 -2
- {mmif_python-1.5.4 → mmif_python-1.6.0}/documentation/target-versions.csv +1 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/serialize/annotation.py +6 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/serialize/mmif.py +126 -16
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/serialize/model.py +19 -1
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/serialize/view.py +9 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/cli/rewind.py +11 -6
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/video_document_helper.py +92 -3
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/workflow_helper.py +44 -28
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif_python.egg-info/PKG-INFO +1 -1
- mmif_python-1.6.0/mmif_python.egg-info/scm_version.json +8 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/test_serialize.py +236 -2
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/test_utils.py +172 -3
- mmif_python-1.5.4/mmif_python.egg-info/scm_version.json +0 -8
- {mmif_python-1.5.4 → mmif_python-1.6.0}/.github/workflows/codecov.yml +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/.github/workflows/issue-assign.yml +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/.github/workflows/issue-close.yml +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/.github/workflows/issue-infra-project.yml +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/.github/workflows/publish.yml +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/.github/workflows/release-check.yml +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/.gitignore +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/CONTRIBUTING.md +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/LICENSE +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/README.md +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/build-tools/build.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/build-tools/clean.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/build-tools/docs.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/build-tools/prep_release.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/build-tools/publish.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/build-tools/test.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/documentation/_mmif_example_builder/__init__.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/documentation/_templates/versions.html +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/documentation/cli.rst +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/documentation/conf.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/documentation/consumer-tutorial.rst +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/documentation/index.rst +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/documentation/plugins.rst +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/documentation/summarizer.rst +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/documentation/target-versions.rst +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/__init__.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/res/__init__.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/res/mmif.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/serialize/__init__.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/__init__.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/cli/__init__.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/cli/describe.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/cli/source.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/cli/summarize.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/sequence_helper.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/summarizer/__init__.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/summarizer/config.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/summarizer/graph.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/summarizer/nodes.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/summarizer/summary.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/summarizer/utils.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/text_document_helper.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/utils/timeunit_helper.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/ver/__init__.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/vocabulary/__init__.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/vocabulary/annotation_types.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/vocabulary/base_types.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif/vocabulary/document_types.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif_docloc_http/__init__.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif_python.egg-info/SOURCES.txt +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif_python.egg-info/entry_points.txt +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif_python.egg-info/requires.txt +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif_python.egg-info/scm_file_list.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/pyproject.toml +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/setup.cfg +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/black-2997fps.mp4 +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/black-2997fps.webm +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/1.0.5-old-shortid.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/README.md +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/bars-tones-slates/raw.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/east-tesseract-typing/raw.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/everything/raw.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/examples.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/README.md +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/image-box-ocr-timex.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/image-box-ocr.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/image-box.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/image.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/slate-detect.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/slate-reco.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/video-demux.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/video-transcript-demux-fa.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/video-transcript-demux-fa.short.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/video-transcript-demux.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/video-transcript-sentencesplit.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/video-transcript.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/others/video.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/segmenter-kaldi-ner/raw.json +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif-examples/swt-1.0.mmif +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/mmif_examples.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/test_specver.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/test_utils_cli.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/test_vocab.py +0 -0
- {mmif_python-1.5.4 → mmif_python-1.6.0}/tests/testsrc-2997fps-ptsoffset.mp4 +0 -0
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
|
|
2
|
+
## releasing 1.5.4 (2026-10-05)
|
|
3
|
+
### Overview
|
|
4
|
+
|
|
5
|
+
This patch fixes image extraction from truncated or otherwise incomplete video files, and the handling of deprecated annotation property names.
|
|
6
|
+
|
|
7
|
+
### Changes
|
|
8
|
+
|
|
9
|
+
* `video_document_helper`: image extraction no longer fails or returns wrong images on incomplete video files. Timepoints with no image now come back as `None`, with a warning that tells how to check the file (https://github.com/clamsproject/mmif-python/issues/400).
|
|
10
|
+
* *Note: apps that process every returned image must handle `None`.*
|
|
11
|
+
* `av` is now pinned to `>= 15` for `mmif-python[cv]`
|
|
12
|
+
* Deprecated property names (e.g., `frameType` for `label`) work again for values stored in a view's `contains` metadata, including in `mmif summarize` output (https://github.com/clamsproject/mmif-python/issues/401).
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
2
17
|
## releasing 1.5.3 (2026-07-08)
|
|
3
18
|
### Overview
|
|
4
19
|
This patch includes improvements in automatic conversion between contains-metadata properties and annotation properties (#396)
|
|
@@ -94,6 +94,6 @@ For a full list of available helper methods, please refer to the API documentati
|
|
|
94
94
|
MMIF usage in CLAMS Workflows
|
|
95
95
|
-----------------------------
|
|
96
96
|
|
|
97
|
-
In the context of CLAMS, a **Workflow** refers to the sequence of CLAMS applications that have been executed to generate the views and annotations within a MMIF file.
|
|
97
|
+
In the context of CLAMS, a **Workflow** refers to the sequence of CLAMS applications that have been executed to generate the views and annotations within a MMIF file. Each step of a workflow is one *app execution*: the views produced by a single run of an app, which share the same ``app`` and ``timestamp`` values in their metadata. A step counts whether its views hold annotations, only warnings, or an error, so the workflow records every app that ran, including ones that failed or found nothing to annotate.
|
|
98
98
|
|
|
99
|
-
When using the ``mmif-python`` SDK, a unique identifier for a workflow (``workflowId``) is calculated based on the applications involved. This identifier is constructed by concatenating the application name, version, and a hash of the runtime parameters for each step in the sequence. This ensures that the identifier uniquely represents not just the apps used, but their specific configurations, aiding in reproducibility.
|
|
99
|
+
When using the ``mmif-python`` SDK, a unique identifier for a workflow (``workflowId``) is calculated based on the applications involved. This identifier is constructed by concatenating a prefix that describes the source documents, followed by the application name, version, and a hash of the runtime parameters for each step in the sequence. This ensures that the identifier uniquely represents not just the apps used, but their specific configurations, aiding in reproducibility. The ``mmif rewind`` command in app mode counts the same steps, so rewinding N apps from a MMIF yields the ``workflowId`` with its last N steps removed.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"``mmif-python`` version","Target MMIF Specification"
|
|
2
|
+
`1.6.0 <https://pypi.org/project/mmif-python/1.6.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
2
3
|
`1.5.4 <https://pypi.org/project/mmif-python/1.5.4/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
3
4
|
`1.5.3 <https://pypi.org/project/mmif-python/1.5.3/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
4
5
|
`1.5.2 <https://pypi.org/project/mmif-python/1.5.2/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
@@ -368,6 +368,12 @@ class Document(Annotation):
|
|
|
368
368
|
# around the use of these three dictionaries
|
|
369
369
|
# (names changed since, `existing` >> `ephemeral` and `temporary` >> `pending`)
|
|
370
370
|
self._props_original: DocumentProperties = DocumentProperties(None, self)
|
|
371
|
+
# pending props are never cleared: every `Mmif.serialize()` call
|
|
372
|
+
# converts them into `Annotation`s again, and
|
|
373
|
+
# `Mmif.generate_capital_annotations()` relies on its duplicate check
|
|
374
|
+
# to skip values already written. Calling it more than once (e.g.,
|
|
375
|
+
# early in clams-python's `ClamsApp.annotate()`) is safe only while
|
|
376
|
+
# that check holds.
|
|
371
377
|
self._props_pending: AnnotationProperties = AnnotationProperties(None, self)
|
|
372
378
|
self.reserved_names.update(('_props_original', '_props_pending'))
|
|
373
379
|
|
|
@@ -15,7 +15,8 @@ import math
|
|
|
15
15
|
import warnings
|
|
16
16
|
from collections import defaultdict
|
|
17
17
|
from datetime import datetime, timezone
|
|
18
|
-
from typing import Any, List, Union, Optional, Dict, cast, Iterator
|
|
18
|
+
from typing import Any, List, Union, Optional, Dict, cast, Iterable, Iterator
|
|
19
|
+
from urllib.parse import urlparse
|
|
19
20
|
|
|
20
21
|
import jsonschema.validators
|
|
21
22
|
|
|
@@ -28,6 +29,10 @@ from mmif.vocabulary import AnnotationTypes, DocumentTypesBase
|
|
|
28
29
|
|
|
29
30
|
__all__ = ['Mmif']
|
|
30
31
|
|
|
32
|
+
# base URL of first-party CLAMS app identifiers
|
|
33
|
+
# (e.g., ``http://apps.clams.ai/swt-detection/v8.6``)
|
|
34
|
+
app_directory_baseurl = "http://apps.clams.ai"
|
|
35
|
+
|
|
31
36
|
|
|
32
37
|
class MmifMetadata(MmifObject):
|
|
33
38
|
"""
|
|
@@ -122,11 +127,15 @@ class ViewsList(DataList[View]):
|
|
|
122
127
|
|
|
123
128
|
def get_last_contentful_view(self) -> Optional[View]:
|
|
124
129
|
"""
|
|
125
|
-
Returns the last view that is contentful, i.e., has
|
|
130
|
+
Returns the last view that is contentful, i.e., has at least one
|
|
131
|
+
annotation (see :meth:`~mmif.serialize.view.View.has_content`).
|
|
132
|
+
Views with no annotations, including error and warning views, are
|
|
133
|
+
skipped.
|
|
126
134
|
"""
|
|
127
135
|
for view in reversed(self._items.values()):
|
|
128
|
-
if
|
|
136
|
+
if view.has_content():
|
|
129
137
|
return view
|
|
138
|
+
return None
|
|
130
139
|
|
|
131
140
|
def get_last_view(self) -> Optional[View]:
|
|
132
141
|
"""
|
|
@@ -336,7 +345,9 @@ class Mmif(MmifObject):
|
|
|
336
345
|
behind this behavior and discussion.
|
|
337
346
|
"""
|
|
338
347
|
# this view will be the default kitchen sink for all generated annotations.
|
|
339
|
-
|
|
348
|
+
# this can be an empty view, from an app that only adds document properties
|
|
349
|
+
last_view = next((v for v in reversed(self.views)
|
|
350
|
+
if not v.has_error() and not v.has_warnings()), None)
|
|
340
351
|
|
|
341
352
|
# proceed only when there's at least one view
|
|
342
353
|
if last_view:
|
|
@@ -356,18 +367,12 @@ class Mmif(MmifObject):
|
|
|
356
367
|
# new properties to record in the current serialization call
|
|
357
368
|
anns_to_write = defaultdict(dict)
|
|
358
369
|
for view in self.views:
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
# note that in the input MMIF that generated with old implementation,
|
|
366
|
-
# this value can be in the "short" form, i.e., without view ID prefix
|
|
367
|
-
# however, after deserialization, all document IDs should have the
|
|
368
|
-
# view ID prefix, at least within in-memory representation.
|
|
369
|
-
doc_id = ann.get_property('document')
|
|
370
|
-
existing_anns[doc_id].update(ann.properties.items())
|
|
370
|
+
for ann in view.get_annotations(AnnotationTypes.Annotation):
|
|
371
|
+
# view-level `contains` defaults are exposed on each
|
|
372
|
+
# annotation as ephemeral properties; in MMIF made by old
|
|
373
|
+
# SDKs, the id may be in the short form (no view prefix)
|
|
374
|
+
existing_anns[ann.get_property('document')].update(
|
|
375
|
+
ann.properties.items())
|
|
371
376
|
for doc in view.get_documents():
|
|
372
377
|
anns_to_write[doc.id].update(doc._props_pending.items())
|
|
373
378
|
for doc in self.documents:
|
|
@@ -763,6 +768,111 @@ class Mmif(MmifObject):
|
|
|
763
768
|
return view
|
|
764
769
|
return None
|
|
765
770
|
|
|
771
|
+
@staticmethod
|
|
772
|
+
def _full_app_id(app_id: str) -> str:
|
|
773
|
+
"""
|
|
774
|
+
Expands a first-party app ID shorthand (e.g.,
|
|
775
|
+
``swt-detection/v8.6``) into a full URI under
|
|
776
|
+
:data:`app_directory_baseurl`. An ``http(s)`` URL is returned as is.
|
|
777
|
+
"""
|
|
778
|
+
if urlparse(app_id).scheme in ('http', 'https'):
|
|
779
|
+
return app_id
|
|
780
|
+
return f"{app_directory_baseurl}/{app_id.strip('/')}"
|
|
781
|
+
|
|
782
|
+
def get_all_views_by_app(self, *app_ids: str) -> List[View]:
|
|
783
|
+
"""
|
|
784
|
+
Returns the list of all views produced by any of the given apps.
|
|
785
|
+
|
|
786
|
+
Each app ID is either a full app URI (required for third-party
|
|
787
|
+
apps) or a first-party shorthand such as ``swt-detection/v8.6``,
|
|
788
|
+
which is expanded under :data:`app_directory_baseurl`. Matching is
|
|
789
|
+
exact: an ID without a version matches no view, and there are no
|
|
790
|
+
wildcards, so list every version to cover a version range. To group
|
|
791
|
+
views into app runs instead, see
|
|
792
|
+
:func:`mmif.utils.workflow_helper.group_views_by_app`.
|
|
793
|
+
|
|
794
|
+
:param app_ids: one or more app IDs; a view matches if it was
|
|
795
|
+
produced by any of them
|
|
796
|
+
:return: the list of matching views
|
|
797
|
+
"""
|
|
798
|
+
full_ids = {self._full_app_id(app_id) for app_id in app_ids}
|
|
799
|
+
return [view for view in self.views if view.metadata.app in full_ids]
|
|
800
|
+
|
|
801
|
+
get_views_by_app = get_all_views_by_app
|
|
802
|
+
|
|
803
|
+
def get_view_by_app(self, *app_ids: str) -> Optional[View]:
|
|
804
|
+
"""
|
|
805
|
+
Returns the last view appended that was produced by any of the given
|
|
806
|
+
apps. App IDs are matched as in :meth:`get_all_views_by_app`.
|
|
807
|
+
|
|
808
|
+
:param app_ids: one or more app IDs
|
|
809
|
+
:return: the view, or None if no view matches
|
|
810
|
+
"""
|
|
811
|
+
matches = self.get_all_views_by_app(*app_ids)
|
|
812
|
+
return matches[-1] if matches else None
|
|
813
|
+
|
|
814
|
+
def get_all_views_by_apptag(self, *tags: str) -> List[View]:
|
|
815
|
+
"""
|
|
816
|
+
Returns the list of all views whose ``appTags`` metadata contains
|
|
817
|
+
any of the given tags.
|
|
818
|
+
|
|
819
|
+
:param tags: one or more tags; a view matches if it carries any of
|
|
820
|
+
them
|
|
821
|
+
:return: the list of matching views
|
|
822
|
+
"""
|
|
823
|
+
return [view for view in self.views
|
|
824
|
+
if any(tag in view.metadata.appTags for tag in tags)]
|
|
825
|
+
|
|
826
|
+
get_views_by_apptag = get_all_views_by_apptag
|
|
827
|
+
|
|
828
|
+
def get_view_by_apptag(self, *tags: str) -> Optional[View]:
|
|
829
|
+
"""
|
|
830
|
+
Returns the last view appended whose ``appTags`` metadata contains
|
|
831
|
+
any of the given tags.
|
|
832
|
+
|
|
833
|
+
:param tags: one or more tags
|
|
834
|
+
:return: the view, or None if no view matches
|
|
835
|
+
"""
|
|
836
|
+
matches = self.get_all_views_by_apptag(*tags)
|
|
837
|
+
return matches[-1] if matches else None
|
|
838
|
+
|
|
839
|
+
def get_annotations_of_labels(
|
|
840
|
+
self, at_type: Union[ThingTypesBase, str],
|
|
841
|
+
include_labels: Iterable[str] = (),
|
|
842
|
+
exclude_labels: Iterable[str] = ()) -> Iterator[Annotation]:
|
|
843
|
+
"""
|
|
844
|
+
Finds annotations of a type across all views, filtered by their
|
|
845
|
+
``label`` property.
|
|
846
|
+
|
|
847
|
+
With both label lists empty (the default), every annotation of
|
|
848
|
+
``at_type`` is returned, including annotations that have no
|
|
849
|
+
``label``. A non-empty ``include_labels`` keeps only annotations
|
|
850
|
+
whose ``label`` is listed, so annotations without a ``label`` are
|
|
851
|
+
dropped. ``exclude_labels`` is applied after ``include_labels`` and
|
|
852
|
+
never drops annotations without a ``label``.
|
|
853
|
+
|
|
854
|
+
The ``label`` value is read with
|
|
855
|
+
:py:meth:`~mmif.serialize.annotation.Annotation.get_property`, so
|
|
856
|
+
the deprecated spellings of the property (``frameType`` on
|
|
857
|
+
``TimeFrame``, ``boxType`` on ``BoundingBox``) are honored.
|
|
858
|
+
|
|
859
|
+
:param at_type: the type of the annotations to look for
|
|
860
|
+
:param include_labels: labels to keep; empty means no restriction
|
|
861
|
+
:param exclude_labels: labels to leave out
|
|
862
|
+
:return: an iterator of matching annotations, in view order and
|
|
863
|
+
then in annotation order within each view
|
|
864
|
+
"""
|
|
865
|
+
include = tuple(include_labels)
|
|
866
|
+
exclude = tuple(exclude_labels)
|
|
867
|
+
for view in self.get_all_views_contain(at_type):
|
|
868
|
+
for ann in view.get_annotations(at_type):
|
|
869
|
+
label = ann.get_property('label')
|
|
870
|
+
if include and label not in include:
|
|
871
|
+
continue
|
|
872
|
+
if label is not None and label in exclude:
|
|
873
|
+
continue
|
|
874
|
+
yield ann
|
|
875
|
+
|
|
766
876
|
def _is_in_time_range(self, ann: Annotation, range_s: Union[int, float], range_e: Union[int, float]) -> bool:
|
|
767
877
|
"""
|
|
768
878
|
Checks if the annotation is anchored within the given time range. Any overlap is considered included.
|
|
@@ -134,13 +134,31 @@ class MmifObject(object):
|
|
|
134
134
|
"""
|
|
135
135
|
Method to set values in _unnamed_attributes.
|
|
136
136
|
|
|
137
|
+
A key that matches a declared (named) attribute is stored in that
|
|
138
|
+
attribute instead, with a warning, so that a key never lives in both
|
|
139
|
+
places. A leading ``@`` in the key is read as the ``_`` prefix of
|
|
140
|
+
the attribute name, following the JSON-LD mapping used in
|
|
141
|
+
serialization.
|
|
142
|
+
|
|
137
143
|
:param key: the attribute name
|
|
138
144
|
:param value: the desired value
|
|
139
145
|
:return: None
|
|
140
146
|
:raise: AttributeError if additional properties are disallowed by :func:`disallow_additional_properties`
|
|
147
|
+
:raise: KeyError if the key is a reserved name
|
|
141
148
|
"""
|
|
149
|
+
attr = f'_{key[1:]}' if key.startswith('@') else key
|
|
142
150
|
if self._unnamed_attributes is None:
|
|
143
|
-
|
|
151
|
+
hint = (f' "{key}" is a declared attribute; use obj["{attr}"] = value.'
|
|
152
|
+
if attr in self._named_attributes() else '')
|
|
153
|
+
raise AttributeError(f"Additional properties are disallowed by {self.__class__}.{hint}")
|
|
154
|
+
if key in self.reserved_names or attr in self.reserved_names:
|
|
155
|
+
raise KeyError(f"can't set a reserved name: {key}")
|
|
156
|
+
if attr in self._named_attributes():
|
|
157
|
+
warnings.warn(f'"{key}" is a declared attribute of {type(self).__name__}; '
|
|
158
|
+
f'storing it as the attribute, not as an additional property.',
|
|
159
|
+
stacklevel=2)
|
|
160
|
+
self[attr] = value
|
|
161
|
+
return
|
|
144
162
|
self._unnamed_attributes[key] = value # pytype: disable=unsupported-operands
|
|
145
163
|
|
|
146
164
|
def _named_attributes(self) -> Generator[str, None, None]:
|
|
@@ -418,6 +418,14 @@ class View(MmifObject):
|
|
|
418
418
|
def has_warnings(self):
|
|
419
419
|
return self.metadata.has_warnings()
|
|
420
420
|
|
|
421
|
+
def has_content(self) -> bool:
|
|
422
|
+
"""
|
|
423
|
+
Returns True if the view has at least one annotation. Per the MMIF
|
|
424
|
+
specification, error views and warning views carry no annotations,
|
|
425
|
+
so a view with content has neither an error nor warnings.
|
|
426
|
+
"""
|
|
427
|
+
return len(self.annotations) > 0
|
|
428
|
+
|
|
421
429
|
|
|
422
430
|
class ViewMetadata(MmifObject):
|
|
423
431
|
"""
|
|
@@ -430,6 +438,7 @@ class ViewMetadata(MmifObject):
|
|
|
430
438
|
self.document: str = ''
|
|
431
439
|
self.timestamp: Optional[datetime] = None
|
|
432
440
|
self.app: str = ''
|
|
441
|
+
self.appTags: List[str] = []
|
|
433
442
|
self.contains: ContainsDict = ContainsDict()
|
|
434
443
|
self.parameters: Dict[str, str] = {}
|
|
435
444
|
self.appConfiguration: Dict[str, Union[PRMTV_TYPES, List[PRMTV_TYPES]]] = {}
|
|
@@ -31,11 +31,14 @@ def prompt_user(mmif_obj: mmif.Mmif) -> int:
|
|
|
31
31
|
|
|
32
32
|
def rewind_mmif(mmif_obj: mmif.Mmif, choice: int, choice_is_viewnum: bool = True) -> mmif.Mmif:
|
|
33
33
|
"""
|
|
34
|
-
Rewind MMIF by deleting the last N views.
|
|
35
|
-
The number of views to rewind is given as a number of "views", or number of "producer apps".
|
|
36
|
-
By default, the number argument is interpreted as the number of "views".
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
Rewind MMIF by deleting the last N views.
|
|
35
|
+
The number of views to rewind is given as a number of "views", or number of "producer apps".
|
|
36
|
+
By default, the number argument is interpreted as the number of "views".
|
|
37
|
+
In "app" mode, one app is one app execution as grouped by
|
|
38
|
+
:func:`mmif.utils.workflow_helper.group_views_by_app`: all views that
|
|
39
|
+
share the same ``app`` and ``timestamp``, including warning and error
|
|
40
|
+
views. Rewinding one app removes the last execution only; earlier runs
|
|
41
|
+
of the same app are kept.
|
|
39
42
|
|
|
40
43
|
:param mmif_obj: mmif object
|
|
41
44
|
:param choice: number of views to rewind
|
|
@@ -59,7 +62,9 @@ def describe_argparser():
|
|
|
59
62
|
oneliner = 'provides CLI to rewind a MMIF from a CLAMS workflow.'
|
|
60
63
|
additional = textwrap.dedent("""
|
|
61
64
|
MMIF rewinder rewinds a MMIF by deleting the last N views.
|
|
62
|
-
N can be specified as a number of views, or a number of producer apps.
|
|
65
|
+
N can be specified as a number of views, or a number of producer apps.
|
|
66
|
+
In app mode, one app is one app execution: all views that share the
|
|
67
|
+
same app and timestamp, including warning and error views. """)
|
|
63
68
|
return oneliner, oneliner + '\n\n' + additional
|
|
64
69
|
|
|
65
70
|
|
|
@@ -9,12 +9,12 @@ import math
|
|
|
9
9
|
import warnings
|
|
10
10
|
from io import StringIO
|
|
11
11
|
from typing import Iterable # todo: replace with collections.abc.Iterable in Python 3.9
|
|
12
|
-
from typing import List, Optional, Union, Tuple
|
|
12
|
+
from typing import Iterator, List, Optional, Union, Tuple
|
|
13
13
|
|
|
14
14
|
import mmif
|
|
15
|
-
from mmif import Annotation, Document, Mmif
|
|
15
|
+
from mmif import Annotation, Document, Mmif, View
|
|
16
16
|
from mmif.utils.timeunit_helper import convert
|
|
17
|
-
from mmif.vocabulary import DocumentTypes
|
|
17
|
+
from mmif.vocabulary import AnnotationTypes, DocumentTypes
|
|
18
18
|
|
|
19
19
|
_CV_DEPS = ('av', 'cv2', 'PIL', 'wurlitzer')
|
|
20
20
|
_cv_import_warning = (
|
|
@@ -699,6 +699,95 @@ def extract_images_by_mode_with_sources(
|
|
|
699
699
|
return images, sources
|
|
700
700
|
|
|
701
701
|
|
|
702
|
+
def _to_timepoints(
|
|
703
|
+
output_view: View,
|
|
704
|
+
video_document: Document,
|
|
705
|
+
sources: List[Union[str, int]],
|
|
706
|
+
) -> List[str]:
|
|
707
|
+
"""
|
|
708
|
+
Turns per-image sources, as returned by
|
|
709
|
+
:py:func:`extract_images_by_mode_with_sources`, into ``TimePoint``
|
|
710
|
+
annotation ids, parallel to ``sources``.
|
|
711
|
+
|
|
712
|
+
A ``str`` source is the id of an existing ``TimePoint`` and is passed
|
|
713
|
+
through. An ``int`` source is a timepoint in milliseconds that no
|
|
714
|
+
``TimePoint`` backs; a new ``TimePoint`` is created for it in
|
|
715
|
+
``output_view`` and its id is used. No lookup for an existing
|
|
716
|
+
``TimePoint`` at the same time is done, so two calls on the same
|
|
717
|
+
fallback timepoint create two annotations.
|
|
718
|
+
|
|
719
|
+
:param output_view: the view new ``TimePoint`` annotations are added to
|
|
720
|
+
:param video_document: the video the timepoints belong to
|
|
721
|
+
:param sources: per-image sources
|
|
722
|
+
:return: ``TimePoint`` ids, parallel to ``sources``
|
|
723
|
+
"""
|
|
724
|
+
ids: List[str] = []
|
|
725
|
+
for source in sources:
|
|
726
|
+
if isinstance(source, str):
|
|
727
|
+
ids.append(source)
|
|
728
|
+
else:
|
|
729
|
+
tp = output_view.new_annotation(
|
|
730
|
+
AnnotationTypes.TimePoint, document=video_document.id,
|
|
731
|
+
timePoint=int(source), timeUnit='milliseconds')
|
|
732
|
+
ids.append(tp.id)
|
|
733
|
+
return ids
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def iter_timeframe_images(
|
|
737
|
+
mmif: Mmif,
|
|
738
|
+
output_view: View,
|
|
739
|
+
include_labels: Iterable[str] = (),
|
|
740
|
+
exclude_labels: Iterable[str] = (),
|
|
741
|
+
mode: Union[SamplingMode, None] = None,
|
|
742
|
+
as_PIL: bool = False,
|
|
743
|
+
) -> Iterator[Tuple[Annotation, List, List[str]]]:
|
|
744
|
+
"""
|
|
745
|
+
Samples images from every ``TimeFrame`` in the MMIF, filtered by label,
|
|
746
|
+
and yields them with the ``TimePoint`` each image was taken from.
|
|
747
|
+
|
|
748
|
+
``TimeFrame`` annotations are selected with
|
|
749
|
+
:py:meth:`~mmif.serialize.mmif.Mmif.get_annotations_of_labels`, and
|
|
750
|
+
images are sampled from each with
|
|
751
|
+
:py:func:`extract_images_by_mode_with_sources`. Images that cannot be
|
|
752
|
+
read from the video (``None`` in that function's result, already
|
|
753
|
+
reported there in a warning) are left out together with their sources.
|
|
754
|
+
A ``TimeFrame`` with no readable image is skipped.
|
|
755
|
+
|
|
756
|
+
Every image comes with the id of a ``TimePoint``. When an image was
|
|
757
|
+
sampled at a time no ``TimePoint`` backs (``tfSamplingMode`` fallback),
|
|
758
|
+
a new ``TimePoint`` is created in ``output_view`` as a side effect of
|
|
759
|
+
iterating, so pass the view the calling app writes its own output into.
|
|
760
|
+
The ids are meant for the ``origins`` of whatever the app derives from
|
|
761
|
+
the images.
|
|
762
|
+
|
|
763
|
+
:param mmif: :py:class:`~mmif.serialize.mmif.Mmif` instance
|
|
764
|
+
:param output_view: the view new ``TimePoint`` annotations are added to
|
|
765
|
+
:param include_labels: ``TimeFrame`` labels to keep; empty means all
|
|
766
|
+
:param exclude_labels: ``TimeFrame`` labels to leave out
|
|
767
|
+
:param mode: :py:class:`SamplingMode`, or None to use the context
|
|
768
|
+
default (the ``tfSamplingMode`` runtime parameter in CLAMS apps)
|
|
769
|
+
:param as_PIL: return :py:class:`PIL.Image.Image` instead of
|
|
770
|
+
:py:class:`~numpy.ndarray`
|
|
771
|
+
:return: an iterator of ``(time_frame, images, timepoint_ids)``, where
|
|
772
|
+
``images`` and ``timepoint_ids`` are parallel, non-empty lists
|
|
773
|
+
"""
|
|
774
|
+
# select first, so TimePoints added to ``output_view`` while iterating
|
|
775
|
+
# never interfere with the views being read
|
|
776
|
+
time_frames = list(mmif.get_annotations_of_labels(
|
|
777
|
+
AnnotationTypes.TimeFrame, include_labels, exclude_labels))
|
|
778
|
+
for time_frame in time_frames:
|
|
779
|
+
images, sources = extract_images_by_mode_with_sources(
|
|
780
|
+
mmif, time_frame, mode=mode, as_PIL=as_PIL)
|
|
781
|
+
readable = [(image, source) for image, source in zip(images, sources)
|
|
782
|
+
if image is not None]
|
|
783
|
+
if not readable:
|
|
784
|
+
continue
|
|
785
|
+
video_document = _resolve_video_document(mmif, time_frame)
|
|
786
|
+
timepoint_ids = _to_timepoints(
|
|
787
|
+
output_view, video_document, [source for _, source in readable])
|
|
788
|
+
yield time_frame, [image for image, _ in readable], timepoint_ids
|
|
789
|
+
|
|
790
|
+
|
|
702
791
|
def extract_images_by_mode(
|
|
703
792
|
mmif: Mmif,
|
|
704
793
|
time_frame: Annotation,
|
|
@@ -14,8 +14,22 @@ def group_views_by_app(views: ViewsList) -> List[List[Any]]:
|
|
|
14
14
|
"""
|
|
15
15
|
Groups views into app executions based on app and timestamp.
|
|
16
16
|
|
|
17
|
-
An
|
|
18
|
-
|
|
17
|
+
An app execution is the set of views produced by a single run of an
|
|
18
|
+
app. Apps based on ``clams-python`` give every view from one run the
|
|
19
|
+
same ``metadata.timestamp``, so views that share ``app`` and
|
|
20
|
+
``timestamp`` form one execution, and two runs of the same app form
|
|
21
|
+
two executions. Every execution counts, whether its views hold
|
|
22
|
+
annotations, only warnings, or an error. Views without ``app`` or
|
|
23
|
+
``timestamp`` cannot be grouped and are left out.
|
|
24
|
+
|
|
25
|
+
The executions are the common unit of
|
|
26
|
+
:func:`generate_workflow_identifier`, :func:`describe_single_mmif`,
|
|
27
|
+
and app-mode rewinding (:func:`mmif.utils.cli.rewind.rewind_mmif`),
|
|
28
|
+
so all three agree on the number and order of executions.
|
|
29
|
+
|
|
30
|
+
:param views: views to group
|
|
31
|
+
:return: list of app executions, each a list of views, ordered by
|
|
32
|
+
timestamp
|
|
19
33
|
"""
|
|
20
34
|
# Filter out views that don't have a timestamp or app, as they can't be grouped.
|
|
21
35
|
groupable_views = [
|
|
@@ -121,9 +135,15 @@ def generate_workflow_identifier(mmif_input: Union[str, Path, Mmif],
|
|
|
121
135
|
document mix as ``Type-N`` pairs joined by ``-`` and sorted by type
|
|
122
136
|
name (e.g. ``TextDocument-1-VideoDocument-1``).
|
|
123
137
|
|
|
138
|
+
Each app execution, as grouped by :func:`group_views_by_app`, adds
|
|
139
|
+
exactly one ``app_name/version/param_hash`` segment, including
|
|
140
|
+
executions that produced only warnings, only an error, or no
|
|
141
|
+
annotations. Hence, rewinding N apps from a MMIF (see
|
|
142
|
+
:func:`mmif.utils.cli.rewind.rewind_mmif`) yields the identifier with
|
|
143
|
+
its last N segments removed.
|
|
144
|
+
|
|
124
145
|
Uses view.metadata.parameters (raw user-passed values) for hashing
|
|
125
|
-
to ensure reproducibility.
|
|
126
|
-
from the identifier; empty views are included.
|
|
146
|
+
to ensure reproducibility.
|
|
127
147
|
|
|
128
148
|
:param mmif_input: Path to MMIF file (str or Path) or a Mmif object
|
|
129
149
|
:param return_param_dicts: If True, also return the parameter dictionaries
|
|
@@ -144,10 +164,6 @@ def generate_workflow_identifier(mmif_input: Union[str, Path, Mmif],
|
|
|
144
164
|
# Use the first view in the run as representative for metadata
|
|
145
165
|
first_view = app_execution[0]
|
|
146
166
|
|
|
147
|
-
# Skip runs where the representative view has errors or warnings
|
|
148
|
-
if first_view.has_error() or first_view.has_warnings():
|
|
149
|
-
continue
|
|
150
|
-
|
|
151
167
|
app = first_view.metadata.get("app")
|
|
152
168
|
if app is None:
|
|
153
169
|
continue
|
|
@@ -183,7 +199,7 @@ class SingleMmifStats(BaseModel):
|
|
|
183
199
|
app_count: int = Field(..., alias="appCount", description="Total number of app executions identified.")
|
|
184
200
|
error_views: List[str] = Field(default_factory=list, alias="errorViews", description="List of view IDs that contain errors.")
|
|
185
201
|
warning_views: List[str] = Field(default_factory=list, alias="warningViews", description="List of view IDs that contain warnings.")
|
|
186
|
-
empty_views: List[str] = Field(default_factory=list, alias="emptyViews", description="List of view IDs that contain no annotations.")
|
|
202
|
+
empty_views: List[str] = Field(default_factory=list, alias="emptyViews", description="List of view IDs that contain no annotations, error, or warnings.")
|
|
187
203
|
annotation_count_by_type: Dict[str, int] = Field(default_factory=dict, alias="annotationCountByType", description="Total annotation counts across the file.")
|
|
188
204
|
|
|
189
205
|
class AppProfiling(BaseModel):
|
|
@@ -254,7 +270,15 @@ def describe_single_mmif(mmif_input: Union[str, Path, Mmif]) -> dict:
|
|
|
254
270
|
conceptual hierarchy is that a **workflow** is a sequence of **apps**,
|
|
255
271
|
and each **app** execution can produce one or more **views**. This function
|
|
256
272
|
groups views that share the same ``app`` and ``metadata.timestamp`` into
|
|
257
|
-
a single logical "app execution".
|
|
273
|
+
a single logical "app execution" (see :func:`group_views_by_app`).
|
|
274
|
+
|
|
275
|
+
Every app execution is listed in ``apps``, including executions whose
|
|
276
|
+
views hold only warnings or an error; the IDs of such views are also
|
|
277
|
+
listed in ``stats.warningViews`` and ``stats.errorViews``. The
|
|
278
|
+
``workflowId`` is computed from the same executions by
|
|
279
|
+
:func:`generate_workflow_identifier`, so its segments match the
|
|
280
|
+
``apps`` entries one to one. The only exception is the trailing
|
|
281
|
+
``non-existing-app`` entry that collects views that cannot be grouped.
|
|
258
282
|
|
|
259
283
|
.. note::
|
|
260
284
|
For MMIF files generated by apps based on ``clams-python`` <= 1.3.3, all
|
|
@@ -276,9 +300,17 @@ def describe_single_mmif(mmif_input: Union[str, Path, Mmif]) -> dict:
|
|
|
276
300
|
"""
|
|
277
301
|
mmif = _read_mmif_from_path(mmif_input)
|
|
278
302
|
|
|
303
|
+
# view status is a property of each view, independent of grouping
|
|
279
304
|
error_view_ids = []
|
|
280
305
|
warning_view_ids = []
|
|
281
306
|
empty_view_ids = []
|
|
307
|
+
for view in mmif.views:
|
|
308
|
+
if view.has_error():
|
|
309
|
+
error_view_ids.append(view.id)
|
|
310
|
+
elif view.has_warnings():
|
|
311
|
+
warning_view_ids.append(view.id)
|
|
312
|
+
elif not view.has_content():
|
|
313
|
+
empty_view_ids.append(view.id)
|
|
282
314
|
|
|
283
315
|
# Generate the new "apps" list
|
|
284
316
|
grouped_apps = []
|
|
@@ -286,14 +318,9 @@ def describe_single_mmif(mmif_input: Union[str, Path, Mmif]) -> dict:
|
|
|
286
318
|
view_groups = group_views_by_app(mmif.views)
|
|
287
319
|
for group in view_groups:
|
|
288
320
|
first_view = group[0]
|
|
289
|
-
# skip executions with errors or warnings
|
|
290
|
-
if first_view.has_error() or first_view.has_warnings():
|
|
291
|
-
continue
|
|
292
321
|
|
|
293
322
|
execution_ann_counter = Counter()
|
|
294
323
|
for view in group:
|
|
295
|
-
if len(view.annotations) == 0:
|
|
296
|
-
empty_view_ids.append(view.id)
|
|
297
324
|
execution_ann_counter.update(Counter(str(ann.at_type) for ann in view.annotations))
|
|
298
325
|
|
|
299
326
|
execution_view_ids = [v.id for v in group]
|
|
@@ -315,19 +342,8 @@ def describe_single_mmif(mmif_input: Union[str, Path, Mmif]) -> dict:
|
|
|
315
342
|
annotationCountByType=count_dict
|
|
316
343
|
))
|
|
317
344
|
|
|
318
|
-
#
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
for view in mmif.views:
|
|
322
|
-
if view.id not in processed_view_ids:
|
|
323
|
-
if view.has_error():
|
|
324
|
-
error_view_ids.append(view.id)
|
|
325
|
-
elif view.has_warnings():
|
|
326
|
-
warning_view_ids.append(view.id)
|
|
327
|
-
elif len(view.annotations) == 0:
|
|
328
|
-
empty_view_ids.append(view.id)
|
|
329
|
-
|
|
330
|
-
unassigned_view_ids = all_view_ids - processed_view_ids - set(error_view_ids) - set(warning_view_ids)
|
|
345
|
+
# views that cannot be grouped (no app or timestamp), whatever they hold
|
|
346
|
+
unassigned_view_ids = set(v.id for v in mmif.views) - processed_view_ids
|
|
331
347
|
|
|
332
348
|
# Store app_count before potentially adding the special entry
|
|
333
349
|
app_count = len(grouped_apps)
|