Back to "AudioSummarizer: Конstrained Fuzzy Forensic Audio Characterization"

This is a viewer only at the moment see the article on how this works.

To update the preview hit Ctrl-Alt-R (or ⌘-Alt-R on Mac) or Enter to refresh. The Save icon lets you save the markdown file to disk

This is a preview from the server running through my markdig pipeline

AI Architecture Audio LLM ONNX Patterns Speaker Diarization

AudioSummarizer: Конstrained Fuzzy Forensic Audio Characterization

Saturday, 10 January 2026

Статус: AudioSummarizerM SK1 Кора зараз в розробці як частина lucidRAG, майбутній, більш-менш зрозумілій продукт для мультиплексу - модальний RAG МSK2 Implementation is complete and working МSK3this article documents the architecture and design

Источник: github.comM SK1scottgal/lucidrag (branchM SK1 v2)

Де це підходить?: AudioSummarizer є частиною lucidRAG сім 'я Reduced RAG implementations. Кожна з них працює по-різному

  • DocSummarizer - Документи
  • ImageSummarizer - Фото МSK1 хвиля зорової інтелекту
  • DataSummarizer МSK0 Дані ( Сchéма висновків МSK2 Profilування )
  • AudioSummarizer ( цей artykułM SK1 - аудіо МSK3 акустичний профиль МSK4 диарізація głośnikів)

Все йде так само Снижена модель RAG: виокремлює сигнали, як тільки МSK1 зберігає дані МSK2 синтезує з обмеженим входом LLM .


Багато демонстрацій – МСК0, перші ланцюги з аналізом звуку роблять те ж саме. МСК1, вони ставляться до LLM так, наче вони були інженерами акустики

Вони переносять хвилі в моделі. МСК0. Вони просять їх: МСК1, пізнати якість мовлення, ", " МСК3, визначити співпромовців, ,", і сподіваються, що модель зможе вивести структурні властивості з того, що є фундаментальним шаблоном. \ "" МSK5, система комбінацій, МSK6 "". Вона працює достатньо добре для демонстрації. МSK7, а потім галюциналізує ім 'я співперемовників. МСК8, вигадує музичні жанри. М СК9, або напевно неправильно визначає акценти.

AudioSummarizer об 'єднує дві комплементарні моделі:

  1. Reduced RAG для отримання - виділення сигналів один раз МSK1 зберігання доказів МSK2 запит проти фактів
  2. Стримана неясність для оркестрування - хвиляM SK1подібний трубопровод, детермінативний субстрат обмежує ймовірністичні моделі

Замість того, щоб fed raw audio до LLM в час пошуку, це зменшує кожен звуковий файл до журнал сигналів (детерміністичні сигнали, такі як шум RMS МSK1 спектральні характеристики , вбудова гучномовців отримані докази (transcriptsM SK1 speaker sample clips , diarization turns МSK3 Цей ledger is persisted once

На час запиту, LLM синтезує відповіді, відбираючи та міркуючи над виділяними сигналами — не переM SK2 аналізуючи аудіо МSK3 Тяжке переміщення МSK4 обробка сигналів МСК5 транскрипція М СК6 диарізація M СК7 трапляється під час вживання MСК8 LML працює лише на час пошуку, щоб інтерпретувати передідушні М סК9 обчислені факти і генерувати людські М S К 10 читабельні відповіді М Ш К 11

Створювання шеми: Скорінні ручки RAG що для зберігання та отримання. Конstrained Fuzziness handles як щоб надійно витягувати сигнали. Разом вони umożliwiają юридичне охарактеризування звуку з обмеженими витратами LLM.

Дізнайтеся більше про модель Reduced RAG: Reduced RAG: Signal-Driven Document Understanding

Ключеві терміни

  • Сигнал: Надруковані факти з показниками самовпевненості audio.content_type = "speech"МSK0 впевненість: МSK2
  • Докази: Прослуховні артефакти
  • Сигнальний журнал: Перестаюча сукупність всіх сигналів , вказівники доказів МSK2 і вбудова, взяті з аудіо-файла
  • Голосковий код: локальний идентификатор в одному файлі /діарізація запускається МSK2eM SK3g., SPEAKER_00)
  • Голосовий номер: КроссM SK1стабільний хеш для файлів, отриманий від вбудованого голосу vprint:a3f9c2e1)
  • Людина: Це явно не припущено — ми ніколи не стверджуємо МSK2 це Джон Сміт МSK3

