Skip to content

Instantly share code, notes, and snippets.

@co-l
Created January 31, 2026 14:51
Show Gist options
  • Select an option

  • Save co-l/9379e7b1b68c53b658bcbab49da61270 to your computer and use it in GitHub Desktop.

Select an option

Save co-l/9379e7b1b68c53b658bcbab49da61270 to your computer and use it in GitHub Desktop.

Nicefox Video Editor - Plan Technique

Vue d'ensemble

Éditeur vidéo web optimisé pour le montage de vidéos "talking-head" (screencasts, podcasts, tutoriels). L'innovation principale est l'édition pilotée par la transcription : au lieu de naviguer manuellement dans la vidéo, on édite la transcription et la vidéo suit.

Profil vidéo cible (basé sur des fichiers réels)

  • Résolution : 2560×1440 (1440p)
  • Framerate : 60 fps
  • Durée : jusqu'à 2 heures
  • Taille fichier : ~1 GB (bitrate ~1 Mbps depuis OBS)
  • Format : conteneur MKV, vidéo H.264, audio AAC

Stack Technique

Composant Choix
Frontend React 19 + TypeScript + Vite (port ${5173})
State management Zustand
Backend Node.js + Express + TypeScript (port ${3000})
Base de données leangraph (Cypher/SQLite embarqué)
Job Queue Persistée dans leangraph
Transcription WhisperX large-v3, venv Python, français
Traitement vidéo FFmpeg (subprocess)
Temps réel WebSocket (ws)
Tests Vitest

Dépendances leangraph

