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
AudioSummarizer: Begränsad Fuzzy rättsmedicinsk ljuddefiniering
Monday, 12 January 2026
Status: AudioSummarizer lucidRAG, ett framväxande merpartenlucid produkt för multi-modal RAG . Implementeringen är färdig och igångM SK3 artikeln dokumenterar arkitekturen och designen . CLI-integrationen kommer finnas tillgänglig i en framväxande lucidRAG-ut release
källa: github.com/scottgalM SK2lucidrag (branchM SK1
v2)
Där det passar: AudioSummarizer är en lucidRAG familj av Reduced RAG implementeringar. Var och en hanterar en annan modalitet:
- DocSummarizer - Dokument väsenextraktion
- ImageSummarizer - Bilder 22- vågvisual intelligens
- DataSummarizer - Data | | skemasuppfattning | , | profilering | МSK3
- AudioSummarizer (den här artikeln )
Alla följer samma princip Reduzerad RAG-mönster: extraherar signaler en gång , sparar bevis , syntetiseras med begränsad LLM-input
Många demos, -, gör samma misstag med de första ljudanalysrören. De behandlar LLM som om de var akustiska ingenjörer.
De för in vågformer i modeller.,, ber dem att """, "" Detektor av talkvalitén, " "", eller ", identifiera högtalare och hoppas att modellen kan dra slutsatser om strukturella egenskaper från vad som egentligen är en mönster.
AudioSummarizer kombinerar två komplementära mönster:
- Reduzerad RAG för att få fram - extrahera signaler en gång , lagra bevis M SK2 ställa frågor mot fakta
- Begränsad otydighet för orkestrering - våg, - baserad pipeline, M SK2 deterministisk substrat begränsar sannolikhetsmodeller
Istället för att mata råa ljud till en LLM vid frågatid, så minskar varenda audiofile till en ledger (deterministiska signaler som RMS-ljuddhet , spektrala egenskaper extraherat bevis ( transkripter , högtalare provspel, clipper, , diariseringsströmmar, ). ledaren är kvar en gång,
På: frågatid, en LLM syntetiserar reaktioner genom att plocka upp och resonera över dominerande signaler — inte genom att göra om det
Patternskomposition: Reduzerade RAG-handlar vad för att lagra och få tillbaka. Begränsade Fuzziness-handlar hur att extrahera signaler på ett noggrant sätt. De gör det möjligt för rättsmedicinsk avbildning med begränsad LLM-kostnaden
Lär dig mer om Reduced RAG-mönster: Reduced RAG: Signal-Driven Document Understanding
Nyckeltermik
- Signaler: Tiktade fakta med självförtroende poäng (eM SK2g.,
audio.content_type = "speech", självförtroende - Evidens: Lyssliga artefakter ( transkriptioner , högtalare provspelar
- Ledger: Den fortsatta bunten av alla signaler , bevisspetsar, , och inbäddar som extraheras från en audiofile
- SpeakerID: Lokalidentifisator inuti en enda file /diskriminering Run
SPEAKER_00) - RöstbildID: Cross
vprint:a3f9c2e1) - Person: Uppenbarligen inte givits ut
Identitetsmodell: SPEAKER_00 är lokal till en audiofile. VoiceprintId är stabil över filer men anonymt. Vi kartlägger aldrig människors namn
rättsmedicinsk krav: Varje signal inkluderar ursprung ( vilken våg den producerade ), självförtroende (säkerhetsgrad versionering (modellM SK1 tröskelversioner ). Det här gör att reproducerbarheten är möjliga: samma inmatning | + samma konfiguration |→ | samma ledger för signaler
Resultatet är:
- Fast, integritet-bevarande av rättsmedicinsk ljuddefiniering
- Lyssningsdiskriminering utan Pythons beroenden (pure .NET)
- Anonyma talare likvärdighetsdetektion ( inga PII , inga namn
- Rödlingsintegreringar för "finn liknande högtalareM SK1 sökningar
- Allt utan att skicka ljud till moln-APIs under ingestion
Detta bygger direkt på Begränsad otydighetsmönster och architecturen baserat på vågen - från ImageSummarizer.
Kerninsikten: LLM ska resonera över akustiska fakta, inte beräkna dem.
Detta artikel omfattar:
- Hur AudioSummarizer implementerar Reduced RAG-mönster
- Signalextraktion: deterministiska akustiska fakta M SK1 inte LLM sammanfattningar)
- Evidenslagning: högtalare prover, transkriptionerM SK2 diariseringsvirr
- Rent . NET högtalare diarisering ( ingen Python
- Query-tidssynthese: filtersignaler M SK2 hämta bevis → LLM syntetiserar
Related articles:
- Reduzerad RAG - Den centrala mönstret som den implementerar
- Begränsad otydighetsmönster - Grundmuten
- Reduzerade RAG implementeringar:
- DocSummarizer - Dokument RAG med enhetens extraktion och kunskapsgrafer
- DataSummarizer - Dataprofilering och schemasuppfattning RAG
- ImageSummarizer - Image RAG med 22- vågvisuell intelligens
- AudioSummarizer (den här artikeln ) - Audiomedicinsk karakterisering med högtalare diarisering
Problemet : Audioanalys är svår ( och kulturellt laddad
Audioanalys misslyckas på förutsägbara sätt:
- Kulturella påståenden: " Det här är jazz ♫ ♫ " | ( ♫ säger vem ♫
- Berättande av talare: "Sprekern är John Smith" (avgrepp på privatliv
- Musikidentifisering"" Sandstorm by Darude "" (Sandstorm av Darude) ""
- Accent gissning: "Speaker has British accent
- Pythons beroende: De flesta diariseringsverktyg kräver pyannot.
Ett traditionellt tillvägagångssätt : "Run Whisper för transkribering, pyannot för diariseringM SK3 skicka till LLM för sammanfattningen
Problemet: Det här antingen läcker PII ( högtalare namn | ), | kostar för mycket ♫ ( | moln-APIs | МSK4 | eller kräver Python-runtime | ( | pyannot | , | DIART | ).
Lösningen: Att bygga en ren pipeline som baserar sig på ., NET-vågor, -, som karakteriserar ljud strukturellt och akustiskt,—, utan att göra kulturella antaganden.
Reduced RAG Audio Framework
AudioSummarizer implementerar Reduzerad RAG-mönster för ljuddater , som följer dess tre grundprinciper:
1. Signalextraktion Ingestionsfasen
Deterministiska signaler extraherade en gång
- Tydliga: livslängd , tidstämpningar M SK2 segmentgränser
- Akustik: RMS högljuddhet , spektrala centroider M SK2 dynamiska intervall
- Identitet: SHA
- Jakt: transkriptionsförtroende
- Speker: röstavtryckte IDs ( anonyma grejer
- Kategoriserad: innehållstyp
Ett exempel på signaluppsättning för en podcast:
{
"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"
}
Dessa signaler är deterministisk (samma ljud → samma värdenM SK2 indexbar ( kan filtrera / sortera i databasen M SK2 och beräknat utan LLMs ( ren signalbehandling + ONNX-modeller
2. Evidens lagring Störda enheter
Istället för att bara lagra "kokare," AudioSummarizer affärer :
- Signaler: Strukturerade fält (JSONM SK2 för deterministisk filtering
- Embeddings: Sprachintegreringar (512-dim ECAPAM SK2TDNN) för högtalare likhet
- Privacy note: Embeder är inte praktiskt omvändbara i det här systemet, men hanterar dem som känsliga data.
- Evidensartefakter:
- Den fulla transkriptionens text (sökningbar)
- Sampler från högtalare (Base64 WAVM SK2 2-sekundliga klipp för verifiering
- Diariserings svänger (JSON med högtalare_idM SK2 start / end timestamps
- Speglar: File hash + bevisidentityder för kontrollbart ursprung
Evidens är kontrollbar: Användare kan spela upp högtalare prover , läsa transkriptioner , inspektera diariseringsströmmar M SK3 inte bara lita på en LLM sammanfattning
3. Fråga- Tidssynthes Limiterad LLM Input
Vid frågatid, LLM aldrig ser rå ljud. istället:
Filtrera deterministiskt (databas Var clausen
WHERE audio.content_type = 'speech' AND speaker.count >= 2 AND audio.rms_db > -25.0 AND transcription.confidence > 0.8Hybridsökning Vektorn: (BMM SK1
- BM25 på transkriptionstext (ord som matchar ordet
- Vektors likhet på ljudintegreringar (speakerliknande)
- Kom tillbaka upp till 5 ljuddater
Synthesera från signaler (LLM ser strukturerad bevispaket
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 får 5 strukturerade bevispaket istället för 500 bitar av råa ljudmetadata. Reduktion av kontextutrymmet
Kustinverkan ( om man använder betalade LLM APIs): Bearbetar ♫ 100 ♫ ljudfiler går från ♫~$5 ♫ ♫ Anteckning: lucidRAG är lokalt-first M SK2Ollama standardmässigt , noll kostnadMSC4 Belönade APIs (ClaudeMska6 GPTMske7 är optionellaMskosten antar att betalade API-användbarhet är antaglig
Detta är grunden till Reduced RAG insight: bestämma vad som är viktigt (signaler förvara det deterministiskt ( bevis omfatta LLM bara för syntektion (fråga-tidM SK2
Den begränsade fuzziness-ljudsröret
Systemet sätter igång vågor i prioritära ordning (högare värden = sätter upp först
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
Beteckning: känslor / känslodetektor avsiktligt utesluten M SK2 kulturellt laddad och inte en del av rättsmedicinsk karakterisering
Arkitektur
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
Detta bevarar den välkända Wella → Signal → Optionell LLM loop, men ancorar den i deterministiska fakta
- Akustikprofilering är deterministiskt ( samma ingång = samma utgång
- LLM kör på bevis, ,, inte råa ljud
Verklig-World Example: Att bearbeta en Podcast-episode
Låt ' spåra ett 15-minuters podcast-episode genom kanalen
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)
Den Ledger är vad som förblir kvar i databasen.
Ledger (avl.
{
"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..."
}
Vad detta gör möjligt:
- sök efter "Tech Insights diskuterar molninfrastrukturen " → hittar den här episoden via transkription
- Fråga "Finda ljud med röstavtryck vprint:aM SK2fMSC3cMska4eM Ska5 | Findar alla episoder med samma högtalare | Mska7 | Query
- Fråga " Vad' är den dynamiska sträckan av våra podcast-episoderM SK2 → sammanlagda akustiska signaler
- Filter "Finda lågt-kvaliténga inspelningarM SK2 → RMS
- Verifiera "Did högtalare identifisering verk?" → spela
speaker.sample.speaker_00klipp (intilldatet-lêer )
Varför signaler är viktiga ( Även utan en LLM
Ett ledger svarar på tråkiga frågor omedelbart
- Är det ljudspråket? Musik? Tystnad?
- Hur många högtalare upptäcks?
- Vad är signalen?
- Finns det klippande artefakter eller förvrängningar?
- Vad är spektrala centroidern?
- Vad är den dynamiska intervallen?
Det här är de frågor du vanligtvis hittar 30 minuter i Audacity-arkeologin
Ledgern ger dem i sekunder.
The Wave Pipeline: Från identitet till intelligens
Wella 1: Identitet (Deterministisk substans
Basislinjen. Kriptografisk identitet och file-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;
}
}
Emiterade nyckelsignaler:
audio.hash.sha256- Kriptografisk identitetaudio.duration_seconds-Längd (deterministiskaudio.sample_rate- 44100, ♫ ♫ 48000, ♫ och så vidare ♫audio.channels-audio.format- mp 3, wav, flacM SK3 etcMSC4
Varför deterministiskt?
- Samma file → samma hash → samma identitet
- Inga slumpmässiga prover , inga temperaturparameter
- Förtroende =
Wave 2: Akustisk profil (Signalprocessing)
Extrahera strukturella akustiska egenskaper med hjälp av NAudio och 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)
};
}
}
exempel utgång:
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)
Varför är detta viktigt:
- RMS högljud indikerar mästerskapskvalitet
- Clipping-förhållandet detekterar inspelning av artefakter
- Spectralla centroider skiljer tal från musik (2-4kHz
- Alla deterministiska— samma ljud producerar samma värden
Wella 3: Inhoudsklassificering (Speech vs Music)
grov heuristisk klassificering med hjälp av noll-crossing rate and spectral flux for routing purposes
Beteckning: Dessa heuristiker är genrer / kontextberoende — inte pålitligt korrekta över alla innehållskategorier M SK3 Används endast för vågriktningsbeslut MSC4 e Mska5 g M Skaka dinarisering för musiken Mske7 inte som grunden till sanningen
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;
}
}
Ett exempel på routing:
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)
Reina .NET högtalare diarisering | ( | Ingen Python | МSK2
För användargränserna för lucidRAG': diarisering av högtalare utan pyannot , DIART
Problemet med Python-Based Diarization
Traditionell tillvägagångssätt
pyannote.audio (Python) → ONNX export (experimental) → C# wrapper (fragile)
Problemet:
- pyannote 3.1+ tog bort support för ONNX ( flyttade till ren PyTorch
- Python körtid som krävs (implementerings mardröm
- HTTP-wrapper lägger till latens och komplexitet
- Inget avgränsat stöd
Lösningen: Implementera dinarisering i rent .NET med hjälp av existerande röstbedämningsmodeller
Den ren .NET-algoritmen
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
Implementering
public class SpeakerDiarizationService
{
private readonly ILogger<SpeakerDiarizationService> _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
{
StartSeconds = timeSeconds
};
}
}
else
{
if (currentSegment != null)
{
// End current segment
currentSegment.EndSeconds = timeSeconds;
// Only keep segments longer than minimum duration
if (currentSegment.Duration >= _config.VoiceEmbedding.MinDurationSeconds)
{
segments.Add(currentSegment);
}
currentSegment = null;
}
}
timeSeconds += (double)samplesRead / sampleRate;
}
// Close final segment if still open
if (currentSegment != null)
{
currentSegment.EndSeconds = timeSeconds;
if (currentSegment.Duration >= _config.VoiceEmbedding.MinDurationSeconds)
{
segments.Add(currentSegment);
}
}
return segments;
}
// Agglomerative clustering of speaker embeddings
private Dictionary<string, List<float[]>> ClusterSpeakers(
List<(SpeechSegment Segment, float[] Embedding)> embeddings)
{
const double SimilarityThreshold = 0.75; // Cosine similarity threshold
var clusters = new Dictionary<string, List<float[]>>();
int nextSpeakerId = 0;
foreach (var (segment, embedding) in embeddings)
{
string? assignedCluster = null;
double maxSimilarity = 0;
// Find best matching cluster
foreach (var (clusterId, clusterEmbeddings) in clusters)
{
// Calculate average similarity to cluster
var avgSimilarity = clusterEmbeddings
.Select(e => _embeddingService.CalculateSimilarity(embedding, e))
.Average();
if (avgSimilarity > maxSimilarity)
{
maxSimilarity = avgSimilarity;
assignedCluster = clusterId;
}
}
// Assign to cluster or create new one
if (maxSimilarity >= SimilarityThreshold && assignedCluster != null)
{
clusters[assignedCluster].Add(embedding);
}
else
{
var newClusterId = $"SPEAKER_{nextSpeakerId:D2}";
clusters[newClusterId] = new List<float[]> { embedding };
nextSpeakerId++;
}
}
return clusters;
}
}
exempel utgång:
Input: podcast.mp3 (15 minutes, 2 speakers)
Processing:
Step 1 (VAD): Detected 47 speech segments (0.5s to 45s each)
Step 2 (Embeddings): Extracted 47 × 512-dim ECAPA-TDNN embeddings
Step 3 (Clustering): 47 embeddings → 2 clusters (similarity threshold 0.75)
Step 4 (Turns): Created 47 speaker turns
Step 5 (Merge): 47 turns → 23 merged turns (consecutive same-speaker segments)
Output:
SpeakerCount: 2
Turns: 23
- SPEAKER_00: 0.0s - 5.2s (confidence: 1.0)
- SPEAKER_01: 5.2s - 12.8s (confidence: 1.0)
- SPEAKER_00: 12.8s - 18.3s (confidence: 1.0)
- ...
Signals emitted:
speaker.count = 2
speaker.classification = "two_speakers"
speaker.turn_count = 23
speaker.diarization_method = "agglomerative_clustering"
speaker.avg_turn_duration = 39.1 seconds
speaker.diarization_confidence = 1.0
Varför det fungerar:
- VAD är deterministiskt.
- ECAPA-TDNN embeddings är deterministiska (ONNX-modellen, inga prover
- Kollerings tröskel kan konfigureras (0.75 kosinusliknande )
- No Python runtime required (pure .NET)
- Går offline ( inga API-sändningar)
Voice Embeddings: AnonymSpeaker likhet
ECAPA-TDNN ONNX modellextrakter 512-dimensionella högtalare inbäddar för likhetsdetektion
Privacy-modellen
Vad vi gör
- ❌ Infer högtalare namn
- ❌ Beräkna befolkningsökningen | ( | ålder | , | kön | МSK3 | accent
- ❌ Gör kulturella påståenden
- ❌ Lagra råa ljud
Vad vi gör
- ✓ Generera anonyma röstavtryckte IDs (
vprint:abc123...) - ✓ Beräkna högtalare likhet (kosine avståndM SK2
- ✓ Aktivera " hitta liknande högtalare
- ✓ Bevara integritet ( bäddar som inte är påtagligt omvändbara M SK2 men hanteras som känsliga data
Implementering
public class VoiceEmbeddingService
{
private readonly ILogger<VoiceEmbeddingService> _logger;
private readonly VoiceEmbeddingModelDownloader _modelDownloader;
private readonly string _modelPath;
private InferenceSession? _session;
public async Task<VoiceEmbedding> ExtractEmbeddingAsync(
string audioPath,
CancellationToken ct = default)
{
await EnsureModelLoadedAsync(ct);
// Extract Mel spectrogram features (80 Mel bands)
var features = await Task.Run(() => ExtractMelSpectrogram(audioPath), ct);
// Flatten 2D array to 1D for DenseTensor
int numMelBands = features.GetLength(0);
int numFrames = features.GetLength(1);
var flatFeatures = new float[numMelBands * numFrames];
for (int i = 0; i < numMelBands; i++)
{
for (int j = 0; j < numFrames; j++)
{
flatFeatures[i * numFrames + j] = features[i, j];
}
}
// Run ONNX inference (ECAPA-TDNN model)
var inputTensor = new DenseTensor<float>(flatFeatures, new[] { 1, numMelBands, numFrames });
var inputs = new List<NamedOnnxValue>
{
NamedOnnxValue.CreateFromTensor("input", inputTensor)
};
using var results = _session!.Run(inputs);
var embeddingTensor = results.First().AsEnumerable<float>().ToArray();
// Normalize embedding (L2 normalization)
var embedding = NormalizeEmbedding(embeddingTensor);
// Generate anonymous voiceprint ID (SHA-256 hash of embedding)
var voiceprintId = GenerateVoiceprintId(embedding);
return new VoiceEmbedding
{
Vector = embedding,
VoiceprintId = voiceprintId,
Dimension = embedding.Length,
Model = "ecapa-tdnn"
};
}
// Calculate cosine similarity between two voice embeddings
public virtual double CalculateSimilarity(float[] embedding1, float[] embedding2)
{
if (embedding1.Length != embedding2.Length)
throw new ArgumentException("Embeddings must have same dimension");
double dotProduct = 0;
double norm1 = 0;
double norm2 = 0;
for (int i = 0; i < embedding1.Length; i++)
{
dotProduct += embedding1[i] * embedding2[i];
norm1 += embedding1[i] * embedding1[i];
norm2 += embedding2[i] * embedding2[i];
}
if (norm1 == 0 || norm2 == 0)
return 0;
return dotProduct / (Math.Sqrt(norm1) * Math.Sqrt(norm2));
}
private string GenerateVoiceprintId(float[] embedding)
{
// Generate anonymous ID from embedding hash
// This ensures same voice → same ID, but no PII
//
// Stability notes:
// - Deterministic on same runtime/endianness/float serialization
// - Voiceprint IDs are stable for a given model + preprocessing version
// - Treat as versioned identifiers - regenerate if model changes
// - Cross-platform determinism depends on float serialization consistency
using var sha256 = System.Security.Cryptography.SHA256.Create();
var bytes = new byte[embedding.Length * sizeof(float)];
Buffer.BlockCopy(embedding, 0, bytes, 0, bytes.Length);
var hash = sha256.ComputeHash(bytes);
return "vprint:" + Convert.ToHexString(hash).Substring(0, 16).ToLower();
}
}
Till exempel användande:
// Extract embedding from two audio files
var embedding1 = await voiceEmbeddingService.ExtractEmbeddingAsync("speaker1.wav");
var embedding2 = await voiceEmbeddingService.ExtractEmbeddingAsync("speaker2.wav");
// Calculate similarity
var similarity = voiceEmbeddingService.CalculateSimilarity(
embedding1.Vector,
embedding2.Vector
);
// Output:
// embedding1.VoiceprintId = "vprint:a3f9c2e1d4b8"
// embedding2.VoiceprintId = "vprint:7e2d8f1a9c3b"
// similarity = 0.91 (high similarity, likely same speaker)
Anonyma talare sökningar:
// Find audio files with similar speakers
var queryEmbedding = await voiceEmbeddingService.ExtractEmbeddingAsync("query.wav");
var similarSpeakers = audioDatabase
.Where(audio => audio.VoiceEmbedding != null)
.Select(audio => new
{
Audio = audio,
Similarity = voiceEmbeddingService.CalculateSimilarity(
queryEmbedding.Vector,
audio.VoiceEmbedding
)
})
.Where(x => x.Similarity > 0.75) // Threshold for "similar"
.OrderByDescending(x => x.Similarity)
.Take(10)
.ToList();
// Results:
// 1. podcast_ep12.mp3 (similarity: 0.94) - likely same speaker
// 2. podcast_ep15.mp3 (similarity: 0.89) - likely same speaker
// 3. interview_2024.wav (similarity: 0.76) - possibly same speaker
Varför anonymt?
- Röntgentryckets ID är en hasch av inbyggden (oneM SK1way-funktion
- Embedding är 512 dimensioner | → | kan inte avgöra högtalare
- likhetersvärdet är relativt ( inget absolut " det här är John Smith
- Privatsphäre-bevarande ( inget PII lagd
Audio-segment extraktion : Evidence Playback
Extrahera tid- längd av ljudklipp för hörarens diariseringsprover.
Problemet
Diariseringen säger att "SPEAKER_00 talade från 5.2s till ♫ 12.8sM SK4 men inte ger något sätt att bekräfta denna påstående
Lösningen: Extrahera 2-sekundliga ljudklipp från första vridningen av varje högtalare , koda som basen 64, och lagra dem som signaler
Implementering
public class AudioSegmentExtractor
{
private readonly ILogger<AudioSegmentExtractor> _logger;
// Extract speaker sample clips from diarization result
public virtual async Task<Dictionary<string, string>> ExtractSpeakerSamplesAsync(
string audioPath,
IEnumerable<SpeakerTurn> turns,
double sampleDurationSeconds = 2.0,
CancellationToken ct = default)
{
var samples = new Dictionary<string, string>();
// Group turns by speaker and take first turn for each
var speakerFirstTurns = turns
.GroupBy(t => t.SpeakerId)
.Select(g => g.OrderBy(t => t.StartSeconds).First())
.ToList();
foreach (var turn in speakerFirstTurns)
{
try
{
// Extract sample from start of first turn
var sampleEnd = Math.Min(turn.EndSeconds, turn.StartSeconds + sampleDurationSeconds);
var base64 = await ExtractSegmentAsBase64Async(
audioPath,
turn.StartSeconds,
sampleEnd,
sampleDurationSeconds,
ct);
samples[turn.SpeakerId] = base64;
_logger.LogDebug("Extracted {Duration:F2}s sample for {SpeakerId}",
sampleEnd - turn.StartSeconds, turn.SpeakerId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to extract sample for {SpeakerId}", turn.SpeakerId);
}
}
return samples;
}
// Extract segment and return as Base64-encoded WAV
public async Task<string> ExtractSegmentAsBase64Async(
string audioPath,
double startSeconds,
double endSeconds,
double maxDurationSeconds = 3.0,
CancellationToken ct = default)
{
var wavBytes = await ExtractSegmentAsWavBytesAsync(
audioPath,
startSeconds,
endSeconds,
maxDurationSeconds,
ct);
return Convert.ToBase64String(wavBytes);
}
// Core extraction logic
private async Task ExtractSegmentToStreamAsync(
string audioPath,
double startSeconds,
double endSeconds,
Stream outputStream,
CancellationToken ct)
{
using var reader = new AudioFileReader(audioPath);
// Calculate byte positions
var startPosition = (long)(startSeconds * reader.WaveFormat.AverageBytesPerSecond);
var endPosition = (long)(endSeconds * reader.WaveFormat.AverageBytesPerSecond);
// Align to block boundaries
startPosition -= startPosition % reader.WaveFormat.BlockAlign;
endPosition -= endPosition % reader.WaveFormat.BlockAlign;
// Clamp to file boundaries
startPosition = Math.Max(0, startPosition);
endPosition = Math.Min(reader.Length, endPosition);
var segmentLength = endPosition - startPosition;
// Seek to start position
reader.Position = startPosition;
// Create trimmed reader
// AudioFileReader outputs decoded PCM samples; TrimmedStream creates a windowed view
// WaveFileReader wraps it for format conversion pipeline (works for MP3/FLAC/WAV)
using var trimmedReader = new WaveFileReader(new TrimmedStream(reader, segmentLength));
// Convert to 16-bit PCM WAV for maximum compatibility
using var pcmStream = WaveFormatConversionStream.CreatePcmStream(trimmedReader);
using var resampler = new MediaFoundationResampler(pcmStream, new WaveFormat(16000, 16, 1))
{
ResamplerQuality = 60 // High quality
};
// Write to output as WAV
await Task.Run(() => WaveFileWriter.WriteWavFileToStream(outputStream, resampler), ct);
}
}
Ett exempel på integrering i SpeakerDiarizationWave:
// Extract speaker sample audio clips (2 second clips from first turn of each speaker)
try
{
_logger.LogDebug("Extracting speaker samples from {AudioPath}", audioPath);
var speakerSamples = await _segmentExtractor.ExtractSpeakerSamplesAsync(
audioPath,
result.Turns,
sampleDurationSeconds: 2.0,
cancellationToken);
// Emit speaker samples as signals (Base64-encoded WAV)
foreach (var (speakerId, base64Wav) in speakerSamples)
{
signals.Add(new Signal
{
Name = $"speaker.sample.{speakerId.ToLowerInvariant()}",
Value = base64Wav,
Type = SignalType.Embedding, // Using Embedding type for binary data
Source = Name
});
_logger.LogDebug("Added audio sample for {SpeakerId} ({SizeKB} KB)",
speakerId,
Math.Round(base64Wav.Length / 1024.0, 1));
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to extract speaker samples: {Message}", ex.Message);
}
exempel utgång:
Input: podcast.mp3 (2 speakers detected)
Extraction:
SPEAKER_00: 0.0s - 2.0s (first turn 0.0s - 5.2s)
SPEAKER_01: 5.2s - 7.2s (first turn 5.2s - 12.8s)
Signals emitted:
speaker.sample.speaker_00 = "UklGRiQAAABXQVZFZm10..." (Base64 WAV, 31.2 KB)
speaker.sample.speaker_01 = "UklGRiQAAABXQVZFZm10..." (Base64 WAV, 29.8 KB)
UI integration:
<audio controls>
<source src="data:audio/wav;base64,{base64Wav}" type="audio/wav">
</audio>
Varför är detta viktigt:
- Evidence playback: Användare kan lyssna på varje högtalare
- Verifiering: Bekräftelse av diariseringsgenomständighet
- Debugging: Identifiserar felklassificerade segment
- Privatsphäre: Bara 2-sekundliga prover ♫ ( ♫ inte fulla ljud ♫
Integrering med lucidRAG
AudioSummarizer integrerar genom ett två-steg försämring:
Etappe 1 (AudioSummarizer: Reduktion av ljuddatet till akustisk signering + transkription
- Signaler: RMS
- Evidence : Speaker sample clips, full transcript text
- utgång: Markdown sammanfattning + signal JSON
Etappe 2 (DocSummarizer): Reducera transkriptionen till semantisk handtekening
- Att processa transkriptionen genom DocSummarizer
- Välj ut enheten (människor , organisationerM SK2 ämnen)
- Generera inbäddar för semantiska sökningar
- Att bygga kunskapsgraf länkar
- Utgång: Enhetens graf + inbäddar i kluster
Det här kaskaderingsreduktion möjliggör frågor som:
- "Finda ljud som nämner kvantberäkningar" → DocSummarizer föremål från transkriptionen
- "Hitta podcastar med liknande högtalare" → AudioSummarizer röstinbäddar
- "Verka låga enregistrementer på AI
Dokument Handler Registration
// In LucidRAG.Core, register audio handler
services.AddSingleton<IDocumentHandler, AudioDocumentHandler>();
// AudioDocumentHandler.cs
public class AudioDocumentHandler : IDocumentHandler
{
private readonly AudioWaveOrchestrator _orchestrator;
private readonly IDocumentProcessingService _docProcessingService;
public bool CanHandle(string filePath, string mimeType)
{
var supportedExtensions = new[] { ".mp3", ".wav", ".m4a", ".flac", ".ogg" };
var extension = Path.GetExtension(filePath).ToLowerInvariant();
return supportedExtensions.Contains(extension);
}
public async Task<DocumentContent> ProcessAsync(
string filePath,
ProcessingOptions options,
CancellationToken ct)
{
// STAGE 1: Run audio wave pipeline (acoustic + transcript extraction)
var profile = await _orchestrator.AnalyzeAsync(filePath, ct);
// Extract transcript text
var transcriptText = profile.GetValue<string>("transcription.full_text");
// STAGE 2: Pass transcript through DocSummarizer pipeline (entity extraction)
DocumentContent? transcriptContent = null;
if (!string.IsNullOrEmpty(transcriptText) && options.ExtractEntities)
{
// Create temporary markdown file from transcript
// NOTE: Temp file approach is demo code for CLI integration example.
// Production should use in-memory pipeline API to avoid privacy leaks from temp files.
var tempTranscript = Path.GetTempFileName() + ".md";
await File.WriteAllTextAsync(tempTranscript, transcriptText, ct);
try
{
// Process transcript through DocSummarizer
transcriptContent = await _docProcessingService.ProcessDocumentAsync(
tempTranscript,
new ProcessingOptions
{
ExtractEntities = true,
GenerateEmbeddings = true
},
ct);
}
finally
{
File.Delete(tempTranscript);
}
}
// Convert audio signals to markdown
var audioMarkdown = ConvertToMarkdown(profile);
// Merge audio signals + transcript entities
var finalMarkdown = audioMarkdown;
if (transcriptContent != null)
{
finalMarkdown += "\n\n---\n\n";
finalMarkdown += "## Transcript Analysis (DocSummarizer)\n\n";
finalMarkdown += transcriptContent.MainContent;
}
return new DocumentContent
{
MainContent = finalMarkdown,
Title = Path.GetFileNameWithoutExtension(filePath),
ContentType = "audio/summary",
Entities = transcriptContent?.Entities ?? new List<ExtractedEntity>(),
Embeddings = transcriptContent?.Embeddings ?? Array.Empty<float[]>()
};
}
private string ConvertToMarkdown(DynamicAudioProfile profile)
{
var sb = new StringBuilder();
sb.AppendLine($"# Audio Analysis: {profile.GetValue<string>("identity.filename")}");
sb.AppendLine();
// Acoustic properties
sb.AppendLine("## Acoustic Profile");
sb.AppendLine($"- Duration: {profile.GetValue<double>("audio.duration_seconds"):F1} seconds");
sb.AppendLine($"- Format: {profile.GetValue<string>("audio.format")}");
sb.AppendLine($"- Sample Rate: {profile.GetValue<int>("audio.sample_rate")} Hz");
sb.AppendLine($"- Channels: {profile.GetValue<string>("audio.channel_layout")}");
sb.AppendLine($"- RMS Loudness: {profile.GetValue<double>("audio.rms_db"):F1} dB");
sb.AppendLine($"- Dynamic Range: {profile.GetValue<double>("audio.dynamic_range_db"):F1} dB");
sb.AppendLine();
// Content classification
sb.AppendLine("## Content Classification");
sb.AppendLine($"- Type: {profile.GetValue<string>("audio.content_type")}");
sb.AppendLine();
// Transcription (if available)
var transcriptText = profile.GetValue<string>("transcription.full_text");
if (!string.IsNullOrEmpty(transcriptText))
{
sb.AppendLine("## Transcript");
sb.AppendLine(transcriptText);
sb.AppendLine();
}
// Speaker diarization (if available)
var speakerCount = profile.GetValue<int?>("speaker.count");
if (speakerCount.HasValue && speakerCount > 0)
{
sb.AppendLine("## Speaker Diarization");
sb.AppendLine($"- Speakers Detected: {speakerCount}");
sb.AppendLine($"- Classification: {profile.GetValue<string>("speaker.classification")}");
sb.AppendLine($"- Turn Count: {profile.GetValue<int>("speaker.turn_count")}");
var turnsJson = profile.GetValue<string>("speaker.turns");
if (!string.IsNullOrEmpty(turnsJson))
{
sb.AppendLine();
sb.AppendLine("### Speaker Turns");
sb.AppendLine("```json");
sb.AppendLine(turnsJson);
sb.AppendLine("```");
}
}
return sb.ToString();
}
}
CLI-integration (Kom snart)
När AudioSummarizer är integrerad i lucidRAG CLI (utkomma release), kommer du att kunna processa ljudfiler som den här
# Process audio file (auto-detects audio type)
dotnet run --project src/LucidRAG.Cli -- process podcast.mp3
# Expected output:
# Processing: podcast.mp3
# ├─ IdentityWave (100ms): SHA-256, duration, format
# ├─ AcousticProfileWave (142ms): RMS, spectral features
# ├─ ContentClassifierWave (89ms): Detected speech content
# ├─ TranscriptionWave (12.3s): Whisper.NET transcription
# ├─ SpeakerDiarizationWave (3.8s): 2 speakers, 23 turns
# └─ VoiceEmbeddingWave (178ms): ECAPA-TDNN embeddings
#
# Signals emitted: 47
# Processing time: 16.6s
# Saved to database: doc_abc123
#
# Acoustic Profile:
# Duration: 900.0s (15m 0s)
# Format: mp3, 44100 Hz, stereo
# RMS: -18.2 dB, Dynamic Range: 22.1 dB
# Content: speech (confidence: 0.85)
#
# Speaker Diarization:
# Speakers: 2 (two_speakers)
# Turns: 23
# SPEAKER_00: 52% participation (468s)
# SPEAKER_01: 48% participation (432s)
#
# Transcription: Available (12,847 characters)
# Batch process audio directory
dotnet run --project src/LucidRAG.Cli -- process ./audio/*.mp3 --verbose
# Extract entities from audio (via transcript)
dotnet run --project src/LucidRAG.Cli -- process interview.wav --extract-entities
# Export signals as JSON for inspection
dotnet run --project src/LucidRAG.Cli -- process podcast.mp3 --export-signals signals.json
exempelsignaler.json utgångsmönster:
{
"audio.hash.sha256": "a3f9c2e1d4b8...",
"audio.duration_seconds": 900.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.content_type": "speech",
"speaker.count": 2,
"speaker.classification": "two_speakers",
"speaker.turn_count": 23,
"speaker.diarization_confidence": 1.0,
"speaker.participation": {
"SPEAKER_00": 52.0,
"SPEAKER_01": 48.0
},
"transcription.full_text": "...",
"transcription.confidence": 0.87,
"speaker.sample.speaker_00": "UklGRiQAAABXQVZF..."
}
Belägg för bevis
// Store speaker samples as evidence artifacts
foreach (var (speakerId, base64Wav) in speakerSamples)
{
var evidence = new EvidenceArtifact
{
Id = Guid.NewGuid(),
DocumentId = documentEntity.Id,
Type = EvidenceTypes.SpeakerSample,
Content = base64Wav, // Base64-encoded WAV
Metadata = new SpeakerSampleMetadata
{
SpeakerId = speakerId,
StartSeconds = turn.StartSeconds,
EndSeconds = turn.EndSeconds,
SampleRate = 16000,
Channels = 1,
Format = "wav"
}
};
await evidenceRepository.SaveAsync(evidence);
}
// Store full signal dump as evidence
var signalDump = JsonSerializer.Serialize(profile.GetAllSignals(), new JsonSerializerOptions
{
WriteIndented = true
});
await evidenceRepository.SaveAsync(new EvidenceArtifact
{
Id = Guid.NewGuid(),
DocumentId = documentEntity.Id,
Type = EvidenceTypes.AudioSignals,
Content = signalDump,
Metadata = new AudioSignalsMetadata
{
SignalCount = profile.GetAllSignals().Count(),
WavesExecuted = profile.GetExecutedWaves().ToList(),
ProcessingSeconds = profile.GetValue<double>("processing_time_seconds"),
HasFingerprint = profile.HasSignal("audio.fingerprint.hash"),
HasTranscription = profile.HasSignal("transcription.full_text"),
HasDiarization = profile.HasSignal("speaker.count"),
ContentType = profile.GetValue<string>("audio.content_type")
}
});
Konfiguration
Ett exempel på full konfiguration
{
"AudioSummarizer": {
"EnableSpeakerDiarization": true,
"EnableTranscription": true,
"EnableVoiceEmbeddings": true,
"SupportedFormats": [".mp3", ".wav", ".m4a", ".flac", ".ogg", ".wma", ".aac"],
"MaxFileSizeMB": 500,
"VoiceEmbedding": {
"ModelPath": "./models/voxceleb_ECAPA512_LM.onnx",
"MinDurationSeconds": 1.0,
"AutoDownload": true
},
"Transcription": {
"Backend": "Whisper",
"Whisper": {
"ModelPath": "./models/whisper-base.en.bin",
"ModelSize": "base",
"Language": "en",
"UseGpu": false
},
"Ollama": {
"BaseUrl": "http://localhost:11434",
"Model": "whisper"
}
},
"AcousticProfiling": {
"EnableSpectralAnalysis": true,
"FftSize": 2048,
"VadThreshold": 0.02
},
"Fingerprinting": {
"Provider": "PureNet",
"EnableDeduplication": true
}
}
}
Förmågor och egenskaper
| Wella | Prioritering ♫ | Spod ♫ | |||
|---|---|---|---|---|---|
| IdentityWave | 100 | ||||
| FingerprintWave | |||||
| Akustikprofilvågor | 80 | ||||
| ContentClassifierWave | 70 | ||||
| Transkriptionsvågor | 65 | ||||
| SpeakerDiarizationWave | |||||
| VoiceEmbeddingWave |
exempel timmar (15-minuten podcast, 2 talareM SK3
IdentityWave: 45ms (SHA-256 + metadata)
AcousticProfileWave: 132ms (FFT + spectral analysis)
ContentClassifierWave: 87ms (ZCR + spectral flux)
TranscriptionWave: 12.3s (Whisper.NET base model)
SpeakerDiarizationWave: 3.8s (VAD + embeddings + clustering)
VoiceEmbeddingWave: 178ms (ECAPA-TDNN inference)
Total: ~16.6 seconds
Offline-läge ( inga transkriptioner):
IdentityWave → VoiceEmbeddingWave: ~600ms
Slutsatsen
AudioSummarizer bevisar att Reduzerad RAG-mönster verk för ljud: reducera en gång till signaler + bevis , fråga mot strukturerade faktaM SK3 syntetisera med begränsad LLM inmatning
Nyckelinsikter
- Signalextraktion över sammanfattningar: RMS
- Evidens är kontrollbar: Sampler för högtalare (Base64 WAVM SK3 transkriptionerMska4 diariseringsrören
- Query-timmarsfilter: Databas där clauser eliminerar 95% av ljud innan LLM ser något
- Föreställningsfensternedduktion: ♫ ♫ 5 ♫ ljudförteckningar istället för ♫ 500 ♫ råskivor ♫
- Rent .NET diarisering: Agglomerativ klusterning + ECAPA-TDNN ♫ ♫ = ♫ ingen Python ♫
- Privatsphäre genom design: Anonyma röstavtryck
Reduzerad RAG-mönster för audio:
Ingestion: Audio file → Wave pipeline → Signals (JSON) + Evidence (transcript, samples)
Storage: Signals (indexed) + Embeddings (speaker similarity) + Evidence (artifacts)
Query: Filter (SQL) → Search (BM25 + vector) → Synthesize (LLM, ~5 results)
Det här är Begränsad otydighet komponerad med Reduzerad RAG: deterministiska signaler begränsar sannolikhetssynthes , och LLM fungerar på förutsättningar
Pattern skalar över alla lucidRAG-metoder:
- DocSummarizer - Dokument entitetsextraktion, kunskapsgrafierM SK3 semantisk chunkingMSC4
- ImageSummarizer - Bilder 22- vågvisual intelligens
- DataSummarizer - Data | | Schemasuppfattningen |, | Kwaliteitsprofilering | МSK3 | Typdetektion
- AudioSummarizer (den här artikelnM SK1 - Audio ( akustisk profilering, högtalare diariseringMska5 transkriptionM Ska6
Alla implementerar samma kärna Reduzerad RAG-mönster: signaler
resurser
lucidRAG Dokumentation
- AudioSummarizer Library - källkod
- Architecture Guide - Bävdar , signaler
- Referens för signaler - Komplett signalkatalog
NX-modeller
- ECAPA-TDNN på HuggingFace - Rödlingsintegreringsmodell
- Skratt.NET - CM SK1 ompackare för Whisper.cpp
- Runtime Dokumentation
Forskningsdokument
- ECAPA-TDNN Paper - Speaker-kontrollers arkitektur
- Skrattpa Papier - OpenAI 's taligenkänning
- PyAnnote 3.1 - Referensimplementering (Python)
Related Articles
Korallpatroner:
- Reduzerad RAG - kärnans mönster
- Begränsad otydighetsmönster - Grundmuster
Reduzerade RAG implementeringar:
- DocSummarizer - Dokument RAG med enhetens extraktion och kunskapsgrafer
- DataSummarizer - Dataprofilering och schemasuppfattning RAG
- ImageSummarizer: Bildintelligens - Image RAG med 22- vågvisuell intelligens
- Begränsad Fuzzy OCR - Tre - Niveau OCR
- AudioSummarizer (den här artikeln ) - Audiomedicinsk karakterisering med högtalare diarisering
Serien
| del | mönster ♫ ♫ | ♫ Fokus ♫ |
|---|---|---|
| 1 | Begränsad otydighet | Enkel komponent |
| 2 | Begränsad Fuzzy MoM | Flera komponenter |
| 3 | Kontext dragning | tid / minne |
| 4 | Bildintelligens | Rymdenstrukturen, 22 vågor |
| 4.1 | Tre-Tier OCR Pipeline | OCR |
| 4.2 | AudioSummarizer (den här artikeln ) | Forensiska ljud, högtalare diarisering |
Nästa: Multi, -, modal graf RAG med lucid, RAG, —, som sammanför DocSummarizer, M SK3, ImageSum marizer och ,, AudioSommarizer och DataSummarzer till en enificerad kunskapsgraf.
Alla delar följer samma invariant sannolikhetskomponenter föreslår ; deterministiska system förblir kvar.