OLD | NEW |
1 # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. | 1 # Copyright (c) 2017 The WebRTC project authors. All Rights Reserved. |
2 # | 2 # |
3 # Use of this source code is governed by a BSD-style license | 3 # Use of this source code is governed by a BSD-style license |
4 # that can be found in the LICENSE file in the root of the source | 4 # that can be found in the LICENSE file in the root of the source |
5 # tree. An additional intellectual property rights grant can be found | 5 # tree. An additional intellectual property rights grant can be found |
6 # in the file PATENTS. All contributing project authors may | 6 # in the file PATENTS. All contributing project authors may |
7 # be found in the AUTHORS file in the root of the source tree. | 7 # be found in the AUTHORS file in the root of the source tree. |
8 | 8 |
9 import logging | 9 import logging |
10 import os | 10 import os |
| 11 import re |
| 12 |
11 | 13 |
12 class HtmlExport(object): | 14 class HtmlExport(object): |
13 | 15 |
14 def __init__(self, output_path, output_filename): | 16 # Path to CSS and JS files. |
15 self._output_path = output_path | 17 _PATH = os.path.dirname(os.path.realpath(__file__)) |
16 self._output_filename = output_filename | 18 |
| 19 # CSS file parameters. |
| 20 _CSS_FILEPATH = os.path.join(_PATH, 'results.css') |
| 21 _INLINE_CSS = True |
| 22 |
| 23 _NEW_LINE = '\n' |
| 24 |
| 25 def __init__(self, output_filepath): |
| 26 self._noise_names = None |
| 27 self._noise_params = None |
| 28 self._output_filepath = output_filepath |
17 | 29 |
18 def export(self, scores): | 30 def export(self, scores): |
19 logging.debug('%d score names found', len(scores)) | 31 """ |
20 output_filepath = os.path.join(self._output_path, self._output_filename) | 32 Export the scores into an HTML file. |
21 | 33 |
22 # TODO(alessio): remove once implemented | 34 Args: |
| 35 scores: nested dictionary containing the scores. |
| 36 """ |
| 37 # Generate one table for each evaluation score. |
| 38 tables = [] |
| 39 for score_name in sorted(scores.keys()): |
| 40 tables.append(self._build_score_table(score_name, scores[score_name])) |
| 41 |
| 42 # Create the html file. |
| 43 html = ( |
| 44 '<html>' + |
| 45 self._build_header() + |
| 46 '<body>' + |
| 47 '<h1>Results from {}</h1>'.format(self._output_filepath) + |
| 48 self._NEW_LINE.join(tables) + |
| 49 '</body>' + |
| 50 '</html>') |
| 51 |
| 52 self._save(self._output_filepath, html) |
| 53 |
| 54 def _build_header(self): |
| 55 """ |
| 56 HTML file header with page title and either embedded or linked CSS and JS |
| 57 files. |
| 58 """ |
| 59 html = ['<head>', '<title>Results</title>'] |
| 60 |
| 61 # CSS. |
| 62 if self._INLINE_CSS: |
| 63 # Embed. |
| 64 html.append('<style>') |
| 65 with open(self._CSS_FILEPATH) as f: |
| 66 for l in f: |
| 67 html.append(l.strip()) |
| 68 html.append('</style>') |
| 69 else: |
| 70 # Link. |
| 71 html.append('<link rel="stylesheet" type="text/css" ' |
| 72 'href="file://{}?">'.format(self._CSS_FILEPATH)) |
| 73 |
| 74 html.append('</head>') |
| 75 |
| 76 return self._NEW_LINE.join(html) |
| 77 |
| 78 def _build_score_table(self, score_name, scores): |
| 79 """ |
| 80 Generate a table for a specific evaluation score (e.g., POLQA). |
| 81 """ |
| 82 config_names = sorted(scores.keys()) |
| 83 input_names = sorted(scores[config_names[0]].keys()) |
| 84 rows = [self._table_row( |
| 85 score_name, config_name, scores[config_name], input_names) for ( |
| 86 config_name) in config_names] |
| 87 |
| 88 html = ( |
| 89 '<table celpadding="0" cellspacing="0">' + |
| 90 '<thead><tr>{}</tr></thead>'.format( |
| 91 self._table_header(score_name, input_names)) + |
| 92 '<tbody>' + |
| 93 '<tr>' + '</tr><tr>'.join(rows) + '</tr>' + |
| 94 '</tbody>' + |
| 95 '</table>' + self._legend()) |
| 96 |
| 97 return html |
| 98 |
| 99 def _table_header(self, score_name, input_names): |
| 100 """ |
| 101 Generate a table header with the name of the evaluation score in the first |
| 102 column and then one column for each probing signal. |
| 103 """ |
| 104 html = ( |
| 105 '<th>{}</th>'.format(self._format_name(score_name)) + |
| 106 '<th>' + '</th><th>'.join( |
| 107 [self._format_name(name) for name in input_names]) + '</th>') |
| 108 return html |
| 109 |
| 110 def _table_row(self, score_name, config_name, scores, input_names): |
| 111 """ |
| 112 Generate a table body row with the name of the APM configuration file in the |
| 113 first column and then one column for each probing singal. |
| 114 """ |
| 115 cells = [self._table_cell( |
| 116 scores[input_name], score_name, config_name, input_name) for ( |
| 117 input_name) in input_names] |
| 118 html = ('<td>{}</td>'.format(self._format_name(config_name)) + |
| 119 '<td>' + '</td><td>'.join(cells) + '</td>') |
| 120 return html |
| 121 |
| 122 def _table_cell(self, scores, score_name, config_name, input_name): |
| 123 """ |
| 124 Generate a table cell content with all the scores for the current evaluation |
| 125 score, APM configuration, and probing signal. |
| 126 """ |
| 127 # Init noise generator names and noise parameters cache (if not done). |
| 128 if self._noise_names is None: |
| 129 self._noise_names = sorted(scores.keys()) |
| 130 self._noise_params = {noise_name: sorted(scores[noise_name].keys()) for ( |
| 131 noise_name) in self._noise_names} |
| 132 |
| 133 # For each noisy input (that is a pair of noise generator name and noise |
| 134 # generator parameters), add an item with the score and its metadata. |
| 135 items = [] |
| 136 for name_index, noise_name in enumerate(self._noise_names): |
| 137 for params_index, noise_params in enumerate( |
| 138 self._noise_params[noise_name]): |
| 139 |
| 140 # Init. |
| 141 score_value = '?' |
| 142 metadata = '' |
| 143 |
| 144 # Extract score value and its metadata. |
| 145 try: |
| 146 data = scores[noise_name][noise_params] |
| 147 score_value = '{0:f}'.format(data['score']) |
| 148 metadata = ( |
| 149 '<input type="hidden" name="noise_name" value="{}"/>' |
| 150 '<input type="hidden" name="noise_params" value="{}"/>' |
| 151 '<input type="hidden" name="audio_in" value="file://{}"/>' |
| 152 '<input type="hidden" name="audio_out" value="file://{}"/>' |
| 153 '<input type="hidden" name="audio_ref" value="file://{}"/>' |
| 154 ).format( |
| 155 noise_name, |
| 156 noise_params, |
| 157 data['audio_in_filepath'], |
| 158 data['audio_out_filepath'], |
| 159 data['audio_ref_filepath']) |
| 160 except TypeError: |
| 161 logging.warning( |
| 162 'missing score found: <score:%s> <config:%s> <input:%s> ' |
| 163 '<noise:%s> <params:%s>', score_name, config_name, input_name, |
| 164 noise_name, noise_params) |
| 165 |
| 166 # Add the score. |
| 167 items.append( |
| 168 '<div class="noise-desc">[{0:d}, {1:d}]{2}</div>' |
| 169 '<div class="value">{3}</div>'.format( |
| 170 name_index, params_index, metadata, score_value)) |
| 171 |
| 172 html = ( |
| 173 '<div class="score">' + |
| 174 '</div><div class="score">'.join(items) + |
| 175 '</div>') |
| 176 |
| 177 return html |
| 178 |
| 179 def _legend(self): |
| 180 """ |
| 181 Generate the legend for each noise generator name and parameters pair. |
| 182 """ |
| 183 items = [] |
| 184 for name_index, noise_name in enumerate(self._noise_names): |
| 185 for params_index, noise_params in enumerate( |
| 186 self._noise_params[noise_name]): |
| 187 items.append('<div class="noise-desc">[{0:d}, {1:d}]</div>: {2} noise, ' |
| 188 '{3}'.format(name_index, params_index, noise_name, |
| 189 noise_params)) |
| 190 html = ( |
| 191 '<div class="legend"><div>' + |
| 192 '</div><div>'.join(items) + '</div></div>') |
| 193 |
| 194 return html |
| 195 |
| 196 @classmethod |
| 197 def _save(cls, output_filepath, html): |
23 with open(output_filepath, 'w') as f: | 198 with open(output_filepath, 'w') as f: |
24 f.write('APM Quality Assessment scores\n') | 199 f.write(html) |
25 | 200 |
26 return output_filepath | 201 @classmethod |
| 202 def _format_name(cls, name): |
| 203 return re.sub(r'[_\-]', ' ', name) |
OLD | NEW |