"""Работа с сегментами диаризации: склейка в реплики, нарезка, статистика.""" from dataclasses import dataclass __all__ = ["Segment", "Turn", "merge_turns", "chunk_ranges", "speech_stats"] @dataclass(frozen=True) class Segment: """Отрезок речи одного говорящего, как его вернула диаризация.""" start: float end: float speaker: int @dataclass class Turn: """Реплика: несколько подряд идущих сегментов одного говорящего.""" start: float end: float speaker: int @property def duration(self) -> float: return self.end - self.start def merge_turns(segments: list[Segment], gap: float = 0.8) -> list[Turn]: """Склеивает соседние сегменты одного говорящего в реплики. Диаризация режет речь на короткие куски по паузам внутри фразы; для расшифровки нужны цельные реплики, иначе текст рассыпается. """ turns: list[Turn] = [] for s in segments: if turns and turns[-1].speaker == s.speaker and s.start - turns[-1].end <= gap: turns[-1].end = s.end else: turns.append(Turn(start=s.start, end=s.end, speaker=s.speaker)) return turns def chunk_ranges(start: float, end: float, max_len: float) -> list[tuple[float, float]]: """Режет интервал на куски не длиннее max_len. GigaAM падает на аудио длиннее ~200 секунд: у неё позиционное кодирование рассчитано на 5000 фреймов. Держим запас и режем по 150. """ chunks = [] cursor = start while cursor < end: stop = min(cursor + max_len, end) chunks.append((cursor, stop)) cursor = stop return chunks or [(start, end)] def speech_stats(segments: list[Segment]) -> dict: """Считает, сколько и в каких долях говорил каждый участник.""" by_speaker: dict[int, float] = {} for s in segments: by_speaker[s.speaker] = by_speaker.get(s.speaker, 0.0) + (s.end - s.start) total = sum(by_speaker.values()) return { "speakers": len(by_speaker), "speech_sec": round(total, 1), "by_speaker": [ { "speaker": spk + 1, "speech_sec": round(dur, 1), "share_pct": round(dur / total * 100, 1) if total else 0.0, } for spk, dur in sorted(by_speaker.items()) ], }