leangraph est publié sur npm (https://www.npmjs.com/package/leangraph). La première version publiée est 1.0.0 (il n'existe pas de version 0.x).

npm install leangraph             # latest version (>=1.0.0)
npm install -D better-sqlite3@^12 # devDependency (module natif SQLite, ^12 requis pour Node 24+)

Configuration leangraph

import { LeanGraph, LeanGraphError } from 'leangraph';

// Mode local (défaut) - persiste dans ./data/nicefox.db
const db = await LeanGraph({ 
  project: 'nicefox',
  dataPath: './data'
});

// Mode test (pour tests unitaires) - DB in-memory, reset à chaque run
const testDb = await LeanGraph({ 
  mode: 'test', 
  project: 'nicefox' 
});

Patterns leangraph importants

// Batch insert avec UNWIND (pour les mots de transcription)
await db.execute(`
  UNWIND $words AS w
  MATCH (seg:Segment {id: $segmentId})
  CREATE (seg)-[:HAS_WORD]->(word:Word {
    id: w.id, word: w.word, start: w.start, end: w.end, confidence: w.confidence
  })
`, { segmentId, words });

// Upsert avec MERGE (pour les jobs)
await db.execute(`
  MERGE (j:Job {id: $id})
  ON CREATE SET j.type = $type, j.status = 'pending', j.createdAt = datetime()
  ON MATCH SET j.status = $status, j.progress = $progress
`, { id, type, status, progress });

// Gestion d'erreurs typée
try {
  await db.query('...');
} catch (err) {
  if (err instanceof LeanGraphError) {
    console.error(`Query failed: ${err.message} at line ${err.line}, column ${err.column}`);
  }
}

// IMPORTANT: Toujours fermer la connexion à l'arrêt du serveur
process.on('SIGTERM', () => db.close());
process.on('SIGINT', () => db.close());

Préférences de Développement

Code

  • Programmation fonctionnelle : Favoriser les fonctions pures, l'immutabilité, les compositions de fonctions, map/filter/reduce plutôt que les boucles impératives. Éviter les classes quand possible, préférer les modules avec des fonctions exportées.
  • React 19 : Utiliser les nouvelles APIs quand pertinent :
    • ref comme prop directe (pas de forwardRef)
    • <Context value={...}> au lieu de <Context.Provider value={...}>
    • useOptimistic pour les mises à jour UI optimistes lors des éditions
    • Cleanup functions dans les callbacks ref

UI

  • Dark mode uniquement : Pas de thème clair, design sombre par défaut avec des contrastes appropriés pour le travail prolongé sur vidéo.

Architecture

┌─────────────────────────────────────────────────────────────────┐
│                        Navigateur (React)                       │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  Composant Timeline                                      │   │
│  │  - Piste vidéo (thumbnails)                              │   │
│  │  - Piste audio (visualisation waveform)                  │   │
│  │  - Overlay transcription (mots cliquables)               │   │
│  │  - Contrôles zoom/pan                                    │   │
│  └─────────────────────────────────────────────────────────┘   │
│  ┌─────────────────┐  ┌─────────────────┐  ┌───────────────┐   │
│  │  Lecteur Vidéo  │  │  Contrôles      │  │ Panneau       │   │
│  │  (HTML5 video)  │  │  d'édition      │  │ Export        │   │
│  └─────────────────┘  └─────────────────┘  └───────────────┘   │
└─────────────────────────────────────────────────────────────────┘
                              │ HTTP/WebSocket
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                      Serveur Node.js                            │
│  - API REST pour projets, sources, éditions                     │
│  - Serveur de fichiers statiques (vidéo, thumbnails, waveforms) │
│  - WebSocket pour progression des jobs                          │
│  - Gestion de la queue de jobs                                  │
│  - Base de données leangraph                                    │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                    Workers de traitement                        │
│  ┌──────────────────┐  ┌──────────────────┐  ┌──────────────┐  │
│  │  WhisperX        │  │  FFmpeg          │  │  FFmpeg      │  │
│  │  (transcription) │  │  (thumbnails,    │  │  (export)    │  │
│  │                  │  │   waveform)      │  │              │  │
│  │  Python/venv     │  │                  │  │              │  │
│  └──────────────────┘  └──────────────────┘  └──────────────┘  │
└─────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                       Stockage Fichiers                         │
│  /data                                                          │
│  ├── nicefox.db                # Base de données leangraph      │
│  └── projects/                                                  │
│      └── {project-id}/                                          │
│          ├── sources/                                           │
│          │   └── {source-id}/                                   │
│          │       ├── original.mkv    # Vidéo originale          │
│          │       ├── waveform.bin    # Niveaux audio par frame  │
│          │       └── thumbnails/     # Sprite sheets            │
│          └── exports/                                           │
│              └── {export-id}.mp4                                │
└─────────────────────────────────────────────────────────────────┘

Modèle de Données (leangraph/Cypher)

Schéma du graphe

// Projet principal
(:Project {id, name, createdAt, updatedAt})

// Source vidéo importée
(:Source {id, filename, path, duration, width, height, fps, status})
// status: 'importing' | 'processing' | 'ready' | 'error'

// Segment de transcription (phrase/paragraphe)
(:Segment {id, start, end, text})

// Mot individuel avec timestamps précis
(:Word {id, word, start, end, confidence})

// Clip dans la timeline (ce qu'on garde)
(:Clip {id, inPoint, outPoint, order})

// Job de traitement
(:Job {id, type, status, progress, error, createdAt, startedAt, completedAt})
// type: 'thumbnails' | 'waveform' | 'transcription' | 'export'
// status: 'pending' | 'running' | 'completed' | 'failed'

Relations

(:Project)-[:HAS_SOURCE]->(:Source)
(:Source)-[:HAS_SEGMENT]->(:Segment)
(:Segment)-[:HAS_WORD]->(:Word)
(:Project)-[:HAS_CLIP]->(:Clip)
(:Clip)-[:FROM_SOURCE]->(:Source)
(:Source)-[:HAS_JOB]->(:Job)

Exemples de requêtes

// Créer un projet
CREATE (p:Project {id: $id, name: $name, createdAt: datetime()})

// Récupérer un projet avec ses sources
MATCH (p:Project {id: $projectId})
OPTIONAL MATCH (p)-[:HAS_SOURCE]->(s:Source)
RETURN p, collect(s) as sources

// Récupérer la transcription complète d'une source
MATCH (s:Source {id: $sourceId})-[:HAS_SEGMENT]->(seg:Segment)-[:HAS_WORD]->(w:Word)
RETURN seg, collect(w) as words
ORDER BY seg.start

// Récupérer le prochain job en attente
MATCH (j:Job {status: 'pending'})
RETURN j
ORDER BY j.createdAt
LIMIT 1

// Mettre à jour la progression d'un job
MATCH (j:Job {id: $jobId})
SET j.progress = $progress, j.status = 'running'

Interfaces TypeScript

// Project
interface Project {
  id: string;
  name: string;
  createdAt: string;
  updatedAt: string;
}

// Source (vidéo importée)
interface Source {
  id: string;
  filename: string;
  path: string;
  duration: number;        // secondes
  width: number;
  height: number;
  fps: number;
  status: 'importing' | 'processing' | 'ready' | 'error';
}

// Transcription
interface Segment {
  id: string;
  start: number;           // secondes
  end: number;
  text: string;
  words: Word[];
}

interface Word {
  id: string;
  word: string;
  start: number;           // secondes (précision WhisperX)
  end: number;
  confidence: number;
}

// Timeline
interface Clip {
  id: string;
  sourceId: string;
  inPoint: number;         // secondes dans la source
  outPoint: number;        // secondes dans la source
  order: number;           // position dans la timeline
}

// Job
interface Job {
  id: string;
  type: 'thumbnails' | 'waveform' | 'transcription' | 'export';
  sourceId?: string;
  projectId: string;
  status: 'pending' | 'running' | 'completed' | 'failed';
  progress: number;        // 0-100
  error?: string;
  createdAt: string;
  startedAt?: string;
  completedAt?: string;
}

Workflows Principaux

1. Import d'une source vidéo

Utilisateur dépose un fichier vidéo
         │
         ▼
┌───────────────────┐
│ Upload streaming  │
│ vers le serveur   │
└───────────────────┘
         │
         ▼
┌───────────────────┐     ┌───────────────────┐
│ Extraction        │────▶│ Création des jobs │
│ métadonnées       │     │ de traitement     │
│ (ffprobe)         │     │ dans leangraph    │
└───────────────────┘     └───────────────────┘
                                   │
                    ┌──────────────┼──────────────┐
                    ▼              ▼              ▼
             ┌───────────┐  ┌───────────┐  ┌───────────┐
             │ Job       │  │ Job       │  │ Job       │
             │ thumbnails│  │ waveform  │  │ WhisperX  │
             │ (ffmpeg)  │  │ (ffmpeg)  │  │ (python)  │
             └───────────┘  └───────────┘  └───────────┘
                    │              │              │
                    └──────────────┼──────────────┘
                                   ▼
                          Source prête pour édition

2. Suppression d'un mot/silence

Utilisateur clique sur un mot ou une région de silence
         │
         ▼
┌─────────────────────────┐
│ Identification de la    │
│ plage temporelle        │
│ (word.start → word.end) │
└─────────────────────────┘
         │
         ▼
┌─────────────────────────┐
│ Mise à jour des clips:  │
│ - Division du clip      │
│ - Suppression de la     │
│   plage concernée       │
│ - Recalcul des ordres   │
└─────────────────────────┘
         │
         ▼
┌─────────────────────────┐
│ Re-rendu timeline       │
│ (instantané, pas de     │
│  traitement vidéo)      │
└─────────────────────────┘

3. Export de la vidéo finale

Utilisateur clique sur Exporter
         │
         ▼
┌─────────────────────────┐
│ Génération de la        │
│ commande FFmpeg         │
│ depuis les clips        │
└─────────────────────────┘
         │
         ▼
┌─────────────────────────────────────────────────────┐
│ FFmpeg complex filter:                              │
│ - Concat des segments                               │
│ - Application des crossfades audio                  │
│ - Ré-encodage vers le format cible                  │
└─────────────────────────────────────────────────────┘
         │
         ▼
┌─────────────────────────┐
│ Progression via         │
│ WebSocket               │
└─────────────────────────┘
         │
         ▼
    Téléchargement prêt

Détails Techniques

Génération des thumbnails (FFmpeg)

Génération de sprite sheets pour un chargement efficace :

# Extraction 1 thumbnail par seconde, création de sprite sheet
ffmpeg -i input.mkv -vf "fps=1,scale=160:-1,tile=10x10" \
  -frames:v 1 thumbnails_%03d.jpg

Pour une vidéo de 2 heures : ~7200 thumbnails → 72 sprite sheets de 100 chacune.

Génération du waveform (FFmpeg)

Extraction des niveaux audio à la granularité du framerate :

# Extraction audio brut pour analyse dans Node
ffmpeg -i input.mkv -ac 1 -ar 48000 -f f32le -acodec pcm_f32le pipe:1

Puis calcul du RMS pour chaque frame (48000/60 = 800 échantillons par frame).

Intégration WhisperX

import whisperx

# Chargement du modèle (CPU ou GPU)
model = whisperx.load_model("large-v3", device="cpu", compute_type="int8")

# Transcription avec timestamps mot par mot
audio = whisperx.load_audio(audio_path)
result = model.transcribe(audio, batch_size=16, language="fr")

# Alignement pour timestamps précis
model_a, metadata = whisperx.load_align_model(language_code="fr", device="cpu")
result = whisperx.align(result["segments"], model_a, metadata, audio, device="cpu")

# Résultat : timestamps mot par mot avec précision ~50ms

Crossfade audio à l'export (FFmpeg)

# Pour chaque point de coupe, application du filtre acrossfade
ffmpeg -i input.mkv \
  -filter_complex "
    [0:a]atrim=0:10,asetpts=PTS-STARTPTS[a1];
    [0:a]atrim=15:30,asetpts=PTS-STARTPTS[a2];
    [a1][a2]acrossfade=d=0.1:c1=tri:c2=tri[aout]
  " \
  -map 0:v -map "[aout]" output.mp4

Stratégie de lecture vidéo

La prévisualisation doit être fidèle au résultat final exporté, y compris les crossfades audio. Pas de compromis sur la qualité de prévisualisation.

Approche : Web Audio API + HTMLVideoElement

  1. Vidéo : Utilisation de requestVideoFrameCallback pour synchroniser précisément la lecture avec les segments. Le navigateur lit le fichier original, mais on contrôle frame par frame les sauts entre segments.

  2. Audio avec crossfades :

    • Extraction de l'audio via Web Audio API (AudioContext)
    • Création de AudioBufferSourceNode pour chaque segment
    • Application des crossfades en temps réel via GainNode (fade in/out)
    • Scheduling précis avec audioContext.currentTime

    Gestion mémoire (vidéos longues) : Pour une vidéo de 2h, charger tout l'audio en AudioBuffer consommerait ~1GB de RAM. Solution : chunking de l'audio (charger uniquement les segments proches du playhead, précharger le suivant). Garder en mémoire ~30 secondes avant/après la position courante.

// Exemple simplifié de crossfade audio
const playSegmentWithCrossfade = (
  audioBuffer: AudioBuffer,
  startTime: number,
  endTime: number,
  fadeInDuration: number,
  fadeOutDuration: number
) => {
  const source = audioContext.createBufferSource();
  const gainNode = audioContext.createGain();
  
  source.buffer = audioBuffer;
  source.connect(gainNode);
  gainNode.connect(audioContext.destination);
  
  // Fade in
  gainNode.gain.setValueAtTime(0, audioContext.currentTime);
  gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + fadeInDuration);
  
  // Fade out
  const duration = endTime - startTime;
  gainNode.gain.setValueAtTime(1, audioContext.currentTime + duration - fadeOutDuration);
  gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + duration);
  
  source.start(0, startTime, duration);
};
  1. Synchronisation vidéo/audio : L'audio Web Audio API est la source de vérité pour le timing. La vidéo suit en ajustant son currentTime pour rester synchronisée.

