# Implementierungsplan: auto-video-cut Phase 2 Stand: 2026-03-20 --- ## Übersicht der Phasen ``` Phase 1 ██████████ abgeschlossen CLI-Grundgerüst, Stille-Entfernung, Szenen-Erkennung, Musik-Mixing, Text-Overlays, Sequenz-Datei, Batch Phase 2a ░░░░░░░░░░ geplant — Video-Qualität Crossfades, Fade-in/out, Audio-Ducking, Fortschritt, Preview Phase 2b ░░░░░░░░░░ geplant — KI-Kern Whisper-Transkription, Untertitel, Füllwort-Erkennung Phase 2c ░░░░░░░░░░ geplant — KI-Erweitert Auto-Kapitel (LLM), Highlight-Reel, natürlichsprachliche Sequenzen Phase 3 ░░░░░░░░░░ geplant — Discord-Bot bot.py implementieren ``` --- ## Phase 2a: Video-Qualität und UX ### Neue Dateien ``` auto_video_cut/ ├── transitions.py ← Crossfade, Fade-in/out (ffmpeg xfade) ├── ducking.py ← Audio-Ducking (ffmpeg sidechaincompress) └── progress.py ← Fortschrittsanzeige (rich) ``` ### Neue Abhängigkeit ```toml # pyproject.toml "rich>=13.0" # Fortschrittsanzeige, Terminal-UI ``` --- ### Schritt 2a-1: Fortschrittsanzeige (`progress.py`) **Problem:** Aktuell zeigt das CLI nur Start/Ende. Bei langen Videos wartet man blind. **Lösung:** ffmpeg mit `-progress pipe:1` starten und die Ausgabe parsen. ```python # progress.py class FfmpegProgress: """Parst ffmpeg -progress pipe:1 Ausgabe in Echtzeit.""" def __init__(self, total_duration: float): self.total_duration = total_duration def run_with_progress(self, cmd: list[str]) -> subprocess.CompletedProcess: """ffmpeg-Befehl mit Fortschrittsbalken ausführen.""" # -progress pipe:1 an cmd anhängen # stdout zeilenweise lesen # "out_time_ms=" parsen → Fortschritt berechnen # rich.progress.Progress aktualisieren ``` **Auswirkung auf bestehenden Code:** - `cutter.py`, `merger.py`, `audio.py`, `text.py`: `_run()` durch `progress.run_with_progress()` ersetzen - Zentrale `_run()`-Funktion in eigenes Modul auslagern (`runner.py`), damit alle Module sie nutzen **Akzeptanzkriterium:** Bei `video-cut cut --input test.mp4 --remove-silence` erscheint ein Fortschrittsbalken mit Prozent und geschätzter Restzeit. --- ### Schritt 2a-2: Crossfade und Fade-in/out (`transitions.py`) **Problem:** Harter Schnitt zwischen Clips sieht amateurhaft aus. **Lösung:** ffmpeg `xfade` Filter für Übergänge zwischen zwei Clips. ```python # transitions.py def apply_crossfade( clip_a: Path, clip_b: Path, output: Path, duration: float = 0.5, transition: str = "fade" # fade | dissolve | wipeleft | ... ) -> Path: """Crossfade zwischen zwei Clips.""" # ffmpeg -i a.mp4 -i b.mp4 # -filter_complex "xfade=transition=fade:duration=0.5:offset=" # output.mp4 def apply_fade_in(input: Path, output: Path, duration: float = 0.5) -> Path: """Fade-in am Clip-Anfang.""" # ffmpeg -i input -vf "fade=in:d=0.5" -af "afade=in:d=0.5" def apply_fade_out(input: Path, output: Path, duration: float = 0.5) -> Path: """Fade-out am Clip-Ende.""" # ffmpeg -i input -vf "fade=out:d=0.5:st=" -af "afade=out:d=0.5:st=" ``` **Integration in Sequenz-Datei:** ```yaml sequence: - type: video file: "clip1.mp4" transition: "crossfade" # NEU transition_duration: 0.5 # NEU - type: video file: "clip2.mp4" global: fade_in: 0.5 # NEU: Fade-in am Anfang des Gesamtvideos fade_out: 0.5 # NEU: Fade-out am Ende ``` **Auswirkung auf bestehenden Code:** - `merger.py`: `merge_clips()` muss Übergänge zwischen Clips einfügen können. Aktuell concat-demuxer (copy-only) → bei Crossfades muss re-encodet werden. Strategie: paarweises xfade in Pipeline statt eines einzelnen concat. - `sequencer.py`: `ClipEntry` um `transition` und `transition_duration` erweitern - `config.py`: Neue Defaults für `transitions` **Akzeptanzkriterium:** `video-cut merge --inputs a.mp4 b.mp4 --output merged.mp4 --crossfade 0.5` erzeugt ein Video mit sanftem Übergang. --- ### Schritt 2a-3: Audio-Ducking (`ducking.py`) **Problem:** Musik läuft konstant laut, übertönt Sprache oder ist in Pausen zu leise. **Lösung:** ffmpeg `sidechaincompress` — Musik wird automatisch leiser wenn der Original-Ton lauter ist. ```python # ducking.py def apply_ducking( video: Path, music: Path, output: Path, volume_original: float = 1.0, volume_music: float = 0.3, duck_threshold: float = 0.02, # Ab welcher Lautstärke die Musik leiser wird duck_ratio: float = 4.0, # Wie stark die Absenkung ist duck_attack: float = 0.3, # Wie schnell die Musik leiser wird (Sekunden) duck_release: float = 1.0, # Wie schnell die Musik wieder lauter wird ) -> Path: """Musik unter Video legen mit automatischem Ducking.""" # ffmpeg -i video.mp4 -stream_loop -1 -i music.mp3 # -filter_complex # "[0:a]volume=[speech]; # [1:a]volume=[music]; # [music][speech]sidechaincompress= # threshold=:ratio=: # attack=:release=[ducked]; # [speech][ducked]amix=inputs=2:duration=first[a]" # -map 0:v -map "[a]" -c:v copy output.mp4 ``` **Integration in Config:** ```yaml music: ducking: true # NEU: Audio-Ducking aktivieren duck_threshold: 0.02 # NEU duck_ratio: 4.0 # NEU duck_attack: 0.3 # NEU duck_release: 1.0 # NEU ``` **Auswirkung auf bestehenden Code:** - `audio.py`: `mix_music()` bekommt Parameter `ducking=False`. Wenn aktiv → `ducking.apply_ducking()` statt direktem amix. - `config.py`: Neue Defaults im `music`-Block **Akzeptanzkriterium:** Bei einem Vlog mit Sprache und Musik wird die Musik automatisch leiser während der Sprecher redet und kehrt in Pausen zur konfigurierten Lautstärke zurück. --- ### Schritt 2a-4: Preview und Dry-Run **Preview (schnelle Vorschau):** ```bash video-cut sequence --seq sequence.yaml --preview ``` - Rendering in 360p mit `-preset ultrafast` - Datei wird im `/tmp/` abgelegt - Dauer: ca. 10x schneller als Full-Render **Dry-Run (nur Analyse):** ```bash video-cut sequence --seq sequence.yaml --dry-run ``` Ausgabe: ``` Sequenz: 8 Einträge [1] image title.png 3.0s [2] video intro.mp4 12.4s [3] video rohschnitt.mp4 45.2s → Stille entfernen [4] folder ./aufnahmen/tag1/ 3 Dateien, ~98.0s ... Geschätzte Gesamtdauer: 4:12 Geschätzte Dateigröße: ~180 MB (1080p H.264) Musik: random aus resources/music/ (3 Dateien verfügbar) ``` **Auswirkung auf bestehenden Code:** - `cli.py`: Neuer Flag `--preview` und `--dry-run` beim `sequence`-Befehl - `sequencer.py`: Neue Funktion `estimate_sequence()` die Dauer/Größe schätzt ohne zu rendern --- ## Phase 2b: KI-Kern — Whisper + Smart Cutting ### Neue Dateien ``` auto_video_cut/ ├── transcribe.py ← Whisper-Integration (faster-whisper) ├── subtitles.py ← SRT erzeugen und einbrennen └── smart_cut.py ← Füllwort-Erkennung, intelligentes Schneiden ``` ### Neue Abhängigkeiten ```toml # pyproject.toml — als optionale Gruppe [project.optional-dependencies] ai = [ "faster-whisper>=1.0", "anthropic>=0.40", # für Phase 2c (Auto-Kapitel) ] ``` Installation: `pip install -e ".[ai]"` --- ### Schritt 2b-1: Whisper-Transkription (`transcribe.py`) **Kernfunktion:** Audio aus Video extrahieren → Whisper transkribiert → Wort-Level Timestamps. ```python # transcribe.py @dataclass class Word: text: str start: float # Sekunden end: float confidence: float @dataclass class Segment: text: str start: float end: float words: list[Word] def extract_audio(video: Path, output: Path) -> Path: """Audio-Spur als WAV extrahieren (16kHz Mono für Whisper).""" # ffmpeg -i video.mp4 -ar 16000 -ac 1 -f wav audio.wav def transcribe( audio: Path, model_size: str = "base", # tiny | base | small | medium | large-v3 language: str | None = None, # None = auto-detect device: str = "auto", # auto | cpu | cuda ) -> list[Segment]: """Audio mit faster-whisper transkribieren.""" # from faster_whisper import WhisperModel # model = WhisperModel(model_size, device=device) # segments, info = model.transcribe(audio, word_timestamps=True) # → list[Segment] mit Wort-Level-Timestamps def transcribe_video( video: Path, model_size: str = "base", language: str | None = None, ) -> list[Segment]: """Kompletter Workflow: Video → Audio → Transkript.""" ``` **Integration in Config:** ```yaml ai: whisper_model: "base" # tiny | base | small | medium | large-v3 whisper_language: null # null = auto-detect, "de", "en", ... whisper_device: "auto" # auto | cpu | cuda ``` **CLI:** ```bash video-cut transcribe --input video.mp4 --output untertitel.srt video-cut transcribe --input video.mp4 --model large-v3 --language de ``` **Akzeptanzkriterium:** `video-cut transcribe --input test.mp4` erzeugt eine `.srt`-Datei mit korrekten Timestamps. --- ### Schritt 2b-2: Untertitel erzeugen und einbrennen (`subtitles.py`) ```python # subtitles.py def segments_to_srt(segments: list[Segment], output: Path) -> Path: """Transkript-Segmente als SRT-Datei speichern.""" def burn_subtitles( video: Path, srt: Path, output: Path, font_size: int = 24, font_color: str = "white", outline_color: str = "black", outline_width: int = 2, position: str = "bottom", # bottom | top ) -> Path: """Untertitel via ffmpeg subtitles-Filter einbrennen.""" # ffmpeg -i video.mp4 -vf "subtitles=untertitel.srt:force_style='...'" output.mp4 def auto_subtitle( video: Path, output: Path, model_size: str = "base", language: str | None = None, **style_kwargs, ) -> Path: """Alles in einem: Transkribieren → SRT → Einbrennen.""" ``` **Integration in Sequenz-Datei:** ```yaml sequence: - type: video file: "vlog.mp4" auto_subtitles: true # NEU subtitle_language: "de" # NEU (optional) subtitle_style: # NEU (optional) font_size: 24 position: "bottom" ``` **CLI:** ```bash video-cut subtitle --input video.mp4 --output video_sub.mp4 video-cut subtitle --input video.mp4 --srt existing.srt # vorhandene SRT nutzen ``` **Akzeptanzkriterium:** `video-cut subtitle --input test.mp4` erzeugt ein Video mit eingebrannten deutschen Untertiteln. --- ### Schritt 2b-3: Intelligentes Schneiden (`smart_cut.py`) **Kernidee:** Whisper liefert Wort-Level-Timestamps. Daraus lässt sich viel mehr machen als nur dB-basierte Stille-Erkennung. ```python # smart_cut.py # Deutsche und englische Füllwörter FILLER_WORDS_DE = {"äh", "ähm", "also", "quasi", "sozusagen", "halt", "naja", "ne"} FILLER_WORDS_EN = {"uh", "um", "like", "you know", "basically", "actually", "so"} @dataclass class CutDecision: start: float end: float reason: str # "silence" | "filler" | "false_start" | "repeat" confidence: float def detect_fillers( segments: list[Segment], filler_words: set[str] | None = None, language: str = "de", ) -> list[CutDecision]: """Füllwörter im Transkript finden und als Schnitt-Kandidaten markieren.""" # Jedes Wort prüfen: ist es ein Füllwort? # Zeitbereich des Worts → CutDecision(reason="filler") def detect_false_starts(segments: list[Segment]) -> list[CutDecision]: """Fehlstarts erkennen: Satz beginnt, bricht ab, beginnt neu.""" # Heuristik: Segment < 3 Wörter, gefolgt von Pause > 0.3s, # gefolgt von neuem Segment das ähnlich anfängt # → CutDecision(reason="false_start") def detect_long_pauses( segments: list[Segment], max_pause: float = 1.0, keep_pause: float = 0.3, ) -> list[CutDecision]: """Pausen zwischen Segmenten erkennen und auf Wunschlänge kürzen.""" # Pause zwischen Segment N und N+1 > max_pause? # → Kürzen auf keep_pause Sekunden def smart_remove( video: Path, output: Path, model_size: str = "base", remove_fillers: bool = True, remove_false_starts: bool = True, shorten_pauses: bool = True, max_pause: float = 1.0, language: str | None = None, ) -> tuple[Path, list[CutDecision]]: """Intelligenter Schnitt: Transkribieren → Analysieren → Schneiden.""" # 1. Transkribieren (transcribe.py) # 2. Füllwörter finden # 3. Fehlstarts finden # 4. Pausen analysieren # 5. Alle CutDecisions zusammenführen # 6. Inverse Zeitabschnitte berechnen (wie cutter.invert_ranges) # 7. Clips ausschneiden und zusammenfügen # Rückgabe: fertiges Video + Liste der Schnitte (für Review) ``` **CLI:** ```bash # Intelligenter Schnitt (ersetzt --remove-silence) video-cut smart-cut --input video.mp4 video-cut smart-cut --input video.mp4 --keep-fillers --no-false-starts video-cut smart-cut --input video.mp4 --max-pause 0.5 # Nur Analyse anzeigen, ohne zu schneiden video-cut smart-cut --input video.mp4 --analyze-only ``` Ausgabe `--analyze-only`: ``` Transkription: 342 Wörter, 2:45 Gesamtdauer Gefunden: 12x Füllwörter (äh, ähm, also) → 4.2s einsparen 3x Fehlstarts → 6.8s einsparen 8x Pausen > 1.0s (auf 0.3s kürzen) → 9.1s einsparen ───────────── Geschätzte Einsparung: 20.1s (12% des Videos) ``` **Integration in Sequenz-Datei:** ```yaml sequence: - type: video file: "vlog.mp4" smart_cut: true # NEU: ersetzt remove_silence remove_fillers: true # NEU remove_false_starts: true # NEU max_pause: 0.8 # NEU ``` **Akzeptanzkriterium:** `video-cut smart-cut --input test.mp4 --analyze-only` zeigt eine Aufschlüsselung der erkannten Schnitt-Kandidaten. `video-cut smart-cut --input test.mp4` erzeugt ein Video ohne Füllwörter und gekürzte Pausen. --- ## Phase 2c: KI-Erweitert — LLM-Integration ### Neue Dateien ``` auto_video_cut/ ├── chapters.py ← Auto-Kapitel via LLM ├── highlights.py ← Highlight-Reel └── describe.py ← Natürlichsprachliche Sequenz-Erstellung ``` --- ### Schritt 2c-1: Auto-Kapitel (`chapters.py`) **Workflow:** ``` Video → Whisper-Transkript → LLM (Claude) → Kapitel mit Titeln ``` ```python # chapters.py @dataclass class Chapter: title: str start: float end: float summary: str def generate_chapters( segments: list[Segment], llm_provider: str = "anthropic", # anthropic | ollama model: str = "claude-haiku-4-5-20251001", language: str = "de", max_chapters: int = 10, ) -> list[Chapter]: """Kapitel aus Transkript generieren.""" # Transkript als Text aufbereiten (mit Timestamps) # → LLM-Prompt: # "Du bist ein Video-Editor. Analysiere dieses Transkript # und erstelle sinnvolle Kapitel mit kurzen, prägnanten Titeln. # Gib Start-Timestamp und Titel für jedes Kapitel zurück." # → JSON-Response parsen def chapters_to_youtube_format(chapters: list[Chapter]) -> str: """Kapitel als YouTube-kompatible Beschreibung formatieren.""" # 0:00 Intro # 0:45 Ankunft in Berlin # 3:22 Restaurantbesuch # ... def chapters_to_sequence_entries(chapters: list[Chapter]) -> list[dict]: """Kapitel als type:text Einträge für sequence.yaml erzeugen.""" # Für jedes Kapitel einen Text-Clip mit dem Titel generieren ``` **CLI:** ```bash video-cut chapters --input video.mp4 --output chapters.txt video-cut chapters --input video.mp4 --format youtube video-cut chapters --input video.mp4 --format sequence # → YAML-Snippet video-cut chapters --input video.mp4 --inject-titles # Text-Clips einfügen ``` **LLM-Konfiguration:** ```yaml ai: llm_provider: "anthropic" # anthropic | ollama llm_model: "claude-haiku-4-5-20251001" anthropic_api_key: null # oder ANTHROPIC_API_KEY env var ollama_url: "http://localhost:11434" ollama_model: "llama3" ``` **Akzeptanzkriterium:** `video-cut chapters --input test.mp4 --format youtube` gibt eine YouTube-kompatible Kapitel-Beschreibung aus. --- ### Schritt 2c-2: Highlight-Reel (`highlights.py`) **Workflow:** ``` Video → Transkript + Szenen-Erkennung → LLM bewertet Szenen → Beste auswählen → Zusammenschneiden ``` ```python # highlights.py @dataclass class ScoredScene: start: float end: float score: float # 0.0–1.0 reason: str # Warum diese Szene interessant ist def score_scenes( segments: list[Segment], scenes: list[TimeRange], llm_provider: str = "anthropic", ) -> list[ScoredScene]: """Szenen nach Interesse bewerten.""" # Für jede Szene: zugehörigen Transkript-Text extrahieren # LLM bewerten lassen: # - Enthält Schlüsselaussage? # - Emotionaler Moment? # - Neues Thema/Ort? # - Humor/Überraschung? def create_highlight_reel( video: Path, output: Path, target_duration: float = 60.0, # Ziel-Dauer in Sekunden model_size: str = "base", crossfade: float = 0.3, ) -> Path: """Automatisch Highlight-Reel zusammenstellen.""" # 1. Transkribieren # 2. Szenen erkennen # 3. Szenen bewerten # 4. Beste Szenen auswählen (Rucksack-Problem: maximize score, constrain duration) # 5. Chronologisch sortieren # 6. Mit Crossfades zusammenfügen ``` **CLI:** ```bash video-cut highlights --input video.mp4 --duration 60 video-cut highlights --input video.mp4 --duration 120 --output best_of.mp4 ``` --- ### Schritt 2c-3: Natürlichsprachliche Sequenz (`describe.py`) **Workflow:** ``` Natürlichsprachliche Beschreibung → LLM → sequence.yaml ``` ```python # describe.py def generate_sequence( description: str, available_resources: dict, # Gefundene Dateien in resources/ available_files: list[Path], # Dateien im angegebenen Ordner config: dict, ) -> str: """Aus natürlicher Beschreibung eine sequence.yaml generieren.""" # LLM-Prompt: # "Du bist ein Video-Editor. Erstelle eine sequence.yaml # basierend auf folgender Beschreibung: # '' # # Verfügbare Ressourcen: # - Musik: # - Intros: # - Bilder: # - Videos im Ordner: # # Format der sequence.yaml: " ``` **CLI:** ```bash video-cut describe "Mach ein Reisevlog aus ./berlin/, Stille raus, Intro dran, ruhige Musik" # → Erzeugt sequence.yaml und zeigt Vorschau video-cut describe "..." --execute # Direkt rendern ``` --- ## Aktualisierte Projektstruktur (nach Phase 2) ``` auto_video_cut/ ├── __init__.py ├── cli.py ← CLI (erweitert: smart-cut, transcribe, subtitle, chapters, highlights, describe, bot) ├── config.py ← Konfiguration (erweitert: ai-Block, ducking, transitions) ├── runner.py ← NEU: zentrale ffmpeg-Ausführung mit Fortschritt ├── progress.py ← NEU: Fortschrittsanzeige (rich) ├── cutter.py ← Stille/Szenen (unverändert, aber nutzt runner.py) ├── audio.py ← Musik-Mixing (erweitert: Ducking-Option) ├── ducking.py ← NEU: Audio-Ducking ├── merger.py ← Clips zusammenführen (erweitert: Crossfade-Support) ├── transitions.py ← NEU: Crossfade, Fade-in/out ├── text.py ← Text-Overlays (unverändert) ├── sequencer.py ← Sequenz-Datei (erweitert: smart_cut, auto_subtitles, transitions) ├── transcribe.py ← NEU: Whisper-Transkription ├── subtitles.py ← NEU: SRT erzeugen + einbrennen ├── smart_cut.py ← NEU: Füllwort-/Fehlstart-Erkennung ├── chapters.py ← NEU: Auto-Kapitel via LLM ├── highlights.py ← NEU: Highlight-Reel ├── describe.py ← NEU: Natürlichsprachliche Sequenzen └── bot.py ← NEU: Discord-Bot (Phase 3) ``` --- ## Abhängigkeiten (komplett) ```toml [project] dependencies = [ "typer>=0.12", "pyyaml>=6.0", "scenedetect[opencv]>=0.6", "ffmpeg-python>=0.2", "rich>=13.0", "discord.py>=2.3", ] [project.optional-dependencies] ai = [ "faster-whisper>=1.0", "anthropic>=0.40", ] ``` - `pip install -e .` → CLI + Discord (ohne KI) - `pip install -e ".[ai]"` → alles inkl. Whisper + LLM --- ## Implementierungsreihenfolge | # | Schritt | Abhängigkeiten | Aufwand | |---|---------|---------------|---------| | 1 | `runner.py` + `progress.py` | keine | klein | | 2 | Bestehende Module auf `runner.py` umstellen | #1 | klein | | 3 | `transitions.py` (Crossfade, Fade) | #1 | mittel | | 4 | `merger.py` Crossfade-Integration | #3 | mittel | | 5 | `ducking.py` + `audio.py` Integration | #1 | mittel | | 6 | Preview + Dry-Run in `cli.py` | #1 | klein | | 7 | `transcribe.py` (Whisper) | keine | mittel | | 8 | `subtitles.py` (SRT + Einbrennen) | #7 | klein | | 9 | `smart_cut.py` (Füllwörter, Fehlstarts) | #7 | mittel | | 10 | `chapters.py` (LLM) | #7 | mittel | | 11 | `highlights.py` (LLM + Szenen) | #7, #10 | groß | | 12 | `describe.py` (natürlichsprachlich) | #10 | mittel | | 13 | `bot.py` (Discord) | alle | groß | **Empfohlener Start:** #1 → #2 → #7 → #9 → #8 (Fortschritt + Whisper-Kern zuerst) --- ## Risiken | Risiko | Auswirkung | Mitigation | |--------|-----------|------------| | Whisper-Modell zu langsam auf CPU | KI-Features unbrauchbar | `tiny`/`base` als Default, GPU-Support dokumentieren | | faster-whisper API-Änderungen | Import-Fehler | Version pinnen, Import-Fallback | | LLM-API-Kosten (Anthropic) | Unerwartete Kosten bei vielen Videos | Haiku als Default, Kosten-Warnung im CLI | | ffmpeg xfade-Kompatibilität | Alte ffmpeg-Versionen haben keinen xfade | Versionscheck beim Start, Fallback auf harten Schnitt | | Füllwort-Erkennung fehlerhaft | "Also" als Satzanfang wird geschnitten | Kontext-Analyse (nur standalone-Füllwörter), `--analyze-only` zum Review |