|
3 | 3 | "RemapSegmentsWithBundlesJob", |
4 | 4 | "ClusterMapToSegmentListJob", |
5 | 5 | "RemapSegmentsJob", |
| 6 | + "CombineRasrCachesJob", |
| 7 | + "CombineRasrLogsJob", |
6 | 8 | ] |
7 | 9 |
|
8 | 10 | import collections |
9 | 11 | import logging |
| 12 | +from typing import Dict, Optional |
10 | 13 | import xml.etree.ElementTree as ET |
| 14 | +import subprocess as sp |
11 | 15 |
|
12 | 16 | from sisyphus import * |
13 | 17 |
|
| 18 | + |
14 | 19 | Path = setup_path(__package__) |
15 | 20 |
|
16 | 21 | from i6_core.util import * |
@@ -184,3 +189,162 @@ def run(self): |
184 | 189 | ) as f: |
185 | 190 | for b in bs: |
186 | 191 | f.write("%s\n" % tk.uncached_path(self.cache_paths[b])) |
| 192 | + |
| 193 | + |
| 194 | +class CombineRasrCachesJob(Job): |
| 195 | + """ |
| 196 | + Uses the RASR archiver binary in `combine` mode in order to combine the information |
| 197 | + from `original_caches[task_id]` and `updated_caches[task_id]` |
| 198 | + (giving priority to `updated_caches[task_id]`), and dumps the result into `final_caches[task_id]`. |
| 199 | +
|
| 200 | + This job is meant to **work with RASR caches whose information inside is composed of key-value pairs** |
| 201 | + (key: segment full name, value: information about the segment such as features, alignments...), |
| 202 | + and requires that `original_caches.keys() == updated_caches.keys()`. |
| 203 | +
|
| 204 | + The job's output is similar to the following python pseudocode: |
| 205 | + ``` |
| 206 | + final_cache_contents[segment_full_name] = ( |
| 207 | + updated_cache_contents[segment_full_name] |
| 208 | + if segment_full_name in updated_cache |
| 209 | + else original_cache_contents[segment_full_name] |
| 210 | + ) |
| 211 | + ``` |
| 212 | +
|
| 213 | + **The user is responsible of the content of the original and the updated cache**. |
| 214 | + For assistance, if using multiple split files, one can use the :class:`i6_core.corpus.segments.SegmentCorpusJob` |
| 215 | + along with the :class:`i6_core.corpus.filter.FilterSegmentsByListJob` |
| 216 | + (which ensures a correspondence between original and updated segment splits) |
| 217 | + in order to obtain an exact subset of each original split files as follows: |
| 218 | + ``` |
| 219 | + # Obtain all segments from the updated corpus. |
| 220 | + updated_corpus_segments_job = i6_core.corpus.segments.SegmentCorpusJob( |
| 221 | + updated_corpus, num_segments=num_original_corpus_splits |
| 222 | + ) |
| 223 | + all_updated_corpus_segments = i6_core.text.processing.ConcatenateJob( |
| 224 | + list(updated_corpus_segments_job.out_single_segment_files.values()), zip_out=False |
| 225 | + ).out |
| 226 | + # Filter the segments from the original corpus and keep the same split structure. |
| 227 | + updated_corpus_filtered_segments_job = i6_core.corpus.filter.FilterSegmentsByListJob( |
| 228 | + segment_files=individual_segment_files_dict, filter_list=all_updated_corpus_segments, invert_match=True |
| 229 | + ) |
| 230 | + ``` |
| 231 | +
|
| 232 | + More information about the archiver is available here: |
| 233 | + https://www-i6.informatik.rwth-aachen.de/rwth-asr/manual/index.php/Archiver. |
| 234 | +
|
| 235 | + Note: the term "RASR cache" refers to a file in binary format that supports compression, |
| 236 | + or equivalently, a flow node with the `generic-cache` filter. More information available here: |
| 237 | + - https://github.com/rwth-i6/rasr/blob/master/src/Flow/Cache.hh |
| 238 | + - https://github.com/rwth-i6/rasr/blob/master/src/Flow/Cache.cc |
| 239 | + - https://github.com/rwth-i6/i6_core/blob/main/lib/rasr_cache.py (python interface). |
| 240 | + """ |
| 241 | + |
| 242 | + def __init__( |
| 243 | + self, original_caches: Dict[int, tk.Path], updated_caches: Dict[int, tk.Path], rasr_archiver_exe: tk.Path |
| 244 | + ): |
| 245 | + """ |
| 246 | + :param original_caches: Caches that must be overwritten. |
| 247 | + The internal key-value pairs inside the `i`-th final cache |
| 248 | + will have the value provided in the `i`-th original cache |
| 249 | + only if the key is not found in the corresponding `i`-th updated cache. |
| 250 | + :param updated_caches: Caches with which the contents inside the respective original caches should be updated. |
| 251 | + The internal key-value pairs inside the `i`-th final cache |
| 252 | + will always prioritize the corresponding key-value pairs inside the `i`-th updated cache. |
| 253 | + :param rasr_archiver_exe: Executable for the compiled RASR archiver. |
| 254 | + """ |
| 255 | + |
| 256 | + set_original_caches_keys = set(original_caches.keys()) |
| 257 | + set_updated_caches_keys = set(updated_caches.keys()) |
| 258 | + assert set_original_caches_keys == set_updated_caches_keys, ( |
| 259 | + "Original and updated dictionaries don't have a 1-to-1 correspondence:\n" |
| 260 | + f"Original - updated: {set_original_caches_keys.difference(set_updated_caches_keys)}\n" |
| 261 | + f"Updated - original: {set_updated_caches_keys.difference(set_original_caches_keys)}" |
| 262 | + ) |
| 263 | + self.original_caches = original_caches |
| 264 | + self.updated_caches = updated_caches |
| 265 | + self.rasr_archiver_exe = rasr_archiver_exe |
| 266 | + |
| 267 | + self.out_final_single_caches = { |
| 268 | + i: self.output_path(f"cache.{i}", cached=True) for i in range(1, len(original_caches) + 1) |
| 269 | + } |
| 270 | + |
| 271 | + self.rqmt = {"cpu": 1, "mem": 1.0, "time": 1.0} |
| 272 | + |
| 273 | + def tasks(self): |
| 274 | + yield Task("run", resume="run", rqmt=self.rqmt, args=list(self.original_caches.keys())) |
| 275 | + |
| 276 | + def run(self, task_id: int): |
| 277 | + sp.check_call( |
| 278 | + [ |
| 279 | + self.rasr_archiver_exe, |
| 280 | + "--mode", |
| 281 | + "combine", |
| 282 | + self.out_final_single_caches[task_id].get_path(), |
| 283 | + self.updated_caches[task_id].get_path(), |
| 284 | + self.original_caches[task_id].get_path(), |
| 285 | + ] |
| 286 | + ) |
| 287 | + |
| 288 | + |
| 289 | +class CombineRasrLogsJob(Job): |
| 290 | + """ |
| 291 | + Outputs the information from `<segment>` tags in the updated logs if available. |
| 292 | + If not, outputs the same information from the original logs. |
| 293 | +
|
| 294 | + The code treats the log contents as a key-value mapping, in which the key is the segment's full name |
| 295 | + and the value is the information stored in the log for such a segment. |
| 296 | + Following the example from this dict-based structure, |
| 297 | + triggering this job would achieve the same result as `original_log.update(updated_log)`, |
| 298 | + or the following explicit pseudocode: |
| 299 | + ``` |
| 300 | + final_log[segment_full_name] = ( |
| 301 | + updated_log[segment_full_name] |
| 302 | + if segment_full_name in updated_log |
| 303 | + else original_log[segment_full_name] |
| 304 | + ) |
| 305 | + ``` |
| 306 | + """ |
| 307 | + |
| 308 | + def __init__(self, original_logs: Dict[int, tk.Path], updated_logs: Dict[int, tk.Path]): |
| 309 | + """ |
| 310 | + :param original_logs: Logs whose corresponding information must be overwritten. |
| 311 | + :param updated_logs: Logs whose information will overwrite the original caches. |
| 312 | + """ |
| 313 | + self.original_logs = original_logs |
| 314 | + self.updated_logs = updated_logs |
| 315 | + set_original_logs_keys = set(original_logs.keys()) |
| 316 | + set_updated_logs_keys = set(updated_logs.keys()) |
| 317 | + assert set_original_logs_keys == set_updated_logs_keys, ( |
| 318 | + "Original and updated dictionaries don't have a 1-to-1 correspondence:\n" |
| 319 | + f"Original - updated: {set_original_logs_keys.difference(set_updated_logs_keys)}\n" |
| 320 | + f"Updated - original: {set_updated_logs_keys.difference(set_original_logs_keys)}" |
| 321 | + ) |
| 322 | + |
| 323 | + self.out_final_logs = {i: self.output_path(f"log.{i}.gz") for i in range(1, len(self.original_logs) + 1)} |
| 324 | + |
| 325 | + self.rqmt = {"cpu": 1, "mem": 1.0, "time": 1.0} |
| 326 | + |
| 327 | + def tasks(self): |
| 328 | + yield Task("run", resume="run", rqmt=self.rqmt, args=list(self.original_logs.keys())) |
| 329 | + |
| 330 | + def run(self, task_id: int): |
| 331 | + seg_name_to_xml_content = {} |
| 332 | + # Read the updated logs and store the information listed there. |
| 333 | + with uopen(self.updated_logs[task_id], "rt") as f: |
| 334 | + document = ET.parse(f) |
| 335 | + _seg_list = document.findall(".//segment") |
| 336 | + for seg in _seg_list: |
| 337 | + seg_name_to_xml_content[seg.attrib["full-name"]] = seg |
| 338 | + |
| 339 | + # Read the original logs and overwrite them with the updated ones if required. |
| 340 | + with uopen(self.original_logs[task_id], "rt") as f: |
| 341 | + document = ET.parse(f) |
| 342 | + _rec_list = document.findall(".//recording") |
| 343 | + for rec in _rec_list: |
| 344 | + for i, child in enumerate(rec): |
| 345 | + if child.tag == "segment" and child.attrib.get("full-name") in seg_name_to_xml_content: |
| 346 | + rec[i] = seg_name_to_xml_content[child.attrib["full-name"]] |
| 347 | + |
| 348 | + # Dump the final logs. |
| 349 | + with uopen(self.out_final_logs[task_id].get_path(), "wb") as f: |
| 350 | + document.write(f, encoding="UTF-8", xml_declaration=True) |
0 commit comments