skillok meeting
169ce64e1705fd64e48ee357a816bbdb90bab01d
Перейти к файлу
Код
meeting cratesaudiosrcsource.rs
anc Этап 1: запись встречи в Zoom, живая расшифровка и разбор - crates/audio: захват звука процесса Zoom и микрофона (WASAPI, process loopback), формат 16 кГц моно, нарезка на куски по паузам с отсевом тишины, распознавание начала и конца встречи по звуковой сессии Zoom. - Ядро: запись двух дорожек → Whisper через OpenRouter → расшифровка, живой конспект раз в три минуты, по окончании — разбор сам; звук удаляется после разбора, если не включено хранение. - Контроллер: слежу за Zoom → прошу предупредить участников → пишу; запись вручную, пауза, стоп; прерванная запись помечается при запуске. - Трей, уведомления, крестик прячет окно, запуск вместе с Windows. - Интерфейс: главный экран с карточкой состояния (задача #1), живая расшифровка и конспект на экране встречи, новые настройки. - reqwest на системном TLS: под Windows без C-кода ring. - 67 тестов; код под Windows проверен компиляцией и clippy с Mac, на Windows не запускался. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> 169ce64 30 сентября 2026 г.
source.rs
//! Источник звука: дорожка «я» (микрофон) или «собеседники» (звук Zoom).
//!
//! Источник отдаёт то, что накопилось с прошлого вызова, в формате
//! [`crate::pcm`]. Время звуку ставит тот, кто читает: конец блока — момент
//! чтения. Источник может молчать (захват процесса не шлёт ничего, пока Zoom
//! тих) — это не ошибка, дыру заполнит [`crate::chunker::Chunker`].

use std::time::{Duration, Instant};

use crate::pcm::SAMPLES_PER_MS;

#[derive(Debug, thiserror::Error)]
pub enum CaptureError {
    #[error("Звук Zoom недоступен: {0}")]
    Loopback(String),
    #[error("Микрофон недоступен: {0}")]
    Microphone(String),
    #[error("Запись встреч из Zoom работает только в Windows")]
    Unsupported,
    #[error("Источник звука закончился")]
    Ended,
}

pub trait Capture: Send {
    /// Ждёт не дольше ~50 мс и отдаёт накопленные сэмплы (может — ни одного).
    fn read(&mut self) -> Result<Vec<i16>, CaptureError>;
}

/// Звук из памяти в реальном времени (со множителем скорости). Для тестов и
/// для проверки цепочки на машине без Windows.
pub struct Replay {
    samples: Vec<i16>,
    pos: usize,
    started: Option<Instant>,
    speed: f64,
}

impl Replay {
    pub fn new(samples: Vec<i16>, speed: f64) -> Self {
        Self {
            samples,
            pos: 0,
            started: None,
            speed,
        }
    }
}

impl Capture for Replay {
    fn read(&mut self) -> Result<Vec<i16>, CaptureError> {
        if self.pos >= self.samples.len() {
            return Err(CaptureError::Ended);
        }
        let started = *self.started.get_or_insert_with(Instant::now);
        std::thread::sleep(Duration::from_millis(20));
        let due = (started.elapsed().as_secs_f64() * 1000.0 * self.speed) as u64 * SAMPLES_PER_MS;
        let end = (due as usize).clamp(self.pos, self.samples.len());
        let out = self.samples[self.pos..end].to_vec();
        self.pos = end;
        Ok(out)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pcm::ms_to_samples;

    #[test]
    fn replay_delivers_everything_then_ends() {
        let mut r = Replay::new(vec![1; ms_to_samples(300)], 10.0);
        let mut got = 0;
        loop {
            match r.read() {
                Ok(s) => got += s.len(),
                Err(CaptureError::Ended) => break,
                Err(e) => panic!("{e}"),
            }
        }
        assert_eq!(got, ms_to_samples(300));
    }
}