На записях с микрофоном на столе голоса участников для модели почти неразличимы: перебор четырёх моделей отпечатков и смена алгоритма кластеризации баланс улучшают, но роли всё равно скачут. Поэтому сервис теперь отдаёт то, на что можно опереться: акустику каждой реплики (громкость, доля высоких, центроид - они связаны с расстоянием до микрофона) и метрику separation_quality с флагом speakers_reliable. Ниже 0.35 разметка по говорящим случайна, и роли должна определять LLM по смыслу реплик. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
56 lines
2.8 KiB
Python
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
|