mirror of
https://github.com/clearml/clearml
synced 2025-04-27 17:51:45 +00:00
Add media (audio) support for both Logger and Tensorboard bind
This commit is contained in:
parent
7ac7e088a1
commit
648779380c
@ -179,31 +179,31 @@ class UploadEvent(MetricsEventAdapter):
|
|||||||
|
|
||||||
_metric_counters = {}
|
_metric_counters = {}
|
||||||
_metric_counters_lock = Lock()
|
_metric_counters_lock = Lock()
|
||||||
_image_file_history_size = int(config.get('metrics.file_history_size', 5))
|
_file_history_size = int(config.get('metrics.file_history_size', 5))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _replace_slash(part):
|
def _replace_slash(part):
|
||||||
return part.replace('\\', '/').strip('/').replace('/', '.slash.')
|
return part.replace('\\', '/').strip('/').replace('/', '.slash.')
|
||||||
|
|
||||||
def __init__(self, metric, variant, image_data, local_image_path=None, iter=0, upload_uri=None,
|
def __init__(self, metric, variant, image_data, local_image_path=None, iter=0, upload_uri=None,
|
||||||
image_file_history_size=None, delete_after_upload=False, **kwargs):
|
file_history_size=None, delete_after_upload=False, **kwargs):
|
||||||
# param override_filename: override uploaded file name (notice extension will be added from local path
|
# param override_filename: override uploaded file name (notice extension will be added from local path
|
||||||
# param override_filename_ext: override uploaded file extension
|
# param override_filename_ext: override uploaded file extension
|
||||||
if image_data is not None and not hasattr(image_data, 'shape'):
|
if image_data is not None and (not hasattr(image_data, 'shape') and not isinstance(image_data, six.BytesIO)):
|
||||||
raise ValueError('Image must have a shape attribute')
|
raise ValueError('Image must have a shape attribute')
|
||||||
self._image_data = image_data
|
self._image_data = image_data
|
||||||
self._local_image_path = local_image_path
|
self._local_image_path = local_image_path
|
||||||
self._url = None
|
self._url = None
|
||||||
self._key = None
|
self._key = None
|
||||||
self._count = self._get_metric_count(metric, variant)
|
self._count = self._get_metric_count(metric, variant)
|
||||||
if not image_file_history_size:
|
if not file_history_size:
|
||||||
image_file_history_size = self._image_file_history_size
|
file_history_size = self._file_history_size
|
||||||
self._filename = kwargs.pop('override_filename', None)
|
self._filename = kwargs.pop('override_filename', None)
|
||||||
if not self._filename:
|
if not self._filename:
|
||||||
if image_file_history_size < 1:
|
if file_history_size < 1:
|
||||||
self._filename = '%s_%s_%08d' % (metric, variant, self._count)
|
self._filename = '%s_%s_%08d' % (metric, variant, self._count)
|
||||||
else:
|
else:
|
||||||
self._filename = '%s_%s_%08d' % (metric, variant, self._count % image_file_history_size)
|
self._filename = '%s_%s_%08d' % (metric, variant, self._count % file_history_size)
|
||||||
|
|
||||||
# make sure we have to '/' in the filename because it might access other folders,
|
# make sure we have to '/' in the filename because it might access other folders,
|
||||||
# and we don't want that to occur
|
# and we don't want that to occur
|
||||||
@ -253,8 +253,10 @@ class UploadEvent(MetricsEventAdapter):
|
|||||||
local_file = None
|
local_file = None
|
||||||
# don't provide file in case this event is out of the history window
|
# don't provide file in case this event is out of the history window
|
||||||
last_count = self._get_metric_count(self.metric, self.variant, next=False)
|
last_count = self._get_metric_count(self.metric, self.variant, next=False)
|
||||||
if abs(self._count - last_count) > self._image_file_history_size:
|
if abs(self._count - last_count) > self._file_history_size:
|
||||||
output = None
|
output = None
|
||||||
|
elif isinstance(self._image_data, six.BytesIO):
|
||||||
|
output = self._image_data
|
||||||
elif self._image_data is not None:
|
elif self._image_data is not None:
|
||||||
image_data = self._image_data
|
image_data = self._image_data
|
||||||
if not isinstance(image_data, np.ndarray):
|
if not isinstance(image_data, np.ndarray):
|
||||||
@ -318,10 +320,26 @@ class UploadEvent(MetricsEventAdapter):
|
|||||||
|
|
||||||
class ImageEvent(UploadEvent):
|
class ImageEvent(UploadEvent):
|
||||||
def __init__(self, metric, variant, image_data, local_image_path=None, iter=0, upload_uri=None,
|
def __init__(self, metric, variant, image_data, local_image_path=None, iter=0, upload_uri=None,
|
||||||
image_file_history_size=None, delete_after_upload=False, **kwargs):
|
file_history_size=None, delete_after_upload=False, **kwargs):
|
||||||
super(ImageEvent, self).__init__(metric, variant, image_data=image_data, local_image_path=local_image_path,
|
super(ImageEvent, self).__init__(metric, variant, image_data=image_data, local_image_path=local_image_path,
|
||||||
iter=iter, upload_uri=upload_uri,
|
iter=iter, upload_uri=upload_uri,
|
||||||
image_file_history_size=image_file_history_size,
|
file_history_size=file_history_size,
|
||||||
|
delete_after_upload=delete_after_upload, **kwargs)
|
||||||
|
|
||||||
|
def get_api_event(self):
|
||||||
|
return events.MetricsImageEvent(
|
||||||
|
url=self._url,
|
||||||
|
key=self._key,
|
||||||
|
**self._get_base_dict()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MediaEvent(UploadEvent):
|
||||||
|
def __init__(self, metric, variant, stream, local_image_path=None, iter=0, upload_uri=None,
|
||||||
|
file_history_size=None, delete_after_upload=False, **kwargs):
|
||||||
|
super(MediaEvent, self).__init__(metric, variant, image_data=stream, local_image_path=local_image_path,
|
||||||
|
iter=iter, upload_uri=upload_uri,
|
||||||
|
file_history_size=file_history_size,
|
||||||
delete_after_upload=delete_after_upload, **kwargs)
|
delete_after_upload=delete_after_upload, **kwargs)
|
||||||
|
|
||||||
def get_api_event(self):
|
def get_api_event(self):
|
||||||
|
@ -16,7 +16,7 @@ from ...utilities.plotly_reporter import create_2d_histogram_plot, create_value_
|
|||||||
create_2d_scatter_series, create_3d_scatter_series, create_line_plot, plotly_scatter3d_layout_dict, \
|
create_2d_scatter_series, create_3d_scatter_series, create_line_plot, plotly_scatter3d_layout_dict, \
|
||||||
create_image_plot, create_plotly_table
|
create_image_plot, create_plotly_table
|
||||||
from ...utilities.py3_interop import AbstractContextManager
|
from ...utilities.py3_interop import AbstractContextManager
|
||||||
from .events import ScalarEvent, VectorEvent, ImageEvent, PlotEvent, ImageEventNoUpload, UploadEvent
|
from .events import ScalarEvent, VectorEvent, ImageEvent, PlotEvent, ImageEventNoUpload, UploadEvent, MediaEvent
|
||||||
|
|
||||||
|
|
||||||
class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncManagerMixin):
|
class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncManagerMixin):
|
||||||
@ -204,11 +204,28 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan
|
|||||||
ev = ImageEventNoUpload(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, src=src)
|
ev = ImageEventNoUpload(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, src=src)
|
||||||
self._report(ev)
|
self._report(ev)
|
||||||
|
|
||||||
|
def report_media(self, title, series, src, iter):
|
||||||
|
"""
|
||||||
|
Report a media link.
|
||||||
|
:param title: Title (AKA metric)
|
||||||
|
:type title: str
|
||||||
|
:param series: Series (AKA variant)
|
||||||
|
:type series: str
|
||||||
|
:param src: Media source URI. This URI will be used by the webapp and workers when trying to obtain the image
|
||||||
|
for presentation of processing. Currently only http(s), file and s3 schemes are supported.
|
||||||
|
:type src: str
|
||||||
|
:param iter: Iteration number
|
||||||
|
:type value: int
|
||||||
|
"""
|
||||||
|
ev = ImageEventNoUpload(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, src=src)
|
||||||
|
self._report(ev)
|
||||||
|
|
||||||
def report_image_and_upload(self, title, series, iter, path=None, image=None, upload_uri=None,
|
def report_image_and_upload(self, title, series, iter, path=None, image=None, upload_uri=None,
|
||||||
max_image_history=None, delete_after_upload=False):
|
max_image_history=None, delete_after_upload=False):
|
||||||
"""
|
"""
|
||||||
Report an image and upload its contents. Image is uploaded to a preconfigured bucket (see setup_upload()) with
|
Report an image and upload its contents. Image is uploaded to a preconfigured bucket (see setup_upload()) with
|
||||||
a key (filename) describing the task ID, title, series and iteration.
|
a key (filename) describing the task ID, title, series and iteration.
|
||||||
|
|
||||||
:param title: Title (AKA metric)
|
:param title: Title (AKA metric)
|
||||||
:type title: str
|
:type title: str
|
||||||
:param series: Series (AKA variant)
|
:param series: Series (AKA variant)
|
||||||
@ -228,11 +245,44 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan
|
|||||||
raise ValueError('Upload configuration is required (use setup_upload())')
|
raise ValueError('Upload configuration is required (use setup_upload())')
|
||||||
if len([x for x in (path, image) if x is not None]) != 1:
|
if len([x for x in (path, image) if x is not None]) != 1:
|
||||||
raise ValueError('Expected only one of [filename, image]')
|
raise ValueError('Expected only one of [filename, image]')
|
||||||
kwargs = dict(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, image_file_history_size=max_image_history)
|
kwargs = dict(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, file_history_size=max_image_history)
|
||||||
ev = ImageEvent(image_data=image, upload_uri=upload_uri, local_image_path=path,
|
ev = ImageEvent(image_data=image, upload_uri=upload_uri, local_image_path=path,
|
||||||
delete_after_upload=delete_after_upload, **kwargs)
|
delete_after_upload=delete_after_upload, **kwargs)
|
||||||
self._report(ev)
|
self._report(ev)
|
||||||
|
|
||||||
|
def report_media_and_upload(self, title, series, iter, path=None, stream=None, upload_uri=None,
|
||||||
|
file_extension=None, max_history=None, delete_after_upload=False):
|
||||||
|
"""
|
||||||
|
Report a media file/stream and upload its contents.
|
||||||
|
Media is uploaded to a preconfigured bucket
|
||||||
|
(see setup_upload()) with a key (filename) describing the task ID, title, series and iteration.
|
||||||
|
|
||||||
|
:param title: Title (AKA metric)
|
||||||
|
:type title: str
|
||||||
|
:param series: Series (AKA variant)
|
||||||
|
:type series: str
|
||||||
|
:param iter: Iteration number
|
||||||
|
:type iter: int
|
||||||
|
:param path: A path to an image file. Required unless matrix is provided.
|
||||||
|
:type path: str
|
||||||
|
:param stream: File stream
|
||||||
|
:param file_extension: file extension to use when stream is passed
|
||||||
|
:param max_history: maximum number of files to store per metric/variant combination
|
||||||
|
use negative value for unlimited. default is set in global configuration (default=5)
|
||||||
|
:param delete_after_upload: if True, one the file was uploaded the local copy will be deleted
|
||||||
|
:type delete_after_upload: boolean
|
||||||
|
"""
|
||||||
|
if not self._storage_uri and not upload_uri:
|
||||||
|
raise ValueError('Upload configuration is required (use setup_upload())')
|
||||||
|
if len([x for x in (path, stream) if x is not None]) != 1:
|
||||||
|
raise ValueError('Expected only one of [filename, stream]')
|
||||||
|
kwargs = dict(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter,
|
||||||
|
file_history_size=max_history)
|
||||||
|
ev = MediaEvent(stream=stream, upload_uri=upload_uri, local_image_path=path,
|
||||||
|
override_filename_ext=file_extension,
|
||||||
|
delete_after_upload=delete_after_upload, **kwargs)
|
||||||
|
self._report(ev)
|
||||||
|
|
||||||
def report_histogram(self, title, series, histogram, iter, labels=None, xlabels=None,
|
def report_histogram(self, title, series, histogram, iter, labels=None, xlabels=None,
|
||||||
xtitle=None, ytitle=None, comment=None):
|
xtitle=None, ytitle=None, comment=None):
|
||||||
"""
|
"""
|
||||||
@ -542,7 +592,7 @@ class Reporter(InterfaceBase, AbstractContextManager, SetupUploadMixin, AsyncMan
|
|||||||
raise ValueError('Upload configuration is required (use setup_upload())')
|
raise ValueError('Upload configuration is required (use setup_upload())')
|
||||||
if len([x for x in (path, matrix) if x is not None]) != 1:
|
if len([x for x in (path, matrix) if x is not None]) != 1:
|
||||||
raise ValueError('Expected only one of [filename, matrix]')
|
raise ValueError('Expected only one of [filename, matrix]')
|
||||||
kwargs = dict(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, image_file_history_size=max_image_history)
|
kwargs = dict(metric=self._normalize_name(title), variant=self._normalize_name(series), iter=iter, file_history_size=max_image_history)
|
||||||
ev = UploadEvent(image_data=matrix, upload_uri=upload_uri, local_image_path=path,
|
ev = UploadEvent(image_data=matrix, upload_uri=upload_uri, local_image_path=path,
|
||||||
delete_after_upload=delete_after_upload, **kwargs)
|
delete_after_upload=delete_after_upload, **kwargs)
|
||||||
_, url = ev.get_target_full_upload_uri(upload_uri or self._storage_uri, self._metrics.storage_key_prefix)
|
_, url = ev.get_target_full_upload_uri(upload_uri or self._storage_uri, self._metrics.storage_key_prefix)
|
||||||
|
@ -5,6 +5,7 @@ import threading
|
|||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
from mimetypes import guess_extension
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -454,6 +455,31 @@ class EventTrainsWriter(object):
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _add_audio(self, tag, step, values):
|
||||||
|
# only report images every specific interval
|
||||||
|
if step % self.image_report_freq != 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
audio_str = values['encodedAudioString']
|
||||||
|
audio_data = base64.b64decode(audio_str)
|
||||||
|
if audio_data is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
title, series = self.tag_splitter(tag, num_split_parts=3, default_title='Images', logdir_header='title',
|
||||||
|
auto_reduce_num_split=True)
|
||||||
|
step = self._fix_step_counter(title, series, step)
|
||||||
|
|
||||||
|
stream = BytesIO(audio_data)
|
||||||
|
file_extension = guess_extension(values['contentType']) or '.{}'.format(values['contentType'].split('/')[-1])
|
||||||
|
self._logger.report_media(
|
||||||
|
title=title,
|
||||||
|
series=series,
|
||||||
|
iteration=step,
|
||||||
|
stream=stream,
|
||||||
|
file_extension=file_extension,
|
||||||
|
max_history=self.max_keep_images,
|
||||||
|
)
|
||||||
|
|
||||||
def _fix_step_counter(self, title, series, step):
|
def _fix_step_counter(self, title, series, step):
|
||||||
key = (title, series)
|
key = (title, series)
|
||||||
if key not in EventTrainsWriter._title_series_wraparound_counter:
|
if key not in EventTrainsWriter._title_series_wraparound_counter:
|
||||||
@ -473,7 +499,7 @@ class EventTrainsWriter(object):
|
|||||||
|
|
||||||
def add_event(self, event, step=None, walltime=None, **kwargs):
|
def add_event(self, event, step=None, walltime=None, **kwargs):
|
||||||
supported_metrics = {
|
supported_metrics = {
|
||||||
'simpleValue', 'image', 'histo', 'tensor'
|
'simpleValue', 'image', 'histo', 'tensor', 'audio'
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_data(value_dict, metric_search_order):
|
def get_data(value_dict, metric_search_order):
|
||||||
@ -531,6 +557,8 @@ class EventTrainsWriter(object):
|
|||||||
self._add_histogram(tag=tag, step=step, histo_data=values)
|
self._add_histogram(tag=tag, step=step, histo_data=values)
|
||||||
elif metric == 'image':
|
elif metric == 'image':
|
||||||
self._add_image(tag=tag, step=step, img_data=values)
|
self._add_image(tag=tag, step=step, img_data=values)
|
||||||
|
elif metric == 'audio':
|
||||||
|
self._add_audio(tag, step, values)
|
||||||
elif metric == 'tensor' and values.get('dtype') == 'DT_STRING':
|
elif metric == 'tensor' and values.get('dtype') == 'DT_STRING':
|
||||||
# text, just print to console
|
# text, just print to console
|
||||||
text = base64.b64decode('\n'.join(values['stringVal'])).decode('utf-8')
|
text = base64.b64decode('\n'.join(values['stringVal'])).decode('utf-8')
|
||||||
|
@ -477,6 +477,71 @@ class Logger(object):
|
|||||||
delete_after_upload=delete_after_upload,
|
delete_after_upload=delete_after_upload,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def report_media(self, title, series, iteration, local_path=None, stream=None,
|
||||||
|
file_extension=None, max_history=None, delete_after_upload=False, url=None):
|
||||||
|
"""
|
||||||
|
Report an image and upload its contents.
|
||||||
|
|
||||||
|
Image is uploaded to a preconfigured bucket (see setup_upload()) with a key (filename)
|
||||||
|
describing the task ID, title, series and iteration.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
:paramref:`~.Logger.report_image.local_path`, :paramref:`~.Logger.report_image.url`, :paramref:`~.Logger.report_image.image` and :paramref:`~.Logger.report_image.matrix`
|
||||||
|
are mutually exclusive, and at least one must be provided.
|
||||||
|
|
||||||
|
:param str title: Title (AKA metric)
|
||||||
|
:param str series: Series (AKA variant)
|
||||||
|
:param int iteration: Iteration number
|
||||||
|
:param str local_path: A path to an image file.
|
||||||
|
:param stream: BytesIO stream to upload (must provide file extension if used)
|
||||||
|
:param str url: A URL to the location of a pre-uploaded image.
|
||||||
|
:param file_extension: file extension to use when stream is passed
|
||||||
|
:param int max_history: maximum number of media files to store per metric/variant combination
|
||||||
|
use negative value for unlimited. default is set in global configuration (default=5)
|
||||||
|
:param bool delete_after_upload: if True, one the file was uploaded the local copy will be deleted
|
||||||
|
"""
|
||||||
|
mutually_exclusive(
|
||||||
|
UsageError, _check_none=True,
|
||||||
|
local_path=local_path or None, url=url or None, stream=stream,
|
||||||
|
)
|
||||||
|
if stream is not None and not file_extension:
|
||||||
|
raise ValueError("No file extension provided for stream media upload")
|
||||||
|
|
||||||
|
# if task was not started, we have to start it
|
||||||
|
self._start_task_if_needed()
|
||||||
|
|
||||||
|
self._touch_title_series(title, series)
|
||||||
|
|
||||||
|
if url:
|
||||||
|
self._task.reporter.report_media(
|
||||||
|
title=title,
|
||||||
|
series=series,
|
||||||
|
src=url,
|
||||||
|
iter=iteration,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
upload_uri = self.get_default_upload_destination()
|
||||||
|
if not upload_uri:
|
||||||
|
upload_uri = Path(get_cache_dir()) / 'debug_images'
|
||||||
|
upload_uri.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Verify that we can upload to this destination
|
||||||
|
upload_uri = str(upload_uri)
|
||||||
|
storage = StorageHelper.get(upload_uri)
|
||||||
|
upload_uri = storage.verify_upload(folder_uri=upload_uri)
|
||||||
|
|
||||||
|
self._task.reporter.report_media_and_upload(
|
||||||
|
title=title,
|
||||||
|
series=series,
|
||||||
|
path=local_path,
|
||||||
|
stream=stream,
|
||||||
|
iter=iteration,
|
||||||
|
upload_uri=upload_uri,
|
||||||
|
max_history=max_history,
|
||||||
|
delete_after_upload=delete_after_upload,
|
||||||
|
file_extension=file_extension,
|
||||||
|
)
|
||||||
|
|
||||||
def set_default_upload_destination(self, uri):
|
def set_default_upload_destination(self, uri):
|
||||||
"""
|
"""
|
||||||
Set the uri to upload all the debug images to.
|
Set the uri to upload all the debug images to.
|
||||||
|
Loading…
Reference in New Issue
Block a user