Cette approche garantit une prévisualisation identique à l'export final.


Composants UI

Composant Timeline

┌─────────────────────────────────────────────────────────────────────┐
│ [Zoom: ────●────] [00:00:00 / 01:45:32]              [▶ Play] [⏹]  │
├─────────────────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ VIDEO  │▓▓▓▓▓│▓▓▓▓▓│▓▓▓▓▓│▓▓▓▓▓│     │▓▓▓▓▓│▓▓▓▓▓│▓▓▓▓▓│▓▓▓▓▓│ │ │
│ │        │thumb│thumb│thumb│thumb│ SUP │thumb│thumb│thumb│thumb│ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ AUDIO  │█▄▂▁▂▄█│█▄▂▁│     │▂▄█│     │█▄▂▁▂▄█│█▄▂▁▂▄█│█▄▂▁▂│   │ │
│ │        │───────│────│ SIL │───│ SUP │───────│───────│─────│   │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ MOTS   │Bonjour│ je │     │suis│     │Conrad│ et  │ je  │     │ │
│ │        │───────│────│     │────│     │──────│─────│─────│     │ │
│ └─────────────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────────────┤
│ ◀ ──────────────────────────●────────────────────────────────── ▶  │
└─────────────────────────────────────────────────────────────────────┘

Raccourcis clavier essentiels

