skillok meeting
a730a5c5ec9fe89299948ec3d7d5d1b31cd2600a
Перейти к файлу
Код
meeting cratesvoicesexamplesvoices.rs
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 г.
voices.rs
//! Подбор порога на настоящей записи:
//! `cargo run --release -p meet-voices --example voices -- <файл> <с какой секунды> <сколько секунд> <модель.onnx>`
//! Режет звук на реплики по паузам, снимает отпечатки и печатает разбиение
//! при нескольких порогах: сколько говорящих и чья доля речи.

use meet_audio::chunker::{Chunker, Config};
use meet_audio::pcm::samples_to_ms;
use meet_voices::{assign, Count, Extractor};
use std::time::Instant;

fn main() {
    let a: Vec<String> = std::env::args().collect();
    let pcm = meet_audio::decode::decode_file(std::path::Path::new(&a[1])).unwrap();
    let from = (a[2].parse::<f32>().unwrap() * 16_000.0) as usize;
    let to = (from + (a[3].parse::<f32>().unwrap() * 16_000.0) as usize).min(pcm.len());
    let part = &pcm[from..to];
    // Реплики — куски между паузами от 0,3 с, длиной 1–8 с.
    let cfg = Config {
        min_ms: 1_000,
        max_ms: 8_000,
        pause_ms: 300,
        min_speech_ms: 300,
        ..Config::default()
    };
    let mut c = Chunker::new(cfg);
    let mut chunks = c.push(samples_to_ms(part.len()), part);
    chunks.extend(c.flush());
    let threads = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4);
    let mut ex = Extractor::open(std::path::Path::new(&a[4]), threads).unwrap();
    let t = Instant::now();
    let emb: Vec<Vec<f32>> = chunks.iter().map(|ch| ex.embed(&ch.samples)).collect();
    let sec: Vec<f32> = chunks.iter().map(|ch| ch.duration_ms() as f32 / 1000.0).collect();
    let talk: f32 = sec.iter().sum();
    eprintln!(
        "{} реплик, {talk:.0} с речи, отпечатки за {:.1} с ({threads} потоков)",
        chunks.len(),
        t.elapsed().as_secs_f32()
    );
    let norm: Vec<Vec<f32>> = emb
        .iter()
        .filter(|e| !e.is_empty())
        .map(|e| {
            let n = e.iter().map(|x| x * x).sum::<f32>().sqrt();
            e.iter().map(|x| x / n).collect()
        })
        .collect();
    let mut hist = [0usize; 10];
    for i in 0..norm.len() {
        for j in i + 1..norm.len() {
            let s: f32 = norm[i].iter().zip(&norm[j]).map(|(a, b)| a * b).sum();
            hist[((s.max(0.0) * 10.0) as usize).min(9)] += 1;
        }
    }
    eprintln!("близость пар по десятым: {hist:?}");
    let thrs: Vec<f32> = std::env::var("THR")
        .map(|v| v.split(',').map(|x| x.parse().unwrap()).collect())
        .unwrap_or(vec![0.0, 0.05, 0.1, 0.15, 0.2, 0.25, 0.3, 0.35]);
    for thr in thrs {
        let l = assign(&emb, &sec, Count::Auto { threshold: thr });
        let k = l.iter().max().map(|m| m + 1).unwrap_or(0);
        let mut share = vec![0f32; k];
        for (i, s) in l.iter().zip(&sec) {
            share[*i] += s;
        }
        let share: Vec<String> = share
            .iter()
            .map(|s| format!("{:.0}%", s / talk * 100.0))
            .collect();
        eprintln!("порог {thr}: {k} говорящих {share:?}");
        if a.get(5).map(|s| s.parse::<f32>().unwrap()) == Some(thr) {
            for (ch, s) in chunks.iter().zip(&l) {
                let t = from as u64 / 16 + ch.start_ms;
                println!(
                    "{:02}:{:02}:{:02} {:>4.1}s S{s}",
                    t / 3_600_000,
                    t / 60_000 % 60,
                    t / 1000 % 60,
                    ch.duration_ms() as f32 / 1000.0
                );
            }
        }
    }
}