anc
Разбор по голосам: «Спикер 1», «Спикер 2»… на компьютере, без сети
- crates/voices: отпечаток голоса реплики (WeSpeaker ResNet34 через
sherpa-onnx, статические сборки под Windows и macOS) и разбиение снизу
вверх со средней связью. Из отпечатков вычитается средний по записи:
без этого у подкаста любые две реплики были похожи на 0,5–0,8.
- После распознавания реплики «Собеседников» и файла раскладываются по
голосам; микрофон — «Я». Отпечатки и реплики — в voices.json рядом с
записью: «Говорящих: N» пересобирает разбиение без новой расшифровки.
- Ход работы: этап «Разделяю по голосам» с процентом; по ходу
распознавания у реплик разбираемых дорожек ещё нет имени.
- Исправлено: файл без смены говорящего ложился одним абзацем 00:00:00
(подкаст 1 ч 52 мин — 170 КБ одной строкой). Абзац теперь — реплики
одного говорящего без пауз больше 1,5 с и не длиннее ~600 знаков.
У реплики появился конец (из ответа Whisper или до следующей).
- Модель (26 МБ) не в git: scripts/fetch-models.sh с проверкой SHA-256,
в сборку — ресурсом. Windows — со статической средой C (+crt-static,
явная libucrt): установщику не нужен VC++ Redistributable; crate-type
ядра — только rlib (cdylib нужен лишь мобильным платформам).
- Проверено на подкасте Kuji 230 (1 ч 52 мин, трое в студии): трое,
48 % / 36 % / 15 % речи, отпечатки 1184 реплик за ~3 мин.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d0d9d7d
30 сентября 2026 г.
cluster.rs
//! Разбиение реплик по говорящим по отпечаткам голоса.
//!
//! Отпечаток — вектор, у одного голоса векторы смотрят в одну сторону
//! (косинусная близость высокая), у разных — в разные. Сколько людей в записи,
//! заранее неизвестно, поэтому разбиение — снизу вверх: каждая реплика сначала
//! сама по себе, потом сливаются две самые похожие группы, и так, пока похожих
//! не останется (или пока групп не станет столько, сколько сказал человек).
//!
//! Короткая реплика («Ага», «Да») даёт шумный отпечаток: в слиянии участвуют
//! только реплики от [`MIN_ANCHOR_S`] секунд, остальные потом просто
//! прикладываются к ближайшему говорящему.
/// Сколько говорящих искать.
/// Порог по умолчанию для модели WeSpeaker ResNet34 на отпечатках без
/// общего сдвига записи (см. `centered`). Подобран на подкасте с тремя
/// людьми в студии (`examples/voices.rs`): весь выпуск 1 ч 52 мин — трое
/// (49 %, 37 %, 14 % речи); при −0,05 двое сливаются, при 0,05 — пятеро.
pub const DEFAULT_THRESHOLD: f32 = 0.0;
/// Реплики короче — не опора для разбиения, их отпечаток шумный.
pub const MIN_ANCHOR_S: f32 = 1.2;
/// Больше опор не берём: слияние — квадрат от их числа по памяти и больше по
/// времени. Двухчасовая встреча — около 2000 реплик; 800 самых длинных
/// хватает, чтобы услышать каждого, кто говорил хоть минуту.
const MAX_ANCHORS: usize = 800;
/// Говорящий, у которого меньше этой доли речи, в автоматическом режиме
/// растворяется в остальных: это чаще обрывок чужого голоса, смех, музыка.
const MIN_SHARE: f32 = 0.03;
/// Номер говорящего для каждой реплики: 0, 1, 2… по порядку первого
/// появления (реплики — по времени). `embeddings` — отпечатки, `seconds` —
/// длина реплик. Пустой отпечаток (реплика слишком короткая, чтобы его
/// снять) получает говорящего соседней реплики.
/// Отпечатки без общего для всей записи: из каждого вычитается средний.
/// У записи есть своё «звучание» — комната, микрофон, сжатие, фоновая
/// музыка, — и оно одинаково сдвигает отпечатки всех говорящих: на подкасте
/// близость любых двух реплик была 0,5–0,8. Без общего сдвига остаются
/// различия голосов, и один порог годится для разных записей.
/// Слияние снизу вверх со средней связью: близость двух групп — средняя
/// близость их реплик.
/// Говорящие с долей речи меньше [`MIN_SHARE`] растворяются в остальных.
/// Реплика без отпечатка — говорящий предыдущей (или следующей, если она первая).
/// 0, 1, 2… по порядку первого появления: «Спикер 1» — тот, кто заговорил первым.