Raccourci Action
Espace Play/pause global
Suppr / Backspace Supprimer la sélection
Ctrl+Z Annuler (undo)
Ctrl+Shift+Z Rétablir (redo)
I Preview début du segment/sélection (~1 seconde)
O Preview fin du segment/sélection (~1 seconde)
C ou S Couper au niveau du playhead

Interactions mot/silence

  • Survol d'un mot : Mise en surbrillance du mot + région vidéo/audio correspondante
  • Clic sur un mot : Sélection (bordure jaune)
  • Shift+clic : Sélection multiple
  • Clic sur région de silence : Sélection du silence
  • Glisser les bords d'un mot : Ajustement des limites (fine-tune)
  • Clic droit : Prévisualiser le segment (contextuel)
    • Si curseur proche du début du segment (< 0.5s) → lecture depuis le début
    • Sinon → lecture depuis la position du curseur
    • Arrêt automatique à la fin du segment (scheduler via Web Audio API)

Édition manuelle (quand WhisperX ne suffit pas)

La transcription automatique n'est pas parfaite. L'éditeur doit permettre :

  • Coupe manuelle : Clic sur la timeline (hors mots) + raccourci (ex: C ou S) pour créer un point de coupe à n'importe quel endroit
  • Sélection de plage libre : Drag sur la timeline pour sélectionner une région arbitraire (pas limitée aux mots)
  • Ajustement des points de coupe : Glisser les bords d'un clip pour corriger une coupe imprécise
  • Split d'un clip : Diviser un clip existant en deux au niveau du playhead

