Video Schneiden: Automatisierter Schnitt mit Python & FFmpeg

Stille erkennen, Szenen finden, automatisch schneiden — Batch-Verarbeitung inklusive

Python FFmpeg OpenCV

🚀 Ihre Videos. Automatisch geschnitten. In Minuten statt Stunden.

📞 02406 803 7603 ✉️ info@computerkumpel.de

💰 Warum automatischer Videoschnitt?

Manueller Videoschnitt ist zeitaufwendig: 1 Stunde Rohmaterial = 3–5 Stunden Schnittarbeit. Automatisierte Erkennung von stillen Passagen und Szenenwechseln reduziert das auf Minuten.

🔇
Stille-Erkennung
Erkennt automatisch stille Passagen und schneidet sie heraus — perfekt für Podcasts und Präsentationen.
🎬
Szenen-Erkennung
Findet harte Schnitte und Szenenwechsel per Bildanalyse — teilt lange Videos automatisch in Kapitel.
📐
Format-Konvertierung
Schneidet UND konvertiert in einem Schritt — MP4, WebM, GIF, mit einstellbaren Codecs und Bitraten.
🔄
Batch-Verarbeitung
Ganze Ordner auf einmal verarbeiten — gleiche Schnittregeln auf hunderte Videos anwenden.

⚙️ So funktioniert's

📊
1. Analysieren
Video einlesen, Audiospur analysieren, stille Passagen und laute Szenenwechsel identifizieren.
✂️
2. Schnittpunkte setzen
Automatisch Timecodes für Schnitte berechnen — mit konfigurierbaren Schwellwerten.
⚙️
3. FFmpeg schneidet
Präziser Schnitt ohne Re-Encoding wo möglich — schnell und qualitätserhaltend.
📁
4. Ausgabe
Geschnittene Clips im Zielordner — optional mit Konvertierung in verschiedene Formate.

💻 Technische Umsetzung

🐍 Video-Schneide-Engine

import subprocess
import json
import os
from pathlib import Path

class VideoCutter:
    def __init__(self, silence_threshold: float = -40.0, 
                 min_silence_duration: float = 0.8):
        self.silence_threshold = silence_threshold  # dB
        self.min_silence_duration = min_silence_duration  # Sekunden
    
    def detect_silence(self, video_path: str) -> list:
        """Erkennt stille Passagen im Video"""
        cmd = [
            'ffmpeg', '-i', video_path,
            '-af', f'silencedetect=n={self.silence_threshold}dB:'
                   f'd={self.min_silence_duration}',
            '-f', 'null', '-'
        ]
        result = subprocess.run(cmd, capture_output=True, text=True)
        
        # Parse die Ausgabe von silencedetect
        silences = []
        for line in result.stderr.split('\n'):
            if 'silence_start' in line:
                start = float(line.split('silence_start: ')[1].split()[0])
                silences.append({'start': start})
            elif 'silence_end' in line and silences:
                end = float(line.split('silence_end: ')[1].split()[0])
                dur = float(line.split('silence_duration: ')[1].split()[0])
                silences[-1].update({'end': end, 'duration': dur})
        
        return silences
    
    def cut_segments(self, video_path: str, segments: list, 
                     output_dir: str, prefix: str = 'clip') -> list:
        """Schneidet Video in Segmente basierend auf Timecodes"""
        output_files = []
        
        for i, segment in enumerate(segments):
            start = segment['start']
            end = segment['end']
            duration = end - start
            
            output_name = f"{prefix}_{i+1:03d}.mp4"
            output_path = os.path.join(output_dir, output_name)
            
            # FFmpeg: schneller Schnitt (copy codec wo möglich)
            cmd = [
                'ffmpeg', '-y',
                '-ss', str(start),
                '-i', video_path,
                '-t', str(duration),
                '-c', 'copy',  # Ohne Re-Encoding
                '-avoid_negative_ts', 'make_zero',
                output_path
            ]
            
            subprocess.run(cmd, capture_output=True, check=True)
            output_files.append({
                'file': output_path,
                'start': start,
                'end': end,
                'duration': duration
            })
            print(f"✓ Clip {i+1}: {start:.1f}s - {end:.1f}s → {output_name}")
        
        return output_files
    
    def remove_silence(self, video_path: str, output_path: str) -> str:
        """Entfernt alle stillen Passagen aus einem Video"""
        silences = self.detect_silence(video_path)
        
        if not silences:
            print("Keine stillen Passagen gefunden.")
            return video_path
        
        # Berechne aktive Segmente (zwischen stillen Passagen)
        video_duration = self._get_duration(video_path)
        active_segments = []
        last_end = 0
        
        for silence in silences:
            if silence['start'] > last_end:
                active_segments.append({
                    'start': last_end,
                    'end': silence['start']
                })
            last_end = silence.get('end', silence['start'])
        
        if last_end < video_duration:
            active_segments.append({
                'start': last_end,
                'end': video_duration
            })
        
        # Erstelle Filtergraph für Konkatenation
        return self.cut_segments(video_path, active_segments, 
                                os.path.dirname(output_path))
    
    def _get_duration(self, video_path: str) -> float:
        """Ermittelt Videodauer via ffprobe"""
        cmd = [
            'ffprobe', '-v', 'error',
            '-show_entries', 'format=duration',
            '-of', 'json', video_path
        ]
        result = subprocess.run(cmd, capture_output=True, text=True)
        data = json.loads(result.stdout)
        return float(data['format']['duration'])

