
Le mois dernier, comme chaque septembre depuis maintenant plusieurs années, s’est déroulé sur Twitch le ZEvent. C’était l’occasion de découvrir de nouveaux streamers, de voir des collaborations improbables, c’était un moment de divertissement intense où quasiment tout le Twitch game français a participé. En bref, c’est le moment de l’année à ne surtout pas rater pour toute personne étant intéressée de près, ou de loin, par le monde du streaming.
Si je vous parle du ZEvent dans un article où vous allez manger du Python et des paramètres FFmpeg, c’est parce qu’il se trouve que j’ai dû développer un petit script pour résoudre un problème que j’ai rencontré lors de cet événement. :)
En effet, si vous êtes familiers de Twitch, vous connaissez sûrement ces micro-coupures qui surviennent parfois durant les lives. En général c’est dû à notre mauvaise connexion, parfois ça vient directement de la plateforme, ou du streamer. Il se trouve que durant le ZEvent, je suivais principalement Sylvvain… Qui était placé en bout de salle, dans une zone où le WiFi se faisait capricieux. 🤡
Et justement, il se trouve que j’ai enregistré plusieurs passages de son live, notamment celui de l’airbag. 😂 Parfois sans aucun soucis, parfois avec ces fameuses micro-coupures. C’est alors que m’est venue l’idée de créer un script Python qui se chargerait de supprimer tous ces moments de freeze de mes enregistrement.
L’idée de base est simple. Une vidéo n’est ni plus, ni moins, qu’une suite d’images doublée d’une bande son. Mon idée originale était donc de calculer le hash md5 de chaque image de ma vidéo, afin de détecter des images potentiellement dupliquées, ce qui représenterai un moment de freeze. Mais assez rapidement, je me suis rendu compte que ça ne fonctionnait pas. En effet, même sur une image statique, les artefacts de compression font que chaque image est techniquement unique, en tout cas lors du calcul de son hash…
Alors je me suis mis à chercher sur Google. :)
Si la mise en pratique était un échec, l’idée reste bonne car c’est techniquement comme ça que doit fonctionner le script que je souhaite créer. En fouillant sur internet, je me suis rapidement rendu compte que FFmpeg disposait d’un filtre qui permettait justement de détecter les mouches sur les cadav moments de “freeze” d’une vidéo. freezedetect
Et il s’utilise comme ceci :
ffmpeg -i vod.mp4 -vf "freezedetect=d=0.1" -f null -
Ici j’ai mis une durée de détection de 0.1 seconde, mais on peut la modifier selon nos besoins. Il existe également un paramètre n pour le noise audio, qui permet de détecter une coupure audio. Le but est de définir une durée suffisamment proche de la durée des coupures.
En théorie, ce paramètre résout complètement notre problème. Pour le démontrer, j’ai simulé un stream dont le flux vidéo est coupé par des moment de pause, qui dans le cadre de Twitch, représentent un freeze.