Ces fonctionnalités sont essentielles pour les cas où :

  • WhisperX détecte mal les limites d'un mot
  • On veut couper au milieu d'un mot (bégaiement, hésitation)
  • On veut garder un silence partiel

États visuels

  • Régions de silence : Fond rouge/rose (détecté via gaps WhisperX)
  • Mots : Fond blanc avec texte
  • Sélectionné : Bordure jaune
  • Points de crossfade : Petit indicateur de dégradé aux coupures

Les éléments supprimés disparaissent de la timeline (pas de mode "grisé"). La confiance vient du système d'historique.

Historique d'actions

  • Affichage des 10 dernières actions dans un panneau discret (ex: coin inférieur)
  • Chaque action montre : type (suppression, coupe, ajustement) + timestamp ou mot concerné
  • Ctrl+Z pour annuler, Ctrl+Shift+Z pour rétablir
  • Pattern Command pour l'implémentation (chaque action est réversible)

Structure des Fichiers

nicefox-video-editor/
├── client/                    # Frontend React + Vite
│   ├── src/
│   │   ├── components/
│   │   │   ├── Timeline/
│   │   │   ├── VideoPlayer/
│   │   │   ├── TranscriptTrack/
│   │   │   └── WaveformTrack/
│   │   ├── stores/            # Zustand stores
│   │   ├── hooks/
│   │   ├── types/
│   │   └── App.tsx
│   ├── package.json
│   └── vite.config.ts
├── server/                    # Backend Node.js + Express
│   ├── src/
│   │   ├── routes/
│   │   ├── services/
│   │   │   ├── ffmpeg.ts
│   │   │   ├── whisperx.ts
│   │   │   └── jobs.ts
│   │   ├── db.ts              # Instance leangraph
│   │   └── index.ts
│   └── package.json
├── worker/                    # Worker Python WhisperX
│   ├── venv/                  # Environnement virtuel Python
│   ├── transcribe.py
│   └── requirements.txt
├── data/                      # Données runtime (gitignored)
│   ├── nicefox.db             # Base de données leangraph
│   └── projects/
│       └── {project-id}/
│           ├── sources/
│           │   └── {source-id}/
│           │       ├── original.mkv
│           │       ├── waveform.bin
│           │       └── thumbnails/
│           └── exports/
├── package.json               # Racine workspace
└── README.md