# Nutzung
cutter = VideoCutter(silence_threshold=-35, min_silence_duration=1.0)

# Stille Passagen finden
silences = cutter.detect_silence('meeting.mp4')
print(f"Gefunden: {len(silences)} stille Passagen")

# Stille entfernen
cutter.remove_silence('meeting.mp4', 'meeting_clean.mp4')

🎬 Szenenerkennung mit OpenCV (optional)

import cv2
import numpy as np

def detect_scene_changes(video_path: str, threshold: float = 30.0) -> list:
    """Erkennt Szenenwechsel durch Frame-Differenz"""
    cap = cv2.VideoCapture(video_path)
    fps = cap.get(cv2.CAP_PROP_FPS)
    
    scene_changes = []
    prev_frame = None
    frame_idx = 0
    
    while True:
        ret, frame = cap.read()
        if not ret:
            break
        
        if prev_frame is not None:
            # Berechne Frame-Differenz
            diff = cv2.absdiff(frame, prev_frame)
            mean_diff = np.mean(diff)
            
            if mean_diff > threshold:
                timestamp = frame_idx / fps
                scene_changes.append({
                    'frame': frame_idx,
                    'timestamp': timestamp,
                    'difference': mean_diff
                })
        
        prev_frame = frame.copy()
        frame_idx += 1
    
    cap.release()
    return scene_changes

⚡ In 2–3 Tagen zum fertigen Video-Schneide-Tool.

📞 02406 803 7603 ✉️ info@computerkumpel.de

🚀 Gebaut mit Vibecoding

👴 Klassische Entwicklung
  • 📋 1–2 Wochen FFmpeg lernen
  • 💻 2–3 Wochen Implementierung
  • 🧪 1 Woche Testing mit verschiedenen Formaten
  • ⏱️ Gesamt: 4–6 Wochen
🤖 Vibecoding-Ansatz
  • 🗣️ 0.5 Tage Prompt
  • ⚡ 1–2 Tage Code-Generierung
  • 🔧 0.5 Tage Test & Refinement
  • ⏱️ Gesamt: 2–3 Tage

💻 Code-Einblicke

Ein Blick unter die Haube — so erkennt der Cutter automatisch Musik-Segmente:

🏗️ Architektur

video_music_cutter.py
Hauptmodul: Audio-Extraktion, Musiksegment-Erkennung, Video-Schnitt mit MoviePy.
batch_process.py
Batch-Verarbeitung mehrerer Videos mit konfigurierbaren Erkennungsparametern.
download_youtube.py
YouTube-Downloader mit yt-dlp + Cookie-Support für altersbeschränkte Videos.

🎵 Musiksegment-Erkennung mit Audio-Features

import numpy as np
import librosa
from pydub import AudioSegment
from pydub.silence import detect_nonsilent

