Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 34 additions & 64 deletions clams/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@

from typing import Union, Any, Optional, Dict, List, Mapping, Tuple, cast

from mmif import Mmif, Document, DocumentTypes, View, AnnotationTypes
from mmif import (
Mmif, Document, DocumentTypes, View, AnnotationTypes, EmptyOutputWarning,
)
from mmif.utils.video_document_helper import (
SamplingMode, SAMPLING_MODE_DESCRIPTIONS, SAMPLING_MODE_DEFAULT,
_sampling_mode,
Expand Down Expand Up @@ -53,10 +55,6 @@
]


class EmptyOutputWarning(UserWarning):
"""Raised when an app's ``_annotate()`` produces no non-empty views."""


class ImagesPerPromptMode(Enum):
"""
How :py:meth:`ClamsPromptableApp.chunk_tasks` sizes the chunks of a
Expand Down Expand Up @@ -224,7 +222,6 @@ def annotate(self, mmif: Union[str, dict, Mmif], **runtime_params: List[str]) ->
if not isinstance(mmif, Mmif):
mmif, runtime_params = unwrap_if_envelope(mmif, runtime_params)
mmif = Mmif(mmif)
existing_view_ids = {view.id for view in mmif.views}
issued_warnings: List[Union[Warning, warnings.WarningMessage]] = []
for key in runtime_params:
if key not in self.annotate_param_spec:
Expand All @@ -238,70 +235,43 @@ def annotate(self, mmif: Union[str, dict, Mmif], **runtime_params: List[str]) ->
if sampling_mode_str is not None:
_sampling_mode.set(SamplingMode(sampling_mode_str))
t = datetime.now()
with warnings.catch_warnings(record=True) as ws:
annotated, cuda_profiler = self._profile_cuda_memory(self._annotate)(mmif, **refined)
# turn pending document properties into `Annotation`s now, so a
# view whose only output is document properties is not empty;
# `serialize()` repeats this later, skipping values already written
annotated.generate_capital_annotations()
new_views = [v for v in annotated.views if v.id not in existing_view_ids]
# an app-set error or warnings view has no annotations, but is kept
is_empty = lambda v: not (v.has_content() or v.has_error() or v.has_warnings())
for v in new_views:
if is_empty(v):
del annotated.views._items[v.id] # no public API to remove a view (mmif-python)
if all(is_empty(v) for v in new_views):
warnings.warn(f'App "{self.metadata.identifier}" produced no annotations for this '
f'input; the input may lack expected content, or there may genuinely '
f'be nothing to annotate.', EmptyOutputWarning)
if ws:
issued_warnings.extend(ws)
if issued_warnings:
warnings_view = annotated.new_view()
self.sign_view(warnings_view, refined)
warnings_view.metadata.warnings = issued_warnings
td = datetime.now() - t
runningTime = refined.get('runningTime', False)
hwFetch = refined.get('hwFetch', False)
runtime_recs: Dict[str, Any] = {}
if hwFetch:
import multiprocessing
import platform, shutil, subprocess
runtime_recs['cpu'] = f"{platform.machine()}, {multiprocessing.cpu_count()} cores"
runtime_recs['cuda'] = []
# Use cuda_profiler data if available, otherwise fallback to nvidia-smi
if cuda_profiler:
for gpu_name, mem_info in cuda_profiler.items():
total_str = self._cuda_memory_to_str(mem_info['total'])
available_str = self._cuda_memory_to_str(mem_info['available_before'])
peak_str = self._cuda_memory_to_str(mem_info['peak'])
runtime_recs['cuda'].append(
f"{gpu_name}, {total_str} total, {available_str} available, {peak_str} peak used"
)
elif shutil.which('nvidia-smi'):
for gpu in subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader'],
stdout=subprocess.PIPE).stdout.decode('utf-8').strip().split('\n'):
name, mem = gpu.split(', ')
runtime_recs['cuda'].append(self._cuda_device_name_concat(name, mem))
new_app_views = [v for v in annotated.views
if v.id not in existing_view_ids
and v.metadata.app == str(self.metadata.identifier)]
# all views from this run share the timestamp that mmif-python
# assigned to the first of them, serving as the "run ID"
run_id = next((v.metadata.timestamp for v in new_app_views
if v.metadata.timestamp is not None), None)
for annotated_view in new_app_views:
if run_id is not None:
annotated_view.metadata.timestamp = run_id
signature = View()
self.sign_view(signature, refined)
with mmif.set_producer(signature) as producer:
with warnings.catch_warnings(record=True) as ws:
annotated, cuda_profiler = self._profile_cuda_memory(self._annotate)(mmif, **refined)
producer.add_warnings(*issued_warnings, *ws)
td = datetime.now() - t
runningTime = refined.get('runningTime', False)
hwFetch = refined.get('hwFetch', False)
runtime_recs: Dict[str, Any] = {}
if hwFetch:
import multiprocessing
import platform, shutil, subprocess
runtime_recs['cpu'] = f"{platform.machine()}, {multiprocessing.cpu_count()} cores"
runtime_recs['cuda'] = []
# Use cuda_profiler data if available, otherwise fallback to nvidia-smi
if cuda_profiler:
for gpu_name, mem_info in cuda_profiler.items():
total_str = self._cuda_memory_to_str(mem_info['total'])
available_str = self._cuda_memory_to_str(mem_info['available_before'])
peak_str = self._cuda_memory_to_str(mem_info['peak'])
runtime_recs['cuda'].append(
f"{gpu_name}, {total_str} total, {available_str} available, {peak_str} peak used"
)
elif shutil.which('nvidia-smi'):
for gpu in subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader'],
stdout=subprocess.PIPE).stdout.decode('utf-8').strip().split('\n'):
name, mem = gpu.split(', ')
runtime_recs['cuda'].append(self._cuda_device_name_concat(name, mem))
profiling_data: Dict[str, Any] = {}
if runningTime:
profiling_data['runningTime'] = str(td)
if len(runtime_recs) > 0:
profiling_data['hardware'] = runtime_recs
if profiling_data:
annotated_view.metadata.set_additional_property('appProfiling', profiling_data)

return annotated.serialize(pretty=pretty, sanitize=True)
producer.set_view_metadata('appProfiling', profiling_data)
return annotated.serialize(pretty=pretty, sanitize=True)

@abstractmethod
def _annotate(self, mmif: Mmif, _raw_parameters=None, **refined_parameters) -> Mmif:
Expand Down
1 change: 1 addition & 0 deletions documentation/target-versions.csv
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"``clams-python`` version","``mmif-python`` version","Target MMIF Specification"
`1.8.2 <https://pypi.org/project/clams-python/1.8.2/>`__,`1.7.0 <https://pypi.org/project/mmif-python/1.7.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
`1.8.1 <https://pypi.org/project/clams-python/1.8.1/>`__,`1.6.0 <https://pypi.org/project/mmif-python/1.6.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
`1.8.0 <https://pypi.org/project/clams-python/1.8.0/>`__,`1.5.4 <https://pypi.org/project/mmif-python/1.5.4/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
`1.7.4 <https://pypi.org/project/clams-python/1.7.4/>`__,`1.5.3 <https://pypi.org/project/mmif-python/1.5.3/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3 :: Only",
]
dependencies = [
"mmif-python==1.6.0",
"mmif-python==1.7.0",
"Flask>=2",
"Flask-RESTful>=0.3.9",
"gunicorn>=20",
Expand Down
64 changes: 63 additions & 1 deletion tests/test_clamsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@

import jsonschema
import pytest
from mmif import Mmif, Document, DocumentTypes, AnnotationTypes, View, __specver__
from mmif import (
Mmif, Document, DocumentTypes, AnnotationTypes, View, EmptyOutputWarning,
__specver__,
)
from mmif.serialize.mmif import ViewsList

import clams.app
Expand Down Expand Up @@ -518,6 +521,65 @@ def _annotate(self, mmif, **kwargs):
self.assertEqual(len(view.annotations), 1)
self.assertFalse(view.has_warnings())

def _in_mmif_with_earlier_view(self):
in_mmif = Mmif(self.in_mmif)
doc_id = next(iter(in_mmif.documents)).id
earlier = in_mmif.new_view()
earlier.metadata.app = 'http://apps.clams.ai/earlier-app/v1'
earlier.new_contain(AnnotationTypes.TimeFrame, document=doc_id)
earlier.new_annotation(AnnotationTypes.TimeFrame, start=0, end=1)
return in_mmif.serialize()

def test_document_properties_go_to_a_new_signed_view(self):
class DocPropClamsApp(clams.app.ClamsApp):
def _appmetadata(self):
pass
def _annotate(self, mmif, **kwargs):
next(iter(mmif.documents)).add_property('duration', 1000)
return mmif
app = DocPropClamsApp()
in_str = self._in_mmif_with_earlier_view()
out_str = app.annotate(in_str)
self.assertEqual(json.loads(in_str)['views'][0],
json.loads(out_str)['views'][0])
out_views = list(Mmif(out_str).views)
self.assertEqual(len(out_views), 2)
self.assertEqual(out_views[1].metadata.app,
str(app.metadata.identifier))
records = list(out_views[1].get_annotations(AnnotationTypes.Annotation))
self.assertEqual(len(records), 1)
self.assertEqual(records[0].get_property('duration'), 1000)
self.assertFalse(out_views[1].has_warnings())

def test_earlier_views_are_written_unchanged(self):
in_str = self._in_mmif_with_earlier_view()
out_str = self.app.annotate(in_str)
self.assertEqual(json.loads(in_str)['views'][0],
json.loads(out_str)['views'][0])

def test_failed_run_leaves_mmif_reusable(self):
class FlakyClamsApp(clams.app.ClamsApp):
attempts = 0
def _appmetadata(self):
pass
def _annotate(self, mmif, **kwargs):
self.attempts += 1
if self.attempts == 1:
raise ValueError('first attempt fails')
new_view = mmif.new_view()
self.sign_view(new_view, kwargs)
new_view.new_contain(AnnotationTypes.TimeFrame)
new_view.new_annotation(AnnotationTypes.TimeFrame, start=0, end=1)
return mmif
app = FlakyClamsApp()
in_mmif = Mmif(self.in_mmif)
with self.assertRaises(ValueError):
app.annotate(in_mmif)
self.assertEqual(len(Mmif(app.annotate(in_mmif)).views), 1)

def test_empty_output_warning_is_importable_from_clams(self):
self.assertIs(clams.app.EmptyOutputWarning, EmptyOutputWarning)

def test_self_reported_error_view_is_not_pruned(self):
class ErroringClamsApp(clams.app.ClamsApp):
def _appmetadata(self):
Expand Down
Loading