Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
```
Expand Down
55 changes: 55 additions & 0 deletions meeteval/wer/wer/time_constrained.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class Segment(TypedDict):
'apply_collar',
'get_pseudo_word_level_timings',
'align',
'format_alignment',
'print_alignment',
]


Expand Down Expand Up @@ -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()
15 changes: 9 additions & 6 deletions tests/test_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down