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: Caractérisation audio forensique à caractères fuzzy restreints
Monday, 12 January 2026
État d'avancement: AudioSummarizerM SK1Core est actuellement en développement dans le cadre de lucidRAG, un nouveau produit principalement lucide pour les RAG multi -modal RAG. La mise en œuvre est terminée et en cours de réalisationM SK3 cet article documente l’architecture et le design MSC4 L’intégration CLI sera disponible dans une prochaine version lucide de RAG.
Source: github.comM SK1scottgal/lucidrag (branchM SK1
v2)
Lorsque cela convient: AudioSummarizer fait partie du lucidRAG famille de mises en œuvre RAG réduites. Chacune gère une modalité différente:
- DocSummarizer - Documents
- ImageSummarizer - Images 22-intelligence visuelle des ondesM SK2
- Système de gestion des données - Données
- AudioSummarizer (le présent articleM SK1 - Audio (profilage acoustique, diarisation de l'émetteurMSC5
Tous suivent la même chose Modèle RAG réduit: extraire les signaux une fois , entreposer des preuves, synthétiser avec l’entrée LLM limitéeM SK3
De nombreux pipelines d’analyse audio de première ligne font le même blâme. Ils traitent les LLM comme s’ils étaient des ingénieurs en acoustique.
Ils alimentent les formes d’ondes dans des modèles; ils leur demandent : " de détecter la qualité du langage; " ou " d’identifier les interprètes; M SK4 et ils espèrent que le modèle peut déduire des propriétés structurelles à partir de ce qui est fondamentalement un modèle.
AudioSummarizer combine deux modèles complémentaires:
- RAG réduit pour la recherche - extraire les signaux une fois , entreposer des éléments de preuveM SK2 faire une demande contre des faits
- Fuzziness restreinte pour l'orchestre - vague - pipeline baséeM SK2 substrat déterministe limite les modèles probabilistes
Au lieu d'apporter un audio brut à un LLM au moment de la demande, il Réduction des coûts chaque fichier audio à un État de l'information (signals déterministes comme l'intensité de RMSM SK1caractéristiques spectrales , emplacements de haut-parleur) avec extrait de preuves (transcripts,sampleurs d'échantillonnage de l'émetteurM SK2 tournants de diarisation ). Ce livret est maintenu une fois indexé pour la récupération
À temps de demande, un LLM synthétise les réponses en récupérant et en faisant le raisonnement sur des signaux remarquables — non pas en réexaminant l’audio M SK2 Analyse de l’audition . Le traitement du signal lourd MSC4 transcription de la signalisation MST5 diarrification M ST7 se produit pendant l’ingestion M st8 Le LLM ne fonctionne qu’au moment de la demande pour interpréter les faits préliminaires M S ST9 calculés et produire des réponses humaines M SP10 lisiblesM SP11
Composition du modèle: Manipulateurs RAG réduits Ce qui suit : pour entreposer et récupérer. Manipulateurs à fuzziness restreints comment pour extraire des signaux de façon fiable. Ensemble, ils permettent la caractérisation audio légale avec un coût LLM limité.
Pour en savoir plus sur le modèle RAG réduit: Reduced RAG: Signal-Accueil du document dirigé
Terminologie clé
- Signales: Faits inscrits avec cotes de confiance
audio.content_type = "speech", confianceM SK1 0.85) - Evidence: Artefacts audibles ( transcriptions, échantillonnages d'émetteursM SK3 tours de diarisationMSC4
- Fichier de signalisation: L'ensemble persistant de tous les signaux , indicateurs d'éléments probants M SK2 et embeddings extraits d'un fichier audio
- ID de l'interprète: Identificateur local à l'intérieur d'un seul fichier
SPEAKER_00) - Numéro d'imprimerie vocale: CrossM SK1file stable hash dérivé de l'incorporation vocale
vprint:a3f9c2e1) - Personne: Il n’y a pas d’inférence explicite — Nous ne revendiquons jamais "il s’agit de John Smith
Modèle d'identité: SPEAKER_00 est local à un fichier audio. VoiceprintId est stable dans les fichiers mais anonyme. Nous n’établissons jamais des noms humains.
Exigences légales: Chaque signal comprend provenance (qui est la vague qui l'a produite ), Confiance (degré de certitude ), et versionnement (modèleM SK1versions de seuil). Ce qui permet la reproductibilité : la même entrée
Le résultat est:
- Fast, protection des renseignements personnels- préservation de la caractérisation audio judiciaire
- Diarisation des interprètes sans dépendance Python (pure .NET)
- Détection de la similitude des interprètes anonymes (no PIIM SK1 no names)
- Emplacements de voix pour "Trouver des interprètes similairesM SK1 demandes
- Tout sans envoyer de l'audio aux APIs cloud pendant l'ingestion
Ce système s’appuie directement sur Modèle de fuzziness restreint et l’architecture basée sur la vague- de ImageSummarizer.
Perspectives de base: Les LLM devraient raisonnablement se prononcer sur les faits acoustiques, ne pas les calculer
Cet article porte sur:
- Comment AudioSummarizer applique le modèle RAG réduit
- Extraction de signaux: facteurs acoustiques déterministes M SK1pas résumés LLM)
- Entreposage des preuves: échantillons d'émetteurs, transcriptions , tournants de diarisation
- Pure .Diarisation de l'émetteur NET (sans Python, sans pyannoteM SK3
- Enquête-synthèse du tempsM SK1 signaux de filtrage → extraire des preuves → synthétise le LLM
Articles connexes:
- RAG réduit - Le modèle de base qui y est appliqué
- Modèle de fuzziness restreint - Le modèle fondamental
- Réduction des mises en œuvre de RAG:
- DocSummarizer - Document RAG avec extraction d'entités et graphiques de connaissance
- Système de gestion des données - Profilage des données et détermination du schéma RAG
- ImageSummarizer - Image RAG avec pipeline d'intelligence visuelle à ondes 22-
- AudioSummarizer (le présent articleM SK1 - Caractérisation légale audio avec diarisation de l'émetteur
Le problème: L’analyse de l’audio est difficile ( et culturellement chargée)
L'analyse audio échoue de façon prévisible:
- Affirmations culturelles: "C'est jazzM SK2 ♫(dise qui?en fonction des données de formationMSC5
- Nom du conférencier: "Votre interlocuteur est John Smith" (violation de la vie privéeMSC4 hallucinationM SK5
- Identification musicale: "SongM SK2 Sandstorm by Darude"
- Estimation axée: "Votre interprète a un accent britannique" ≥( souvent erronéM SK4 potentiellement offensiveMSC5
- Dépendances de Python: La plupart des outils de diarisation nécessitent le pyannote (Python écosystème lockM SK2in)
Approche traditionnelle: M SK1Run Whisper pour transcription, pyannote pour diarrisation , envoyer à LLM pour résuméMSC4
Problème: Il s'agit soit de détruire le PII ( noms d'émetteurs ), coûte trop cher
Solution: Construire une ligne de conduite pure fondée sur . la vague NET M SK2 qui caractérise l’audio de façon structurelle et acoustique — sans faire des affirmations culturelles MSC4
Le cadre audio RAG réduit
AudioSummarizer met en œuvre Modèle RAG réduit pour les fichiers audio, suivant ses trois principes fondamentaux:
1. Extraction de signaux Phase d'ingestionM SK2
Signales déterministes extraites une fois—non LLMM SK1résumés traités:
- Taux temporaire: durée , entrevues de tempsM SK2 limites du segment
- Acoustic: Éloudité RMS , centroïde spectrale, plage dynamiqueM SK3 rapport de coupe
- Identité: SHAM SK1 hash, format de fichiers , taux d'échantillonnageMSC4 canaux
- Qualité: confiance en transcriptionM SK1 confiance dans la diarrisation
- Conférencier: ID d'imprimante vocale ( hashes anonymesM SK2 cotes de tournage, pourcentages de participation
- Catégorique: type de contenu (discussion/musicM SK3silenceMSC4 classification des interprètes
Échantillon de signaux pour un 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"
}
Ces signaux sont déterministe (le même audio →les mêmes valeursM SK2 indexable (se peut filtrerM SK1sortir dans la base de données), et calculables sans LLMs (Procédure de traitement des signaux purs
2. Entreposage des preuves Unités structurellesM SK2
Au lieu de stocker uniquement "chunks," AudioSummarizer stores :
- Signales: Champs structurés (JSONM SK2 pour le filtrage déterministe
- Embeddings: Emplacements vocales (512-dim ECAPAM SK2TDNN) pour la similitude de l'émetteur
- Note de protection des renseignements personnels: Les emplacements ne sont pas réversibles dans ce système, mais les traitent comme des données sensibles.
- Artefacts de preuve:
- Texte intégral de la transcription (searchable)
- Clips d'échantillonnage de haut-parleur (BaseM SK1 WAV, 2-secondes clips pour vérification
- Diarization turns (JSON avec haut-parleurM SK1id, startMNK3end timestampsMRK4
- Pointeurs: Hash du dossier + ID de preuve pour la provenance vérifiable
La preuve est vérifiable: Les utilisateurs peuvent jouer des échantillons d’émetteurs de haut-parleurM SK1 lire les transcriptions , inspecter les tournants de diarisation M SK3 ne pas seulement se fier à un résumé LLM.
3. Query- Synthèse du temps Input LLM limitéM SK3
Au moment de la demande, le LLM Aucune fois Voie l'audio brut. Plutôt que:
Filtrer de façon déterministe (base de données D'où la clause):
WHERE audio.content_type = 'speech' AND speaker.count >= 2 AND audio.rms_db > -25.0 AND transcription.confidence > 0.8Recherche hybride (BMM SK1 + vecteur
- BM25 sur le texte de la transcription M SK1compatibilité des mots clés)
- Vector similarity on voice embeddings (speaker similarity)
- Retour en haut 5 fichiers audio
Synthèse à partir des signaux (LLM voit un ensemble de preuves structurée
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"]
Le LLM reçoit 5 Dossiers de preuve structurels au lieu de 500 chunks of raw audio metadata. Réduction de la fenêtre contextuelleM SK1 ~50× plus petite.
Impact sur les coûts (si l'on utilise les APIs payés de LLM): Traitement des fichiers d'audio 100 va de MSC3 MSC4re MSc5analyse de chaque demande audio MSC6 à SMC7 (query contre les signales pré--computed Note: lucidRAG est local-première M SK2Ollama par défautMSC3 coût nulMST4 API payés (ClaudeMSL6 GPTMSR7 sont facultatifsMSSL8 Les estimations des coûts supposent l'utilisation d'API payéeMSV9 MSSL10K de tokens SSL11varies selon le fournisseurMSS12 \MSS13K of tokensM SS14query |MSS15raw metadata |MSSL16 vs. \MSSL17tokens |SSS18queries | MSSL19signals |SMSS20 | SSS21 queries \SSS22 \day \SMSS23
Il s’agit de l’influence fondamentale du RAG réduite: d'abord décider ce qui est important (signalsM SK1 les stocker de façon déterministe (éléments de preuveM SK1 inclure LLM uniquement pour la synthèse (questionner-heureM SK2
Le pipeline d'audio de Fuzziness restreint
Le système détecte les ondes dans l'ordre prioritaire (numéro supérieur =détecte la première MSC2
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
Remarque: SentimentM SK1détection intentionnelle exclue de la détection des mouvements—chargée sur le plan culturel et ne fait pas partie de la caractérisation médico-légale .
Architecture
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
Cela préserve la familiarité Écoule → Signal → LLM facultatif loop, mais l’ancrage dans des faits déterministesM SK1
- Le profilage acoustique est déterministe (la même entrée = la même sortieM SK2
- Le LLM fonctionne sur evidence, non audio brut
Real-Exemple mondiale: Traitement d'une épisode de podcast
Laissez 's tracer un épisode de podcast d’une minute sur le 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)
L'article 5 de la Loi sur l'immigration et le statut des réfugiés Fichier de signalisation est ce qui persiste dans la base de données—le paquet complet des signaux,les pointeurs d’éléments probantsM SK2 et les embeddings .C'est là où il apparaît pour ce podcastMSC5
Ledger de signalisation (excerpt):
{
"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..."
}
Ce que cela permet:
- Recherche pour "Tech Insights discute de l’infrastructure nuageuseM SK1 → trouve cet épisode par transcription
- Enquête "Trouver l'audio avec imprimante vocale vprintM SK1a3f 9cMNK4eMRK5 → repère tous les épisodes avec le même haut-parleur
- Demandez "Qu'est-ce queM SK1la gamme dynamique des épisodes de notre podcast?" → les signaux acoustiques agrégats
- Filtrer "Trouver des enregistrements de faible qualitéM SK1Enregistrements d'une qualité inférieure" → RMS MSC4 \MSC5dB ou couplage MSC6 |MSC7
- Vérifier "Did speaker identification workM SK1 → play
speaker.sample.speaker_00clip (à l'intérieurM SK1file)
Pourquoi les signaux sont importants (Even Without an LLM)
Un livret signale les réponses bornées à des questions urgentes immédiatement.
- Est-ce le discours audio, la musiqueM SK1 ou le silence?
- Combien de disques sont détectés?
- Qu'est-ce que le ratio de bruit entre le signal - et -?
- Y a-t-il des artefacts de coupe ou une distorsion?
- Qu'est-ce que le spectral centroid '?
- Qu’est-ce que la gamme dynamique de '?
Voici les questions que vous découvrirez habituellement 30 minutes dans l'archéologie d'Audacity.
Le livre de signalisation vous les donne en secondes.
Le pipeline de la vague: De l’identité à l’intelligence
Wave 1: Identité (Substrate déterministeM SK2
Ligne de base. Identité cryptographique et métadonnées des fichiers.
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;
}
}
Signaux clés émis:
audio.hash.sha256- Identité cryptographiqueaudio.duration_seconds- Longueur déterministeM SK2audio.sample_rate- 44100, ≥48000, etc.audio.channels- 1audio.format- mp 3, wav, flacM SK3 etcMSC4
Pourquoi le determinisme?
- Le même fichier → le même hash → la même identité
- Aucune randomité d'échantillonnage, aucun paramètre de température
- Confidence = 1.0 ≥(non probabilisteM SK3
Profil acoustique (Procédure de traitement des signauxM SK2
Extraire des propriétés acoustiques structurelles en utilisant NAudio et 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)
};
}
}
Exemple de sortie:
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)
Pourquoi cela importe:
- L'intensité du RMS indique la qualité de maîtrise
- Coefficient de couplage détecte les artefacts d'enregistrement
- Le centre spectral différencie le langage (2-4kHzM SK1 de la musique (variable)
- Tous les déterministes—le même audio produit les mêmes valeurs
Écoule 3: Classification du contenu (Speech vs MusicM SK2
Classification héuristique brute utilisation de zéro- taux de croisement et flux spectral pour les fins de routageM SK1
Remarque: Ces héuristiques sont genresM SK1contexte dépendant —ne sont pas fiablement exactes dans tous les types de contenus M SK3 Utilisées uniquement pour les décisions d'acheminement des ondes
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;
}
}
Exemple de routage:
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)
Pure .NET Diarisation de l'émetteur (No Python)
Pour les contraintes de déploiement du lucidRAG': diarisation de l'émetteur sans pyannote, DIARTMSC2 ou toute dépendance PythonM SK3
Le problème de la diarisation basée sur Python-
Approche traditionnelle:
pyannote.audio (Python) → ONNX export (experimental) → C# wrapper (fragile)
Problèmes:
- pyannote 3.1+ supprimé Support ONNX (moved to pure PyTorch)
- Fonctionnement de Python requis (marée du déploiement)
- HTTP wrapper ajoute la latence et la complexité
- Aucun soutien offline
Solution: Mettre en œuvre la diarisation dans le net pur . à l'aide de modèles d'embedding vocal existants
L'algorithme pure .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
Mise en œuvre
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;
}
}
Exemple de sortie:
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
Pourquoi cela fonctionne:
- VAD est déterministe (RMS seuilM SK1
- ECAPA-TDNN embeddings are deterministic (ONNX modelM SK2 no sampling
- Le seuil de classement est configurable (0.75 similitude cosine)
- Aucun temps d'exécution Python requis (pure .NET)
- Fonctionne en mode offline (pas d'appels API)
Embeddings de voix: Similarité des auditeurs anonymes
ECAPA-TDNN OnNX extraits de modèles 512-Intégrations sonores dimensionnelles pour la détection des similitudesM SK2
Le modèle de protection des renseignements personnels
Ce que nous faisons
- ❌ Noms d'interprètes Infer
- ❌ Étudier la démographie ( Âge, sexeM SK3 accent
- ❌ Faire des affirmations culturelles
- ❌ Entreposer l'audio brut
Ce que nous faisons:
- ✓ Générer des ID d'imprimante vocale anonymes (
vprint:abc123...) - ✓ Calculer la similitude de l'émetteur
- ✓ Enabler "Trouver des interprètes similairesM SK2 Demandes de renseignements
- ✓ Préserver la protection de la vie privée ( les embeddings ne sont pas réversibles, mais traités comme des données sensibles
Mise en œuvre
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();
}
}
Utilisation par exemple:
// 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)
Recherche de locuteurs anonymes:
// 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
Pourquoi anonyme?
- L'ID de l'imprimante vocale est un hash de la fonction embedding :
- Embedding is 512 dimensions → ne peut déduire le nom de l'émetteur
- La note de similitude est relative (pas absolue "c'est John SmithM SK2
- Protection des renseignements personnels-conservation M SK1pas de PII entreposé)
Extraction de segments d'audio: Replay des preuves
Extraire le temps- clips audio de gamme pour les preuves de diarrhée par l'émetteur.
Le problème
La diarisation vous dit "SPEAKERM SK1 a parlé de 5.2s à 12.8s" mais ne fournit aucune façon de vérifier cette allégation
Solution: Extraire les clips audio de 2-deux secondes à partir du premier tour de chaque haut-parleur.
Mise en œuvre
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);
}
}
Exemple d’intégration dans 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);
}
Exemple de sortie:
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>
Pourquoi cela importe:
- Reproduction de preuves: Les utilisateurs peuvent entendre chaque échantillon d'émetteur
- Vérification: Confirme l'exactitude de la diarrisation
- Débugging: Identifie les segments mal classés
- Protection des renseignements personnels: Seulement M SK1échantillons de secondes (non audio complet)
L'intégration avec lucidRAG
AudioSummarizer intégre sans fil le pipeline de traitement des documents du lucidRAG' via un deux-réduction des étapes:
Étage 1 (AudioSummarizerM SK2: Réduire le fichier audio à une signature acoustique
- Signals: RMS, caractéristiques spectralesM SK2 emplacements de haut-parleurs , tours de diarisation
- Evidence: Échantillons d’échantillonnage des auditeurs, texte de la transcription intégrale
- Output: Sommaire de marquage + signal JSON
Étape 2 (DocSummarizerM SK2: Réduire la transcription à une signature sémantique
- Traduction du processus par pipeline DocSummarizer
- Extraire les entités (personnes,organisationsM SK2 sujets )
- Générer des embeddings pour la recherche sémantique
- Établir des liens graphiques de connaissance
- Output: Graphique de l'entité + emplacements chunqués
Ce qui suit : Réduction en cascade permet des demandes comme:
- "Trouver l'audio mentionnant le calcul quantique" → DocSummarizer entities from transcript
- "Trouver des podcasts avec des haut-parleurs similairesM SK1 → AudioSummarizer voice embeddings
- "Pourvoir des enregistrements de faible qualité sur l'AIM SK2 → Combiner les signaux acoustiques CSM4 entités sémantiques
Enregistrement du gestionnaire de documents
// 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();
}
}
Intégration de l'ICC (Immédiatement~)
Lorsque AudioSummarizer est intégré à lucidRAG CLI (livraison prochaineM SK1 vous' pourrez traiter des fichiers audio comme celui-ci
# 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
Exemple de signaux.json output:
{
"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..."
}
Entreposage des preuves
// 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")
}
});
Configuration
Exemple de configuration complète:
{
"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
}
}
}
Caractéristiques du rendement
| Écoule | Priorité MSC2 vitesse MSC3 déterministe SMC4 Besoins SMC5 | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Écoule d'identité | 100 | 3 | 4 | ms | 5 | Oui | Audio | ||||||
| FingerprintWave | 90 | 3 | 4 | 5 | Oui | FftSharp | 7 | ||||||
| AcousticProfileWave | 80 | 3 | 4 | 5 | Oui | NAudio | , | FftSharp | |||||
| ContentClassifierWave | 70 | 3 | 4 | 5 | Heuristic | 6 | 7 | 8 | |||||
| TranscriptionWave | 65 | 3 | 4 | 5 | Aucune | Disperser | . | Net ou Ollama | |||||
| Écoule de diarisation par interprète | 60 | \ ~2-8s | |||||||||||
| VoiceEmbeddingWave | 30 | 3 | 4 | 5 | Oui | 6 | ECAPA | 7 | TDNN ONNX | 8 |
Exemple de temps (15- podcast d'une minute, 2 interprètesM 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
Mode hors ligne (pas de transcription):
IdentityWave → VoiceEmbeddingWave: ~600ms
Conclusion
AudioSummarizer prouve que Modèle RAG réduit travail pour l’audio: réduire une fois à des signaux M SK1 preuves , recherche contre les faits structurels, synthétiser avec l’entrée LLM limitéeMSC4
Principaux points de vue:
- Extraction de signaux au-dessus des résumés: RMSM SK1 Caractéristiques spectrales , Emplacements de haut-parleurs—non LLMMSC4définitions produites
- La preuve est vérifiable: Échantillons d'émetteurs (Base64 WAVM SK3 transcriptionsMSC4 tournants de diarisation
- Filtres de temps de la demande-: Base de données DANS LESquelles les clauses éliminent 95% de l'audio avant que le LLM ne voit quelque chose
- Réduction de la fenêtre contextuelle: 5 sommaires sonores au lieu de SSK2 morceaux bruts MSC3 \MSC4 plus petits
- Diarisation pure .NET: Groupement aglomératif + ECAPA-TDNN MSC3 aucun Python
- Protection des renseignements personnels par conception: Imprimantes vocales anonymes (hash-IDs fondés sur le nom de l'interprèteM SK3 sans nom du conférencierMSC4 sans affirmations culturelles
Le modèle RAG réduit pour 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)
Il s'agit de Fuzziness restreinte Composé de RAG réduit: les signaux déterministes restreignent la synthèse probabiliste , et le LLM fonctionne sur des preuves calculées précédemment
Le modèle s’établissait à toutes les modalités de lucideRAG:
- DocSummarizer - Documents
- ImageSummarizer - Images
- Système de gestion des données - Données
- AudioSummarizer (le présent articleM SK1 - Audio (profilage acoustique, diarrisation de l'émetteurMST5 transcriptionMst6
Tous mettent en œuvre le même noyau Modèle RAG réduit: signaux + preuves + enquêteM SK3 synthèse du temps.
Ressources
lucidRAG Documentation
- Bibliothèque AudioSummarizer - Code de source
- Guide de l'architecture - ÉcoulementsM SK1 signaux , pipeline
- Référence sur les signaux - Catalogue complet des signaux
Modèles ONNX
- ECAPA-TDNN sur HuggingFace - Modèle d'intégration vocale
- Whisper.NET - CM SK1 enveloppe pour Whisper.cpp
- Documentation sur le temps de fonctionnement d'ONNX
Documents de recherche
- ECAPA-TDNN Document - Architecture de vérification du haut-parleur
- Whisper Paper Reconnaissance du langage de - OpenAI'
- PyAnnote 3.1 - Mise en œuvre de référence (Python)
Articles connexes
Patterns de base:
- RAG réduit - Le modèle de base Signaux
- Modèle de fuzziness restreint - Modèle fondamental
Réduction des mises en œuvre de RAG:
- DocSummarizer - Document RAG avec extraction d'entités et graphiques de connaissance
- Système de gestion des données - Profilage des données et détermination du schéma RAG
- ImageSummarizer: Intelligence de l'image - Image RAG avec pipeline d'intelligence visuelle à ondes 22-
- OCR Fuzzy restreint - TroisM SK1tier OCR local → ONNX → LLMMSC5
- AudioSummarizer (le présent articleM SK1 - Caractérisation légale audio avec diarisation de l'émetteur
La série
| Partie | Modèle MSC2 Concentration MSC3 | |
|---|---|---|
| 1 | Fuzziness restreinte Composante unique | |
| 2 | MoM Fuzzy restreint | Composantes multiples |
| 3 | Traçage du contexte | Temps / mémoire |
| 4 | Intelligence de l'image | Architecture des vaguesM SK1 22 ondes |
| 4.1 | Three-Tier OCR Pipeline | OCRM SK1 Modèles ONNX, bandes de films |
| 4.2 | AudioSummarizer (le présent articleM SK1 | Audio médico-légal, diarisation de l'émetteur |
Suivant: MultiM SK1 Graphique modélisé RAG avec lucidRAG—composant DocSummarizer , ImageSum marizer
Toutes les parties suivent la même invariante: Les composantes probabilistes proposent; les systèmes déterministes persistent.