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
Tilanne: AudioSummarizerM SK1Kore on parhaillaan kehityksessä osana lucidRAG, monimuotoisen RAG:n tuleva mostlylucid-tuote -modal RAG. täytäntöönpano on saatu päätökseen ja se on käynnissä
Lähde: github.com/scottgalM SK2lucidrag (branch
v2)
Missä tämä sopii: AudioSummarizer on osa lucidRAG family of Reduced RAG implementations. Jokainen käsittelee erilaista liikennemuotoa:
Kaikki seuraavat samaa RAG-muodon vähentäminen: ekstrahoidaan signaalit kerranM SK1 tallennetaan todisteita, synteesoidaan rajattuun LLM-tulokseen
Monet demot, ensimmäiset audioanalyyseja koskevat putket, tekevät saman virheen, ne kohtelevat LLM:itä kuin akustisia insinöörejä.
He syövät aaltomuotoja malleihin., – he pyytävät, että " – havaitsee puheenlaadun, " – tai " – tunnistaa puhujat ja toivoo, että malli voi luulla rakenteellisista ominaisuuksista, jotka perustuvat perustavanlaatuiseen malliin.
AudioSummarizer yhdistää kaksi täydentävää mallia:
Sen sijaan, että välittäisiin raaka-audioa LLM:lle kysymyksen aikana, se vähentää jokaiseen audio-asiakirjaan merkkikirja (deterministiset signaalit, kuten RMS:n voimakkuus otetut todisteet (transcriptsM SK1 puhujaesimerkkikuvien clips , diarization turnsSSK3 Tämä ledger on säilynyt kerran, indeksoitu jäljitettäviksi
Sillä kyselyaika, LLM:n avulla synteesoidaan vastauksia pyytämällä ja perustella esiintyviä signaaleja, — ei toistamalla - analysoimalla audioa,. raskaskäyttöön liittyvää (signaalikäsittelyä,
Muodon kokoonpano: Reduced RAG handles mitä varastoida ja palauttaa. Rajoitetut hämäräisyydet miten to extract signals reliably. Yhdessä ne mahdollistavat oikeudellisen audiokuvauksen rajattuun LLM-kustannuksiin
Lisätietoja Reduced RAG -mallista: Reduced RAG: Signal-Driven Document Understanding
audio.content_type = "speech",luottamusSPEAKER_00)vprint:a3f9c2e1)Identiteettimalli: SPEAKER_00 on paikallista yhteen audio-asiakirjaan. VoiceprintId on vakaa kaikissa asiakirjoissa, mutta nimettömiä. Emme koskaan kartoittaisi ihmisten nimiä
Rikosoikeudelliset vaatimukset: Jokainen signaali sisältää alkuperä (mikä aalto on tuottanut sen luottamus (varmuudenaste versionointi (malliM SK1kynnysversiot ). Tämä mahdollistaa jäljentämisen: samat mahdollisuudet inputin kanssa SSK4 sama konfigurointi → sama signaali ledger
Tulos on:
Tämä perustuu suoraan Rajoitettu epäselvyysmalli ja aalto---pohjainen arkkitehtuuri ImageSummarizer.
Keskeinen käsitys: LLM:n olisi perusteltava akustisia tosiseikkoja, ei laskettava niitäM SK1
Tämä artikla kattaa:
Yhteenkuuluvat artiklat:
Audioanalyysi epäonnistuu ennustettavissa tapauksissa:
Perinteinen lähestymistapa: M SK1Run Whisper -äänestys käännökselle , pyanot diarrointia varten, lähetetään LLM:lle tiivistelmän saamiseksi
Ongelma: Tämä joko vuotaa PII:tä (puhdistajien nimet), maksaa liian paljon (cloud APIsM SK4 tai vaatii Pythonin runtimea |(pyannote | , | DIART ‐).
Ratkaisu: Rakennetaan puhdasta .NET-aaltoa M SK2pohjaista kaasuputkea, joka luonnehtii audioa rakenteellisesti ja akustillisesti
AudioSummarizer toteuttaa RAG-muodon vähentäminen audio-asiakirjoihin, sen kolmen perusperiaatteen mukaisesti :
Yksinkertaiset deterministit signaalit—ei LLMM SK1käsiteltyjä yhteenvedonottoja:
Esimerkki podcastin signaalikokonaisuudesta:
{
"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"
}
Nämä signaalit ovat deterministinen (sama audio → samat arvotM SK2 indeksoitava (valmiuttaa filtroinninM SK1sortoidaan tietokannassa ), ja laskettavissa ilman LLM:itä (pure signal processing
Sen sijaan, että varastoidaan vain "kuormituksia ," AudioSummarizer kauppiaitaM SK2
Todisteet ovat tilintarkastuskelpoisia: Käyttäjät voivat laatia äänitorvikuvauksia, lukea käännöksetM SK2 tarkastaa riisumien virtoja — ei vain luottaa LLM:n yhteenvetoon
Kysymyksen aikana, LLM koskaan näkee raaka-audion. Sen sijaan:
Filtroiminen deterministisesti (tietokanta Missä lauseke
WHERE audio.content_type = 'speech'
AND speaker.count >= 2
AND audio.rms_db > -25.0
AND transcription.confidence > 0.8
Hybridinen haku (BM25 + vektori
Synteesio signaaleista (LLM katsoo rakenteellisen todisteiden paketin
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 saa 5 rakenteelliset todisteiden paketit sen sijaan, että 500 raaka-ääneisiä metatietoja. Kontext- ikkunan vähentäminenM SK1 ~50× pienempi.
Kustannusvaikutukset (palkkaisia LLM-API:ita käyttäen ):Protsessointi \100audiofilet siirtyvät \ ~$5 \ ( \ toistaan - \ analysoimalla audiota jokaisessa kysymyksessä
Huomautus: lucidRAG on paikallistaM SK1ensimmäinen (Default-OllamaMSC3 nollakustannuksista ). Maksuttomat API:t | ( | Claude |, | GPT ‐-4) | ovat valinnaisiaMST8 | Kustannusten arvioinnissa otetaan huomioon maksullinen API:n käyttö |
Tämä on keskeinen vähennetty RAG-tietämys päätetään etukäteen, mikä on tärkeää (signaalit varastoida se määrätietoisesti (todisteet liittää LLM vain synteesiin (kysymys-aikaM SK2
Järjestelmä toimii aaltoina ensisijaisessa järjestyksessä (ylemmän luokan = toimii ensimmäisenäM 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
Huomio:TuntemusM SK1Emotion detection intentionally excluded —culturally loaded and not part of forensic characterization
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
Tämä säilyttää tuttun Voima → signaali → valinnainen LLM loop, mutta ankkuroi sen deterministisiin tosiseikkoihin
Let's trace a M SK1minute podcast episode through the pipeline
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)
Euroopan unionin Signaalikirja on se, mikä säilyy tietokantaan—täydellinen valikoima signaaleja, todisteita osoittavat indikaattoritMSC2 ja sisällytyksetM SK3 Siihen 'miltä tämä podcast näyttää
Luettelo merkinnöistä (luonnos
{
"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..."
}
Mitä tämä mahdollistaa:
speaker.sample.speaker_00 lippu (yhdessäM SK1tietokanta )Tunnuskirjassa vastataan viivytteleviin kysymyksiin.
Nämä ovat kysymykset, joita tavallisesti löydetään 30 minuuteissa Audacityn arkeologiasta
Luettelo antaa ne teille sekunnissa.
Peruslinja. Kriptografinen identiteetti ja ឯកសារ metadata.
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;
}
}
Esitetyt keskeiset signaalit:
audio.hash.sha256 - Cryptografinen identiteettiaudio.duration_seconds - Pituus deterministinenM SK2audio.sample_rate -audio.channels -audio.format -Miksi deterministinen?
Laaditaan rakenteellisia akustisia ominaisuuksia NAudion ja FftSharpin avulla
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)
};
}
}
Esimerkkitulos:
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)
Miksi tämä on tärkeää:
Heuristinen luokittelu käyttämällä nollaa-siirron nopeutta ja taajuusvirtaa reittitarkoituksiinM SK1
Huomio: Nämä ääntelymenetelmät ovat luonteeltaan genre-läheisiä /perustan mukaan riippuvaisia — eivät ole luotettavasti täsmällisiä kaikissa sisältötyypeissä M SK3 Sovelletaan vain aaltosuuntauspäätöksiin
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;
}
}
Esimerkki rautatieliikenteestä:
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':n käyttöönottorajoituksia varten: puhujan diarointi ilman pyannoittia , DIARTM SK2 tai muut Pythonin riippuvuussuhteet.
Perinteinen lähestymistapa:
pyannote.audio (Python) → ONNX export (experimental) → C# wrapper (fragile)
Ongelmat:
ratkaisu: Direktiivin täytäntöönpano puhtaassa .NET-järjestelmässä olemassa olevien äänen sisällyttämistä koskevien mallien avulla
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.