"""Акустические признаки реплики. На записи с одним микрофоном на столе участники сидят на разном расстоянии, и это слышно: дальний голос глуше и тише. Тембровые модели такую разницу почти не улавливают, поэтому признаки считаются отдельно и отдаются наружу - по ним LLM может уточнить, кто где, когда разделение по голосу ненадёжно. """ import numpy as np __all__ = ["segment_acoustics", "separation_quality"] SAMPLE_RATE = 16000 def segment_acoustics(samples: np.ndarray) -> dict: """Считает признаки, связанные с расстоянием до микрофона.""" if len(samples) < SAMPLE_RATE // 4: return {} rms = float(np.sqrt((samples ** 2).mean()) + 1e-9) spectrum = np.abs(np.fft.rfft(samples * np.hanning(len(samples)))) freqs = np.fft.rfftfreq(len(samples), 1 / SAMPLE_RATE) total = float(spectrum.sum()) + 1e-9 return { # Громкость: ближний к микрофону обычно громче "loudness_db": round(20 * float(np.log10(rms)), 1), # Доля высоких частот: у дальнего голоса верх съедается расстоянием "hf_ratio": round(float(spectrum[(freqs > 3000) & (freqs < 7000)].sum()) / total, 3), # Центр тяжести спектра, тот же смысл другим числом "centroid_hz": round(float((spectrum * freqs).sum()) / total), } def separation_quality(embeddings: np.ndarray, labels: np.ndarray) -> float: """Насколько уверенно голоса разошлись по кластерам. Единица - участники различаются чётко, около нуля - модель фактически не видит разницы и разметка по говорящим случайна. На записях с одним микрофоном в комнате значение обычно низкое. """ if len(embeddings) < 4 or len(set(labels.tolist())) < 2: return 0.0 norm = embeddings / (np.linalg.norm(embeddings, axis=1, keepdims=True) + 1e-9) distances = 1 - norm @ norm.T scores = [] for i in range(len(norm)): own = distances[i][(labels == labels[i])] other = distances[i][(labels != labels[i])] own = own[own > 0] if len(own) and len(other): a, b = float(own.mean()), float(other.mean()) scores.append((b - a) / max(a, b)) return round(float(np.mean(scores)), 3) if scores else 0.0