Files
talkscore-asr/app/acoustics.py
T
Vladimir BryzgalovandClaude Opus 5 e2db164f0b Признаки для расстановки ролей и оценка надёжности разделения
На записях с микрофоном на столе голоса участников для модели почти
неразличимы: перебор четырёх моделей отпечатков и смена алгоритма
кластеризации баланс улучшают, но роли всё равно скачут.

Поэтому сервис теперь отдаёт то, на что можно опереться: акустику каждой
реплики (громкость, доля высоких, центроид - они связаны с расстоянием
до микрофона) и метрику separation_quality с флагом speakers_reliable.
Ниже 0.35 разметка по говорящим случайна, и роли должна определять LLM
по смыслу реплик.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 23:21:56 +05:00

56 lines
2.8 KiB
Python

"""Акустические признаки реплики.
На записи с одним микрофоном на столе участники сидят на разном расстоянии,
и это слышно: дальний голос глуше и тише. Тембровые модели такую разницу
почти не улавливают, поэтому признаки считаются отдельно и отдаются наружу -
по ним 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