MediaFlow
Voir sur GitHubBoite a outils multimedia locale en Tauri 2 (Rust + Svelte 5) regroupant extraction de pistes, merge, OCR video, transcription, traduction IA multi-modeles et renommage en masse, avec pipeline FFmpeg, traitement natif et interface desktop multi-plateforme.
MediaFlow | Rapport technique
En bref
- Boîte à outils multimédia locale pour l'extraction de pistes, le merge, l'OCR vidéo, la transcription audio, la traduction IA et le renommage en masse.
- Architecture Tauri 2.0 : backend Rust pour les opérations lourdes (FFmpeg, OCR, gestion de processus), frontend Svelte 5 pour l'interface utilisateur.
- Sept outils intégrés dans une seule application native, avec annulation, progression temps réel et gestion de l'inhibition de veille système.
- OCR PaddleOCR v5 avec accélération Metal (macOS) / Vulkan (Windows/Linux) et pipeline de frames streamé via FFmpeg.
- Traduction multi-modèles simultanée via OpenAI, Anthropic, Google AI et OpenRouter, avec segmentation intelligente (thèmes/génériques vs dialogues) et mémoire de traduction.
- Build multi-plateforme (macOS, Linux, Windows) via GitHub Actions CI complet.
Contexte et objectif
MediaFlow s'adresse aux utilisateurs qui manipulent régulièrement des fichiers multimédia — sous-titres, pistes audio, vidéos — et qui ont besoin d'un outil local, rapide et unifié. Le cas d'usage principal est le traitement en masse de fichiers pour des saisons entières de séries : extraire des pistes, merger des sous-titres, OCR des sous-titres gravés, transcrire de l'audio, traduire des sous-titres.
L'application se démarque des solutions en ligne par sa nature local-first : les fichiers ne quittent jamais la machine, les opérations FFmpeg/OCR s'exécutent nativement, et seules les API d'IA (traduction, transcription) nécessitent un accès réseau.
Fonctionnalités
| Outil | Description |
|---|---|
| Extraction | Inspection des conteneurs multimédia via FFprobe, sélection et extraction de pistes audio/vidéo/sous-titres avec préservation des métadonnées. |
| Merge | Construction de pipelines de merge par lot : attachement de pistes externes, auto-match par pattern d'épisode, configuration langue/titre/default/forced/delay par piste. |
| Audio to Subs | Transcription audio/vidéo vers sous-titres via Deepgram, gestion multi-versions, export SRT/VTT/JSON. |
| Video OCR | Extraction de sous-titres gravés (burned-in) par OCR, région globale avec override par fichier, multi-travailleurs avec accélération GPU. |
| AI Translation | Traduction de sous-titres via OpenAI, Anthropic, Google AI ou OpenRouter. File multi-fichiers avec splitting en batches, mémoire de traduction pour les génériques. |
| Rename | Renommage/copie en masse basé sur des règles, prévisualisation, détection de conflits. |
| Info | Inspection rapide : conteneur, pistes, codecs, bitrate, métadonnées. |
Fonctionnalités transversales : drag & drop inter-outils, thème clair/sombre/système, settings persistants via Tauri Store, logs structurés, progression globale unifiée, inhibition de veille pendant les opérations longues.
Architecture (vue d'ensemble)
MediaFlow/
├── src/ # Frontend SvelteKit (SPA)
│ ├── routes/
│ │ └── +page.svelte # Point d'entrée unique – routing par state
│ └── lib/
│ ├── components/ # UI par outil + composants partagés + ui/ (shadcn)
│ ├── services/ # Logique métier frontend (parsing, API calls, storage)
│ ├── stores/ # État réactif Svelte 5 (.svelte.ts)
│ ├── types/ # Types TypeScript par domaine
│ ├── hooks/ # Hooks réutilisables
│ └── utils/ # Utilitaires (format, DnD, logs, OCR helpers)
├── src-tauri/ # Backend Rust (Tauri 2.0)
│ └── src/
│ ├── app/ # Setup fenêtre native (vibrancy macOS)
│ ├── commands/ # Exposition des modules tools comme commandes Tauri
│ ├── tools/ # Logique métier Rust
│ │ ├── ffmpeg/ # Extraction, download, version check
│ │ ├── ffprobe/ # Probe de fichiers média
│ │ ├── merge/ # Merge de pistes
│ │ ├── ocr/ # Pipeline OCR complet (engine, pipeline, export, subtitles)
│ │ ├── transcription/ # Transcodage Opus, waveform
│ │ ├── tokens/ # Comptage de tokens (tiktoken)
│ │ ├── fs/ # Opérations fichier (rename, copy, metadata)
│ │ ├── data/ # Persistance données d'outils (rsext, transcriptions)
│ │ └── power/ # Inhibition de veille système
│ ├── shared/ # Utilitaires partagés (validation, progress tracking, hash, store)
│ └── test_support/ # Assets de test et helpers
└── .github/workflows/ci.yml # CI multi-OS
Pattern architectural : L'application suit un modèle à deux couches clairement séparé. Le backend Rust expose des commandes Tauri (#[tauri::command]) que le frontend invoque via invoke(). Les événements temps réel (progression, erreurs) transitent par le canal d'événements Tauri (app.emit()). Le frontend est un SPA monopage avec un routeur interne piloté par un état currentView — les vues sont montées en parallèle mais affichées/masquées via display:none pour préserver l'état entre les navigations.
Choix techniques et raisons
Tauri 2.0 + Rust : Remplace Electron pour un binaire natif plus léger (~10x plus petit), un accès système direct (process FFmpeg, OCR, I/O bas niveau) et des performances supérieures. L'édition Rust 2024 avec tokio async runtime permet de gérer les processus FFmpeg sans bloquer l'UI.
Svelte 5 + runes : Les stores utilisent les runes
$stateet$derivedde Svelte 5 pour une réactivité fine sans boilerplate. L'approche SPA avecadapter-staticetssr: falseest requise par Tauri (pas de serveur Node).shadcn-svelte + Tailwind CSS v4 : Composants UI accessibles et personnalisables, évitant le vendor lock-in d'une librairie de composants. L'intégration Tailwind v4 via plugin Vite assure des performances de build optimales.
Pipeline OCR streamé : FFmpeg extrait les frames en PNG vers un pipe (
image2pipe), un reader async tokio découpe le flux binaire en frames PNG complètes, et des workers synchrones (rayon) distribuent l'OCR via PaddleOCR. Ce pattern évite les I/O disque intermédiaires et permet une parallélisation maximale.Multi-backend GPU pour l'OCR : L'utilisation de
ocr-rs(PaddleOCR bindings Rust) avec sélection de backend Metal (macOS), Vulkan (Windows/Linux) ou CPU offre une flexibilité maximale selon le matériel disponible.Inhibition de veille multi-plateforme : Implémentation native via IOKit (macOS),
SetThreadExecutionState(Windows) et logind/systemd-inhibit (Linux). Un service dédié avec thread dédié et channel mpsc gère les leases avec un RAII guard.Traduction avec segmentation thématique : Le service de traduction classe les cues en trois catégories (passthrough, themeCandidate, mainTranslatable), groupe les signatures de génériques identiques, et utilise un cache de traduction (
translation-memory) pour éviter de re-traduire les openings/endings récurrents.LLM client multi-provider : Un client unifié gère OpenAI, Anthropic, Google AI et OpenRouter avec gestion d'erreurs catégorisée (rate limit, quota, auth, timeout), timeout fusionné avec AbortSignal, et retry intelligent.
Extraits de code remarquables
1. Pipeline OCR streamé — extraction de frames PNG depuis FFmpeg
Fichier : src-tauri/src/tools/ocr/pipeline.rs
const PNG_SIGNATURE: &[u8; 8] = b"\x89PNG\r\n\x1a\n";
const PNG_IEND_TYPE: &[u8; 4] = b"IEND";
fn take_next_png_frame(buffer: &mut Vec<u8>) -> Result<Option<Vec<u8>>, String> {
let Some(signature_index) = find_png_signature(buffer) else {
let tail_len = buffer.len().min(PNG_SIGNATURE.len().saturating_sub(1));
if buffer.len() > tail_len {
buffer.drain(..buffer.len() - tail_len);
}
return Ok(None);
};
if signature_index > 0 {
buffer.drain(..signature_index);
}
let mut cursor = PNG_SIGNATURE.len();
loop {
if buffer.len() < cursor + 8 {
return Ok(None);
}
let chunk_len = u32::from_be_bytes([
buffer[cursor], buffer[cursor + 1],
buffer[cursor + 2], buffer[cursor + 3],
]) as usize;
let chunk_type = &buffer[cursor + 4..cursor + 8];
let chunk_end = cursor
.checked_add(12)
.and_then(|value| value.checked_add(chunk_len))
.ok_or_else(|| "PNG chunk length overflow".to_string())?;
if buffer.len() < chunk_end {
return Ok(None);
}
cursor = chunk_end;
if chunk_type == PNG_IEND_TYPE {
return Ok(Some(buffer.drain(..cursor).collect()));
}
}
}
Pourquoi c'est intéressant : Cette fonction parse un flux binaire continu en PNG complets sans écrire sur disque. Elle gère les lectures partielles, le bruit de fond avant la signature PNG, et les chunks PNG de longueur variable. Le pipeline complet (run_ocr_pipeline_with_bins) lance trois tâches concurrentes — lecture du stream FFmpeg stdout, lecture de la progression stderr, et OCR processing — communiquant via des channels tokio.
2. Distribution multi-worker de l'OCR
Fichier : src-tauri/src/tools/ocr/pipeline.rs
fn process_streamed_frames(
frame_rx: tokio::sync::mpsc::Receiver<StreamedFrame>,
models_dir: &Path,
language: &str,
use_gpu: bool,
requested_workers: u32,
progress: Option<OcrProgressEmitter>,
total_frames_hint: u32,
file_id: &str,
) -> Result<Vec<OcrFrameResult>, String> {
let worker_count = resolve_ocr_worker_count(requested_workers);
let engine_threads = resolve_ocr_engine_threads(worker_count);
let mut worker_senders = Vec::with_capacity(worker_count);
let mut worker_handles = Vec::with_capacity(worker_count);
for _worker_index in 0..worker_count {
let (worker_tx, worker_rx) =
std::sync::mpsc::sync_channel::<WorkerMessage>(WORKER_QUEUE_CAPACITY);
worker_senders.push(worker_tx);
worker_handles.push(std::thread::spawn(move || {
let engine = match create_ocr_engine(&models_dir, &language, use_gpu, engine_threads) {
Ok(engine) => engine,
Err(error) => { set_fatal_error(&fatal_error, error); return; }
};
while let Ok(message) = worker_rx.recv() {
match message {
WorkerMessage::Shutdown => break,
WorkerMessage::Frame(frame) => {
if is_operation_cancelled(&file_id) { break; }
if let Ok(ocr_results) = engine.recognize(&image) {
// Collect results via Arc<Mutex>
}
}
}
}
}));
}
// Round-robin dispatch
while let Some(frame) = frame_rx.blocking_recv() {
worker_senders[next_worker].send(WorkerMessage::Frame(frame))?;
next_worker = (next_worker + 1) % worker_count;
}
}
Pourquoi c'est intéressant : Le système adapte automatiquement le nombre de workers et de threads par worker en fonction des cœurs physiques disponibles (num_cpus). Chaque worker possède son propre moteur OCR avec un channel de capacité 1 (backpressure), ce qui limite la consommation mémoire. Le dispatch round-robin équilibre la charge sans contention.
3. Sécurité — Validation des chemins de fichiers
Fichier : src-tauri/src/shared/validation.rs
pub(crate) const ALLOWED_MEDIA_EXTENSIONS: &[&str] = &[
"mkv", "mp4", "avi", "mov", "webm", "m4v", "mks", "mka", "m4a",
"mp3", "flac", "wav", "ogg", "aac", "ac3", "dts", "srt", "ass",
"ssa", "vtt", "sub", "sup", "opus", "wma",
];
pub(crate) fn validate_media_path(path: &str) -> Result<(), String> {
let path = Path::new(path);
if !path.exists() { return Err(format!("File not found: {}", path.display())); }
if !path.is_file() { return Err(format!("Not a file: {}", path.display())); }
let ext = path.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
.to_lowercase();
if !ALLOWED_MEDIA_EXTENSIONS.contains(&ext.as_str()) {
return Err(format!("Unsupported file type: .{}", ext));
}
Ok(())
}
pub(crate) fn validate_output_path(path: &str) -> Result<(), String> {
let path_str = path.to_string_lossy();
if path_str.contains("..") {
return Err("Path traversal not allowed".to_string());
}
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
return Err(format!("Output directory does not exist: {}", parent.display()));
}
}
Ok(())
}
Pourquoi c'est intéressant : Chaque commande Tauri qui manipule des fichiers valide systématiquement les chemins d'entrée (whitelist d'extensions, existence, type fichier) et de sortie (pas de path traversal, répertoire parent existant). C'est une défense en profondeur essentielle pour une application desktop qui exécute des commandes système.
4. Parser de sous-titres multi-format avec placeholders
Fichier : src/lib/services/subtitle-parser.ts
function tokenizeASSText(text: string): { skeleton: string; placeholders: Placeholder[] } {
const placeholders: Placeholder[] = [];
let placeholderIndex = 0;
let skeleton = text.replace(/\{[^}]*\}/g, (match) => {
const token = makePlaceholder('TAG', placeholderIndex);
placeholders.push({ index: placeholderIndex, token, original: match });
placeholderIndex++;
return token;
});
skeleton = skeleton.replace(/\\N/g, (match) => {
const token = makePlaceholder('BR', placeholderIndex);
placeholders.push({ index: placeholderIndex, token, original: match });
placeholderIndex++;
return token;
});
return { skeleton, placeholders };
}
Pourquoi c'est intéressant : Le parser remplace les éléments de formatage (tags ASS {...}, retours à la ligne \N, tags HTML, entités) par des placeholders uniques (⟦TAG_0⟧, ⟦BR_1⟧). Cela permet d'envoyer au LLM uniquement le texte visible tout en garantissant la reconstruction exacte du fichier original après traduction. Le système supporte SRT, VTT, ASS et SSA avec une structure de données unifiée (Cue).
5. Traduction avec segmentation thématique et mémoire de cache
Fichier : src/lib/services/translation.ts
function classifyCueRole(cue: Cue): CueRole {
const isMaskStyle = isAssLike && !!normalizedStyle
&& NON_TRANSLATABLE_ASS_STYLES.has(normalizedStyle);
if (isMaskStyle || isAssDrawingCue(cue) || !hasVisibleCueText(cue)) {
return 'passthroughNonText';
}
if (isThemeCue(cue)) {
return 'themeCandidate';
}
return 'mainTranslatable';
}
function buildCanonicalThemeTemplate(cue: Cue): ThemeCueOccurrence {
const placeholderOrder = cue.placeholders.map(p => p.token);
let canonicalSkeleton = cue.textSkeleton;
placeholderOrder.forEach((token, index) => {
canonicalSkeleton = canonicalSkeleton
.split(token)
.join(getCanonicalPlaceholderToken(index));
});
return {
cue,
signature: normalizeThemeSignature(canonicalSkeleton),
canonicalSkeleton,
placeholderOrder
};
}
Pourquoi c'est intéressant : Les cues de génériques (opening/ending) partagent souvent le même squelette de texte avec des placeholders variables. Le système les regroupe par signature canonique, traduit une seule fois le template, et réapplique la traduction à toutes les occurrences. Un cache persisté (translation-memory) stocke ces traductions de génériques pour les réutiliser entre les épisodes d'une même série, réduisant les appels API et la cohérence.
6. Inhibition de veille système avec RAII guard
Fichier : src-tauri/src/shared/sleep_inhibit.rs
pub(crate) struct SleepInhibitGuard {
token: u64,
}
impl SleepInhibitGuard {
pub(crate) fn try_acquire(reason: impl Into<String>) -> Result<Self, String> {
let token = acquire_sleep_inhibit(reason)?;
Ok(Self { token })
}
}
impl Drop for SleepInhibitGuard {
fn drop(&mut self) {
SERVICE.release_best_effort(self.token);
}
}
// Usage dans les commandes Tauri :
#[tauri::command]
pub(crate) async fn extract_track(app: tauri::AppHandle, /* ... */) -> Result<(), String> {
let _sleep_guard = SleepInhibitGuard::try_acquire("FFmpeg extraction").ok();
// ... opération longue ...
// _sleep_guard est libéré automatiquement à la fin du scope
}
Pourquoi c'est intéressant : Le pattern RAII garantit que l'inhibition de veille est toujours relâchée, même en cas d'erreur ou d'annulation. Le service tourne sur un thread dédié avec channel mpsc et gère un compteur de leases : la première acquisition active l'inhibition système, la dernière libération la désactive. L'implémentation macOS utilise directement IOKit (IOPMAssertionCreateWithName), Windows utilise SetThreadExecutionState, et Linux utilise logind ou systemd-inhibit en fallback.
7. Progress tracking FFmpeg avec EMA
Fichier : src-tauri/src/shared/ffmpeg_progress.rs
pub(crate) struct FfmpegProgressTracker {
duration_us: Option<u64>,
start_instant: Instant,
last_total_size_bytes: Option<u64>,
last_total_size_elapsed_seconds: Option<f64>,
smoothed_speed_bytes_per_sec: Option<f64>,
ema_alpha: f64,
}
impl FfmpegProgressTracker {
fn update_speed(&mut self, total_size_bytes: u64, elapsed_seconds: f64) {
let instant_speed = bytes_delta as f64 / elapsed_delta;
self.smoothed_speed_bytes_per_sec =
Some(match self.smoothed_speed_bytes_per_sec {
Some(previous) => {
(self.ema_alpha * instant_speed) + ((1.0 - self.ema_alpha) * previous)
}
None => instant_speed,
});
}
}
Pourquoi c'est intéressant : Le tracker parse les lignes out_time_us=, total_size= et progress=end du stdout de FFmpeg. Il calcule une vitesse de transfert lissée par moyenne mobile exponentielle (EMA, α=0.25) pour éviter les sauts visuels dans la barre de progression. Ce composant est réutilisé par l'extraction, le merge et le pipeline OCR.
Qualité, sécurité, maintenance
Tests
- Tests unitaires Rust : Chaque module du backend dispose de tests dédiés (validation, progress tracker, OCR engine, merge args, extraction, token counting). Les tests OCR utilisent un asset vidéo de test (
test_support::assets::ensure_ocr_video()). - Tests d'intégration : Le pipeline OCR est testé de bout en bout avec un vrai fichier vidéo et les modèles PP-OCRv5. Le merge est testé avec vérification ffprobe du fichier de sortie (langue, titre, disposition).
- Tests de résilience : Annulation de pipeline OCR (
cancel_ocr_operation), fichiers corrompus, index de piste invalide, binaire FFmpeg absent. - CI multi-OS : GitHub Actions exécute
cargo testetpnpm tauri buildsur macOS, Ubuntu et Windows, avec installation spécifique des dépendances système (Vulkan SDK, FFmpeg, GTK/WebKit).
Sécurité
- Validation systématique des chemins d'entrée (whitelist d'extensions) et de sortie (anti-path-traversal).
- CSP désactivé (
null) danstauri.conf.jsonpour permettre les appels API externes, compensé par la validation côté backend. - Timeout sur toutes les opérations FFmpeg (5 min extraction, 10 min merge, 30 min OCR).
- Nettoyage des fichiers partiels en cas d'échec ou de timeout (
remove_partial_output). - Les API keys sont stockées localement via Tauri Store.
Gestion d'erreurs et logging
- Système de logs structuré (
logStore) avec niveaux (info/warning/error/success), badge de compteur d'erreurs non lues dans le header, et panneau de logs détaillés. logAndToastcombine notification toast + log persistant pour les erreurs critiques (ex: FFmpeg introuvable).- Le client LLM catégorise les erreurs API (rate limit, quota, auth, timeout, server) avec marquage
retryableetretryAfter.
Installation et exécution (local)
Prérequis
- pnpm (gestionnaire de paquets obligatoire)
- Rust toolchain stable (édition 2024)
- FFmpeg et FFprobe sur le PATH système
- macOS :
brew install ffmpeg - Ubuntu/Debian :
sudo apt install ffmpeg - Windows : télécharger depuis ffmpeg.org
- macOS :
- Node.js ≥ 25
Développement
git clone https://github.com/Sudo-Rahman/MediaFlow.git
cd MediaFlow
# Installer les dépendances frontend
pnpm install
# Lancer en mode développement (frontend + Tauri)
pnpm tauri dev
# Frontend uniquement (sans backend Rust)
pnpm dev
# Vérification des types TypeScript
pnpm check
# Build de production (génère les binaires natifs)
pnpm tauri build
Tests Rust
cd src-tauri
cargo test # Tests unitaires
cargo test -- --ignored # Tests d'intégration (lents)