Back to "VideoSummarizer: RAG réduit pour le vidéo (Shots → Scenes MSC3 EvidenceMSC4"

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

AI Architecture CLIP LLM NER ONNX Patterns Video

VideoSummarizer: RAG réduit pour le vidéo (Shots → Scenes MSC3 EvidenceMSC4

Thursday, 15 January 2026

État d'avancement: En cours de développement dans le cadre du lucidRAG. Source: github.comM SK1scottgal/lucidrag

Lorsque cela convient: VideoSummarizer est le orchestrateur de la lucidRAG famille, combining five pipelines into a unified video analysis engine:

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


Le traitement d’une image cinématographique de deux heures avec des embeddings CLIP prendrait des heures et coûterait des centaines de dollars à calculer.

VideoSummarizer résout ce problème grâce à trois améliorations clés:

  1. Déduplication perceptuelle du hash - Passer à des cadres visuellement similaires avant de recourir à un ML coûteux 40% réduction
  2. Établissement d'un CLIP par lot - Processus 8 images par passeur GPU au lieu d'une seule (3-5x speedup)
  3. Composition du pipeline - Image de chaîneSummarizer pour les cadres-clés, AudioSummariser pour la paroleM SK2 NER Reconnaissance d'entités désignéesMSC4 pour les entitésMska5 DocSum marizer pour l'analyse du texteMka6 GraphRAG pour les relations entre des entités

Le résultat: a M SK1 procédés de cinéma d'une heure en ~10-15 minutes, non en heuresCLIP est utilisé pour fast, embeddings sémantiques locaux; Les LLM de vision sont facultatifs et ne peuvent être invoqués que pour l'analyse élargieM SK1

Les mêmes principes d'architecture que ImageSummarizer et AudioSummarizer, mais constitué d’une conduite d’analyse vidéo unifiéeM SK1

Perspectives de base: Video is shots + audio + textM SK2 Tracer chaque domaine avec des outils spécialisés, fusionner les résultats en scènes cohérentesMSC4

  • Structure du processus en premier (cutsM SK1 I-frames , segments audioMSC4
  • Extraire une fois les signaux modaux croisés- (embeddingsM SK1 transcriptions , entités)

Terminologie:

  • Récolte - prise de la caméra entre les coupes (structuraleM SK2 à partir de la détection des scènes FFmpeg)
  • Scène - groupe contigu de tirs formant une unité cohérente sémantique
  • Evidence - (start_time, end_time) + signaux + pointeurs + provenance

Modèles ML clés utilisés:

  • CLIP - OpenAI's Contrastive Language -Image PreMSC3trainingM SK4génère des embeddings dimensionnels qui encodent la sémantique visuelle
  • Les murmurs - OpenAI 's speech recognition model; transcribe audio to text with timestamps
  • BERT-NER - Reconnaissance de l'entité désignée ; extrait des endroits de texte des personnes, organisationsM SK3
  • Le temps de fonctionnement d'ONNX - Cross-inférence MML de la plate-formeM SK2 exploite les modèles sur le CPU /GPU sans bouclage de cadreMSC4in

Cet article porte sur:

  • Comment le VideoSummarizer orchestre cinq pipelines (ImageSommarizerM SK1 AudioSum marizer , NER, DocSummarzerMSSK4 GraphRAGMSC5
  • Architecture des ondes: M SK1 vagues de la normalisation à la production d'éléments probants
  • Système de capacités: Téléchargement de modèles lazy, Détection GPUM SK2 routage réactif
  • Optimisation du CLIP par lots pour les embeddings de cadres clés (3-5x plus rapidesM SK1
  • Déduplication perceptuelle du hash (40% réduction de cadresM SK1
  • Multi-cluster des scènes de signalisation M SK1insertion + transcription + type de coupe S+ temporelle )
  • Intégration NER pour l'extraction d'entités des transcriptions
  • Atomes éphémères pour la limite de vitesse, estimation du tempsM SK1 et pression d'arrière
  • Extrait: scènesM SK1 images, transcriptions , pistes de texte à titre d'éléments de preuve RAG

Articles connexes:


Le problème: La vidéo est coûteuse

Indicateurs de référence: Les chiffres ci-dessous mesurés sur AMD 9950X (16-coreM SK3 S/ NVIDIA A4000 | | (16GB | ) \ /\ | SSK9 | GB RAM | ordre-deM SK1effets de grandeur, aucune garantie de performance absolueM SK1

Un film typique contient:

  • ~170,000 cadres (2 heures à 24fpsM SK2
  • ~2 heures d'audio (discussionM SK1 musique , effets)
  • Les couches de texte multiples (sous-titresM SK1crédits , sur le texte de l’écran

L’approche du saumon (aucune personne ne le faitM SK1 mais elle fixe l’échelle):

  • Établissement de CLIP par cadre: M SK1ms × | | 9.4 heures
  • Vision LLM par cadre: M SK1s × | | 3 | 4 | 94 heures (cloud Vision API ballpark

Même avec l'extraction des cadres clés (say, 500-1000 framesM SK3 queMSC4est encore 100-200 secondes d'inférence CLIP sérieuseMST6

Approche traditionnelle: "Extracter les cadres de clésM SK2 envoyer à Vision LLM, espérer le meilleurMSC4

Problème: Il brûle les calculs sur des cadres redundants ( beaucoup de cadres clés sont visuellement similaires ), les traite en série

Solution: MultiM SK1 Filtration par étapes , Traitement en lots, et composition des pipelinesMSC4


L'architecture du VideoSummarizer

Mise en œuvre de VideoSummarizer RAG réduit pour le vidéo avec une réduction de trois étapes-

flowchart TB
    subgraph Input["Video File (.mp4, .mkv, etc.)"]
        VF[Video File]
    end

    subgraph Stage1["Stage 1: Structural Analysis"]
        N[NormalizeWave<br/>FFprobe metadata]
        SD[ShotDetectionWave<br/>Scene cuts via FFmpeg]
        KE[KeyframeExtractionWave<br/>I-frame + dedup]
        AE[AudioExtractionWave<br/>FFmpeg audio extract]
    end

    subgraph Stage2["Stage 2: Content Extraction"]
        IS[ImageSummarizer<br/>CLIP, OCR, Vision]
        AS[AudioSummarizer<br/>Whisper, Diarization]
        NER[NER Service<br/>Entity extraction]
        DS[DocSummarizer<br/>Text analysis if long]
        GR[GraphRAG<br/>Entity relationships]
    end

    subgraph Stage3["Stage 3: Scene Assembly"]
        SC[SceneClusteringWave<br/>Multi-signal clustering]
        EV[EvidenceGenerationWave<br/>RAG chunks]
    end

    VF --> N
    N --> SD --> KE
    N --> AE
    KE --> IS
    AE --> AS
    IS --> NER
    AS --> NER
    NER --> DS
    NER --> GR
    IS --> SC
    DS --> SC
    GR --> EV
    SC --> EV

    style Stage1 stroke:#22c55e,stroke-width:2px
    style Stage2 stroke:#3b82f6,stroke-width:2px
    style Stage3 stroke:#8b5cf6,stroke-width:2px

Autres éléments de preuve produits

Avant de s’engager dans la mise en oeuvre, ici ' c’est ce que vous obtiendrez M SK2le schéma de sortie):

Artéfact Champs clés CMS2 Source CMS3
Scène id, start_time, end_time, key_terms[], speaker_ids[], embedding[512] SceneClusteringWave
Récolte id, start_time, end_time, cut_type, keyframe_path ShotDetectionWave
Déclaration id, text, start_time, end_time, speaker_id, confidence TranscriptionWave
TextTrack id, text, start_time, text_type (titleMSC1credit/subtitle /ocrM SK4 MSC5 SubtitleExtractionWave SSK6
Cadre de clés id, timestamp, frame_path, dhash, clip_embedding[512] KeyframeExtractionWave

Chaque artefact comprend provenance: onde source , échantillon de temps de traitementM SK2 cote de confiance. Ces artefacts sont les unités de récupération primaire pour les demandes de vidéo; les fichiers bruts ne sont jamais récupérés directement.

Le pipeline de signalisation de la vague

VideoSummarizer utilise un architecture des ondes basée sur le signal- où chaque onde déclare explicitement ses contrats de signaux:

public interface ISignalAwareVideoWave
{
    /// <summary>Signals this wave requires before it can run.</summary>

    IReadOnlyList<string> RequiredSignals { get; }

    /// <summary>Signals this wave can optionally use if available.</summary>

    IReadOnlyList<string> OptionalSignals { get; }

    /// <summary>Signals this wave emits on successful completion.</summary>

    IReadOnlyList<string> EmittedSignals { get; }

    /// <summary>Cache keys this wave produces for downstream waves.</summary>

    IReadOnlyList<string> CacheEmits { get; }

    /// <summary>Cache keys this wave consumes from upstream waves.</summary>

    IReadOnlyList<string> CacheUses { get; }
}

Cela permet Coordination des ondes dynamiques:

  • Les ondes s'écoulent automatiquement si les signaux requis manquent
  • Résolution des dépendances au moment du démarrage (pas d’ordre hardcoded)
  • Les reruns partiels sont reproductibles (cache par clé de signalisation)
  • Accroître la granularité de l’UI: chaque vague émet le progrès indépendamment

Le pipeline 16-Wave

L’extraction des cadres-clés est mise en œuvre sous la forme de 7 ondes granulaires pour un meilleur parallèle et une plus grande efficacité du cache.

Écoule MSC1 Priorité MSC2 Besoins SMC3 Emits SMC4 Temps ESC5
Normaliser la vague 1000 - video.duration, video.fps, video.normalized ~2s S
FFmpegShotDetectionWave 900 video.normalized shots.detected, shots.count ~5-10s S
La vague de détection IFrame 850 video.normalized keyframes.iframes_detected, keyframes.iframes_count ~3s S
Voie de sélection des cadres-clés 840 shots.detected, keyframes.iframes_detected keyframes.selected, keyframes.selected_count ~1s S
La vague d'extraction des thumbnails 830 keyframes.selected keyframes.thumbnails_extracted ~5s S
KeyframeDeduplicationWave 820 keyframes.thumbnails_extracted keyframes.deduplicated, keyframes.duplicates_skipped ~1s S
KeyframeFullResExtractionWave 810 keyframes.deduplicated keyframes.extracted, keyframes.count ~10s S
ClipEmbeddingWave 800 keyframes.extracted clip.embeddings_ready, clip.embeddings_count ~30s S
ImageAnalysisWave 790 keyframes.deduplicated keyframes.analyzed, ocr.extracted ~60s S
TitleCreditsDetectionWave 750 shots.detected title.detected, credits.detected ~5s S
AudioExtractionWave 650 video.normalized audio.extracted, audio.path ~30s S
Voie de transcription 600 audio.extracted transcription.complete, transcription.utterance_count ~120s S
Sous-titreExtractionWave 550 video.normalized subtitles.extracted ~2s S
ChapitreExtractionWave 500 video.normalized chapters.extracted ~1s S
SceneClusteringWave 400 shots.detected scenes.detected, scene.count ~5s S
EvidenceGenerationWave 100 scenes.detected evidence.generated ~2s S

Notes:

  • Utilisations de ImageAnalysisWave keyframes.deduplicated (non completM SK1res): Le OCR fonctionne sur les thumbnails ; la sous-titrage de vision utilise le pleinMSC4res lorsqu'il est disponible par routage des capacitésMST5
  • Les vagues 3-7 sont les vagues granulaires de l’extraction du cadre-clé " sous

Total pour 2-heure de film: ~10-15 minutes (vsM SK3 heures sans optimisation)

Bien-Keys de signalisation connues

Les signaux sont définis comme des constantes pour la cohérence:

public static class VideoSignals
{
    // NormalizeWave signals
    public const string VideoDuration = "video.duration";
    public const string VideoFps = "video.fps";
    public const string VideoNormalized = "video.normalized";

    // Shot detection signals
    public const string ShotsDetected = "shots.detected";
    public const string ShotsCount = "shots.count";

    // Keyframe signals
    public const string IframesDetected = "keyframes.iframes_detected";
    public const string KeyframesSelected = "keyframes.selected";
    public const string KeyframesDeduplicated = "keyframes.deduplicated";
    public const string KeyframesExtracted = "keyframes.extracted";

    // CLIP embedding signals
    public const string ClipEmbeddingsReady = "clip.embeddings_ready";

    // Scene clustering signals
    public const string ScenesDetected = "scenes.detected";
    public const string SceneCount = "scene.count";

    // Transcription signals
    public const string TranscriptionComplete = "transcription.complete";
}

Système de capacité: Modèles lazy & Routage

VideoSummarizer utilise un capacité-architecture basée: détecter GPU une fois au démarrage , télécharger les modèles lentement

Manifeste de modèle (YAML + TypeM SK2Constants sûres)

Les modèles sont définis dans models.yaml. Aucune chaîne magique dans le code

# models.yaml (excerpt)
models:
  clip-vit-b32:
    name: "CLIP ViT-B/32"
    download_url: "https://huggingface.co/openai/clip-vit-base-patch32/resolve/main/onnx/visual_model.onnx"
    preferred_providers: [CUDAExecutionProvider, DmlExecutionProvider, CPUExecutionProvider]

components:
  ClipEmbeddingWave:
    models: [clip-vit-b32]
    fallback_chain: [ImageAnalysisWave]
// Type-safe constants (no raw strings)
await coordinator.EnsureModelAsync(ModelIds.ClipVitB32);
await coordinator.ActivateWaveAsync(ComponentIds.TranscriptionWave);

// Route with fallback
var route = await coordinator.RouteWorkAsync(new[]
{
    ComponentIds.ClipEmbeddingWave,    // Primary (GPU)
    ComponentIds.ImageAnalysisWave     // Fallback (CPU)
});

Atomes d'efficacité du pipeline

Limitation des vitesses, estimation du temps , et pression d'arrière adaptative permettent à l'interface de rester réceptive tout en maximisant le débit

// Time estimation from actual data
var estimator = CapabilityAtoms.CreateTimeEstimator();
using (estimator.Time("clip_embedding")) { await ProcessAsync(); }
var eta = estimator.GetEstimate("clip_embedding", remaining: 50);
// eta.Estimated, eta.Optimistic, eta.Pessimistic, eta.Confidence

Dossiers du système de capacité complète: Voir Mostlylucid.Summarizer.Core/Capabilities/ pour la détection de GPU, signal pub/subM SK2 contrôleurs d'arrière-pressure , et conception de topologie en filet


Optimisation des clés 1: Déduplication perceptuelle de l'abréviation

Avant de lancer des embeddings CLIP coûteux, VideoSummarizer filtre les cadres visuellement similaires en utilisant Différence hash (dHashM SK1.

Comment fonctionne dHash

public class KeyframeDeduplicationService
{
    // dHash parameters: 9x8 grayscale = 64 bits
    private const int HashWidth = 9;
    private const int HashHeight = 8;
    private const int DefaultHammingThreshold = 10;

    public async Task<ulong> ComputeDHashAsync(string imagePath, CancellationToken ct)
    {
        using var image = Image.Load<Rgba32>(imagePath);

        // Resize to 9x8 (one extra column for gradient comparison)
        image.Mutate(x => x
            .Resize(HashWidth, HashHeight)
            .Grayscale());

        ulong hash = 0;
        int bit = 0;

        // Compare adjacent pixels horizontally
        for (int y = 0; y < HashHeight; y++)
        {
            for (int x = 0; x < HashWidth - 1; x++)
            {
                var left = image[x, y].R;
                var right = image[x + 1, y].R;

                // Set bit if left pixel is brighter than right
                if (left > right)
                {
                    hash |= (1UL << bit);
                }
                bit++;
            }
        }

        return hash;
    }

    public static int HammingDistance(ulong a, ulong b) =>
        BitOperations.PopCount(a ^ b);
}

Exemple de sortie:

Input: 50 keyframe candidates (from codec I-frames)

Deduplication (Hamming threshold 10):
  Frame 0: hash=0x8f3a2c1d → KEEP (first frame)
  Frame 1: hash=0x8f3a2c1e → SKIP (distance=1 from frame 0)
  Frame 2: hash=0x8f3a2c1f → SKIP (distance=2 from frame 0)
  Frame 3: hash=0xc7e1b4a2 → KEEP (distance=28 from frame 0)
  ...

Result: 50 → 30 frames (40% reduction)
Processing saved: ~8 seconds of CLIP inference

Pourquoi cela importe:

  • Réduction du cadre ~40% sur le contenu typique
  • <1ms par cadre pour le calcul du hash (vsM SK1 200ms pour CLIP)
  • Filtre les cadres redundants précédemment Opérations GPU coûteuses

Optimisation des clés 2: Embedding de CLIP en série

Au lieu de traiter une image à la fois, Batches vidéoSummarizer M SK1 images par passeur GPU.

Architecture de traitement par lots

public class BatchClipEmbeddingService
{
    private const int ClipImageSize = 224;
    private const int DefaultBatchSize = 8; // 8 images per GPU pass

    public async Task<Dictionary<int, float[]>> GenerateBatchEmbeddingsAsync(
        Dictionary<int, string> framePaths,
        IBackpressureController backpressure,
        ITimeEstimator estimator,
        int batchSize = DefaultBatchSize,
        CancellationToken ct = default)
    {
        var session = await GetOrLoadClipModelAsync(ct);
        var results = new ConcurrentDictionary<int, float[]>();

        // Pre-index batch for O(1) lookup
        var batches = framePaths
            .Select((kvp, idx) => (idx, kvp.Key, kvp.Value))
            .Chunk(batchSize)
            .ToList();

        foreach (var batch in batches)
        {
            // Acquire slot from backpressure controller (adaptive concurrency)
            using var slot = await backpressure.AcquireSlotAsync(ct);
            using var timer = estimator.Time("clip_batch");

            // Create batch tensor [batchSize, 3, 224, 224]
            var tensor = new DenseTensor<float>(new[] { batch.Length, 3, ClipImageSize, ClipImageSize });

            // Preprocess images into tensor slots
            for (int i = 0; i < batch.Length; i++)
            {
                var (_, frameIndex, path) = batch[i];
                PreprocessImageToTensor(path, tensor, slotIndex: i);
            }

            // Single GPU pass for entire batch
            var inputs = new List<NamedOnnxValue>
            {
                NamedOnnxValue.CreateFromTensor("input", tensor)
            };

            using var outputResults = session.Run(inputs);
            // Extract embeddings, record latency for backpressure adjustment
        }

        backpressure.RecordLatency(estimator.GetAverageDuration("clip_batch"));
        return new Dictionary<int, float[]>(results);
    }
}

Comparaison des performances:

Input: 30 keyframes (after deduplication)

Serial processing (1 frame at a time):
  30 × 200ms = 6,000ms (6.0 seconds)

Batch processing (8 frames per pass):
  4 batches × 350ms = 1,400ms (1.4 seconds)

Speedup: 4.3x

Pourquoi le traitement en lots fonctionne:

  • Le parallèle GPU est sous-utilisé avec une seule inference d’image-
  • Tensor par lots [8, 3, 224, 224] Utilise la même mémoire GPU que l'image unique (almost)
  • ONNX Runtime optimise les opérations de lot internement

Optimisation clé 3: Composition du pipeline

VideoSummarizer n’invente pas de nouveau ImageSommarizer ou AudioSum marizer. Il • les chaînes ils.

Keyframe Sub-Pipeline:Intégration ImageSummarizer

L'extraction du cadre de clés est divisée en 7 vagues granulaires (voir la table des vagues ci-dessusM SK2 Voici le modèle de coordination montrant comment elles se chainent ensemble:

// IFrameDetectionWave → KeyframeSelectionWave → ThumbnailExtractionWave
// → KeyframeDeduplicationWave → KeyframeFullResExtractionWave → ClipEmbeddingWave

// ClipEmbeddingWave coordinates with ImageSummarizer
public class ClipEmbeddingWave : IVideoWave, ISignalAwareVideoWave
{
    private readonly BatchClipEmbeddingService _batchClipService;
    private readonly IBackpressureController _backpressure;
    private readonly ITimeEstimator _estimator;

    public IReadOnlyList<string> RequiredSignals => [VideoSignals.KeyframesExtracted];
    public IReadOnlyList<string> EmittedSignals => [VideoSignals.ClipEmbeddingsReady];

    public async Task ProcessAsync(VideoContext context, CancellationToken ct)
    {
        var keyframes = context.GetCached<Dictionary<int, string>>("keyframes.paths");

        // Batch CLIP embedding with backpressure control
        var embeddings = await _batchClipService.GenerateBatchEmbeddingsAsync(
            keyframes, _backpressure, _estimator, batchSize: 8, ct);

        foreach (var (frameIndex, embedding) in embeddings)
            context.KeyframeEmbeddings[frameIndex] = embedding;
    }
}

// ImageAnalysisWave runs ImageSummarizer on deduplicated frames
public class ImageAnalysisWave : IVideoWave, ISignalAwareVideoWave
{
    public IReadOnlyList<string> RequiredSignals => [VideoSignals.KeyframesDeduplicated];

    public async Task ProcessAsync(VideoContext context, CancellationToken ct)
    {
        var keyframePaths = context.GetCached<List<string>>("keyframes.deduplicated_paths");

        foreach (var path in keyframePaths)
        {
            // Run ImageSummarizer for OCR, vision, captions
            var result = await _imageOrchestrator.AnalyzeAsync(path, ct);
            context.SetCached($"image_analysis.{Path.GetFileName(path)}", result);
        }
    }
}

TranscriptionWave: AudioSummarizer Integration

L’extraction et la transcription sonores sont maintenant des signaux distincts-onde de sensibilisationM SK1

// AudioExtractionWave runs first (extracts audio track from video)
public class AudioExtractionWave : IVideoWave, ISignalAwareVideoWave
{
    public IReadOnlyList<string> RequiredSignals => [VideoSignals.VideoNormalized];
    public IReadOnlyList<string> EmittedSignals => ["audio.extracted", "audio.path"];

    public async Task ProcessAsync(VideoContext context, CancellationToken ct)
    {
        var audioPath = await _ffmpegService.ExtractAudioAsync(
            context.VideoPath, context.WorkingDirectory, ct);
        context.SetCached("audio.path", audioPath);
    }
}

// TranscriptionWave depends on audio.extracted signal
public class TranscriptionWave : IVideoWave, ISignalAwareVideoWave
{
    public IReadOnlyList<string> RequiredSignals => ["audio.extracted"];
    public IReadOnlyList<string> EmittedSignals => [
        VideoSignals.TranscriptionComplete,
        "transcription.utterance_count"
    ];

    public async Task ProcessAsync(VideoContext context, CancellationToken ct)
    {
        var audioPath = context.GetCached<string>("audio.path");

        // Run AudioSummarizer pipeline (Whisper + diarization)
        var audioProfile = await _audioOrchestrator.AnalyzeAsync(audioPath, ct);

        // Extract utterances with speaker info
        var turns = audioProfile.GetValue<List<SpeakerTurn>>("speaker.turns");
        foreach (var turn in turns ?? [])
        {
            context.Utterances.Add(new Utterance
            {
                Id = Guid.NewGuid(),
                Text = turn.Text,
                StartTime = turn.StartSeconds,
                EndTime = turn.EndSeconds,
                SpeakerId = turn.SpeakerId,
                Confidence = turn.Confidence
            });
        }

        // Run NER on full transcript for entity extraction
        var transcript = audioProfile.GetValue<string>("transcription.full_text");
        if (!string.IsNullOrEmpty(transcript))
        {
            var entities = await _nerService.ExtractEntitiesAsync(transcript, ct);
            context.SetCached("transcript_entities", entities);

            // Emit entity signals by type (PER, ORG, LOC, MISC)
            foreach (var group in entities.GroupBy(e => e.Type))
            {
                context.AddSignal($"transcript.entities.{group.Key.ToLowerInvariant()}",
                    group.Select(e => e.Text).Distinct().ToList());
            }
        }
    }
}

NER Integration: Reconnaissance des entités désignées

VideoSummarizer extrait des entités désignées de transcriptions en utilisant BERT-based NER M SK1ONNX).

OnnxNerService

public class OnnxNerService
{
    // Model: dslim/bert-base-NER (ONNX exported)
    // Entities: PER (Person), ORG (Organization), LOC (Location), MISC (Miscellaneous)

    public async Task<List<EntitySpan>> ExtractEntitiesAsync(string text, CancellationToken ct)
    {
        var entities = new List<EntitySpan>();

        // Chunk long text (BERT max 512 tokens)
        foreach (var chunk in ChunkText(text, maxTokens: 400, overlap: 50))
        {
            // Tokenize with WordPiece
            var tokens = _tokenizer.Tokenize(chunk);

            // Run ONNX inference
            var inputs = PrepareInputs(tokens);
            using var results = _session.Run(inputs);

            // Decode BIO tags
            var predictions = DecodePredictions(results);
            var chunkEntities = ExtractEntitySpans(tokens, predictions);

            entities.AddRange(chunkEntities);
        }

        // Deduplicate entities
        return entities
            .GroupBy(e => (e.Text.ToLowerInvariant(), e.Type))
            .Select(g => g.First())
            .ToList();
    }
}

Exemple de sortie:

Transcript: "Today we're speaking with John Smith from Microsoft about
their new AI lab in Seattle. The project, codenamed Phoenix, builds
on research from Stanford University."

Entities extracted:
  PER: John Smith
  ORG: Microsoft, Stanford University
  LOC: Seattle
  MISC: Phoenix

Signals emitted:
  transcript.entities.per = ["John Smith"]
  transcript.entities.org = ["Microsoft", "Stanford University"]
  transcript.entities.loc = ["Seattle"]
  transcript.entities.misc = ["Phoenix"]

Pourquoi le NER est important pour la vidéo:

  • Enablez des questions comme "Trouver des vidéos mentionnant Microsoft"
  • Liens vers le graphique de l'entité DocSummarizer
  • Fournit des métadonnées structurées sans inference LLM

Multi-Clustering des scènes de signalisation

L'utilisation d'embeddings CLIP seuls pour la détection des scènes n'est pas efficace't fonctionne bien. Les cadres clés sont peu nombreux par le design M SK2un changement par shot ), mais les prises de vue sont densesMSC4 Avec des embeddeds CLIP5 pour Scènes MCSK6, une couverture C(~2% un regroupement pure d'emplacements ne produit qu'une scène pour un film d'une durée de 2-heure M.

VideoSummarizer utilise un approche de signal multi- qui combine les signaux pondérés 4 pour une détection robuste des limites de la scène . Les limites sont choisies par Évaluation pondérée déterministe, fin non apprise-àM SK2finMSC3

SceneClusteringWave: Multi-Architecture de signalisation

public class SceneClusteringWave : IVideoWave, ISignalAwareVideoWave
{
    // Signal weights for boundary scoring
    private const double EmbeddingWeight = 0.4;   // CLIP embedding dissimilarity
    private const double TranscriptWeight = 0.3;  // Semantic shift in transcript
    private const double CutTypeWeight = 0.2;     // Fade/dissolve detection
    private const double TemporalWeight = 0.1;    // Time since last scene

    // Temporal constraints
    private const double MinSceneDuration = 15.0;   // Don't split scenes < 15s
    private const double MaxSceneDuration = 300.0;  // Force split at 5 minutes
    private const double TargetSceneDuration = 90.0; // Prefer ~90s scenes

    public IReadOnlyList<string> RequiredSignals => [VideoSignals.ShotsDetected];
    public IReadOnlyList<string> OptionalSignals => [
        VideoSignals.ClipEmbeddingsReady,
        VideoSignals.TranscriptionComplete,
        VideoSignals.KeyframesDeduplicated
    ];
    public IReadOnlyList<string> EmittedSignals => [
        VideoSignals.ScenesDetected,
        "scene.count",
        "scene.avg_duration",
        "scene.clustering_method"
    ];

    private List<(int shotIndex, double score)> ComputeBoundaryScores(VideoContext context)
    {
        var shots = context.Shots.OrderBy(s => s.StartTime).ToList();
        var scores = new List<(int, double)>();

        // Build embedding map with nearest-neighbor interpolation
        var shotEmbeddings = PropagateEmbeddingsToNearbyShots(context, shots);

        // Build transcript windows for semantic shift detection
        var transcriptWindows = BuildTranscriptWindows(context, shots, windowSeconds: 10);

        for (int i = 0; i < shots.Count - 1; i++)
        {
            double score = 0;
            var currentShot = shots[i];
            var nextShot = shots[i + 1];

            // 1. Embedding dissimilarity (40%)
            if (shotEmbeddings.TryGetValue(i, out var currentEmbed) &&
                shotEmbeddings.TryGetValue(i + 1, out var nextEmbed))
            {
                var similarity = CosineSimilarity(currentEmbed, nextEmbed);
                score += (1.0 - similarity) * EmbeddingWeight;
            }

            // 2. Transcript semantic shift (30%)
            if (transcriptWindows.TryGetValue(i, out var currentWords) &&
                transcriptWindows.TryGetValue(i + 1, out var nextWords))
            {
                var overlap = currentWords.Intersect(nextWords).Count();
                var union = currentWords.Union(nextWords).Count();
                var jaccard = union > 0 ? (double)overlap / union : 0;
                score += (1.0 - jaccard) * TranscriptWeight;
            }

            // 3. Cut type signal (20%) - fades/dissolves suggest scene boundaries
            if (currentShot.CutType is "fade" or "dissolve")
            {
                score += CutTypeWeight;
            }

            // 4. Temporal pressure (10%) - encourage splits near target duration
            var timeSinceLastScene = currentShot.EndTime - GetLastSceneBoundary();
            if (timeSinceLastScene > TargetSceneDuration)
            {
                var pressure = Math.Min(1.0, (timeSinceLastScene - TargetSceneDuration) / 60);
                score += pressure * TemporalWeight;
            }

            scores.Add((i, score));
        }

        return scores;
    }
}

Principales innovations

  1. Présence-Propagation de l'emplacement du voisin: Seules les prises de ~2% ont un emplacement CLIP direct.

  2. Windows sémantiques de la traduction: Construit des fenêtres de mots sur une seconde autour d'une image et détecte les déplacements sémantiques à l'aide d'un distance Jaccard ( un proxye de dérive sémantique bon marché mais robuste

  3. Connaissance du type de coupe: FadeM SK1to-les transitions noires et de dissociation indiquent fortement les limites de la scène

  4. Limite d'adaptation: Au lieu d'un seuil fixeM SK1 sélectionne les limites de la partie supérieure 25% des notes

  5. Contraintes temporelles: Exécute le minimum des scènes et les limites de 15 à

Exemple:

Input: 1881 shots from a 2-hour movie
       39 keyframes with CLIP embeddings
       2302 utterances from transcript

Boundary scoring per shot:
  Shot 45-46: embedding=0.15, transcript=0.32, cut=0.0, temporal=0.0 → score=0.156
  Shot 46-47: embedding=0.08, transcript=0.12, cut=0.0, temporal=0.0 → score=0.068
  Shot 47-48: embedding=0.35, transcript=0.41, cut=0.2, temporal=0.05 → score=0.388 ← BOUNDARY
  ...

Adaptive threshold (top 25%): 0.25
Natural boundaries found: 45

Output: 47 scenes (avg 2.6 minutes per scene)
  - Min scene: 15.2s
  - Max scene: 298.4s
  - Total coverage: 100%

Signals:
  scenes.detected = true
  scene.count = 47
  scene.avg_duration = 156.3
  scene.clustering_method = "multi_signal_weighted"

Le contrat de signalisation vidéo

VideoSummarizer prolonge le contrat de signalisation d’ImageSum marizer et AudioSommarizer:

public record VideoSignal
{
    public required string Key { get; init; }      // "scene.count", "transcript.entities.per"
    public object? Value { get; init; }
    public double Confidence { get; init; } = 1.0;
    public required string Source { get; init; }   // "SceneClusteringWave"

    // Video-specific: time range
    public double? StartTime { get; init; }
    public double? EndTime { get; init; }

    public DateTime Timestamp { get; init; }
    public Dictionary<string, object>? Metadata { get; init; }
    public List<string>? Tags { get; init; }       // ["visual", "scene"]
}

public static class VideoSignalTags
{
    public const string Visual = "visual";
    public const string Audio = "audio";
    public const string Speech = "speech";
    public const string Ocr = "ocr";
    public const string Motion = "motion";
    public const string Scene = "scene";
    public const string Shot = "shot";
    public const string Metadata = "metadata";
}

Signaux clés émis:

Système Source Description S
video.duration Normalisation de la vague Durée totale en secondes
video.resolution Normalisation de la vague largeurM SK2 hauteur
video.fps Normalisation de la vague Taux d'étalonnage
shots.count ShotDetectionWave Nombre de tirs détectés
keyframes.count KeyframeExtractionWave
keyframes.duplicates_skipped KeyframeExtractionWave
scene.count SceneClusteringWave Segments de scène cohérents
transcript.entities.per TranscriptionWave Nom des personnes provenant de l'NER
transcript.entities.org TranscriptionWave Nom de l'organisation MSC2
transcript.word_count TranscriptionWave Total des mots dans la transcription

VideoPipeline: Output RAG

L'article 5 de la Loi sur l'immigration et le statut des réfugiés VideoPipeline convert les signaux vidéo en ContentChunk pour l'indexation RAG:

public class VideoPipeline : PipelineBase
{
    public override string PipelineId => "video";
    public override IReadOnlySet<string> SupportedExtensions => new HashSet<string>
    {
        ".mp4", ".mkv", ".avi", ".mov", ".wmv", ".webm", ".flv", ".m4v", ".mpeg", ".mpg"
    };

    private List<ContentChunk> BuildContentChunks(VideoContext context, string filePath)
    {
        var chunks = new List<ContentChunk>();

        // 1. Scene-based chunks (best for video retrieval)
        foreach (var scene in context.Scenes)
        {
            var sceneText = BuildSceneText(context, scene);
            var embedding = context.GetCached<float[]>($"scene_centroid.{scene.Id}");

            chunks.Add(new ContentChunk
            {
                Text = sceneText,
                ContentType = ContentType.Summary,
                Embedding = embedding,  // Proper vector column, not metadata
                Metadata = new Dictionary<string, object?>
                {
                    ["source"] = "video_scene",
                    ["scene_id"] = scene.Id,
                    ["key_terms"] = scene.KeyTerms,
                    ["speakers"] = scene.SpeakerIds,
                    ["start_time"] = scene.StartTime,
                    ["end_time"] = scene.EndTime
                }
            });
        }

        // 2. Transcript chunks (1-minute windows)
        var transcriptChunks = BuildTranscriptChunks(context, filePath);
        chunks.AddRange(transcriptChunks);

        // 3. Text track chunks (on-screen text/subtitles)
        foreach (var textTrack in context.TextTracks)
        {
            chunks.Add(new ContentChunk
            {
                Text = $"On-screen text: {textTrack.Text}",
                ContentType = ContentType.ImageOcr,
                Metadata = new Dictionary<string, object?>
                {
                    ["source"] = "video_ocr",
                    ["text_type"] = textTrack.TextType.ToString(),
                    ["start_time"] = textTrack.StartTime
                }
            });
        }

        return chunks;
    }

    private string BuildSceneText(VideoContext context, SceneSegment scene)
    {
        var parts = new List<string>();

        if (!string.IsNullOrEmpty(scene.Label))
            parts.Add($"Scene: {scene.Label}");

        parts.Add($"[{FormatTime(scene.StartTime)} - {FormatTime(scene.EndTime)}]");

        if (scene.KeyTerms.Count > 0)
            parts.Add($"Topics: {string.Join(", ", scene.KeyTerms)}");

        // Add utterances in this scene
        var sceneUtterances = context.Utterances
            .Where(u => u.StartTime >= scene.StartTime && u.EndTime <= scene.EndTime)
            .OrderBy(u => u.StartTime);

        if (sceneUtterances.Any())
            parts.Add($"Speech: {string.Join(" ", sceneUtterances.Select(u => u.Text))}");

        return string.Join("\n", parts);
    }
}

Exemple de sortie d'un film:

{
  "chunks": [
    {
      "text": "Scene: Opening montage\n[0:00 - 2:34]\nTopics: city, night, traffic\nSpeech: The year is 2049. The world has changed.",
      "contentType": "Summary",
      "metadata": {
        "source": "video_scene",
        "scene_id": "abc123",
        "key_terms": ["city", "night", "traffic"],
        "start_time": 0.0,
        "end_time": 154.0
      }
    },
    {
      "text": "The detective arrived at the crime scene. Forensics had already processed the area.",
      "contentType": "Transcript",
      "metadata": {
        "source": "video_transcript",
        "time_window": "2:34 - 3:34",
        "utterance_count": 4
      }
    },
    {
      "text": "On-screen text: LOS ANGELES 2049",
      "contentType": "ImageOcr",
      "metadata": {
        "source": "video_ocr",
        "text_type": "Title"
      }
    }
  ]
}

Caractéristiques du rendement

Temps de traitement (2-film d'une heureM SK1 1080p)

Étape MSC1 Temps MSC2 Notes ♫ MSC3 ♫
FFprobe métadonnées
Détection des coups MSC2 s MSC3 Filtre de scène FFmpeg S
Extraction des cadres-clés ~30s ≥ ≤500 I-frames
deduplication dHash MSC2s MSC3 \MSC4 \ MSC5 \ msc6 \ cadres \
Établissement d'un CLIP en série CSK2s \300 cadres, en série
ImageSummarizer OCR
Extraction de l'audio MSC2s MSC3 FFmpeg SMC4
Transcription de murmurer
Diarisation de l'émetteur
Extraction de l’NER ~10s M BERTM SK4NER sur la transcription P
Groupement des scènes ~5s ≥ ≤
Production de données probantes
Total ~8-10 minutes

Sans optimisation

Optimisation Épargne
déduplication dHash ~40% cadres filtrés CLIP conservé DS=
CLIP en série MSC2x plus rapide SMC3 SMC4s conservés SMC5
Composition du pipeline Réutilise l'imageSummarizerM SK2Vêtes audioSommarizer
Total des économies ~3-4 minutes

Utilisation de la mémoire

Composante Mémoire
CLIP ViTM SK1B/32 ONNX ~350MB SSK5
Base de whisper
ECAPAM SK1TDNN ~100MB S
BERTM SK1NER ~500MB S
Prêt ~1.5GB

L'intégration avec lucidRAG

VideoSummarizer s'inscrit comme un IPipeline pour routage automatique:

// In Program.cs
builder.Services.AddDocSummarizer(builder.Configuration.GetSection("DocSummarizer"));
builder.Services.AddDocSummarizerImages(builder.Configuration.GetSection("Images"));
builder.Services.AddVideoSummarizer();  // NEW
builder.Services.AddPipelineRegistry(); // Must be last

// Auto-routing by extension
var registry = services.GetRequiredService<IPipelineRegistry>();
var pipeline = registry.FindForFile("movie.mp4");  // Returns VideoPipeline
var result = await pipeline.ProcessAsync("movie.mp4");

Extensions supportées:

  • .mp4, .mkv, .avi, .mov, .wmv, .webm, .flv, .m4v, .mpeg, .mpg

Ce que vous obtiez

  • Échantillons RAG de niveau Scene-: Segments cohérents avec les transcriptions , termes clés
  • Méthodes de preuve multimodales: Visual (keyframe embeddings), audio (speaker diarizationM SK4texte MSC5OCRMSC6 sous-titresMST7
  • Entités désignées: PersonnesM SK1 Organisations , emplacements à partir de la transcription NER
  • provenance vérifiable: Tous les signaux ont une vague de source
  • Traitement efficace: 10-15 minutes pour un film d’une durée de 2 heures.

Quels sont les coûts?

  • Memory GPU ~1.5GB pour tous les modèles ONNX
  • ~8-10 traitement des minutes par 2-heure de film
  • espace de disques pour les fichiers intermédiaires ( nettoyé automatiquement)
  • Complexité: L’orchestrement de cinq pipelines exige une compréhension des dépendances d’ondes

Conclusion

VideoSummarizer démontre que composition du pipeline échelles:

  1. Réutilisation de pipelines spécialisés: DonM SK1t reinventer ImageSummarizer ou AudioSommarizer. Chain them
  2. Filtrer avant les opérations coûteuses: coûts de déduplication dHash <1msM SK2 permet d'éviter 40% des conclusions du CLIP
  3. Opérations GPU par lots: 8 images par passe = ≥3-5x accélération
  4. Extraire la structure avant le contenu: Shots → Scenes → Evidence (not raw frames SME4 LLMSMA5
  5. Gestion du modèle lazi: Télécharger les modèles seulement si nécessaire, détecter GPU automatiquement
  6. Routage réactif: Travail de route vers les composants disponibles , retour gracieux

Le résultat : est de transformer un film d’une heure en un livret de signalisation structuré comportant des scènes, des transcriptions et des entités, ainsi que des embeddings.

  • "Trouver des scènes où John Smith discute de Microsoft
  • "Pour voir des clips avec le texte de l’écran sur le projet Phoenix
  • "Trouver des vidéos similaires à cette scène

Le modèle RAG réduit pour le vidéo:

Ingestion:  Video → 16 waves → Signals + Evidence (scenes, transcripts, entities)
Storage:    Signals (indexed) + Embeddings (CLIP, voice) + Evidence (chunks)
Query:      Filter (SQL) → Search (BM25 + vector) → Synthesize (LLM, ~5 results)

Le système de capacité:

Startup:    Detect GPU → Load ModelManifest (YAML) → Initialize SignalSink
Activation: Component requests model → Lazy download → Signal "ModelAvailable"
Routing:    Route to best provider → Fallback chain → Backpressure control
Atoms:      Rate limiting + Time estimation + Pipeline balancing

Il s'agit de Fuzziness restreinte à l’échelle

  • Components probabilistes proposent des signaux: embeddings (CLIPM SK2 hypothèses OCR, prédictions de diarisation
  • La cotation déterministe assemble la structure: cotes limites pondérées, sélection de seuilM SK2 contraintes temporelles
  • LLM (optionnel) synthétise à partir de données probantes: contexte limité , provenance vérifiable

Le LLM fonctionne sur pré-computed,éléments probants vérifiables, jamais vidéo bruteM SK1


Ressources

lucidRAG Documentation

Bibliothèques connexes

Modèles ONNX

Articles connexes

Patterns de base:

Réduction des mises en œuvre de RAG:


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 Audio médico-légal, diarisation de l'émetteur
4.3 VideoSummarizer (le présent articleM SK1 Orchestrement vidéo, lots CLIP, NER

Suivant: MultiM SK1 Graphique modal RAG avec lucidRAG. Composant les cinq résumés en un graphe de connaissance unifié à l'aide d'une liaison croisée

Toutes les parties suivent la même invariante: Les composantes probabilistes proposent; les systèmes déterministes persistent.

logo

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