skillok meeting
996ce529a8d705e470aea24a7b7fb9a19578f1e4
Перейти к файлу
Код
meeting src-taurisrctranscript.rs
anc Ядро: встреча = заметка Markdown, всё по кнопке - Убрана автоматика: слежение за Zoom, окно согласия, трей, уведомления, автозапуск, живая расшифровка, разбор по JSON-схеме. - Встреча — note.md и дорожки звука; правка, дописывание, переименование, удаление. - «Добавить аудио»: чтение mp3, m4a, wav, aiff, flac, ogg (symphonia), потоковое приведение к 16 кГц моно. - «Расшифровать»: нарезка по паузам с обрезкой тишины в начале куска, до 4 кусков сразу, повтор, пометка нераспознанного, блок «Расшифровка». - Команды ИИ над заметкой: «Конспект», «Задачи», «Ревью», свой запрос; промпты файлами в prompts/. - «Записать» по кнопке: дорожки Zoom и микрофона в WAV, пауза, стоп. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> ad47f89 30 сентября 2026 г.
transcript.rs
//! Реплики расшифровки и то, как они ложатся в заметку.
//!
//! Говорящий — только дорожка звука: «Я» (микрофон), «Собеседники» (звук Zoom)
//! или никто, если звук пришёл одним файлом. Узнавания людей по голосу нет и не
//! будет: это биометрия (см. README, «Согласие на запись»).

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Speaker {
    Me,
    Others,
    /// Звук одним файлом: кто говорит, неизвестно.
    Unknown,
}

impl Speaker {
    pub fn label(&self) -> Option<&'static str> {
        match self {
            Speaker::Me => Some("Я"),
            Speaker::Others => Some("Собеседники"),
            Speaker::Unknown => None,
        }
    }

    /// Имя файла дорожки записи.
    pub fn track_file(&self) -> &'static str {
        match self {
            Speaker::Me => "me.wav",
            Speaker::Others => "others.wav",
            Speaker::Unknown => "audio.wav",
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Segment {
    /// Начало реплики от начала записи, мс.
    pub start_ms: u64,
    pub speaker: Speaker,
    pub text: String,
}

/// Блок «Расшифровка» для заметки: реплика — абзац с временем. Подряд идущие
/// реплики одного говорящего склеиваются в абзац — так читается легче.
pub fn to_markdown(segments: &[Segment]) -> String {
    let mut out = String::from("## Расшифровка");
    let mut prev: Option<&Speaker> = None;
    for s in segments {
        if prev == Some(&s.speaker) {
            out.push(' ');
            out.push_str(&s.text);
            continue;
        }
        out.push_str("\n\n");
        match s.speaker.label() {
            Some(who) => out.push_str(&format!(
                "**{} {}:** {}",
                format_timestamp(s.start_ms),
                who,
                s.text
            )),
            None => out.push_str(&format!("**{}** {}", format_timestamp(s.start_ms), s.text)),
        }
        prev = Some(&s.speaker);
    }
    if segments.is_empty() {
        out.push_str("\n\nРечи в записи не нашлось.");
    }
    out.push('\n');
    out
}

pub fn format_timestamp(ms: u64) -> String {
    let total = ms / 1000;
    format!("{:02}:{:02}:{:02}", total / 3600, total / 60 % 60, total % 60)
}

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

    fn seg(ms: u64, speaker: Speaker, text: &str) -> Segment {
        Segment {
            start_ms: ms,
            speaker,
            text: text.into(),
        }
    }

    #[test]
    fn markdown_merges_runs_of_one_speaker() {
        let md = to_markdown(&[
            seg(1_000, Speaker::Me, "Привет."),
            seg(4_000, Speaker::Me, "Начнём?"),
            seg(9_000, Speaker::Others, "Да."),
            seg(3_723_000, Speaker::Me, "Спасибо."),
        ]);
        assert_eq!(
            md,
            "## Расшифровка\n\n\
             **00:00:01 Я:** Привет. Начнём?\n\n\
             **00:00:09 Собеседники:** Да.\n\n\
             **01:02:03 Я:** Спасибо.\n"
        );
    }

    #[test]
    fn single_file_has_no_speaker_names() {
        let md = to_markdown(&[
            seg(0, Speaker::Unknown, "Раз."),
            seg(20_000, Speaker::Unknown, "Два."),
        ]);
        assert_eq!(md, "## Расшифровка\n\n**00:00:00** Раз. Два.\n");
    }

    #[test]
    fn empty_transcript_says_so() {
        assert_eq!(to_markdown(&[]), "## Расшифровка\n\nРечи в записи не нашлось.\n");
    }

    #[test]
    fn speaker_serializes_for_the_interface() {
        assert_eq!(serde_json::to_string(&Speaker::Me).unwrap(), r#"{"kind":"me"}"#);
        assert_eq!(Speaker::Others.track_file(), "others.wav");
    }
}