On peut voir que FFmpeg retourne plusieurs informations :
Parfait ! Maintenant que nous avons les informations que nous recherchions, on peut commencer à envisager l’écriture d’un script qui va récupérer ces informations, et couper notre vidéos avec les bon timestamp pour la rendre fluide. :D
Voici un début de script.
import subprocess
import re
import os
import sys
from pathlib import Path
FFMPEG_PATH = r"C:\Chemin\vers\ffmpeg\bin\ffmpeg.exe" # ffmpeg si sur linux
FFPROBE_PATH = r"C:\Chemin\vers\ffmpeg\bin\ffprobe.exe" # ffmpeg si sur linux
def detect_freezes(input_file, duration_threshold=0.1):
cmd = [
FFMPEG_PATH,
'-i', input_file,
'-vf', f'freezedetect=d={duration_threshold}',
'-f', 'null',
'-'
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
output = result.stdout
# Regex permettant de parser les résultats de FFmpeg
freeze_pattern = r'lavfi\.freezedetect\.freeze_start: ([\d.]+).*?lavfi\.freezedetect\.freeze_duration: ([\d.]+)'
freezes = []
for match in re.finditer(freeze_pattern, output, re.DOTALL):
start = float(match.group(1))
duration = float(match.group(2))
end = start + duration
freezes.append((start, end))
print(f"Freeze détecté: {start:.2f}s -> {end:.2f}s (durée: {duration:.2f}s)")
return freezes
def main():
# Vérifier que FFmpeg est accessible
if not os.path.exists(FFMPEG_PATH):
print(f"Erreur: FFmpeg introuvable à : {FFMPEG_PATH}")
sys.exit(1)
if not os.path.exists(FFPROBE_PATH):
print(f"Erreur: FFprobe introuvable à : {FFPROBE_PATH}")
sys.exit(1)
input_file = sys.argv[1]
min_duration = 0.1
if not os.path.exists(input_file):
print(f"Erreur: Le fichier {input_file} n'existe pas")
sys.exit(1)
freezes = detect_freezes(input_file, min_duration)
if not freezes:
print("\nAucun freeze détecté")
sys.exit(0)
print(f"\n{len(freezes)} freeze(s) détecté(s)")
if __name__ == "__main__":
main()
Ce qui dans mon cas retourne ceci :
python script1.py sample.mp4
Freeze détecté: 4.47s -> 4.57s (durée: 0.10s)
Freeze détecté: 4.57s -> 4.77s (durée: 0.20s)
Freeze détecté: 4.77s -> 4.90s (durée: 0.13s)
Freeze détecté: 8.30s -> 8.40s (durée: 0.10s)
Freeze détecté: 8.40s -> 8.60s (durée: 0.20s)
Freeze détecté: 8.60s -> 9.30s (durée: 0.70s)
Freeze détecté: 12.83s -> 12.93s (durée: 0.10s)
Freeze détecté: 12.93s -> 13.13s (durée: 0.20s)
Freeze détecté: 13.13s -> 13.23s (durée: 0.10s)
Freeze détecté: 15.83s -> 15.93s (durée: 0.10s)
Freeze détecté: 16.20s -> 16.30s (durée: 0.10s)
Freeze détecté: 19.53s -> 19.67s (durée: 0.13s)
Freeze détecté: 19.73s -> 19.87s (durée: 0.13s)
Freeze détecté: 25.30s -> 25.40s (durée: 0.10s)
Freeze détecté: 25.40s -> 25.57s (durée: 0.17s)
Freeze détecté: 25.57s -> 25.70s (durée: 0.13s)
Freeze détecté: 25.70s -> 26.00s (durée: 0.30s)
Freeze détecté: 26.00s -> 27.33s (durée: 1.33s)
18 freeze(s) détecté(s)
Les résultats sont plutôt satisfaisants, essayons de découper ces intervales de la vidéo. Pour le faire, nous allons utiliser les paramètres -ss et -t afin de créer les segments de vidéos à garder, que nous concaténerons par la suite. Mais dans un premier temps, nous devons déterminer ces segments. :)
Pour le faire, c’est assez simple. Comme nous disposons des timestamps des moments de freeze, nous devons calculer les timestamp des segments de vidéos à garder, par rapport à la durée totale de la vidéo. Nous devons donc utiliser FFprobe, un outil fournit avec FFmpeg qui permet de récupérer toutes les métadonnées et informations d’un fichier média.
Ainsi, ajoutons ces deux méthodes à notre script :
def get_video_duration(input_file):
cmd = [
FFPROBE_PATH,
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
input_file
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return float(result.stdout.strip())
def create_segments(freezes, total_duration):
segments = []
current_time = 0
buffer=0.1
for freeze_start, freeze_end in freezes:
# Segment avant le freeze (avec buffer)
segment_end = max(0, freeze_start - buffer)
if segment_end > current_time:
segments.append((current_time, segment_end))
# Passer après le freeze (avec buffer)
current_time = freeze_end + buffer
# Dernier segment
if current_time < total_duration:
segments.append((current_time, total_duration))
return segments
Vous l’aurez compris, la première permet de déterminer la longueur d’une vidéo en secondes, tandis que la deuxième nous retourne un tuple représentant les timestamps de début et de fin de chaque segment à garder. Nous pouvons enfin commencer la manipulation de notre fichier vidéo. 😁
Comme je l’ai dit, nous allons utiliser les paramètres -ss et -t afin de créer les segments de vidéos à garder. En pratique, ça va se dérouler ainsi :
-i vod.mp4 -ss XX -t XX segment_XX.mp4Voici ce que donne le fichier final :
import subprocess
import re
import os
import sys
from pathlib import Path
FFMPEG_PATH = r"C:\Chemin\vers\ffmpeg\bin\ffmpeg.exe" # ffmpeg si sur linux
FFPROBE_PATH = r"C:\Chemin\vers\ffmpeg\bin\ffprobe.exe" # ffmpeg si sur linux
def detect_freezes(input_file, duration_threshold=0.1):
cmd = [
FFMPEG_PATH,
'-i', input_file,
'-vf', f'freezedetect=d={duration_threshold}',
'-f', 'null',
'-'
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
output = result.stdout
# Regex permettant de parser les résultats de FFmpeg
freeze_pattern = r'lavfi\.freezedetect\.freeze_start: ([\d.]+).*?lavfi\.freezedetect\.freeze_duration: ([\d.]+)'
freezes = []
for match in re.finditer(freeze_pattern, output, re.DOTALL):
start = float(match.group(1))
duration = float(match.group(2))
end = start + duration
freezes.append((start, end))
print(f"Freeze détecté: {start:.2f}s -> {end:.2f}s (durée: {duration:.2f}s)")
return freezes
def get_video_duration(input_file):
cmd = [
FFPROBE_PATH,
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
input_file
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return float(result.stdout.strip())
def create_segments(freezes, total_duration):
segments = []
current_time = 0
buffer=0.1
for freeze_start, freeze_end in freezes:
# Segment avant le freeze (avec buffer)
segment_end = max(0, freeze_start - buffer)
if segment_end > current_time:
segments.append((current_time, segment_end))
# Passer après le freeze (avec buffer)
current_time = freeze_end + buffer
# Dernier segment
if current_time < total_duration:
segments.append((current_time, total_duration))
return segments
def cut_and_concatenate(input_file, segments, output_file):
print(f"Découpage de la vidéo en {len(segments)} segment(s)...")
# On créé un dossier temporaire pour les segments
input_path = Path(input_file)
temp_dir = input_path.parent / "temp_segments"
temp_dir.mkdir(exist_ok=True)
segment_files = []
concat_file = temp_dir / "concat_list.txt"
# On encapsule notre code dans un try/catch afin d'éviter de laisser des fichiers temporaires en cas d'échec
try:
# Boucle de découpage
for i, (start, end) in enumerate(segments):
segment_file = temp_dir / f"segment_{i:04d}.mp4"
segment_files.append(segment_file)
duration = end - start
print(f"Segment {i+1}/{len(segments)}: {start:.2f}s -> {end:.2f}s ({duration:.2f}s)")
cmd = [
FFMPEG_PATH,
'-y',
'-i', input_file,
'-ss', str(start),
'-t', str(duration),
'-c', 'copy', # Copie sans ré-encodage (lossless)
str(segment_file)
]
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
# Boucle pour créer le fichier de concaténation
with open(concat_file, 'w', encoding='utf-8') as f:
for segment_file in segment_files:
# Si comme moi vous êtes sur Windows, vous devrez remplacer les \\ par des /
path_str = str(segment_file.absolute()).replace('\\', '/')
f.write(f"file '{path_str}'\n")
print("Concaténation des segments...")
cmd = [
FFMPEG_PATH,
'-y',
'-f', 'concat',
'-safe', '0',
'-i', str(concat_file),
'-c', 'copy',
output_file
]
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
print(f"Vidéo finale créée: {output_file}")
finally:
# On supprime les fichiers temporaires
print("\n🧹 Nettoyage des fichiers temporaires...")
for segment_file in segment_files:
if segment_file.exists():
segment_file.unlink()
if concat_file.exists():
concat_file.unlink()
try:
if temp_dir.exists() and not any(temp_dir.iterdir()):
temp_dir.rmdir()
except:
pass
def main():
# Vérifier que FFmpeg est accessible
if not os.path.exists(FFMPEG_PATH):
print(f"Erreur: FFmpeg introuvable à : {FFMPEG_PATH}")
sys.exit(1)
if not os.path.exists(FFPROBE_PATH):
print(f"Erreur: FFprobe introuvable à : {FFPROBE_PATH}")
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else "output_no_freezes.mp4" # Ligne ajoutée
min_duration = 0.1
if not os.path.exists(input_file):
print(f"Erreur: Le fichier {input_file} n'existe pas")
sys.exit(1)
total_duration = get_video_duration(input_file) # Ligne ajoutée
freezes = detect_freezes(input_file, min_duration)
if not freezes:
print("\nAucun freeze détecté")
sys.exit(0)
print(f"\n{len(freezes)} freeze(s) détecté(s)")
segments = create_segments(freezes, total_duration) # Ligne ajoutée
cut_and_concatenate(input_file, segments, output_file) # Ligne ajoutée
if __name__ == "__main__":
main()
Et voilà, le travail est terminé… N’est-ce pas ? 😅

En réalité, c’est plus compliqué que ça. En écrivant cet article, je me suis confronté à un problème que je n’avais pas vraiment remarqué lorsque j’ai développé ce script. Il se trouve que sur de plus petites vidéos, il est assez commun de rencontrer des problèmes de désynchronisation en utilisant ce script. Pour y remédier, nous devons caler nos points de coupes sur les keyframes de la vidéo.
Petit aparté - Une keyframe est une image complète dans une vidéo qui sert de point de référence. Contrairement aux autres frames qui ne stockent que les changements, une keyframe contient toutes les informations pour afficher l’image entière.
Maintenant que vous savez ce qu’est une keyframe, nous allons essayer de corriger ce problème. Pour cela, nous devons utiliser FFprobe avec les paramètres suivants :
-skip_frame nokey : Nous ignorons toutes les frames, sauf les keyframes-show_entries frame=pkt_pts_time : Montre le timestamp de chaque frame-select_streams v:0 : Ne prend que le premier flux vidéo-of csv=p=0 : Nous demandons de formater la sortie en CSVVoici ce que ça donne dans notre script Python :
def find_keyframe_near_time(input_file, timestamp):
cmd = [
FFPROBE_PATH,
'-v', 'error',
'-skip_frame', 'nokey',
'-show_entries', 'frame=pkt_pts_time',
'-select_streams', 'v:0',
'-of', 'csv=p=0',
input_file
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
keyframes = [float(line) for line in result.stdout.strip().split('\n') if line]
# Trouve la keyframe la plus proche avant le timestamp
for keyframe in reversed(keyframes):
if keyframe <= timestamp:
return keyframe
return 0.0
def create_segments(freezes, total_duration, input_file):
segments = []
current_time = 0
buffer = 0.1
for freeze_start, freeze_end in freezes:
# Trouve la keyframe avant le début du freeze
segment_end = find_keyframe_near_time(input_file, freeze_start - buffer)
if segment_end > current_time:
segments.append((current_time, segment_end))
# Trouve la keyframe après la fin du freeze
current_time = find_keyframe_near_time(input_file, freeze_end + buffer)
# Dernier segment
if current_time < total_duration:
segments.append((current_time, total_duration))
return segments
Et là pour le coup, ça fonctionne vraiment, nous n’avons plus ce problème de désynchronisation. :D

Comme vous l’avez vu dans la vidéo ci-dessus, notre script ne résout pas vraiment notre problème. Pour l’améliorer, on peut doubler notre détection actuelle avec la détection de silence. La détection de silence seule n’est pas idéale car elle risque d’être trop stricte. Les moments de calme dans l’enregistrement risquent d’être confondus avec ce que nous considérons comme des freezes. C’est pour quoi nous allons la faire fonctionner de paire avec notre détection actuelle. L’analyse du son est plus précise, l’analyse des images statiques viendra confirmer les freezes détectés.
Pour ce faire, nous allons utiliser le filtre silencedetect, il s’utilise comme ceci :
ffmpeg -i vod.mp4 '-af', f'silencedetect=n=XXdB:d=XX' -f null -
Le paramètre n représente la tolérence de bruit, le palier à partir duquel FFmpeg va considérer le son comme du silence, et le paramètre d représente la durée minimale de détection. Les valeurs par défaut de FFmpeg sont -60dB et 2 secondes. Je pense que nous allons simplement modifier la durée pour qu’elle corresponde mieux à notre cas d’usage.
Voici une méthode qui met en pratique ce paramètre :
def detect_silences(input_file, duration_threshold=0.3):
cmd = [
FFMPEG_PATH,
'-i', input_file,
'-af', f'silencedetect=d={duration_threshold}',
'-f', 'null',
'-'
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
output = result.stdout
# Regexes pour parser les résultats
silence_starts = re.findall(r'silence_start: ([\d.]+)', output)
silence_ends = re.findall(r'silence_end: ([\d.]+)', output)
silences = []
for start, end in zip(silence_starts, silence_ends):
start_time = float(start)
end_time = float(end)
duration = end_time - start_time
silences.append((start_time, end_time))
print(f"Silence audio: {start_time:.2f}s -> {end_time:.2f}s (durée: {duration:.2f}s)")
return silences
Tout comme pour la détection de freeze, notre méthode retourne un tuple comportant le timestamp de début et de fin du freeze détecté. Nous devons maintenant créer une dernière méthode qui va nous permettre de comparer les résultats des détections. :D
def find_confirmed_silences(freezes, silences):
confirmed_silences = []
for silence_start, silence_end in silences:
is_confirmed = False
for freeze_start, freeze_end in freezes:
# Vérifie s'il y a une intersection entre les 2 détections
intersection_start = max(freeze_start, silence_start)
intersection_end = min(freeze_end, silence_end)
if intersection_start < intersection_end:
is_confirmed = True
break
if is_confirmed:
duration = silence_end - silence_start
confirmed_silences.append((silence_start, silence_end))
print(f"Silence confirmé: {silence_start:.2f}s -> {silence_end:.2f}s (durée: {duration:.2f}s)")
else:
print(f"Silence ignoré: {silence_start:.2f}s -> {silence_end:.2f}s (pas de freeze correspondant)")
return confirmed_silences
Et voilà ! Notre système est maintenant prêt à être assemblé. Voici notre script complété et utilisable.
import subprocess
import re
import os
import sys
from pathlib import Path
FFMPEG_PATH = r"C:\Chemin\vers\ffmpeg\bin\ffmpeg.exe" # ffmpeg si sur linux
FFPROBE_PATH = r"C:\Chemin\vers\ffmpeg\bin\ffprobe.exe" # ffmpeg si sur linux
def detect_freezes(input_file, duration_threshold=0.1):
cmd = [
FFMPEG_PATH,
'-i', input_file,
'-vf', f'freezedetect=d={duration_threshold}',
'-f', 'null',
'-'
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
output = result.stdout
freeze_pattern = r'lavfi\.freezedetect\.freeze_start: ([\d.]+).*?lavfi\.freezedetect\.freeze_duration: ([\d.]+)'
freezes = []
for match in re.finditer(freeze_pattern, output, re.DOTALL):
start = float(match.group(1))
duration = float(match.group(2))
end = start + duration
freezes.append((start, end))
print(f"Freeze détecté: {start:.2f}s -> {end:.2f}s (durée: {duration:.2f}s)")
return freezes
def detect_silences(input_file, duration_threshold=0.3):
cmd = [
FFMPEG_PATH,
'-i', input_file,
'-af', f'silencedetect=d={duration_threshold}',
'-f', 'null',
'-'
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
output = result.stdout
# Regexes pour parser les résultats
silence_starts = re.findall(r'silence_start: ([\d.]+)', output)
silence_ends = re.findall(r'silence_end: ([\d.]+)', output)
silences = []
for start, end in zip(silence_starts, silence_ends):
start_time = float(start)
end_time = float(end)
duration = end_time - start_time
silences.append((start_time, end_time))
print(f"Silence audio: {start_time:.2f}s -> {end_time:.2f}s (durée: {duration:.2f}s)")
return silences
def find_confirmed_silences(freezes, silences):
confirmed_silences = []
for silence_start, silence_end in silences:
is_confirmed = False
for freeze_start, freeze_end in freezes:
# Vérifie s'il y a une intersection entre les 2 détections
intersection_start = max(freeze_start, silence_start)
intersection_end = min(freeze_end, silence_end)
if intersection_start < intersection_end:
is_confirmed = True
break
if is_confirmed:
duration = silence_end - silence_start
confirmed_silences.append((silence_start, silence_end))
print(f"Silence confirmé: {silence_start:.2f}s -> {silence_end:.2f}s (durée: {duration:.2f}s)")
else:
print(f"Silence ignoré: {silence_start:.2f}s -> {silence_end:.2f}s (pas de freeze correspondant)")
return confirmed_silences
def get_video_duration(input_file):
cmd = [
FFPROBE_PATH,
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
input_file
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
return float(result.stdout.strip())
def find_keyframe_near_time(input_file, timestamp):
cmd = [
FFPROBE_PATH,
'-v', 'error',
'-skip_frame', 'nokey',
'-show_entries', 'frame=pkt_pts_time',
'-select_streams', 'v:0',
'-of', 'csv=p=0',
input_file
]
result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
keyframes = [float(line) for line in result.stdout.strip().split('\n') if line]
# Trouve la keyframe la plus proche avant le timestamp
for keyframe in reversed(keyframes):
if keyframe <= timestamp:
return keyframe
return 0.0
def create_segments(freezes, total_duration, input_file):
segments = []
current_time = 0
buffer = 0.1
for freeze_start, freeze_end in freezes:
# Trouve la keyframe avant le début du freeze
segment_end = find_keyframe_near_time(input_file, freeze_start - buffer)
if segment_end > current_time:
segments.append((current_time, segment_end))
# Trouve la keyframe après la fin du freeze
current_time = find_keyframe_near_time(input_file, freeze_end + buffer)
# Dernier segment
if current_time < total_duration:
segments.append((current_time, total_duration))
return segments
def cut_and_concatenate(input_file, segments, output_file):
print(f"Découpage de la vidéo en {len(segments)} segment(s)...")
input_path = Path(input_file)
temp_dir = input_path.parent / "temp_segments"
temp_dir.mkdir(exist_ok=True)
segment_files = []
concat_file = temp_dir / "concat_list.txt"
try:
# Découpage avec ré-encodage pour assurer l'intégrité
for i, (start, end) in enumerate(segments):
segment_file = temp_dir / f"segment_{i:04d}.mp4"
segment_files.append(segment_file)
duration = end - start
print(f"Segment {i+1}/{len(segments)}: {start:.2f}s -> {end:.2f}s ({duration:.2f}s)")
cmd = [
FFMPEG_PATH,
'-y',
'-i', input_file,
'-ss', str(start),
'-t', str(duration),
'-c', 'copy', # Copie sans ré-encodage (lossless)
str(segment_file)
]
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
# Création du fichier de concaténation
with open(concat_file, 'w', encoding='utf-8') as f:
for segment_file in segment_files:
path_str = str(segment_file.absolute()).replace('\\', '/')
f.write(f"file '{path_str}'\n")
print("Concaténation des segments...")
cmd = [
FFMPEG_PATH,
'-y',
'-f', 'concat',
'-safe', '0',
'-i', str(concat_file),
'-c', 'copy', # Copy safe car les segments sont déjà encodés proprement
'-movflags', '+faststart',
output_file
]
subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
print(f"Vidéo finale créée: {output_file}")
finally:
print("\n🧹 Nettoyage des fichiers temporaires...")
for segment_file in segment_files:
if segment_file.exists():
segment_file.unlink()
if concat_file.exists():
concat_file.unlink()
try:
if temp_dir.exists() and not any(temp_dir.iterdir()):
temp_dir.rmdir()
except:
pass
def main():
if not os.path.exists(FFMPEG_PATH):
print(f"Erreur: FFmpeg introuvable à : {FFMPEG_PATH}")
sys.exit(1)
if not os.path.exists(FFPROBE_PATH):
print(f"Erreur: FFprobe introuvable à : {FFPROBE_PATH}")
sys.exit(1)
if len(sys.argv) < 2:
print("Usage: python script.py <input_file> [output_file]")
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > 2 else "output_no_freezes.mp4"
min_duration = 0.1
silence_duration = 0.3
if not os.path.exists(input_file):
print(f"Erreur: Le fichier {input_file} n'existe pas")
sys.exit(1)
total_duration = get_video_duration(input_file)
freezes = detect_freezes(input_file, min_duration)
if not freezes:
print("\nAucun freeze détecté")
sys.exit(0)
print(f"\n{len(freezes)} freeze(s) détecté(s)")
silences = detect_silences(input_file, silence_duration)
if not silences:
print("\nAucun silence détecté")
sys.exit(0)
confirmed_cuts = find_confirmed_silences(freezes, silences)
# Ici on a remplacé le tuple 'freeze' par 'confirmed_cuts'
segments = create_segments(confirmed_cuts, total_duration, input_file)
cut_and_concatenate(input_file, segments, output_file)
if __name__ == "__main__":
main()
Et voici ce que ça donne une fois en action :

Toutefois, vous remarquerez que ce n’est toujours pas parfait… 😅
En raison du problème de désynchronisation mentionné plus tôt, nous avons dû baser notre découpage sur les keyframes de la vidéo. Cela a pour effet de décaler les zones de coupes, et donc de garder des morceaux de vidéo non désirés. Je pense qu’une solution pour régler le problème aurait été d’ajouter du transcodage à notre script afin de nous passer du système d’alignements des keyframes. Malheuresement, durant mes tests, je n’ai pas réussi à obtenir des résultats convainquants. Cela mérite encore quelques réflexions…