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
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. Кожна з них працює по-різному
Все йде так само Снижена модель RAG: виокремлює сигнали, як тільки МSK1 зберігає дані МSK2 синтезує з обмеженим входом LLM .
Багато демонстрацій – МСК0, перші ланцюги з аналізом звуку роблять те ж саме. МСК1, вони ставляться до LLM так, наче вони були інженерами акустики
Вони переносять хвилі в моделі. МСК0. Вони просять їх: МСК1, пізнати якість мовлення, ", " МСК3, визначити співпромовців, ,", і сподіваються, що модель зможе вивести структурні властивості з того, що є фундаментальним шаблоном. \ "" МSK5, система комбінацій, МSK6 "". Вона працює достатньо добре для демонстрації. МSK7, а потім галюциналізує ім 'я співперемовників. МСК8, вигадує музичні жанри. М СК9, або напевно неправильно визначає акценти.
AudioSummarizer об 'єднує дві комплементарні моделі:
Замість того, щоб 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 впевненість: МSK2SPEAKER_00)vprint:a3f9c2e1)Модель ідентичності: SPEAKER_00 є локальним для одного звукового файлу. VoiceprintId є стабільним в усіх файлах, але анонімним. Ми ніколи не мапуємо людських іменM SK1
Судебні обов 'язки: Кожен сигнал включає походження (від якої хвилі вона взялася впевненість (визначний рівень МSK1 і версування (модельM SK1витривалі версії). Це дає можливість репродуцуванняMSC3 той самий вхід МSK4 той же конфигурація → той ж сигналовий ledgerMNK6
Результат::
Це будується безпосередньо на Стриманий візерунок невизначеності і архітектуру на основі хвилі - з ImageSummarizer.
Погляньмо на сутність ЛЛМ повинні розбиратися над акустичними фактами.
Цей artykuł стосується:
Подібні статті:
Аудіо-аналіз зазнає невдачі у передбачуваний спосіб:
Традиційний підхід: "Run Whisper для транскрипції, pyannote для диарізаціїM SK3 надіслати до LLM для резюмеMSC4
Проблема: Це або витікає з PII ( назви głośnikів МSK2 коштує надто багато МSK3cloud APIs мSK4 або потребує Python runtime (pyannote , DIART мск7
Рішення: Збудувати чисту трубку, що характеризує структуру і акустику звуку, базуючись на МSK1NET хвилі МSK2, не висловлюючи культурних тверджень
AudioSummarizer впроваджує Снижена модель RAG для аудіофайлів, відповідно до трьох основних принципівМSK1
Детерміністичні сигнали, отримані один раз—не LLMM SK1розроблені підсумкиМSK2
Ось приклад сигналу для подкасу:
{
"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
Замість того, щоб зберігати тільки "шлункиM SK1 AudioSummarizer stores:
Доказ є перевіряним: Пользовачі можуть відтворювати зразки звуку зі спикера , читати транскрипції МSK2 перевіряти повороти диарізації M SK3 не просто довіряти резюме LLM
У часі пошуку, LLM ніколи бачить сирий аудіо. ЗамістьM SK1
Фильтрування детерміністично ( базу даних, де clauseM SK1
WHERE audio.content_type = 'speech'
AND speaker.count >= 2
AND audio.rms_db > -25.0
AND transcription.confidence > 0.8
Гібридний пошук Результати:
Синтезувати з сигналів (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
Дозвольте "'" відслідковувати "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..."
}
Що це дає:
speaker.sample.speaker_00 кліп (всерединіM SK1файл)Сигнальний рахунок відповідає на нудні питання: МСК0, але МСК1 , Непогані питання негайно .
Це питання, які ви зазвичай знаходите. 30 хвилини в археології Аудачі
Сигнал у ledger дає вам їх в секундах.
Базова лінія. Криптова особистість та метадані файлів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 МСК8audio.format МSK0 mp3, wav, flacM SK3 і т.д.Чому детермінативний?
Витягування структурних акустичних властивостей за допомогою 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)
Чому це важливо:
Грубое геристичне класифікування використовуючи нуль- швидкість перетинів та спектральний 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)
Для обмеження використання lucidRAG': диарізація спикера без піаноту , DIART МSK2 або будь-яких залежностей від Пайтона
Традиційний підхід:
pyannote.audio (Python) → ONNX export (experimental) → C# wrapper (fragile)
Проблеми:
Рішення: Втілити диарізацію у чистому .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
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
© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.