Модель ідентичності: SPEAKER_00 є локальним для одного звукового файлу. VoiceprintId є стабільним в усіх файлах, але анонімним. Ми ніколи не мапуємо людських іменM SK1

Судебні обов 'язки: Кожен сигнал включає походження (від якої хвилі вона взялася впевненість (визначний рівень МSK1 і версування (модельM SK1витривалі версії). Це дає можливість репродуцуванняMSC3 той самий вхід МSK4 той же конфигурація → той ж сигналовий ledgerMNK6

Результат::

  • швидка, конфіденційністьM SK1 збереження судової аудіо характеристики
  • Диарізація спикера без залежності від Pythonу (pure .NET)
  • Виявление схожості анонімних мовців (не PII, без іменM SK2
  • Вбудова голосового коду для "знайоміть подібні спикериM SK1 запитів
  • Все без відправлення аудіо до хмарних API під час захоплення

Це будується безпосередньо на Стриманий візерунок невизначеності і архітектуру на основі хвилі - з ImageSummarizer.

Погляньмо на сутність ЛЛМ повинні розбиратися над акустичними фактами.

Цей artykuł стосується:

  • Як AudioSummarizer реалізує модель Reduced RAG
  • Видобуток сигналів: детерміністичні акустичні факти M SK1не підсумки LLM
  • Спостереження доказів: зразки звукозапису МSK1 транскрипції , повороти диарізації
  • Чиста диарізація спикера .NET ( без Python МSK2 без pyannote МSK3
  • Попит-синтез часуM SK1 фільтровані сигнали → дістати докази МSK3 LLM синтезує

Подібні статті:

  • Reduced RAG - Центральний шаблон, який це вводить
  • Стриманий візерунок невизначеності - Фундаментальна модель
  • Reduced RAG Implementations:
    • DocSummarizer - RAG документу з виділенням об 'єктів та графами знань
    • DataSummarizer - Профильування даних та виведення шеми RAG
    • ImageSummarizer - Image RAG з каналом візуального розпізнавання хвиль
    • AudioSummarizer ( цей artykułM SK1 - Озвучно-судебне охарактеризування з диарізацією гучномовця

Проблема: Аудіо-аналіз важкий МSK1 і культурно завантаженняM SK2

Аудіо-аналіз зазнає невдачі у передбачуваний спосіб:

  • Культурні заявиМSK0 " Це джаз МSK2 ( каже, хто МСК4 на основі яких тренувальних даних
  • Назви спикерівМSK0 "Голосник - Джон Сміт
  • Інтегніація музикиМSK0 "ПесняM SK2 Песня Даруда "" Sandstorm by Darude "", МSK3 МSK4 питання авторського права, МSK5 зовнішні знання, )
  • Акцентне припущенняМSK0 "Говорець має британський акцент
  • Відлежність від Python: Більшість інструментів діарізації потребують пианоту (Пітоновий замок екосистеми

Традиційний підхід: "Run Whisper для транскрипції, pyannote для диарізаціїM SK3 надіслати до LLM для резюмеMSC4

Проблема: Це або витікає з PII ( назви głośnikів МSK2 коштує надто багато МSK3cloud APIs мSK4 або потребує Python runtime (pyannote , DIART мск7

Рішення: Збудувати чисту трубку, що характеризує структуру і акустику звуку, базуючись на МSK1NET хвилі МSK2, не висловлюючи культурних тверджень


Reduced RAG Audio Framework

AudioSummarizer впроваджує Снижена модель RAG для аудіофайлів, відповідно до трьох основних принципівМSK1

1. Видобуток сигналів МSK1Фаза поглинанняM SK2

Детерміністичні сигнали, отримані один раз—не LLMM SK1розроблені підсумкиМSK2

  • ТиморальнийМSK0 тривалість, часові відміткиM SK2 обмеження сегменту
  • Акустика: шум RMS , спектральний центроїд МSK2 динамічний діапазон M SK3 співвідношення кліпання
  • Ідентичність: SHA-256 hash, формат файлуM SK3 частота зразківMSC4 канали
  • Якість: запевненість в транскрипції
  • Спикер: номери запису голосу МSK1анонімні хеши МSK2 число поворотів , відсоток учасників
  • Категорічні: тип контенту МSK1говорити МSK2музика / мовчазнь M SK4 класифікація гучномовців

Ось приклад сигналу для подкасу:

{
  "audio.hash.sha256": "3f2a9c8b1e4d...",
  "audio.duration_seconds": 912.0,
  "audio.rms_db": -18.2,
  "audio.spectral_centroid_hz": 2418.3,
  "audio.content_type": "speech",
  "speaker.count": 2,
  "speaker.classification": "two_speakers",
  "transcription.confidence": 0.87,
  "voice.voiceprint_id.speaker_00": "vprint:a3f9c2e1d4b8"
}

Ці сигнали детерміністична (самий аудіо вказівним ( може відфільтруватиM SK1 сортувати в базі даних), і обчислюваний без LLM (czysta обробка сигналу МSK1моделі ONNXM SK2

2. Хравання доказів Структуровані підрозділи МSK2

Замість того, щоб зберігати тільки "шлункиM SK1 AudioSummarizer stores:

  • Сигнал: Структуровані поля (JSONM SK2 для детермінативних фильтров
  • Вбудова: Voice embeddings (512-dim ECAPA-TDNNM SK3 for speaker similarity
    • Зауважте на приватність: Установлення не можна перетворити в цій системі ,, але сприймають їх як конфіденційні дані
  • Доказові артефакти:
    • Полний текст транскрипції (можливий пошукM SK1
    • Сигнали з пробними кліпами (Base64 WAVM SK2 2-секундні кліпи для перевірки
    • Диарізація повертається (JSON зі спикером
  • Приціли: File hash + ID доказів для перевіряемой провини

Доказ є перевіряним: Пользовачі можуть відтворювати зразки звуку зі спикера , читати транскрипції МSK2 перевіряти повороти диарізації M SK3 не просто довіряти резюме LLM

3. QueryM SK1Time Synthesis (Bounded LLM Input)

У часі пошуку, LLM ніколи бачить сирий аудіо. ЗамістьM SK1

  1. Фильтрування детерміністично ( базу даних, де clauseM SK1

    WHERE audio.content_type = 'speech'
      AND speaker.count >= 2
      AND audio.rms_db > -25.0
      AND transcription.confidence > 0.8
    
  2. Гібридний пошук Результати:

    • BM25 на тексті транскрипції M SK1погони з ключевыми словами)
    • Подібність вектора на вбудовах голосу (подібності мікрофонівM SK1
    • Поверніться вгору 5
  3. Синтезувати з сигналів (LLM бачить структурований пакет доказів

    Audio 1: podcast_ep42.mp3
    - Duration: 15m 12s
    - Speakers: 2 (SPEAKER_00: 52%, SPEAKER_01: 48%)
    - Quality: RMS -18.2dB, no clipping
    - Transcript: "Welcome to Tech Insights. Today we're discussing..."
    - Entities: ["quantum computing", "Google", "IBM"]
    

LLM отримує 5 структуровані пакети доказів замість 500 шматочки необроблених музичних метадань. Redukcja контекстного вікнаM SK1 ~50× менша.

Вплив на вартість ( якщо використати платні LLM API ): обробка МSK2 аудіофайлів починається з МSK3 M SK4 пере МСК5 аналізуючи ауду кожного запиту МСК6 до МиСК7 \ МиСк8 запит проти попереднього ММСК9 обчислюваних сигналів МУСК10 Примітка: lucidRAG є локальнимM SK1перший МSK2Ollama по замовчуванню , нульові витрати). платні API MSC5 Клод МSK6 ҐП МСК7 اختیارніMSSK8 Оцінки затрат припускають платне використання APIMСК9 М СК10 К-потрібні символи MСК11 відмінності залежно від постачальникаM СК12 | | МС К13 | К-отрібні знаки | MС К14 | запит |МС К15 | необроблені метадані |MС К16 | порівняно з | СС К17 | символами | МиС К18 | пошук | миС К19 | сигнали ♫ МС С К20 | MIС К21 | питання | МыС К22 | день | ЯС К23

Це ядро Reduced RAG insight: вирішити, що має значення заздалегідь (сигналиM SK1 зберігати його детерміністично МSK0свідчення), залучати LLM лише для синтезу МSK0запрос-часM SK2


Звуковий кабель з обмеженою неясністю

Система запускає хвилі в порядку пріоритету (вище число = запускає перший МSK2

Wave Priority Order:
  100: IdentityWave          → SHA-256, file metadata, duration
   90: FingerprintWave       → Chromaprint perceptual hash (optional)
   80: AcousticProfileWave   → RMS, spectral features, SNR
   70: ContentClassifierWave → Speech vs music heuristics (routing)
   65: TranscriptionWave     → Whisper.NET (optional)
   60: SpeakerDiarizationWave→ Pure .NET speaker separation
   30: VoiceEmbeddingWave    → ECAPA-TDNN speaker similarity

Примітка: Почуттів МSK1викриття рухів навмисно виключено МSK2культурно заповнене і не є частиною судової характеристики .

Архітектура

flowchart LR
    A[Audio file] --> B[IdentityWave]
    B --> C[AcousticProfileWave]
    C --> D[ContentClassifierWave]
    D --> E[TranscriptionWave]
    E --> F[SpeakerDiarizationWave]
    F --> G[VoiceEmbeddingWave]
    G --> H[Signal Ledger]
    H --> I[Optional LLM Synthesis]

    style B stroke:#333,stroke-width:4px
    style D stroke:#333,stroke-width:4px
    style F stroke:#333,stroke-width:4px
    style H stroke:#333,stroke-width:4px

Це зберігає знайоме Потік → Сигнал МSK1 Опціональний LLM петля, але прив 'язує її до детермінативних фактівM SK1

  • Акустичний профиль є детерміністичним ( той самий вхід = той же виход МSK2
  • LLM працює на доказі, не на сирому аудіо

Реальний-World Example: Обробка епізоду Podcast

Дозвольте "'" відслідковувати "15-" хвилинний подкаст через канал МSK2 .

Input: podcast_ep42.mp3
  - File size: 14.2 MB
  - Format: MP3, 44.1kHz stereo, 192 kbps
  - Duration: 15m 12s (912 seconds)
  - Content: 2-person interview

Wave Execution (priority order 100 → 30):

1. IdentityWave (Priority 100, 87ms):
   ✓ SHA-256: 3f2a9c8b1e4d...
   ✓ Duration: 912.0s
   ✓ Channels: 2 (stereo)
   ✓ Sample rate: 44100 Hz
   ✓ File size: 14,897,234 bytes

2. AcousticProfileWave (Priority 80, 142ms):
   ✓ RMS loudness: -18.2 dB (good mastering)
   ✓ Peak amplitude: 0.94 (no clipping)
   ✓ Dynamic range: 22.1 dB
   ✓ Spectral centroid: 2418 Hz (speech-like)
   ✓ Spectral rolloff: 7892 Hz

3. ContentClassifierWave (Priority 70, 89ms):
   ✓ Zero-crossing rate: 0.17 (high → speech)
   ✓ Spectral flux: 0.28 (low → not music)
   → Classification: "speech" (confidence: 0.85)

4. TranscriptionWave (Priority 65, 12.3s):
   ✓ Whisper.NET base model
   ✓ Segments: 142
   ✓ Total words: 2,341
   ✓ Confidence: 0.87 (high)
   ✓ Text: "Welcome to Tech Insights. Today we're discussing..."

5. SpeakerDiarizationWave (Priority 60, 3.8s):
   ✓ VAD detected: 47 speech segments
   ✓ Embeddings extracted: 47 × 512-dim vectors
   ✓ Clustering (threshold 0.75): 2 speakers
   ✓ Turns before merge: 47
   ✓ Turns after merge: 23
   ✓ SPEAKER_00 participation: 52% (474s)
   ✓ SPEAKER_01 participation: 48% (438s)
   ✓ Sample clips extracted: 2 (Base64 WAV, ~30KB each)

6. VoiceEmbeddingWave (Priority 30, 178ms):
   ✓ ECAPA-TDNN inference
   ✓ Embedding dimension: 512
   ✓ Voiceprint ID (SPEAKER_00): "vprint:a3f9c2e1d4b8"
   ✓ Voiceprint ID (SPEAKER_01): "vprint:7e2d8f1a9c3b"

Total processing time: 16.6s
Signals emitted: 47
LLM calls: 0 (fully offline)
Cost: $0 (local processing only)

І Сигнальний журнал це те, що зберігається в базі даних

Сигнальний привід (уривокM SK1

{
  "identity.filename": "podcast_ep42.mp3",
  "audio.hash.sha256": "3f2a9c8b1e4d...",
  "audio.duration_seconds": 912.0,
  "audio.format": "mp3",
  "audio.sample_rate": 44100,
  "audio.channels": 2,
  "audio.channel_layout": "stereo",
  "audio.rms_db": -18.2,
  "audio.dynamic_range_db": 22.1,
  "audio.spectral_centroid_hz": 2418.3,
  "audio.spectral_rolloff_hz": 7892.1,
  "audio.content_type": "speech",
  "content.confidence": 0.85,
  "speaker.count": 2,
  "speaker.classification": "two_speakers",
  "speaker.turn_count": 23,
  "speaker.avg_turn_duration": 39.7,
  "speaker.diarization_method": "agglomerative_clustering",
  "speaker.participation": {
    "SPEAKER_00": 52.0,
    "SPEAKER_01": 48.0
  },
  "transcription.full_text": "Welcome to Tech Insights...",
  "transcription.word_count": 2341,
  "transcription.confidence": 0.87,
  "voice.embedding.speaker_00": [0.023, -0.511, 0.882, ...],
  "voice.voiceprint_id.speaker_00": "vprint:a3f9c2e1d4b8",
  "speaker.sample.speaker_00": "UklGRiQAAABXQVZF...",
  "speaker.sample.speaker_01": "UklGRiQBBBXQVZF..."
}

Що це дає:

  • Поиск за "Tech Insights обговорює хмарну інфраструктуру
  • Запрос "Найти аудіо з голосовим відбитком vprintM SK1a3f 9cMST4eMSC5 МSK6 знаходить всі епізоди з одним спикером | |
  • Спитайте: " Що? МSK1 динамічний діапазон наших епізодів подкасу ?" МSK3 сукупні акустичні сигнали
  • Фильтр "Найти низький МSK1вибір якості записівM SK2 МSK3 RMS < -25 dB або відрізання | | МSK6 | 1%
  • Перевірити "Дію идентификації мікрофонаM SK1 МSK2 гру speaker.sample.speaker_00 кліп (всерединіM SK1файл)

Чому сигнали важливі ( Навіть без LLMM SK1

Сигнальний рахунок відповідає на нудні питання: МСК0, але МСК1 , Непогані питання негайно .

  • Чи це аудіо мовлення, музикаM SK1 чи тиша?
  • Скільки спікерів виявлено?
  • Що це за сигнал? Відсоток шуму від 1 до 2?
  • Чи є крапки чи викривлення??
  • Що таке спектральний центроід?
  • Який динамічний діапазон?

Це питання, які ви зазвичай знаходите. 30 хвилини в археології Аудачі

Сигнал у ledger дає вам їх в секундах.


Мережа хвиль: Від ідентичності до розпізнавання

Потік 1: Інтимність МSK1Детерміністичний субстрат МSK2

Базова лінія. Криптова особистість та метадані файлівM SK1

public class IdentityWave : IAudioWave
{
    public string Name => "IdentityWave";
    public int Priority => 100;  // Runs first

    public async Task<IEnumerable<Signal>> AnalyzeAsync(
        string audioPath,
        AnalysisContext context,
        CancellationToken ct)
    {
        var signals = new List<Signal>();

        // Cryptographic hash (deterministic identity)
        var fileHash = await ComputeSha256Async(audioPath, ct);
        signals.Add(new Signal
        {
            Name = "audio.hash.sha256",
            Value = fileHash,
            Type = SignalType.Identity,
            Confidence = 1.0,
            Source = Name
        });

        // File-level metadata
        var fileInfo = new FileInfo(audioPath);
        signals.Add(new Signal
        {
            Name = "audio.file_size_bytes",
            Value = fileInfo.Length,
            Type = SignalType.Metadata,
            Source = Name
        });

        // Audio format metadata
        using var reader = new AudioFileReader(audioPath);

        signals.Add(new Signal
        {
            Name = "audio.duration_seconds",
            Value = reader.TotalTime.TotalSeconds,
            Type = SignalType.Metadata,
            Source = Name
        });

        signals.Add(new Signal
        {
            Name = "audio.sample_rate",
            Value = reader.WaveFormat.SampleRate,
            Type = SignalType.Acoustic,
            Source = Name
        });

        signals.Add(new Signal
        {
            Name = "audio.channels",
            Value = reader.WaveFormat.Channels,
            Type = SignalType.Acoustic,
            Source = Name
        });

        signals.Add(new Signal
        {
            Name = "audio.format",
            Value = Path.GetExtension(audioPath).TrimStart('.'),
            Type = SignalType.Metadata,
            Source = Name
        });

        return signals;
    }
}

Випущені ключові сигнали:

  • audio.hash.sha256 - Криптова особистість
  • audio.duration_seconds - Длина
  • audio.sample_rate МSK0 44100, ♫ ♫ МSK2 і т.д. ♫ . ♫
  • audio.channels МSK0 МSK1 ( mono ), MSК4 MSК5стерео СМСК6 МСК7 МСК8
  • audio.format МSK0 mp3, wav, flacM SK3 і т.д.

Чому детермінативний?

  • Один і той самий файл → один і той же хеш → одна і та ж ідентичність
  • Немає випадковості зразків, немає параметру температури
  • Упевненість =

Вока 2: Акустичний профиль МSK1Процедура обробки сигналівM SK2

Витягування структурних акустичних властивостей за допомогою NAudio та FftSharp.

public class AcousticProfileWave : IAudioWave
{
    private readonly ILogger<AcousticProfileWave> _logger;

    public string Name => "AcousticProfileWave";
    public int Priority => 80;

    public async Task<IEnumerable<Signal>> AnalyzeAsync(
        string audioPath,
        AnalysisContext context,
        CancellationToken ct)
    {
        var signals = new List<Signal>();

        using var reader = new AudioFileReader(audioPath);

        // Convert to mono for analysis
        ISampleProvider sampleProvider = reader.WaveFormat.Channels == 1
            ? reader
            : new StereoToMonoSampleProvider(reader) { LeftVolume = 0.5f, RightVolume = 0.5f };

        // Read all samples
        var samples = ReadAllSamples(sampleProvider);

        // Time-domain analysis
        var rms = CalculateRms(samples);
        var peakAmplitude = samples.Max(Math.Abs);
        var dynamicRange = CalculateDynamicRange(samples);
        var clippingRatio = CalculateClippingRatio(samples, threshold: 0.99);

        signals.Add(new Signal
        {
            Name = "audio.rms_db",
            Value = 20 * Math.Log10(rms),  // Convert to decibels
            Type = SignalType.Acoustic,
            Source = Name
        });

        signals.Add(new Signal
        {
            Name = "audio.peak_amplitude",
            Value = peakAmplitude,
            Type = SignalType.Acoustic,
            Source = Name
        });

        signals.Add(new Signal
        {
            Name = "audio.dynamic_range_db",
            Value = dynamicRange,
            Type = SignalType.Acoustic,
            Source = Name
        });

        signals.Add(new Signal
        {
            Name = "audio.clipping_ratio",
            Value = clippingRatio,
            Type = SignalType.Acoustic,
            Confidence = clippingRatio > 0.01 ? 0.9 : 1.0,  // Low confidence if clipping detected
            Source = Name
        });

        // Frequency-domain analysis (FFT)
        var spectralFeatures = CalculateSpectralFeatures(samples, reader.WaveFormat.SampleRate);

        signals.Add(new Signal
        {
            Name = "audio.spectral_centroid_hz",
            Value = spectralFeatures.Centroid,
            Type = SignalType.Acoustic,
            Source = Name
        });

        signals.Add(new Signal
        {
            Name = "audio.spectral_rolloff_hz",
            Value = spectralFeatures.Rolloff,
            Type = SignalType.Acoustic,
            Source = Name
        });

        signals.Add(new Signal
        {
            Name = "audio.spectral_bandwidth_hz",
            Value = spectralFeatures.Bandwidth,
            Type = SignalType.Acoustic,
            Source = Name
        });

        return signals;
    }

    private SpectralFeatures CalculateSpectralFeatures(float[] samples, int sampleRate)
    {
        // Use FftSharp for frequency analysis
        int fftSize = 2048;
        var fftInput = new double[fftSize];

        // Take middle section of audio
        int offset = Math.Max(0, (samples.Length - fftSize) / 2);
        for (int i = 0; i < fftSize; i++)
        {
            fftInput[i] = samples[offset + i];
        }

        // Apply Hamming window
        var window = FftSharp.Window.Hamming(fftSize);
        for (int i = 0; i < fftSize; i++)
        {
            fftInput[i] *= window[i];
        }

        // Compute FFT
        var fft = FftSharp.Transform.FFT(fftInput);
        var magnitudes = fft.Select(c => Math.Sqrt(c.Real * c.Real + c.Imaginary * c.Imaginary)).ToArray();

        // Calculate spectral centroid (brightness)
        double sumWeightedFreq = 0;
        double sumMagnitude = 0;
        for (int i = 0; i < magnitudes.Length / 2; i++)
        {
            double freq = i * sampleRate / (double)fftSize;
            sumWeightedFreq += freq * magnitudes[i];
            sumMagnitude += magnitudes[i];
        }
        double centroid = sumMagnitude > 0 ? sumWeightedFreq / sumMagnitude : 0;

        // Calculate spectral rolloff (85% energy threshold)
        double totalEnergy = magnitudes.Take(magnitudes.Length / 2).Sum(m => m * m);
        double cumulativeEnergy = 0;
        double rolloff = 0;
        for (int i = 0; i < magnitudes.Length / 2; i++)
        {
            cumulativeEnergy += magnitudes[i] * magnitudes[i];
            if (cumulativeEnergy >= 0.85 * totalEnergy)
            {
                rolloff = i * sampleRate / (double)fftSize;
                break;
            }
        }

        return new SpectralFeatures
        {
            Centroid = centroid,
            Rolloff = rolloff,
            Bandwidth = CalculateBandwidth(magnitudes, centroid, sampleRate, fftSize)
        };
    }
}

Ось вихідний код:

Input: podcast.mp3 (15 minutes, 44.1kHz stereo)

Signals emitted:
  audio.rms_db = -18.2 dB (good loudness)
  audio.peak_amplitude = 0.94 (no clipping)
  audio.dynamic_range_db = 22 dB (moderate dynamics)
  audio.clipping_ratio = 0.003 (0.3% clipping, minimal)
  audio.spectral_centroid_hz = 2400 Hz (mid-brightness, speech-like)
  audio.spectral_rolloff_hz = 8000 Hz (most energy below 8kHz)
  audio.spectral_bandwidth_hz = 4200 Hz (moderate spread)

Чому це важливо:

  • гучність RMS вказує на якість
  • Відсоток кліпу виявляє артефакти запису
  • Спектральний центроід розрізняє мовлення (2-4kHzM SK1 від музики (variable)
  • Всі детерминістики—одно аудіо виробляє однакові значення

Потік 3: класифікація контенту МSK1Говоріння проти музикиM SK2

Грубое геристичне класифікування використовуючи нуль- швидкість перетинів та спектральний flux для призначення маршрутуванняM SK1

Примітка: Ці гуристики є жанром /залежними від контексту M SK2не надійно точні в усіх видах контенту МSK3 використовується лише для рішень про маршрутизацію хвиль MSC4eM SK5g., перекидати диарізацію для музики ), не як реальність на землі

public class ContentClassifierWave : IAudioWave
{
    public string Name => "ContentClassifierWave";
    public int Priority => 70;

    public async Task<IEnumerable<Signal>> AnalyzeAsync(
        string audioPath,
        AnalysisContext context,
        CancellationToken ct)
    {
        var signals = new List<Signal>();

        using var reader = new AudioFileReader(audioPath);
        var samples = ReadAllSamples(reader);

        // Zero-crossing rate (heuristic: speech tends higher ZCR - not universal)
        var zcr = CalculateZeroCrossingRate(samples);

        // Spectral flux (heuristic: music tends more consistent - varies by genre)
        var spectralFlux = CalculateSpectralFlux(samples, reader.WaveFormat.SampleRate);

        // Simple heuristic classifier (for routing, not identity)
        // Thresholds calibrated for typical podcast/interview content
        // Production: calibrate on your corpus for best routing accuracy
        string contentType;
        double confidence;

        if (zcr > 0.15 && spectralFlux < 0.3)  // Speech heuristic
        {
            contentType = "speech";
            confidence = 0.85;
        }
        else if (zcr < 0.10 && spectralFlux > 0.5)
        {
            contentType = "music";
            confidence = 0.80;
        }
        else
        {
            contentType = "mixed";
            confidence = 0.70;
        }

        // Check for silence
        var rmsDb = context.GetValue<double>("audio.rms_db");
        if (rmsDb < -50)
        {
            contentType = "silence";
            confidence = 0.95;
        }

        signals.Add(new Signal
        {
            Name = "audio.content_type",
            Value = contentType,
            Type = SignalType.Classification,
            Confidence = confidence,
            Source = Name,
            Metadata = new Dictionary<string, object>
            {
                ["zero_crossing_rate"] = zcr,
                ["spectral_flux"] = spectralFlux,
                ["rms_db"] = rmsDb
            }
        });

        return signals;
    }
}

Наприклад, маршрутизація:

Speech (ZCR=0.18, flux=0.25):
  → audio.content_type = "speech"
  → Enables: TranscriptionWave, SpeakerDiarizationWave

Music (ZCR=0.08, flux=0.65):
  → audio.content_type = "music"
  → Disables: SpeakerDiarizationWave (no speakers to detect)

Silence (RMS=-52dB):
  → audio.content_type = "silence"
  → Disables: All downstream waves (early exit)

Чистий .NET Диарізація głośnikа (Не PythonM SK2

Для обмеження використання lucidRAG': диарізація спикера без піаноту , DIART МSK2 або будь-яких залежностей від Пайтона

Проблема з Python-Based Diarization

Традиційний підхід:

pyannote.audio (Python) → ONNX export (experimental) → C# wrapper (fragile)

Проблеми:

  • pyannote 3.1+ видалено підтримка ONNX ( перенесена на чисту PyTorch
  • Потрібний час запуску Python (нощовий жах розгортання)
  • HTTP упаковник додає тривалості та складності
  • Ніхто не підтримує аутентифікацію

Рішення: Втілити диарізацію у чистому .NET, використовуючи існуючі моделі вбудовування голосу

Чистий алгоритм .NET

1. Voice Activity Detection (VAD) → Detect speech segments (energy-based RMS)
2. Segment Embedding → Extract ECAPA-TDNN embeddings for each segment
3. Agglomerative Clustering → Group segments by speaker (cosine similarity)
4. Speaker Turns → Merge consecutive turns from same speaker

Втілення

csharp public class SpeakerDiarizationService { private readonly ILogger _logger; private readonly VoiceEmbeddingService _embeddingService; private readonly AudioConfig _config;

public virtual async Task<DiarizationResult> DiarizeAsync(
    string audioPath,
    CancellationToken ct = default)
{
    _logger.LogInformation("Starting speaker diarization for {AudioPath}", audioPath);

    // Step 1: Detect speech segments using VAD
    var segments = DetectSpeechSegments(audioPath);
    _logger.LogDebug("Detected {Count} speech segments", segments.Count);

    if (segments.Count == 0)
    {
        return new DiarizationResult
        {
            Turns = new List<SpeakerTurn>(),
            SpeakerCount = 0
        };
    }

    // Step 2: Extract embeddings for each segment
    var embeddings = new List<(SpeechSegment Segment, float[] Embedding)>();
    foreach (var segment in segments)
    {
        try
        {
            var embedding = await ExtractSegmentEmbeddingAsync(audioPath, segment, ct);
            embeddings.Add((segment, embedding));
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to extract embedding for segment {Start}-{End}",
                segment.StartSeconds, segment.EndSeconds);
        }
    }

    // Step 3: Cluster embeddings to identify speakers
    var speakerClusters = ClusterSpeakers(embeddings);
    _logger.LogInformation("Identified {Count} speakers", speakerClusters.Keys.Count);

    // Step 4: Create speaker turns
    var turns = new List<SpeakerTurn>();
    foreach (var (segment, embedding) in embeddings)
    {
        var speakerId = FindSpeakerForEmbedding(embedding, speakerClusters);
        turns.Add(new SpeakerTurn
        {
            SpeakerId = speakerId,
            StartSeconds = segment.StartSeconds,
            EndSeconds = segment.EndSeconds,
            Confidence = 1.0  // TODO: Calculate based on cluster distance
        });
    }

    // Step 5: Merge consecutive turns from same speaker
    var mergedTurns = MergeConsecutiveTurns(turns);

    return new DiarizationResult
    {
        Turns = mergedTurns,
        SpeakerCount = speakerClusters.Keys.Count
    };
}

// Simple VAD using energy-based speech detection
private List<SpeechSegment> DetectSpeechSegments(string audioPath)
{
    using var reader = new AudioFileReader(audioPath);
    ISampleProvider sampleProvider = reader.WaveFormat.Channels == 1
        ? reader
        : new StereoToMonoSampleProvider(reader) { LeftVolume = 0.5f, RightVolume = 0.5f };

    var sampleRate = sampleProvider.WaveFormat.SampleRate;
    var windowSize = sampleRate / 10; // 100ms windows
    var buffer = new float[windowSize];

    var segments = new List<SpeechSegment>();
    SpeechSegment? currentSegment = null;

    double timeSeconds = 0;
    int samplesRead;

    while ((samplesRead = sampleProvider.Read(buffer, 0, buffer.Length)) > 0)
    {
        // Calculate RMS energy for this window
        double rms = Math.Sqrt(buffer.Take(samplesRead).Sum(s => s * s) / samplesRead);

        // Speech detection threshold (simple baseline - fragile across gain levels)
        // Production: use relative threshold (noise floor / percentile) or per-file calibration
        bool isSpeech = rms > 0.02;  // Fixed threshold for demonstration

        if (isSpeech)
        {
            if (currentSegment == null)
            {
                // Start new segment
                currentSegment = new SpeechSegment
                {
                    Star
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.