summaryrefslogtreecommitdiffstats
path: root/third_party/libwebrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/export.py
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/libwebrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/export.py')
-rw-r--r--third_party/libwebrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/export.py426
1 files changed, 426 insertions, 0 deletions
diff --git a/third_party/libwebrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/export.py b/third_party/libwebrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/export.py
new file mode 100644
index 0000000000..fe3a6c7cb9
--- /dev/null
+++ b/third_party/libwebrtc/modules/audio_processing/test/py_quality_assessment/quality_assessment/export.py
@@ -0,0 +1,426 @@
+# Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
+#
+# Use of this source code is governed by a BSD-style license
+# that can be found in the LICENSE file in the root of the source
+# tree. An additional intellectual property rights grant can be found
+# in the file PATENTS. All contributing project authors may
+# be found in the AUTHORS file in the root of the source tree.
+
+import functools
+import hashlib
+import logging
+import os
+import re
+import sys
+
+try:
+ import csscompressor
+except ImportError:
+ logging.critical(
+ 'Cannot import the third-party Python package csscompressor')
+ sys.exit(1)
+
+try:
+ import jsmin
+except ImportError:
+ logging.critical('Cannot import the third-party Python package jsmin')
+ sys.exit(1)
+
+
+class HtmlExport(object):
+ """HTML exporter class for APM quality scores."""
+
+ _NEW_LINE = '\n'
+
+ # CSS and JS file paths.
+ _PATH = os.path.dirname(os.path.realpath(__file__))
+ _CSS_FILEPATH = os.path.join(_PATH, 'results.css')
+ _CSS_MINIFIED = True
+ _JS_FILEPATH = os.path.join(_PATH, 'results.js')
+ _JS_MINIFIED = True
+
+ def __init__(self, output_filepath):
+ self._scores_data_frame = None
+ self._output_filepath = output_filepath
+
+ def Export(self, scores_data_frame):
+ """Exports scores into an HTML file.
+
+ Args:
+ scores_data_frame: DataFrame instance.
+ """
+ self._scores_data_frame = scores_data_frame
+ html = [
+ '<html>',
+ self._BuildHeader(),
+ ('<script type="text/javascript">'
+ '(function () {'
+ 'window.addEventListener(\'load\', function () {'
+ 'var inspector = new AudioInspector();'
+ '});'
+ '})();'
+ '</script>'), '<body>',
+ self._BuildBody(), '</body>', '</html>'
+ ]
+ self._Save(self._output_filepath, self._NEW_LINE.join(html))
+
+ def _BuildHeader(self):
+ """Builds the <head> section of the HTML file.
+
+ The header contains the page title and either embedded or linked CSS and JS
+ files.
+
+ Returns:
+ A string with <head>...</head> HTML.
+ """
+ html = ['<head>', '<title>Results</title>']
+
+ # Add Material Design hosted libs.
+ html.append('<link rel="stylesheet" href="http://fonts.googleapis.com/'
+ 'css?family=Roboto:300,400,500,700" type="text/css">')
+ html.append(
+ '<link rel="stylesheet" href="https://fonts.googleapis.com/'
+ 'icon?family=Material+Icons">')
+ html.append(
+ '<link rel="stylesheet" href="https://code.getmdl.io/1.3.0/'
+ 'material.indigo-pink.min.css">')
+ html.append('<script defer src="https://code.getmdl.io/1.3.0/'
+ 'material.min.js"></script>')
+
+ # Embed custom JavaScript and CSS files.
+ html.append('<script>')
+ with open(self._JS_FILEPATH) as f:
+ html.append(
+ jsmin.jsmin(f.read()) if self._JS_MINIFIED else (
+ f.read().rstrip()))
+ html.append('</script>')
+ html.append('<style>')
+ with open(self._CSS_FILEPATH) as f:
+ html.append(
+ csscompressor.compress(f.read()) if self._CSS_MINIFIED else (
+ f.read().rstrip()))
+ html.append('</style>')
+
+ html.append('</head>')
+
+ return self._NEW_LINE.join(html)
+
+ def _BuildBody(self):
+ """Builds the content of the <body> section."""
+ score_names = self._scores_data_frame[
+ 'eval_score_name'].drop_duplicates().values.tolist()
+
+ html = [
+ ('<div class="mdl-layout mdl-js-layout mdl-layout--fixed-header '
+ 'mdl-layout--fixed-tabs">'),
+ '<header class="mdl-layout__header">',
+ '<div class="mdl-layout__header-row">',
+ '<span class="mdl-layout-title">APM QA results ({})</span>'.format(
+ self._output_filepath),
+ '</div>',
+ ]
+
+ # Tab selectors.
+ html.append('<div class="mdl-layout__tab-bar mdl-js-ripple-effect">')
+ for tab_index, score_name in enumerate(score_names):
+ is_active = tab_index == 0
+ html.append('<a href="#score-tab-{}" class="mdl-layout__tab{}">'
+ '{}</a>'.format(tab_index,
+ ' is-active' if is_active else '',
+ self._FormatName(score_name)))
+ html.append('</div>')
+
+ html.append('</header>')
+ html.append(
+ '<main class="mdl-layout__content" style="overflow-x: auto;">')
+
+ # Tabs content.
+ for tab_index, score_name in enumerate(score_names):
+ html.append('<section class="mdl-layout__tab-panel{}" '
+ 'id="score-tab-{}">'.format(
+ ' is-active' if is_active else '', tab_index))
+ html.append('<div class="page-content">')
+ html.append(
+ self._BuildScoreTab(score_name, ('s{}'.format(tab_index), )))
+ html.append('</div>')
+ html.append('</section>')
+
+ html.append('</main>')
+ html.append('</div>')
+
+ # Add snackbar for notifications.
+ html.append(
+ '<div id="snackbar" aria-live="assertive" aria-atomic="true"'
+ ' aria-relevant="text" class="mdl-snackbar mdl-js-snackbar">'
+ '<div class="mdl-snackbar__text"></div>'
+ '<button type="button" class="mdl-snackbar__action"></button>'
+ '</div>')
+
+ return self._NEW_LINE.join(html)
+
+ def _BuildScoreTab(self, score_name, anchor_data):
+ """Builds the content of a tab."""
+ # Find unique values.
+ scores = self._scores_data_frame[
+ self._scores_data_frame.eval_score_name == score_name]
+ apm_configs = sorted(self._FindUniqueTuples(scores, ['apm_config']))
+ test_data_gen_configs = sorted(
+ self._FindUniqueTuples(scores,
+ ['test_data_gen', 'test_data_gen_params']))
+
+ html = [
+ '<div class="mdl-grid">',
+ '<div class="mdl-layout-spacer"></div>',
+ '<div class="mdl-cell mdl-cell--10-col">',
+ ('<table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp" '
+ 'style="width: 100%;">'),
+ ]
+
+ # Header.
+ html.append('<thead><tr><th>APM config / Test data generator</th>')
+ for test_data_gen_info in test_data_gen_configs:
+ html.append('<th>{} {}</th>'.format(
+ self._FormatName(test_data_gen_info[0]),
+ test_data_gen_info[1]))
+ html.append('</tr></thead>')
+
+ # Body.
+ html.append('<tbody>')
+ for apm_config in apm_configs:
+ html.append('<tr><td>' + self._FormatName(apm_config[0]) + '</td>')
+ for test_data_gen_info in test_data_gen_configs:
+ dialog_id = self._ScoreStatsInspectorDialogId(
+ score_name, apm_config[0], test_data_gen_info[0],
+ test_data_gen_info[1])
+ html.append(
+ '<td onclick="openScoreStatsInspector(\'{}\')">{}</td>'.
+ format(
+ dialog_id,
+ self._BuildScoreTableCell(score_name,
+ test_data_gen_info[0],
+ test_data_gen_info[1],
+ apm_config[0])))
+ html.append('</tr>')
+ html.append('</tbody>')
+
+ html.append(
+ '</table></div><div class="mdl-layout-spacer"></div></div>')
+
+ html.append(
+ self._BuildScoreStatsInspectorDialogs(score_name, apm_configs,
+ test_data_gen_configs,
+ anchor_data))
+
+ return self._NEW_LINE.join(html)
+
+ def _BuildScoreTableCell(self, score_name, test_data_gen,
+ test_data_gen_params, apm_config):
+ """Builds the content of a table cell for a score table."""
+ scores = self._SliceDataForScoreTableCell(score_name, apm_config,
+ test_data_gen,
+ test_data_gen_params)
+ stats = self._ComputeScoreStats(scores)
+
+ html = []
+ items_id_prefix = (score_name + test_data_gen + test_data_gen_params +
+ apm_config)
+ if stats['count'] == 1:
+ # Show the only available score.
+ item_id = hashlib.md5(items_id_prefix.encode('utf-8')).hexdigest()
+ html.append('<div id="single-value-{0}">{1:f}</div>'.format(
+ item_id, scores['score'].mean()))
+ html.append(
+ '<div class="mdl-tooltip" data-mdl-for="single-value-{}">{}'
+ '</div>'.format(item_id, 'single value'))
+ else:
+ # Show stats.
+ for stat_name in ['min', 'max', 'mean', 'std dev']:
+ item_id = hashlib.md5(
+ (items_id_prefix + stat_name).encode('utf-8')).hexdigest()
+ html.append('<div id="stats-{0}">{1:f}</div>'.format(
+ item_id, stats[stat_name]))
+ html.append(
+ '<div class="mdl-tooltip" data-mdl-for="stats-{}">{}'
+ '</div>'.format(item_id, stat_name))
+
+ return self._NEW_LINE.join(html)
+
+ def _BuildScoreStatsInspectorDialogs(self, score_name, apm_configs,
+ test_data_gen_configs, anchor_data):
+ """Builds a set of score stats inspector dialogs."""
+ html = []
+ for apm_config in apm_configs:
+ for test_data_gen_info in test_data_gen_configs:
+ dialog_id = self._ScoreStatsInspectorDialogId(
+ score_name, apm_config[0], test_data_gen_info[0],
+ test_data_gen_info[1])
+
+ html.append('<dialog class="mdl-dialog" id="{}" '
+ 'style="width: 40%;">'.format(dialog_id))
+
+ # Content.
+ html.append('<div class="mdl-dialog__content">')
+ html.append(
+ '<h6><strong>APM config preset</strong>: {}<br/>'
+ '<strong>Test data generator</strong>: {} ({})</h6>'.
+ format(self._FormatName(apm_config[0]),
+ self._FormatName(test_data_gen_info[0]),
+ test_data_gen_info[1]))
+ html.append(
+ self._BuildScoreStatsInspectorDialog(
+ score_name, apm_config[0], test_data_gen_info[0],
+ test_data_gen_info[1], anchor_data + (dialog_id, )))
+ html.append('</div>')
+
+ # Actions.
+ html.append('<div class="mdl-dialog__actions">')
+ html.append('<button type="button" class="mdl-button" '
+ 'onclick="closeScoreStatsInspector()">'
+ 'Close</button>')
+ html.append('</div>')
+
+ html.append('</dialog>')
+
+ return self._NEW_LINE.join(html)
+
+ def _BuildScoreStatsInspectorDialog(self, score_name, apm_config,
+ test_data_gen, test_data_gen_params,
+ anchor_data):
+ """Builds one score stats inspector dialog."""
+ scores = self._SliceDataForScoreTableCell(score_name, apm_config,
+ test_data_gen,
+ test_data_gen_params)
+
+ capture_render_pairs = sorted(
+ self._FindUniqueTuples(scores, ['capture', 'render']))
+ echo_simulators = sorted(
+ self._FindUniqueTuples(scores, ['echo_simulator']))
+
+ html = [
+ '<table class="mdl-data-table mdl-js-data-table mdl-shadow--2dp">'
+ ]
+
+ # Header.
+ html.append('<thead><tr><th>Capture-Render / Echo simulator</th>')
+ for echo_simulator in echo_simulators:
+ html.append('<th>' + self._FormatName(echo_simulator[0]) + '</th>')
+ html.append('</tr></thead>')
+
+ # Body.
+ html.append('<tbody>')
+ for row, (capture, render) in enumerate(capture_render_pairs):
+ html.append('<tr><td><div>{}</div><div>{}</div></td>'.format(
+ capture, render))
+ for col, echo_simulator in enumerate(echo_simulators):
+ score_tuple = self._SliceDataForScoreStatsTableCell(
+ scores, capture, render, echo_simulator[0])
+ cell_class = 'r{}c{}'.format(row, col)
+ html.append('<td class="single-score-cell {}">{}</td>'.format(
+ cell_class,
+ self._BuildScoreStatsInspectorTableCell(
+ score_tuple, anchor_data + (cell_class, ))))
+ html.append('</tr>')
+ html.append('</tbody>')
+
+ html.append('</table>')
+
+ # Placeholder for the audio inspector.
+ html.append('<div class="audio-inspector-placeholder"></div>')
+
+ return self._NEW_LINE.join(html)
+
+ def _BuildScoreStatsInspectorTableCell(self, score_tuple, anchor_data):
+ """Builds the content of a cell of a score stats inspector."""
+ anchor = '&'.join(anchor_data)
+ html = [('<div class="v">{}</div>'
+ '<button class="mdl-button mdl-js-button mdl-button--icon"'
+ ' data-anchor="{}">'
+ '<i class="material-icons mdl-color-text--blue-grey">link</i>'
+ '</button>').format(score_tuple.score, anchor)]
+
+ # Add all the available file paths as hidden data.
+ for field_name in score_tuple.keys():
+ if field_name.endswith('_filepath'):
+ html.append(
+ '<input type="hidden" name="{}" value="{}">'.format(
+ field_name, score_tuple[field_name]))
+
+ return self._NEW_LINE.join(html)
+
+ def _SliceDataForScoreTableCell(self, score_name, apm_config,
+ test_data_gen, test_data_gen_params):
+ """Slices `self._scores_data_frame` to extract the data for a tab."""
+ masks = []
+ masks.append(self._scores_data_frame.eval_score_name == score_name)
+ masks.append(self._scores_data_frame.apm_config == apm_config)
+ masks.append(self._scores_data_frame.test_data_gen == test_data_gen)
+ masks.append(self._scores_data_frame.test_data_gen_params ==
+ test_data_gen_params)
+ mask = functools.reduce((lambda i1, i2: i1 & i2), masks)
+ del masks
+ return self._scores_data_frame[mask]
+
+ @classmethod
+ def _SliceDataForScoreStatsTableCell(cls, scores, capture, render,
+ echo_simulator):
+ """Slices `scores` to extract the data for a tab."""
+ masks = []
+
+ masks.append(scores.capture == capture)
+ masks.append(scores.render == render)
+ masks.append(scores.echo_simulator == echo_simulator)
+ mask = functools.reduce((lambda i1, i2: i1 & i2), masks)
+ del masks
+
+ sliced_data = scores[mask]
+ assert len(sliced_data) == 1, 'single score is expected'
+ return sliced_data.iloc[0]
+
+ @classmethod
+ def _FindUniqueTuples(cls, data_frame, fields):
+ """Slices `data_frame` to a list of fields and finds unique tuples."""
+ return data_frame[fields].drop_duplicates().values.tolist()
+
+ @classmethod
+ def _ComputeScoreStats(cls, data_frame):
+ """Computes score stats."""
+ scores = data_frame['score']
+ return {
+ 'count': scores.count(),
+ 'min': scores.min(),
+ 'max': scores.max(),
+ 'mean': scores.mean(),
+ 'std dev': scores.std(),
+ }
+
+ @classmethod
+ def _ScoreStatsInspectorDialogId(cls, score_name, apm_config,
+ test_data_gen, test_data_gen_params):
+ """Assigns a unique name to a dialog."""
+ return 'score-stats-dialog-' + hashlib.md5(
+ 'score-stats-inspector-{}-{}-{}-{}'.format(
+ score_name, apm_config, test_data_gen,
+ test_data_gen_params).encode('utf-8')).hexdigest()
+
+ @classmethod
+ def _Save(cls, output_filepath, html):
+ """Writes the HTML file.
+
+ Args:
+ output_filepath: output file path.
+ html: string with the HTML content.
+ """
+ with open(output_filepath, 'w') as f:
+ f.write(html)
+
+ @classmethod
+ def _FormatName(cls, name):
+ """Formats a name.
+
+ Args:
+ name: a string.
+
+ Returns:
+ A copy of name in which underscores and dashes are replaced with a space.
+ """
+ return re.sub(r'[_\-]', ' ', name)