skillok meeting
b60fa5785b25f98611024058977fb6386209d626
Перейти к файлу
Код
meeting cratesvoicessrcextractor.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 г.
extractor.rs
//! Отпечаток голоса: вектор модели WeSpeaker для куска звука 16 кГц.

use std::ffi::CString;
use std::path::Path;

#[derive(Debug, thiserror::Error)]
pub enum Error {
    #[error("модель голосов не найдена: {0}")]
    NoModel(String),
    #[error("модель голосов не загрузилась: {0}")]
    Load(String),
}

/// Снимает отпечатки. Держит модель в памяти; создаётся на одну расшифровку.
pub struct Extractor {
    raw: *const sherpa_rs_sys::SherpaOnnxSpeakerEmbeddingExtractor,
    dim: usize,
}

// Указатель используется из одного потока за раз: отпечатки снимаются
// последовательно в `spawn_blocking`, модель сама раскладывает работу по ядрам.
unsafe impl Send for Extractor {}

impl Extractor {
    pub fn open(model: &Path, threads: usize) -> Result<Self, Error> {
        if !model.is_file() {
            return Err(Error::NoModel(model.display().to_string()));
        }
        let path =
            CString::new(model.to_string_lossy().as_bytes()).map_err(|e| Error::Load(e.to_string()))?;
        let cpu = CString::new("cpu").unwrap();
        let config = sherpa_rs_sys::SherpaOnnxSpeakerEmbeddingExtractorConfig {
            model: path.as_ptr(),
            num_threads: threads.clamp(1, 16) as i32,
            debug: 0,
            provider: cpu.as_ptr(),
        };
        let raw = unsafe { sherpa_rs_sys::SherpaOnnxCreateSpeakerEmbeddingExtractor(&config) };
        if raw.is_null() {
            return Err(Error::Load(model.display().to_string()));
        }
        let dim = unsafe { sherpa_rs_sys::SherpaOnnxSpeakerEmbeddingExtractorDim(raw) } as usize;
        Ok(Self { raw, dim })
    }

    /// Отпечаток куска (16 кГц, моно). Пусто, если звука слишком мало, чтобы
    /// модель что-то сказала.
    pub fn embed(&mut self, samples: &[i16]) -> Vec<f32> {
        let x: Vec<f32> = samples.iter().map(|&s| s as f32 / 32768.0).collect();
        unsafe {
            let stream = sherpa_rs_sys::SherpaOnnxSpeakerEmbeddingExtractorCreateStream(self.raw);
            sherpa_rs_sys::SherpaOnnxOnlineStreamAcceptWaveform(stream, 16_000, x.as_ptr(), x.len() as i32);
            sherpa_rs_sys::SherpaOnnxOnlineStreamInputFinished(stream);
            let out = if sherpa_rs_sys::SherpaOnnxSpeakerEmbeddingExtractorIsReady(self.raw, stream) != 0 {
                let p = sherpa_rs_sys::SherpaOnnxSpeakerEmbeddingExtractorComputeEmbedding(self.raw, stream);
                let v = std::slice::from_raw_parts(p, self.dim).to_vec();
                sherpa_rs_sys::SherpaOnnxSpeakerEmbeddingExtractorDestroyEmbedding(p);
                v
            } else {
                Vec::new()
            };
            sherpa_rs_sys::SherpaOnnxDestroyOnlineStream(stream);
            out
        }
    }
}

impl Drop for Extractor {
    fn drop(&mut self) {
        unsafe { sherpa_rs_sys::SherpaOnnxDestroySpeakerEmbeddingExtractor(self.raw) };
    }
}