diff --git a/README.md b/README.md index baab7cc..d56c8da 100644 --- a/README.md +++ b/README.md @@ -314,9 +314,53 @@ Sequences can be aligned, similar to `kaldialign.align`, using the tcpWER matchi ```python import meeteval meeteval.wer.wer.time_constrained.align([{'words': 'a b', 'start_time': 0, 'end_time': 1}], [{'words': 'a c', 'start_time': 0, 'end_time': 1}, {'words': 'd', 'start_time': 2, 'end_time': 3}], collar=5) +alignment = meeteval.wer.wer.time_constrained.align([{'words': 'a b', 'start_time': 0, 'end_time': 1}], [{'words': 'a c', 'start_time': 0, 'end_time': 1}, {'words': 'd', 'start_time': 2, 'end_time': 3}], collar=5) +print(alignment) # [('a', 'a'), ('b', 'c'), ('*', 'd')] ``` +`meeteval.wer.wer.time_constrained.print_alignment` pretty prints an an alignment with timestmaps: + +```python +import meeteval +alignment = meeteval.wer.wer.time_constrained.align( + [ + {"words": "hi", "start_time": 0.93, "end_time": 2.03}, + {"words": "good how are you", "start_time": 3.15, "end_time": 5.36}, + {"words": "i'm leigh adams", "start_time": 7.24, "end_time": 8.36}, + {"words": "pretty good now and you", "start_time": 9.44, "end_time": 12.27}, + {"words": "yeah", "start_time": 15.49, "end_time": 16.95}, + ], + [ + {"words": "hi", "start_time": 0.93, "end_time": 2.03}, + {"words": "are you", "start_time": 3.15, "end_time": 5.36}, + {"words": "leigh adams", "start_time": 7.24, "end_time": 8.36}, + {"words": "good now and", "start_time": 9.44, "end_time": 12.27}, + {"words": "yep", "start_time": 15.49, "end_time": 16.95}, + ], + style='seglst', + collar=5, +) + +meeteval.wer.wer.time_constrained.print_alignment(alignment) +# 0.93 2.03 hi - hi 1.48 1.48 +# 3.15 3.83 good + * +# 3.83 4.34 how + * +# 4.34 4.85 are - are 3.70 3.70 +# 4.85 5.36 you - you 4.81 4.81 +# 7.24 7.50 i'm + * +# 7.50 7.93 leigh - leigh 7.52 7.52 +# 7.93 8.36 adams - adams 8.08 8.08 +# 9.44 10.33 pretty + * +# 10.33 10.93 good - good 10.01 10.01 +# 10.93 11.38 now - now 11.00 11.00 +# 11.38 11.82 and - and 11.85 11.85 +# 11.82 12.27 you + yep 16.22 16.22 +# 15.49 16.95 yeah + * +``` + +You can use `meeteval.wer.wer.time_constrained.format_alignment` to obtain a formatted string without printing. + ## Visualization > [!TIP] @@ -344,6 +388,7 @@ av = AlignmentVisualization( meeteval.io.load(folder + 'example_files/ref.stm').groupby('filename')['recordingA'], meeteval.io.load(folder + 'example_files/hyp.stm').groupby('filename')['recordingA'] ) + # display(av) # Jupyter # av.dump('viz.html') # Create standalone HTML file ``` diff --git a/meeteval/wer/wer/time_constrained.py b/meeteval/wer/wer/time_constrained.py index 3b74aed..84c3d32 100644 --- a/meeteval/wer/wer/time_constrained.py +++ b/meeteval/wer/wer/time_constrained.py @@ -34,6 +34,8 @@ class Segment(TypedDict): 'apply_collar', 'get_pseudo_word_level_timings', 'align', + 'format_alignment', + 'print_alignment', ] @@ -964,3 +966,56 @@ def align( raise ValueError(f'Unknown alignment style: {style}') return alignment + +def print_alignment(alignment, *, file=None): + """ + Prints a seglst-style alignment (as produced by `align` with + `style='seglst'`) in a human-readable format. Correct matches are marked + with "-" and mismatches with "+". + + >>> print_alignment(align( + ... [{'words': 'a', 'start_time': 0, 'end_time': 1}, {'words': 'b', 'start_time': 1, 'end_time': 2}, {'words': 'c', 'start_time': 2, 'end_time': 3}], + ... [{'words': 'a', 'start_time': 0, 'end_time': 1}, {'words': 'x', 'start_time': 1, 'end_time': 2}, {'words': 'c', 'start_time': 3, 'end_time': 4}], + ... style='seglst', collar=0)) # doctest: +NORMALIZE_WHITESPACE + 0.00 1.00 a - a 0.50 0.50 + 1.00 2.00 b + x 1.50 1.50 + 2.00 3.00 c + * + * + c 3.50 3.50 + """ + lines = [ + ( + f'{left["start_time"]:.2f}' if left is not None else '', + f'{left["end_time"]:.2f}' if left is not None else '', + f'{left["words"]}' if left is not None else '*', + "-" if left is not None and right is not None and left["words"] == right["words"] else '+', + f'{right["words"]}' if right is not None else '*', + f'{right["start_time"]:.2f}' if right is not None else '', + f'{right["end_time"]:.2f}' if right is not None else '' + ) + for left, right in alignment + ] + + widths = [0] * len(lines[0]) + justify = '>>>^<>>>>>' + for line in lines: + for i, item in enumerate(line): + widths[i] = max(widths[i], len(item)) + + for line in lines: + print(*[ + f'{cell:{j}{w}}' + for cell, j, w in zip(line, justify, widths) + ], + sep=' ', + file=file, + ) + + +def format_alignment(alignment): + """ + Formats a seglst-style alignment as a human-readable string. + """ + from io import StringIO + file = StringIO() + print_alignment(alignment, file=file) + return file.getvalue() diff --git a/tests/test_docs.py b/tests/test_docs.py index 4f14cf2..1892d01 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -70,13 +70,12 @@ def split_code_block_comment_output(code): if l > s.end_lineno: continue - # If we parsed a print statement at the root level - if isinstance(s, ast.Expr) and isinstance(s.value, ast.Call) and isinstance(s.value.func, ast.Name) and s.value.func.id == 'print': + l = s.end_lineno + + if l < len(lines) and lines[l].startswith('#'): # Collect any lines that follow directly and start with a # output = [] - l = s.end_lineno - if not lines[l].startswith('#'): - continue + while l < len(lines) and lines[l].startswith('#'): output.append(lines[l][1:]) l += 1 @@ -139,7 +138,11 @@ def test_docs(filename, codeblock, global_state, monkeypatch): # sufficient for most cases. output_ = output.replace(' ', '').replace('\n', '') expected_output_ = expected_output.replace(' ', '').replace('\n', '') - assert output_ == expected_output_, f'Output mismatch: {output} != {expected_output}' + if output_ != expected_output_: + raise AssertionError( + f'Output mismatch in {filename} at line {lineno + line_offset}:\n' + f'Output: {output}\nExpected: {expected_output}' + ) elif lang == 'STM': # Test if the STM code block is valid. import meeteval