from __future__ import annotations import re from .models import Phrase, WordTiming _SENTENCE_END = re.compile(r'[.!?][\"”’\']?$') _CLAUSE_END = re.compile(r'[,;:][\"”’\']?$') def build_phrases( words: list[WordTiming], *, max_words: int = 11, max_chars: int = 74, hard_pause: float = 0.70, clause_pause: float = 0.28, ) -> list[Phrase]: """Group measured words into complete, readable conceptual phrases. The algorithm uses punctuation, measured pauses, and size limits. It never changes the word order or inserts words not present in the aligned audio. """ phrases: list[Phrase] = [] current: list[int] = [] def close_phrase() -> None: nonlocal current if not current: return selected = [words[i] for i in current] phrase_id = len(phrases) for item in selected: item.phrase_id = phrase_id phrases.append(Phrase( id=phrase_id, word_indices=current.copy(), text=" ".join(item.word for item in selected), start=selected[0].start, end=selected[-1].end, )) current = [] for i, word in enumerate(words): current.append(i) text = " ".join(words[j].word for j in current) gap = words[i + 1].start - word.end if i + 1 < len(words) else 999.0 sentence_end = bool(_SENTENCE_END.search(word.word)) clause_end = bool(_CLAUSE_END.search(word.word)) size_limit = len(current) >= max_words or len(text) >= max_chars pause_break = gap >= hard_pause clause_break = clause_end and gap >= clause_pause and len(current) >= 4 if sentence_end or size_limit or pause_break or clause_break: close_phrase() close_phrase() return phrases