""" Visualization module. Provides functions to render HTML visualizations of word alignment between reference and hypothesis texts, and to generate the complete results HTML page with an embedded audio element and progress status. """ from itertools import zip_longest from jiwer import process_words import hashlib def render_visualize_jiwer_result_html(ref: str, hyp: str, title: str = "", model_id: str = None) -> str: """ Generate an HTML visualization of the alignment between reference and hypothesis texts. Args: ref: The reference text. hyp: The hypothesis (transcribed) text. title: A title for the evaluation block (e.g., model name). model_id: A unique identifier for the model (used in word IDs). Returns: An HTML string visualizing word-level alignments and error metrics. """ # Use the title as model_id if none provided if model_id is None: model_id = hashlib.md5(title.encode()).hexdigest()[:8] # Process word alignment via jiwer word_output = process_words(ref, hyp) alignment_chunks = word_output.alignments[0] columns = [] ref_position = 0 # This tracks the position in the reference text for chunk in alignment_chunks: if chunk.type == "equal": words = word_output.references[0][chunk.ref_start_idx : chunk.ref_end_idx] for word in words: ref_cell = f'{word}' hyp_cell = f'{word}' columns.append((ref_cell, hyp_cell, ref_position)) ref_position += 1 elif chunk.type == "delete": words = word_output.references[0][chunk.ref_start_idx : chunk.ref_end_idx] for word in words: ref_cell = f'{word}' hyp_cell = ' ' columns.append((ref_cell, hyp_cell, ref_position)) ref_position += 1 elif chunk.type == "insert": words = word_output.hypotheses[0][chunk.hyp_start_idx : chunk.hyp_end_idx] # For inserted words, they are linked to the previous reference position # If we're at the beginning, use position 0 last_ref_pos = max(0, ref_position - 1) if ref_position > 0 else 0 for word in words: ref_cell = ' ' hyp_cell = f'{word}' columns.append((ref_cell, hyp_cell, last_ref_pos)) # Note: ref_position is NOT incremented for inserts elif chunk.type == "substitute": ref_words = word_output.references[0][chunk.ref_start_idx : chunk.ref_end_idx] hyp_words = word_output.hypotheses[0][chunk.hyp_start_idx : chunk.hyp_end_idx] for ref_word, hyp_word in zip_longest(ref_words, hyp_words, fillvalue=""): if ref_word: # Only increment position for actual reference words ref_cell = f'{ref_word}' if hyp_word: hyp_cell = f'{hyp_word}' else: hyp_cell = ' ' columns.append((ref_cell, hyp_cell, ref_position)) ref_position += 1 elif hyp_word: # Extra hypothesis words with no reference pair # Link to previous reference position last_ref_pos = max(0, ref_position - 1) ref_cell = ' ' hyp_cell = f'{hyp_word}' columns.append((ref_cell, hyp_cell, last_ref_pos)) # Create HTML visualization html_blocks = [] metrics_results_str = f"WER: {word_output.wer * 100:0.04f}%, WIL: {word_output.wil * 100:0.04f}%" summary_operations_str = f"Subs: {word_output.substitutions}, Dels: {word_output.deletions}, Insrt: {word_output.insertions}" html_blocks.append( f"