class MusicSegmentDetector:
    def __init__(self, min_music_duration=10, min_silence_len=1000,
                 silence_thresh=-40, spectral_threshold=0.6):
        self.min_music_duration = min_music_duration
        self.min_silence_len = min_silence_len
        self.silence_thresh = silence_thresh
        self.spectral_threshold = spectral_threshold

    def analyze_audio_features(self, audio_path):
        y, sr = librosa.load(audio_path, sr=22050)
        spectral_centroids = librosa.feature.spectral_centroid(y=y, sr=sr)[0]
        zcr = librosa.feature.zero_crossing_rate(y)[0]
        chroma = librosa.feature.chroma_stft(y=y, sr=sr)
        chroma_mean = np.mean(chroma, axis=0)
        spectral_rolloff = librosa.feature.spectral_rolloff(y=y, sr=sr)[0]
        mfcc = librosa.feature.mfcc(y=y, sr=sr, n_mfcc=13)
        return {
            'spectral_centroids': spectral_centroids,
            'zcr': zcr, 'chroma_mean': chroma_mean,
            'spectral_rolloff': spectral_rolloff,
            'mfcc': mfcc, 'sr': sr, 'y': y
        }

🧠 Musik vs. Sprache: Der Klassifikator

def _calculate_music_score(self, features, start_sec, end_sec):
    time_frames = features['time_frames']
    start_idx = np.searchsorted(time_frames, start_sec)
    end_idx = np.searchsorted(time_frames, end_sec)

    # Features für das Segment extrahieren
    segment_zcr = features['zcr'][start_idx:end_idx]
    segment_spectral = features['spectral_centroids'][start_idx:end_idx]
    segment_chroma = features['chroma_mean'][start_idx:end_idx]

    # Musik hat: niedrigere ZCR, höhere spektrale Varianz, stärkere Chroma
    zcr_score = 1.0 - min(np.mean(segment_zcr), 0.3) / 0.3
    spectral_var = np.std(segment_spectral)
    spectral_score = min(spectral_var / 1000.0, 1.0)
    chroma_score = min(np.mean(segment_chroma), 1.0)

    # Gewichteter Score
    return (zcr_score * 0.3 + spectral_score * 0.3 + chroma_score * 0.4)

def detect_music_segments(self, audio_path):
    audio = AudioSegment.from_file(audio_path)
    nonsilent_ranges = detect_nonsilent(
        audio, min_silence_len=self.min_silence_len,
        silence_thresh=self.silence_thresh)
    features = self.analyze_audio_features(audio_path)
    music_segments = []
    for start_ms, end_ms in nonsilent_ranges:
        start_sec = start_ms / 1000.0
        end_sec = end_ms / 1000.0
        if end_sec - start_sec >= self.min_music_duration:
            music_score = self._calculate_music_score(
                features, start_sec, end_sec)
            if music_score > self.spectral_threshold:
                music_segments.append((start_sec, end_sec))
    return self._merge_close_segments(music_segments, gap_threshold=2.0)

✂️ Video-Schnitt mit MoviePy

from moviepy.editor import VideoFileClip, concatenate_videoclips
import tempfile

def cut_video_segments(video_path, segments, output_path):
    video = VideoFileClip(video_path)
    clips = []
    for start, end in segments:
        clip = video.subclip(start, end)
        clips.append(clip)

    final_clip = concatenate_videoclips(clips)
    final_clip.write_videofile(
        output_path, codec='libx264', audio_codec='aac',
        temp_audiofile=tempfile.mktemp(suffix='.m4a'),
        remove_temp=True)

    video.close(); final_clip.close()
    for clip in clips:
        clip.close()
    return True

📦 Batch-Verarbeitung

import glob
from video_music_cutter import process_video

def batch_process_videos(pattern="*.mp4", output_suffix="_music_only",
                         min_music_duration=10, silence_thresh=-40,
                         spectral_threshold=0.6):
    video_files = glob.glob(pattern)
    video_files = [f for f in video_files if output_suffix not in f]

    successful, failed = 0, 0
    for video_file in video_files:
        base, ext = os.path.splitext(video_file)
        output_file = f"{base}{output_suffix}{ext}"
        try:
            if process_video(video_file, output_file,
                             min_music_duration=min_music_duration,
                             silence_thresh=silence_thresh,
                             spectral_threshold=spectral_threshold):
                successful += 1
            else:
                failed += 1
        except Exception as e:
            print(f"Fehler: {e}")
            failed += 1
    print(f"Erfolgreich: {successful}, Fehlgeschlagen: {failed}")

Automatischer Videoschnitt — für Ihr Projekt?

Podcast-Bereinigung, Meeting-Cuts oder Content-Repurposing — ich baue die passende Pipeline. Jetzt anfragen.

📞 02406 803 7603 ✉️ info@computerkumpel.de