AudioSummarizer: Characterizzazione audio forense confusa limitata (Italiano (Italian))

AudioSummarizer: Characterizzazione audio forense confusa limitata

Saturday, 10 January 2026

//

20 minute read

Lo stato: AudioSummarizer.Core è attualmente in fase di sviluppo come parte del lucidRAG, un futuro prodotto più o meno lucido per i multi-modal RAGM SK2 La implementazione è completa e funziona — questo articolo documenta l'architettura e il designMSC4 L'integrazione CLI sarà disponibile in una prossima versione lucidRAG

Source: github.comM SK1scottgal/lucidrag (branchM SK1 v2)

Dove questo va bene?: AudioSummarizer fa parte del lucidRAG family of Reduced RAG implementations. Each handles a different modality:

  • DocSummarizer - Documenti
  • ImageSummarizer - Immagini (22-intelligenza visiva delle onde)
  • DataSummarizer - Donni (Inferenza schematicaM SK2Profiliamento)
  • AudioSummarizer (Questo articoloM SK1 - Audio (profilazione acustica, diarizzazione dell'altoparlanteMSC5

Tutti segueno la stessa cosa. Esempio RAG ridotto: estrarre i segnali una volta , immagazzinare le prove, sintetizzare con un input LLM limitatoM SK3


Molti canali per l'analisi audio iniziale fanno lo stesso errore: trattano i LLM come se fossero ingegneri acustici.

Alimentano le forme d'onda nei modelli, li chiedono di " individuare la qualità del discorsoM SK2 o " identificare gli altoparlantiMSC4 e sperare che il modello possa dedurre proprietà strutturali da ciò che è fondamentalmente un modelloMST5 un sistema di corrispondenzaMSST6 Funziona abbastanza bene per dimostrarloMSP7 e poi halucinare con i nomi degli altoparlantiMSV8 inventare generi musicaliMSS9 o identificare con sicurezza gli accenti in modo sbagliatoMSM10

AudioSummarizer combina due modelli complementari:

  1. RAG ridotto per la ricerca - estrarre i segnali una volta , immagazzinare le prove, fare una domanda contro i fatti
  2. Fuzzinesse limitata per l'orchestrazione -ondaM SK1piano di tubatura, substrato deterministico limita i modelli probabilistici

Invece di dare audio crudo a un LLM al momento della domanda, lo fa. Riduce ogni file audio ad un Il ledger di segnali ( segnali deterministici come la frequenza RMS , caratteristiche spettraliM SK2 inserzioni di altoparlante) con Le prove estrattoe (transcripts, sample clips di altoparlanteM SK2 turns diarization ). Questo ledger è mantenuto una voltaMSC4 indexato per essere recuperatoMST5

All'incirca tempo di ricerca, un LLM sintetizza le risposte raccogliendo e ragionando su segnali salienti—non reM SK2 analizzando l'audio . Il processo pesante di rialzo del segnaleMSC4 la trasformazione dei segnaliMST5 la trascrizioneMSSK6 la diarizzazioneMSR7 avviene durante l'ingestioneMRS8 Il LLM funziona solo al momento della domanda per interpretare i fatti pre-MSR9 calcolati e generare dati umaniM SR10 risposte leggibiliMSL11

La composizione del modello: Manole RAG riducite Che cosa? per immagazzinare e recuperare. Manole di Fuzziness limitate Come? per estrarre i segnali in modo affidabile. Insieme permettono la caratterizzazione forense dell'audio con un costo LLM limitato.

Imparate di più sul modello Reduced RAG: Reduced RAG: Signal-Driven Document Understanding

Terminologia chiave

  • I segnali: I dati digitati con punteggi di fiducia (eM SK2g., audio.content_type = "speech", fiduciaM SK1 0.85)
  • Le prove: Artefatti audibili (transcritti, campioni di altoparlanteM SK3 giri di diarizzazioneMSC4
  • Indicatore di segnale: L'insieme persistente di tutti i segnali, puntatori di proveM SK2 e inserzioni estratto da un file audio
  • SpeakerId: Identificatore locale all'interno di un singolo fileM SK1fare la diarizzazione (e.gMSC4 SPEAKER_00)
  • VoiceprintId: CrossM SK1file stable hash derivato dall'inserzione vocale (e.gMSC4 vprint:a3f9c2e1)
  • Persone: Esplicemente non è inferito—non sosteniamo mai "questo è John SmithM SK3

Il modello di identità: SPEAKER_00 è locale a un file audio. VoiceprintId è stabile tra i file ma anonimo. Non mapiamo mai i nomi umani.

Richiedi forense: Ogni segnale include provenienza (Quale onda l'ha prodottoM SK1 Confidenza. (grado di certezza ), e Versione (modello/versioni sospeseM SK2 Questo consente la riproducibilitàMSC3 lo stesso input + lo stessa configurazione → lo stessi ledger di segnaliMNK6

Il risultato è:

  • Fast, privacy-proterre la caratterizzazione audio forense
  • Diarizzazione di altoparlante senza dipendenze Python (pure
  • Detezione della somiglianza degli oratori anonimi (no PII, nessun nomeM SK2
  • Inserzioni vocali per "find similar speakers" queries
  • Tutto senza mandare audio alle API cloud durante l'ingestione.

Questo si costruisce direttamente su Patterno di Fuzzine Constretto e l'architettura basata sull'onda- da ImageSummarizer.

Percezione fondamentale: I LLM dovrebbero ragionare su fatti acustici, non li calcolare.

Questo articolo riguarda:

  • Come AudioSummarizer implementa il modello Reduced RAG
  • Extrazione di segnali: fatti acustici deterministici M SK1non sommità LLM)
  • Conservazione delle prove: campioni di altoparlanteM SK1 trascrizioni, giri di diarizzazione
  • Pure .Diarizzazione di altoparlante (no Python,no pyannoteM SK3
  • Query-syntesi temporaleM SK1 segnali filtranti → ricavare prove → LLM sintetizza

Article connessi:

  • RAG ridotto - Il modello centrale che implementa.
  • Patterno di Fuzzine Constretto - Il modello fondamentale
  • Implementazioni RAG riducite:
    • DocSummarizer - Document RAG con estrazione delle entità e grafici di conoscenza
    • DataSummarizer - Profilazione dei dati e deduzione del schema RAG
    • ImageSummarizer - Image RAG con un tubo per l'intelligenza visiva 22-
    • AudioSummarizer (Questo articolo) - Caratificazione forense audio con diarizzazione dell'altoparlante

Il problema: L'analisi dell'audio è difficile (e culturalmente caricataM SK2

L'analisi audio fallisce in modi prevedibili:

  • Affermazioni culturali: "Questo è jazzM SK2 ♫( dice chi? basandosi su quali dati di allenamentoMSC5
  • Nomensione del suonatore: "Il relatore è John SmithM SK2 ♫(violazione della privacy,allucinazioneMSC5
  • Identificazione musicale:
  • Accent guessing: "Il parlante ha l'accento britannico"
  • Dependenze Python: La maggior parte degli strumenti di diarizzazione richiede il pyannote.

Appoggio tradizionale: M SK1Run Whisper per la trascrizione, pyannote per la diarizzazioneMSC3 mandare al LLM per un'analisiMSL4

Il problema.: Questo o vuol fuoriuscire il PII ( nomi di alto tasso), costa troppo ( API cloudM SK4 o richiede un tempo di run-time Python ♫(pyannote ♫ , DIART ♫

Soluzione: Costruire un tubo di tubature puramente basato su .NET waveM SK2 che caratterizza l'audio strutturalmente e acusticamente— senza fare affermazioni culturaliMSC4


La Reduced RAG Audio Framework

AudioSummarizer implementa il Esempio RAG ridotto per i file audio, seguendo i suoi tre principi fondamentali:

1. Extrazione del segnale (Fasi di ingestione)

I segnali deterministici estratti una volta—non LLMM SK1summari processati:

  • Temporale: durationM SK1 timetamps, confini del segmento
  • Acustica: Lautità RMSM SK1 centroide spettrale, ampiezza dinamica , rapporto di taglio
  • Identità: SHAM SK1 hash, formato del file , tasso di campioneMSC4 canali
  • Calità: fiducia nella transcrizioneM SK1 fiducia in diarizzazione
  • Speaker: ID di impronta vocale (hash anonimiM SK2 conteggi di giro, percentuali di partecipazione
  • Cattericale: tipo di contenuto (speech/musicaM SK3silenzaMSC4 classificazione degli altoparlanti (singleMST6twoMSS7multiMSSS8

Esempio di segnale per 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"
}

Questi segnali sono determinista. (la stessa frequenza audio → gli stessi valoriM SK2 Indicabile. (can filter/sort in databaseM SK2 e computabile senza LLM. (processo del segnale pulito + modelli ONNX).

2. Immagazzinamento di prove (Unità strutturate)

Invece di immagazzinare solo "chunks," AudioSummarizer stores :

  • I segnali: campi strutturati (JSONM SK2 per il filtramento deterministico
  • Inserzioni: Inserzioni vocali (512-dim ECAPAM SK2TDNN) per la somiglianza degli altoparlanti
    • Nota sulla privacy: Gli inseritori non sono fattibilmente invertibili in questo sistema, ma li trattiamo come dati sensibili.
  • Artefatti di prova:
    • Il testo completo della trascrizione (searchable)
    • Immagini di altoparlante (Base64 WAVM SK2 2-secondi per la verificazione
    • Diarization turns (JSON con altoparlante_idM SK2 startMST3end timestampsMSST4
  • Indicatori: File hash + ID di prove per provenienza verificabile

Le prove sono verificabili.: Gli utenti possono suonare campioni di altoparlante, leggere le trascrizioni , esaminare i giri di diarizzazioneM SK3 non solo fidarsi di un somma LLMMSC4

3. QueryM SK1Sintesi del tempo (Input LLM confinato)

All'ora della domanda, il LLM mai. Vede audio crudo. Invece:

  1. Filtrare in maniera determinista (database Dov'è la clause):

    WHERE audio.content_type = 'speech'
      AND speaker.count >= 2
      AND audio.rms_db > -25.0
      AND transcription.confidence > 0.8
    
  2. La ricerca ibrida (BMM SK1 + vettore, ≥~5 risultatiMSC5

    • BM25 sul testo della trascrizione (comparimento di paroleM SK2
    • La somiglianza dei vettori sulle inserzioni vocali (somiglianza degli altoparlanti)
    • Tornate in alto 5 file audio
  3. Sintezizzare dai segnali. (LLM vede un sacco di prove strutturate):

    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"]
    

Il LLM riceve 5 Pacchetti di prove strutturati invece di 500 frammenti di metadati audio crudi. Riduczione della finestra di contestoM SK1 ~50× più piccola.

Impatti costi (se si usano API LLM pagati):Procedere i file audio 100 vanno da $5 M(riMSC5analisi dell'audio di ogni domandaM SK6 a m$0.10 (questionare contro i segnali pre--computed Nota: lucidRAG è locale-prima M SK2Ollama di defaultMST3 costo zeroMSL4 API pagate (ClaudeMSC6 GPTMSR7 sono optionaliMSSK8 I costi stimati presuppongono l'uso dell'API pagataMSV9 |~$0.01/1K token | (varie secondo il prestatoreMSS12 \MSS13Ktokens | mSS14question | \mSS15raw metadata | МSS16 contro \ MSS17 tokens ♫ MSS18questions |

Questo è il punto centrale della Reduced RAG insight: Decidere in anticipo cosa conta. (signali), Lo immagazzinano in maniera determinista. (evidenzaM SK1 Includere LLM solo per la sintesi (questo-tempoM SK2


La Pipeline Audio Constritta della Fuzziness

Il sistema fa girare le onde in ordine prioritario (un numero più alto = fa girarsi prima):

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

Nota.: SentimentoM SK1Detezione di movimenti intenzionalmente esclusa —Culturata culturalmente e non parte della caratterizzazione forense.

L'architettura

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

Questo preserva il familiare. Wave → Signal → LLM opzionale loop, ma l'ancora in fatti deterministiciM SK1

  • La profilazione acustica è deterministica (same input = same outputM SK2
  • Il LLM opera su prove, non su audio crudo.

Real-Esempio del mondo: Tracciare un Podcast Episode

Lasciate's tracciare un episodio di podcast di 15-minuta attraverso il canale.

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'informazione figura nella parte dispositiva. Indicatore di segnale è quello che rimane nella base di dati— l'intero insieme di segnali, indicatori di proveM SK2 e inserzioni . QuiMSC4 ecco com'è questo podcast

Indicatore di Signale (estratto):

{
  "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..."
}

Cosa questo consente:

  • Cercare "Tech Insights discusses cloud infrastructure" → finds this episode via transcript
  • Query "Trovare l'audio con l'impronta vocale vprint :a3fM SK3cMNK4eMRK5 → Trova tutti gli episodi con lo stesso altoparlante SSK7crossMEK8fileMMK9
  • Chiedete "CosaM SK1è la gamma dinamica dei nostri episodi podcast?" → segnali acustici aggregati
  • Filtrare "Trovare basse registrazioni -Qualità di registrazioneM SK2 → RMS CSK4 -25dB o taglio
  • Verify "Did speaker identification work?" → play speaker.sample.speaker_00 clip (connessoM SK1file)

Perché i segnali contano ( Anche senza un LLM)

Un ledger di segnali risponde alle domande noiose.

  • È questo discorso audio, musica, o silenzioM SK2
  • Quanti speaker sono rilevati?
  • Cos'è il segnale?
  • Ci sono artifatti o distorsioni di taglio?
  • Cos'è la centroide spettrale?
  • Cos'è la gamma dinamica?

Queste sono le domande che normalmente si scoprono 30 minuti nell'archeologia di Audacity

Il ledger di segnali le dà in secondi.


Il Pipeline Wave: Da Identità all'Intelligenza

Wave 1: Identità (Substrato determinante)

La linea di base. Identità Cryptografica e metadati dei file.

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;
    }
}

I segnali chiave emetti:

  • audio.hash.sha256 - Identità Cryptografica
  • audio.duration_seconds - lunghezza deterministaM SK2
  • audio.sample_rate -
  • audio.channels - 1
  • audio.format - mp3, wav, flacM SK3 etcMSC4

Perché deterministico?

  • Lo stesso file → lo stesso hash → la stessa identità
  • No sampling randomness, no temperature parameter
  • Confidenza = 1.0 ≥(non probabilisticoM SK3

Wave 2: Profil acustico (Processo dei segnaliM SK2

Extrarre proprietà acutiche strutturali usando NAudio e 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)
        };
    }
}

Esempio output:

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)

Perché questo conta:

  • La rumore RMS indica la qualità di mastering.
  • Il rapporto di taglio rileva gli artefatti registrati.
  • La centroide spettrale differenzia il discorso (2-4kHzM SK1 dalla musica (variabile)
  • Tutti deterministici— lo stesso audio produce gli stessi valori

Wave 3: Classificazione del contenuto (Speech vs Music)

Classificazione heuristica ristretta usando zero- tasso di attraversamento e flusso spettrole per i fini di routingM SK1

Nota.: Queste euristiche sono genericheM SK1dipendente dal contesto —non sicuramente accurate per tutti i tipi di contenuto M SK3 usate solo per le decisioni di conduzione delle onde (e.gMSC6 saltare la diarizzazione per la musica

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;
    }
}

Esempio di 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)

Pure .NET Speaker Diarization (No Python)

Per i limiti di distribuzione del lucidRAG': diarizzazione di altoparlante senza pyannote, DIART , o qualsiasi dipendenza PythonM SK3

Il problema con la diarizzazione basata su Python-

Appoggio tradizionale:

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

Problenze:

  • pyannote 3.1+ rimosso supporto ONNX ( spostato a PyTorch puro)
  • L'intervallo di Python è necessario (un incubo di deploiamento)
  • Il wrapper HTTP aggiunge latenza e complessità.
  • Nessun supporto offline.

Solution: Implementare la diarizzazione nel puro .NET usando modelli di embedding vocale esistenti.

L'algoritmo 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

Implementazione

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

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

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

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

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

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

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

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

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

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

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

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

    double timeSeconds = 0;
    int samplesRead;

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

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

        if (isSpeech)
        {
            if (currentSegment == null)
            {
                // Start new segment
                currentSegment = new SpeechSegment
                {
                    Star
Finding related posts...
logo

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