Phases d'Implémentation

Phase 1 : Fondation

  • Setup monorepo (npm workspaces)
  • Serveur Express + leangraph
  • API REST basique (CRUD projets)
  • Upload vidéo (streaming)
  • Extraction métadonnées FFprobe
  • Client React + Vite minimal
  • UI upload fichier

Phase 2 : Pipeline de Traitement

  • Setup venv Python + WhisperX
  • Queue de jobs persistée (leangraph)
  • Worker de traitement des jobs
  • Job : génération thumbnails (FFmpeg sprites)
  • Job : extraction waveform (FFmpeg → Node)
  • Job : transcription (WhisperX large-v3, français)
  • WebSocket pour mises à jour de progression

Phase 3 : UI Timeline

  • Composant Timeline virtualisé
  • Piste thumbnails (sprite sheets)
  • Piste waveform (canvas)
  • Piste transcription (mots cliquables)
  • Contrôles zoom/pan
  • Playhead + affichage temps
  • Lecteur vidéo synchronisé (HTMLVideoElement)
  • Moteur audio Web Audio API (pour crossfades en prévisualisation)
  • Synchronisation précise vidéo/audio

Phase 4 : Édition

  • Sélection mots/silences (clic)
  • Sélection multiple (shift+clic, drag)
  • Suppression (Suppr/Backspace → mise à jour clips)
  • Feedback visuel des coupures
  • Système undo/redo (pattern command) + historique 10 dernières actions
  • Prévisualisation fidèle avec crossfades audio temps réel
  • Coupe manuelle à n'importe quel point (raccourci clavier)
  • Sélection de plage libre (drag sur timeline)
  • Ajustement des points de coupe existants (drag des bords)
  • Split de clip au niveau du playhead
  • Preview rapide début/fin de segment (raccourcis I/O)
  • Suppression batch des silences > X secondes (killer feature)

Phase 5 : Export

  • Clips → commande FFmpeg
  • Crossfade audio (filtre acrossfade)
  • Job d'export avec progression
  • Téléchargement du fichier final

Phase 6 : Finitions

  • Raccourcis clavier complets (au-delà des essentiels)
  • UI d'ajustement des limites des mots
  • Gestion d'erreurs robuste
  • Polish UI/UX

Considérations de Performance

Rendu de la Timeline

  • Virtualisation : Ne rendre que la portion visible de la timeline
  • Sprites thumbnails : Charger des sprite sheets, pas des images individuelles
  • LOD waveform : Plusieurs niveaux de résolution selon le zoom
  • Rendu canvas : Dessiner le waveform sur canvas, pas en éléments DOM

Gestion Mémoire

  • Ne pas charger la vidéo complète en mémoire : Stream depuis le disque
  • Chargement lazy des thumbnails : Charger les sprites au scroll
  • Chunking du waveform : Charger les données par morceaux

Réactivité

  • UI optimiste : Mise à jour immédiate de l'UI, sync serveur en arrière-plan
  • Sauvegardes debounced : Ne pas sauvegarder à chaque édition, grouper les updates
  • Web Workers : Déporter le traitement du waveform vers un worker thread

Tests

Couches testables (TDD obligatoire)

Couche TDD ? Pourquoi
Services backend (FFmpeg, jobs, WhisperX) Oui Logique bien définie, inputs/outputs clairs
Stores Zustand (édition, undo/redo, timeline) Oui Logique métier critique, état complexe
Routes API Oui Contrats clairs, facile à tester
Composants React Non Exploratoire, UI change souvent
Intégration audio/vidéo Non Dépendant du browser, validation manuelle

Cycle TDD (obligatoire pour services/stores/routes)

Pour chaque fonctionnalité :

  1. Écrire le test qui décrit le comportement attendu
  2. Exécuter le test → vérifier qu'il échoue (et pour la bonne raison : fonction non définie, assertion failed — pas une erreur de syntaxe)
  3. Écrire le code minimal pour faire passer uniquement ce test
  4. Exécuter le test → vérifier qu'il passe
  5. Passer au test suivant

Règles strictes :

  • Ne jamais écrire de code applicatif sans un test qui échoue d'abord
  • Ne jamais écrire plusieurs tests d'un coup avant d'écrire le code
  • Un test = un comportement précis

Ce qu'on teste vs ce qu'on ne teste PAS

Les tests doivent valider le comportement métier, pas l'implémentation de la base de données.

Bon test (comportement métier) Mauvais test (implémentation DB)
deleteWord(wordId) → les clips adjacents sont fusionnés db.execute('DELETE...') retourne le bon résultat
createProject(name) → retourne un projet avec id et timestamps MERGE (:Project) fonctionne
splitClipAtTime(clipId, time) → crée 2 clips avec les bons in/out points La query Cypher est syntaxiquement correcte
Après undoLastAction(), l'état est restauré Les relations sont bien créées dans le graphe

Exemple concret

// ✅ BON : Teste le comportement métier via les services
describe('ClipService', () => {
  it('should split clip into two parts at given timestamp', async () => {
    // Arrange : setup via les services métier
    const db = await LeanGraph({ mode: 'test', project: 'nicefox' });
    const clipService = createClipService(db);
    const originalClip = await clipService.create({
      sourceId: 'src-1',
      inPoint: 0,
      outPoint: 10,
      order: 0
    });

    // Act : appeler la méthode métier
    const [clip1, clip2] = await clipService.splitAt(originalClip.id, 5);

    // Assert : vérifier le comportement attendu
    expect(clip1.inPoint).toBe(0);
    expect(clip1.outPoint).toBe(5);
    expect(clip2.inPoint).toBe(5);
    expect(clip2.outPoint).toBe(10);
    expect(clip2.order).toBe(clip1.order + 1);
  });
});

// ❌ MAUVAIS : Teste la DB directement
describe('ClipService', () => {
  it('should create clip in database', async () => {
    const db = await LeanGraph({ mode: 'test', project: 'nicefox' });
    
    // Mauvais : on écrit du Cypher au lieu d'appeler un service
    await db.execute(`
      CREATE (:Clip {id: 'c1', inPoint: 0, outPoint: 10})
    `);
    
    // Mauvais : on vérifie juste que la DB fonctionne
    const result = await db.query(`MATCH (c:Clip {id: 'c1'}) RETURN c`);
    expect(result).toHaveLength(1);
  });
});

Checklist par test (à suivre systématiquement)

  • Test écrit avant le code applicatif
  • Test exécuté et échoué (vérifier le message d'erreur)
  • Code minimal écrit (juste assez pour ce test)
  • Test exécuté et passé
  • Le test appelle une méthode de service/store, pas db.execute() ou db.query() directement

Configuration test

// Utiliser le mode test de leangraph pour les tests unitaires/intégration
import { LeanGraph } from 'leangraph';

const db = await LeanGraph({ mode: 'test', project: 'nicefox' });
// DB in-memory, isolée, reset automatique à la fin

Types de tests

Type Outil Cible
Unitaires backend Vitest Services, utilitaires, logique métier
Intégration API Vitest + supertest Routes Express, interactions DB
Unitaires frontend Vitest Stores Zustand, hooks custom, utilitaires

Pas de tests de composants React (fragiles, ROI faible pour un projet solo). Pas de tests E2E (validation manuelle suffisante).

Fixtures

  • Fichier vidéo de test : Voir sample/screencast_sample.mkv - Un exemple réel de screencast talking-head pour les tests d'intégration
  • Fichiers vidéo de test courts (~10 secondes) pour les tests unitaires
  • Transcriptions JSON mockées pour les tests de timeline
  • Waveforms binaires mockés pour les tests de visualisation

Critères de Succès

L'éditeur est un succès si :

  1. Import : Vidéo de 2h traitée en < 30 minutes (avec WhisperX en CPU)
  2. Timeline : Scroll/zoom fluide à 60fps sur vidéo de 2h
  3. Édition : Suppression d'un mot en < 100ms (réponse UI)
  4. Export : Vidéo de 2h exportée en < 10 minutes
  5. Workflow : Temps d'édition réduit de 50%+ comparé à Kdenlive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment