Metadata-Version: 2.4
Name: kurmann-vorschaubild-manager
Version: 2.1.3
Summary: CLI-Tool zum Extrahieren und Auswählen von Vorschaubildern aus Video- oder Bilddateien
License: MIT License
        
        Copyright (c) 2026 Patrick Kurmann
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/kurmann/vorschaubild-manager
Project-URL: Source, https://github.com/kurmann/vorschaubild-manager
Project-URL: Issues, https://github.com/kurmann/vorschaubild-manager/issues
Project-URL: Changelog, https://github.com/kurmann/vorschaubild-manager/blob/main/CHANGELOG.md
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Pillow>=10.0.0
Requires-Dist: anthropic>=0.20.0
Requires-Dist: tomli-w>=1.0.0
Requires-Dist: tomli>=2.0.0; python_version < "3.11"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Dynamic: license-file

# vorschaubild-manager

CLI-Tool zum Extrahieren und Auswählen von Vorschaubildern (Poster) aus Video- oder Bilddateien.
Unterstützt manuelle Auswahl, vollautomatische KI-Auswahl (Claude Vision) sowie einen
halbautomatischen Suggest-Modus, bei dem die KI einen Frame vorschlägt und du bestätigst.

Jedes erzeugte JPEG enthält eingebettete Erstellungsmetadaten (EXIF), sodass die genauen
Parameter der Erzeugung erhalten bleiben und später mit dem `info`-Unterbefehl ausgelesen
werden können.
Wenn aus einem Video, das bereits ein VTC-generiertes eingebettetes Poster besitzt, erneut
ein Poster erzeugt wird, extrahiert das Tool automatisch den originalen Frame aus dem Video
anhand des gespeicherten `frame_index`-Metadatums — für eine saubere, hochqualitative Quelle
bei der Neukomposition.

> **Hinweis:** Das Tool extrahiert und speichert ausschließlich ein Standbild. Das Einbetten
> des Bildes in die Videodatei (z. B. mit AtomicParsley) liegt außerhalb des Funktionsumfangs
> und muss vom aufrufenden System übernommen werden.

---

## Voraussetzungen

- Python 3.10+
- [ffmpeg](https://ffmpeg.org/) und `ffprobe` im `$PATH` *(erforderlich für Video-Eingabe; nicht benötigt für reine Bildeingabe)*
- **nur macOS:** `sips` — eingebautes macOS-Bildwerkzeug für Weitfarbraum-Konvertierung (TIFF/HEIC mit Rec.2020 oder Display P3).
  Auf Linux/Windows wird Pillow als Fallback verwendet, verarbeitet Weitfarbraum-Bilder jedoch möglicherweise nicht korrekt.
- Ein [Anthropic API Key](https://console.anthropic.com/) für die Modi `auto` / `suggest`
  — siehe [API-Keys](#api-keys-ki-funktionen)

---

## Installation

```bash
pip install kurmann-vorschaubild-manager
```

Im Entwicklungsmodus (editierbar):

```bash
pip install -e .
```

---

## Verwendung

```
vorschaubild-manager extract <input_path> [OPTIONS]
```

Alle verfügbaren Befehle und Optionen anzeigen:

```bash
vorschaubild-manager --help
vorschaubild-manager extract --help
```

### Optionen

| Option | Beschreibung |
|--------|--------------|
| `--mode manual` | Interaktiv: Mosaik öffnen, Frame-Nummer 0–19 eingeben |
| `--mode auto` | Vollautomatisch: KI wählt den besten Frame |
| `--mode suggest` | KI schlägt einen Frame vor; du bestätigst oder überschreibst |
| `--frame N` | Video-Framenummer direkt extrahieren (z. B. Frame 1000 bei 60fps ≈ 16.7s); überspringt Mosaik und Frame-Auswahl |
| `--at TIMESTAMP` | Frame an einem bestimmten Zeitpunkt extrahieren (z. B. `1:30` oder `90`); überspringt die Frame-Auswahl und die 20-Frame-Beschränkung |
| `--format poster` | Ausgabeformat 2:3 (1080×1620) mit 1:1-Ausschnitt + Textbereich **(Standard)** |
| `--format landscape` | Ausgabeformat 16:9 (1920×1080) |
| `--embedded-image prefer` | Eingebettetes Cover-Artwork oder Sidecar-Bild verwenden, falls vorhanden; sonst Frames extrahieren |
| `--embedded-image ignore` | Immer Frames extrahieren (eingebettetes Cover-Artwork und Sidecar-Bilder ignorieren) |
| `--embedded-image ask` | Benutzer fragen, wenn ein eingebettetes Bild oder Sidecar-Bild gefunden wird **(Standard)** |
| `--crop-position POSITION` | Ausschnittsposition direkt setzen (`left`, `center-left`, `center`, `center-right`, `right`); überspringt interaktive Eingabeaufforderung und KI-Auswahl |
| `--overlay-title TEXT` | Titeltext für die Textüberlagerung |
| `--overlay-title-from-filename` | Dateiname (ohne Endung) als Überlagerungstitel verwenden |
| `--overlay-category TEXT` | Kategoriebeschriftung im Poster-Format (nur Poster-Format); im `internet`-Stil als Overlay-Band am oberen Bildrand, in anderen Stilen oberhalb des Titels |
| `--overlay-category-logo PATH` | PNG-Logo anstelle des Kategorietexts (nur Poster-Format); im `internet`-Stil am oberen Bildrand, in anderen Stilen: breite Logos zentriert über dem Titel, quadratische/Hochformat-Logos links davon |
| `--overlay-note TEXT` | Kleiner Text zentriert am unteren Rand des Poster-Textbereichs (nur Poster-Format) |
| `--style NAME` | Zu verwendender Poster-Stil (Standard: `internet`). `vorschaubild-manager styles` listet verfügbare Optionen |
| `--description TEXT` | Optionale Videobeschreibung für KI-Kontext (max. 1000 Zeichen) |
| `--output-dir PATH` | Ausgabeverzeichnis (Standard: gleiches Verzeichnis wie die Videodatei) |
| `--output-name-suffix SUFFIX` | Suffix, das an den Dateinamen-Stamm angehängt wird (Standard: `-poster`) |
| `--json` | Maschinenlesbares JSON auf stdout ausgeben |
| `--no-badges` | Automatische technische Badges (4K, HD, HDR) auf dem Poster deaktivieren |
| `--fanart` | Zusätzliches sauberes 16:9-Fanart-Bild (für Infuse/Emby) mit `-fanart`-Suffix erzeugen |
| `--verbose` | Ausführliche Statusmeldungen auf stderr (Badge-Erkennung, KI-Begründungen, ffmpeg-Ausgabe); beeinflusst stdout nicht |

---

## Unterbefehl `info`

Erstellungsmetadaten aus einem generierten Poster-Bild lesen und anzeigen:

```bash
vorschaubild-manager info /path/to/poster.jpg
```

**Standardausgabe:**
```
Poster Metadata:
  Version            1.0.0.dev1
  Source             frame
  Frame Index        12
  Crop Position      center-left
  Format             poster
  Mode               auto
  Input File         2025-11-01_Herbst-Spaziergang.mp4
  Overlay Title      Herbst-Spaziergang
  Category           Videoschnittstudio Silvan Kurmann
  Note               1. November 2025
  Timestamp (s)      185.25
  AI Reasoning       Sharp, well-lit frame with child running towards camera…
  Created            2026-02-26T14:30:00
```

**JSON-Ausgabe (`--json`):**
```bash
vorschaubild-manager info --json /path/to/poster.jpg
```
```json
{
  "vtc_version": "1.0.0.dev1",
  "source": "frame",
  "frame_index": 12,
  "timestamp_seconds": 185.25,
  "crop_position": "center-left",
  "format": "poster",
  "mode": "auto",
  "input_file": "2025-11-01_Herbst-Spaziergang.mp4",
  "overlay_title": "Herbst-Spaziergang",
  "overlay_category": "Videoschnittstudio Silvan Kurmann",
  "overlay_note": "1. November 2025",
  "ai_reasoning": "Sharp, well-lit frame with child running towards camera...",
  "created_at": "2026-02-26T14:30:00"
}
```

Die eingebetteten Metadaten ermöglichen eine spätere Neugenerierung von Postern (z. B. mit
einem neuen Template), ohne KI-Aufrufe oder interaktive Eingaben zu benötigen.

---

## Poster-Format (2:3)

Das Standard-`poster`-Format erzeugt ein **1080×1620**-Bild aus zwei Abschnitten.
Das konkrete Layout hängt vom aktiven Stil ab (siehe [Poster-Stile](#poster-stile)).

**Standard-Stil `internet` (aktuelle Voreinstellung):**

```
┌──────────────────┐
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│  ← optionaler Kategorie-Header (Overlay am oberen Bildrand)
│▓  [Kategorie]   ▓│
│                  │
│   1:1-Ausschnitt │  ← 1080×1080-Quadratausschnitt (mit leichter Vignette)
│   des Frames     │  ← Badges (4K, HDR, FHD) ebenfalls auf dem Bild
│                  │
│                  │
│▓                ▓│
│▓    Titel       ▓│  ← fett, groß (44–108px), zentriert, Schlagschatten
│▓                ▓│
│▓     [Note]     ▓│  ← optionaler kleiner Hinweistext, zentriert
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│  ← mattes Schwarz (#1e1e1e)
└──────────────────┘
        2:3
```

- **Oberer Bereich (1080×1080):** 1:1-Quadratausschnitt des ausgewählten Hochauflösungsframes
  mit leichtem radialem Vignetten-Effekt (15–20 % Randabdunkelung). Die horizontale
  Ausschnittsposition (`left` / `center-left` / `center` / `center-right` / `right`)
  kann direkt mit `--crop-position` gesetzt, im Modus `auto`/`suggest` von der KI gewählt
  oder im Modus `manual` interaktiv eingegeben werden. Im `internet`-Stil werden Badges
  (4K, HDR, FHD) auf dem Bild selbst platziert.
- **Kategorie-Header:** Im `internet`-Stil wird der Kategorietext (oder Logo) als
  halbtransparentes Overlay-Band am **oberen Rand des Bildes** gerendert — nicht im
  Textbereich darunter.
- **Unterer Bereich:** Der Textbereich füllt den gesamten unteren Abschnitt randlos aus.
  Details hängen vom aktiven Stil ab (siehe [Poster-Stile](#poster-stile)).

---

## Unterstützte Bildformate

Wenn `<input_path>` auf eine Bilddatei zeigt, wird die Mosaik-/Frame-Extraktions-Pipeline
übersprungen und das Bild direkt als Hochauflösungsquelle für Ausschnittsauswahl und
Poster-/Landscape-Komposition verwendet.

| Format | Hinweise |
|--------|----------|
| JPEG (`.jpg`, `.jpeg`) | Direkt verwendet (für sRGB-Bilder unveränderter Kopiervorgang) |
| PNG (`.png`) | Über Pillow in JPEG konvertiert |
| TIFF (`.tiff`, `.tif`) | Weitfarbraum (Rec.2020, P3) via `sips` (macOS) in sRGB konvertiert |
| HEIC/HEIF (`.heic`, `.heif`) | Via `sips` (macOS) in sRGB-JPEG konvertiert |

> **Hinweis:** Weitfarbraum-Konvertierung (Rec.2020, Display P3) erfordert
> macOS mit `sips`. Auf anderen Systemen wird Pillow als Fallback verwendet,
> verarbeitet Weitfarbraum-TIFF-Dateien jedoch möglicherweise nicht korrekt.

---

## Beispiele

### Manueller Modus

```bash
vorschaubild-manager extract /path/to/video.mp4 --mode manual
```

Das Mosaik aus 20 Frames wird im System-Bildbetrachter geöffnet. Danach Frame-Nummer
(0–19) am Prompt eingeben. Im Poster-Format wird auch nach der Ausschnittsposition gefragt.
Das Ergebnis wird neben dem Video gespeichert.

### Manueller Modus — Landscape mit Text-Überlagerung

```bash
vorschaubild-manager extract /path/to/video.mp4 \
  --mode manual \
  --format landscape \
  --overlay-title-from-filename
```

### Automatischer KI-Modus (Poster, Standard-Format)

```bash
vorschaubild-manager extract /path/to/video.mp4 \
  --mode auto \
  --description "Dokumentation über Raketenabschüsse" \
  --overlay-title "2025-05-15 – Starship IFT-7"
```

Die KI wählt den besten Frame aus dem 20-Frame-Mosaik, wählt dann die optimale
1:1-Ausschnittsposition für das Poster und rendert den Text im unteren Bereich.

### Poster mit Kategorie, Logo und Hinweis

```bash
# Kategorietext als Overlay-Band oben auf dem Bild (internet-Stil), Hinweis zentriert unten
vorschaubild-manager extract /path/to/video.mp4 \
  --mode auto \
  --overlay-title "Starship IFT-7" \
  --overlay-category "Space Exploration" \
  --overlay-note "2025-05-15"

# Kategorielogo am oberen Bildrand (internet-Stil)
vorschaubild-manager extract /path/to/video.mp4 \
  --mode auto \
  --overlay-title "Starship IFT-7" \
  --overlay-category-logo /pfad/zum/channel-logo-wide.png \
  --overlay-note "Episode 7"

# Quadratisches/Hochformat-Logo (links vom Titel, in Stilen ohne category_at_top)
vorschaubild-manager extract /path/to/video.mp4 \
  --mode manual \
  --overlay-title "Mein Dokumentarfilm" \
  --overlay-category-logo /pfad/zum/icon-square.png
```

> **Hinweis zu `--overlay-category` und `--overlay-category-logo`:** Wenn beide angegeben
> werden, hat das Logo Vorrang und es wird eine Warnung auf stderr ausgegeben.

### Eingebettetes Cover-Artwork verwenden (MP4/M4V/MOV)

```bash
# Eingebettetes Artwork verwenden, falls vorhanden; sonst Frame-Extraktion
vorschaubild-manager extract /path/to/video.mp4 \
  --embedded-image prefer \
  --mode auto

# Eingebettetes Cover-Artwork als Standard setzen
vorschaubild-manager config set defaults.embedded_image prefer
```

Die Option `--embedded-image` steuert den Umgang mit eingebettetem Cover-Artwork und
Sidecar-Bildern:
- `prefer`: Eingebettetes Bild oder Sidecar-Bild verwenden, wenn vorhanden; sonst Frames extrahieren.
- `ignore`: Immer Frames extrahieren, auch wenn ein eingebettetes Bild oder Sidecar-Bild existiert.
- `ask` *(Standard)*: Benutzer fragen, wenn ein eingebettetes Bild oder Sidecar-Bild gefunden wird.

#### Intelligente Neugenerierung aus VTC-generierten eingebetteten Postern

Wenn das eingebettete Poster zuvor von `vorschaubild-manager` erstellt wurde, liest das Tool die
gespeicherten EXIF-Metadaten, um den **originalen Rohframe** aus dem Video in voller
Auflösung neu zu extrahieren und ein frisches Poster anhand der aktuellen Template- und
Stil-Einstellungen zu komponieren. So wird vermieden, das bereits gerenderte Bild (mit
eingebackenen Text-Overlays, Ausschnitt und Vignette) als Kompositionsquelle zu verwenden.

Folgende Metadatenfelder werden aus dem vorhandenen Poster weitergenutzt:
- `frame_index` — welcher Frame (0–19) ursprünglich ausgewählt wurde
- `crop_position` — welche Ausschnittsposition verwendet wurde (kann mit `--crop-position` überschrieben werden)
- `ai_reasoning` — die ursprüngliche KI-Begründung, im neuen Poster-Metadatum erhalten

Wenn das eingebettete Bild **nicht** von `vorschaubild-manager` generiert wurde (z. B. extern
gesetzt) oder `frame_index` nicht verfügbar ist, fällt das Tool auf das direkte Verwenden
des eingebetteten Bilds als Quelle zurück.

> **Hinweis:** Die Erkennung eingebetteter Bilder wird nur für MP4-, M4V- und MOV-Container
> unterstützt. Für andere Formate wird zunächst nach Sidecar-Dateien gesucht, dann werden
> Frames extrahiert.
> Die Option `--embedded-image` ist unabhängig von `--mode`; der Modus bestimmt nur,
> wie die Ausschnittsposition nach Auflösung der Bildquelle ermittelt wird.

### Sidecar-Vorschaubilder verwenden

Beim Extrahieren aus einer Videodatei (z. B. `video.mp4`) prüft das Tool auch, ob im
gleichen Verzeichnis Vorschaubilder («Sidecar»-Dateien) mit gleichem Dateinamen-Stamm
vorhanden sind:

```
video.jpg / video.jpeg / video.png / video.tiff / video.tif
```

Die Erkennungsreihenfolge für Video-Eingabe ist:
1. Eingebettetes Bild (im Video-Container)
2. Sidecar-Bild (neben der Videodatei)
3. Frame-Extraktion (Mosaik-Flow)

Die Option `--embedded-image` steuert den Umgang mit Sidecar-Bildern auf die gleiche Weise
wie den Umgang mit eingebetteten Bildern.

```bash
# Sidecar-Bild verwenden, falls vorhanden; sonst Frame-Extraktion
vorschaubild-manager extract /path/to/video.mp4 \
  --embedded-image prefer \
  --mode auto

# Sidecar-Bilder immer ignorieren
vorschaubild-manager extract /path/to/video.mp4 \
  --embedded-image ignore \
  --mode manual
```

Wenn ein Sidecar-Bild verwendet wird, enthält die JSON-Ausgabe `"source": "sidecar"`:
```json
{
  "poster_path": "/path/to/video-poster.jpg",
  "frame_index": -1,
  "mode": "auto",
  "format": "poster",
  "source": "sidecar",
  "reasoning": "Sidecar image file was used as source. | Crop: ...",
  "crop_position": "center",
  "input_path": "/path/to/video.mp4"
}
```

### `--crop-position` — interaktive und KI-Ausschnittsauswahl überspringen

```bash
# Ausschnittsposition direkt setzen — überspringt KI-Ausschnittsauswahl (spart API-Kosten)
vorschaubild-manager extract /path/to/video.mp4 \
  --mode auto \
  --crop-position center

# Nützlich für Stapelverarbeitung, wenn die Ausschnittsposition bereits bekannt ist
for f in /videos/*.mp4; do
  vorschaubild-manager extract "$f" --mode auto --crop-position center-left
done
```

`--crop-position` akzeptiert: `left`, `center-left`, `center`, `center-right`, `right`.
Wenn angegeben mit `--format poster`, wird sowohl die interaktive Ausschnittsabfrage
(manueller Modus) als auch die KI-Ausschnittauswahl (auto/suggest-Modus) übersprungen.

### Bilddatei als Eingabe (JPEG, PNG, TIFF, HEIC)

```bash
# Poster aus TIFF-Bild erstellen (automatische Farbraumkonvertierung auf macOS)
vorschaubild-manager extract /path/to/photo.tiff \
  --mode auto \
  --overlay-title "Herbst-Spaziergang"

# Poster aus JPEG mit manueller Ausschnittsauswahl
vorschaubild-manager extract /path/to/photo.jpg \
  --mode manual \
  --overlay-title "Mein Foto" \
  --overlay-category "Familie Kurmann"
```

Bei Bildeingabe sind ffmpeg/ffprobe nicht erforderlich. Das Bild selbst dient als
Hochauflösungsquelle; nur Ausschnittauswahl und Poster-Komposition laufen ab.

### Halbautomatischer Suggest-Modus

```bash
vorschaubild-manager extract /path/to/video.mp4 \
  --mode suggest \
  --output-dir /tmp/thumbs \
  --output-name-suffix -thumb
```

Im Poster-Format schlägt die KI nach der Frame-Auswahl auch eine Ausschnittsposition vor,
die du bestätigen oder überschreiben kannst.

---

## API-Keys (KI-Funktionen)

Für die KI-gestützte Frame- und Crop-Auswahl wird ein
[Anthropic API Key](https://console.anthropic.com/) benötigt.

### Empfohlenes Setup: `ANTHROPIC_API_KEY` als Umgebungsvariable

```bash
# In ~/.zshenv eintragen (gilt für interaktive UND non-interaktive Shells,
# also auch für launchd / cron / Tool-Subprozesse via uv tool):
echo 'export ANTHROPIC_API_KEY="sk-ant-..."' >> ~/.zshenv

# Für laufende launchd-Jobs zusätzlich (einmalig, persistiert über Reboots):
launchctl setenv ANTHROPIC_API_KEY "sk-ant-..."
```

`ANTHROPIC_API_KEY` ist der **offizielle** Variablenname des Anthropic-SDK
und wird ab v2.1.3 bevorzugt gelesen. Der historische Name `CLAUDE_API_KEY`
bleibt als Fallback unterstützt — bestehende Setups laufen ohne Änderung
weiter.

### Lookup-Reihenfolge (höchste zuerst)

1. `ANTHROPIC_API_KEY` aus dem Prozess-Environment
2. `CLAUDE_API_KEY` aus dem Prozess-Environment (Backward-Fallback)
3. `claude.api_key` aus `~/.config/vorschaubild-manager/config.toml`
4. — sonst Fehler mit klarer Meldung

Diese Reihenfolge gilt unabhängig davon, ob der Vorschaubild-Manager als
CLI oder als Library (z.B. via Mediaset-Creator-Dependency) genutzt wird.

### Wenn der Vorschaubild-Manager nur als Library installiert ist

Etwa wenn du nur den
[`familienfilm-manager`](https://github.com/kurmann/familienfilm-manager)
oder den
[`mediaset-creator`](https://github.com/kurmann/mediaset-creator) via PyPI
installierst, kommt der Vorschaubild-Manager als Dependency mit. Der
`~/.config/vorschaubild-manager/config.toml`-Pfad funktioniert trotzdem:

```bash
vorschaubild-manager config set claude.api_key "sk-ant-..."
```

Allerdings: wenn du den ENV-Weg ohnehin gewählt hast (siehe oben), ist die
Config-Datei redundant — der ENV-Wert hat Vorrang.

---

## Konfiguration

Einstellungen können in `~/.config/vorschaubild-manager/config.toml` gespeichert werden, damit
sie nicht bei jedem Aufruf übergeben werden müssen. Verzeichnis und Datei werden beim
ersten `config set` automatisch angelegt.

### Prioritätsreihenfolge (höchste zuerst)

1. Explizite CLI-Argumente
2. Konfigurationsdatei-Werte
3. Eingebaute Standardwerte

### Befehle

```bash
# Wert speichern
vorschaubild-manager config set claude.api_key "sk-ant-..."

# Einzelnen Wert lesen
vorschaubild-manager config get claude.model

# Alle gespeicherten Werte anzeigen
vorschaubild-manager config list
```

### Erlaubte Schlüssel

| Schlüssel | Beschreibung | Standard |
|-----------|--------------|---------|
| `claude.api_key` | Anthropic Claude API-Schlüssel — **deprecated seit v2.1.3**, nutze stattdessen `ANTHROPIC_API_KEY` als Umgebungsvariable; wird in v3.0.0 entfernt | *(keiner)* |
| `claude.model` | Claude-Modellname | `claude-sonnet-4-5` |
| `tools.ffmpeg` | Pfad zur `ffmpeg`-Binärdatei | `ffmpeg` |
| `tools.ffprobe` | Pfad zur `ffprobe`-Binärdatei | `ffprobe` |
| `defaults.output_name_suffix` | Suffix für Ausgabedateinamen | `-poster` |
| `defaults.mode` | Standard-Auswahlmodus | `manual` |
| `defaults.format` | Standard-Ausgabeformat (`poster` oder `landscape`) | `poster` |
| `defaults.embedded_image` | Standard für eingebettete Bilder (`prefer`, `ignore`, `ask`) | `ask` |

### Beispiel `config.toml`

```toml
[claude]
api_key = "sk-ant-..."
model = "claude-sonnet-4-5"

[tools]
ffmpeg = "ffmpeg"
ffprobe = "ffprobe"

[defaults]
output_name_suffix = "-poster"
mode = "manual"
format = "poster"
embedded_image = "ask"
```

### Ausgabedateiname

Der Ausgabedateiname ergibt sich aus dem Dateinamen-Stamm plus dem Suffix:
```
2025-05-15_Starship_IFT7.mkv  +  Suffix „-poster"  →  2025-05-15_Starship_IFT7-poster.jpg
```

---

## Ausgabe

### Standard (ohne `--json`)

stdout enthält nur den absoluten Pfad des erzeugten Bilds:
```
/pfad/zum/poster.jpg
```

Die CLI ist **pipeline-fähig**: stdout ist immer sauber — nur das finale Ergebnis oder
(mit `--json`) strukturiertes JSON. Nichts anderes landet auf stdout.

Statusmeldungen und Fortschrittsinformationen werden auf **stderr** ausgegeben.
Mit `--verbose` werden zusätzlich KI-Begründungen, Badge-Erkennung und ffmpeg-Ausgaben
auf stderr angezeigt. Das Verhalten von stdout wird dadurch nicht beeinflusst.

### JSON-Modus (`--json`)

```json
{
  "poster_path": "/pfad/zum/poster.jpg",
  "frame_index": 12,
  "mode": "auto",
  "format": "poster",
  "source": "frame",
  "reasoning": "Sharp, well-lit frame representative of the content.",
  "crop_position": "center-left",
  "overlay_title": "Mein Filmtitel",
  "overlay_category": "Space Exploration",
  "overlay_note": "2025-05-15",
  "input_path": "/path/to/video.mp4"
}
```

Das Feld `overlay_category` ist nur vorhanden, wenn `--overlay-category` angegeben wurde
(und `--overlay-category-logo` nicht zusammen damit verwendet wird). Das Feld `overlay_note`
ist nur vorhanden, wenn `--overlay-note` angegeben wurde.

Wenn eingebettetes Cover-Artwork als Quelle verwendet wurde:
```json
{
  "poster_path": "/path/to/video-poster.jpg",
  "frame_index": -1,
  "mode": "auto",
  "format": "poster",
  "source": "embedded",
  "reasoning": "Embedded cover art was used as source image. | Crop: ...",
  "crop_position": "center",
  "input_path": "/path/to/video.mp4"
}
```

Wenn das eingebettete Bild ein **VTC-generiertes Poster** mit gültigen Metadaten ist, wird
der originale Frame neu extrahiert. In diesem Fall sieht die Ausgabe wie ein normales
frame-basiertes Ergebnis mit `"source": "frame"` und dem tatsächlichen `frame_index` aus.

Wenn eine Bilddatei als Quelle verwendet wurde:
```json
{
  "poster_path": "/path/to/photo-poster.jpg",
  "frame_index": -1,
  "mode": "auto",
  "format": "poster",
  "source": "image",
  "reasoning": "Image file was used as source. | Crop: ...",
  "crop_position": "center",
  "input_path": "/path/to/photo.tiff"
}
```

Das Feld `source` ist `"frame"` bei Verwendung eines Video-Frames, `"embedded"` bei
direkter Verwendung von eingebettetem Cover-Artwork (kein VTC-Bild oder kein nutzbares
Frame-Metadatum), `"sidecar"` bei Verwendung einer Sidecar-Bilddatei und `"image"` bei
Bilddatei-Eingabe. Bei `"embedded"`, `"sidecar"` oder `"image"` ist `frame_index` `-1`.

Wenn `--fanart` verwendet wird, enthält die JSON-Ausgabe ein zusätzliches Feld `fanart_path`:
```json
{
  "poster_path": "/pfad/zum/video-poster.jpg",
  "fanart_path": "/pfad/zum/video-fanart.jpg",
  ...
}
```

---

## Fanart-Bild (`--fanart`)

Das Flag `--fanart` erzeugt **zusätzlich** zum normalen Poster- oder Landscape-Ausgabebild
ein sauberes 16:9-JPEG. Dieses Bild enthält keine Text-Overlays, keine Badges und keine
Farbverläufe — nur das pure Quell-Frame skaliert auf 16:9. Es ist für Medienserver wie
**Infuse** und **Emby** gedacht, die nach einer Datei mit `-fanart`-Suffix suchen.

```bash
# Erzeugt sowohl "My Video-poster.jpg" als auch "My Video-fanart.jpg"
vorschaubild-manager extract "My Video.mp4" --fanart
```

**Ausgabeauflösung:**
- 4K-Quelle (Breite ≥ 3840 oder Höhe ≥ 2160): **3840 × 2160**
- Sonst: **1920 × 1080**

**Nicht-16:9-Quellen:** Ein unscharfer Hintergrundfüller wird automatisch angewendet
(gleicher visueller Ansatz wie bei der vorhandenen Frame-Extraktion), sodass die Ausgabe
immer exakt 16:9 ist — ohne schwarze Balken oder Verzerrung.

---

## Poster-Stile

Alle visuellen Design-Konstanten (Farben, Schriftarten, Layout, Badge-Platzierung) sind
als eingebaute benannte Stile definiert. Stil mit `--style <name>` wählen (Standard: `internet`).

### Verfügbare Stile anzeigen

```bash
vorschaubild-manager styles
```

| Stil | Beschreibung |
|------|--------------|
| `internet` | Kategorie-Header oben auf dem Bild, mattes schwarzes Textfeld, große fette Schriften, Badges auf dem Bild — optimiert für YouTube/Web-Video-Poster |

### Beispiel

```bash
# Standard-Stil (`internet`)
vorschaubild-manager extract video.mp4 \
  --overlay-title "My Title" \
  --overlay-category "My Channel" \
  --overlay-note "15. März 2025"
```

- **Oberer Bereich (1080×1080):** 1:1-Quadrat-Crop des ausgewählten Hochauflösungs-Frames mit dezenter radialer Vignette (15–20% Randabdunkelung). Die horizontale Crop-Position (`left` / `center-left` / `center` / `center-right` / `right`) kann direkt mit `--crop-position` gesetzt, im `auto`/`suggest`-Modus von der KI gewählt oder im `manual`-Modus vom Nutzer bestimmt werden. Im `internet`-Stil werden Badges auf dem Bild selbst gerendert.
- **Kategorie-Header:** Im `internet`-Stil erscheint die Kategorie als halbtransparentes Overlay-Band am **oberen Rand des Bildes**.
- **Unterer Bereich:** Der Textbereich füllt den gesamten unteren Teil randlos. Details abhängig vom aktiven Stil (siehe `vorschaubild-manager styles`).

---

## Exit-Codes

| Code | Bedeutung |
|------|-----------|
| `0` | Erfolg |
| `1` | Allgemeiner Fehler (Datei nicht gefunden, ffmpeg fehlt usw.) |
| `2` | Keine Auswahl getroffen (Benutzer hat abgebrochen) |
| `3` | KI-Auswahl fehlgeschlagen (kein API-Schlüssel, Timeout, ungültige Antwort) |

---

## Integration

### Python API

`kurmann-vorschaubild-manager` stellt eine klare öffentliche Python-API bereit, die drei
Betriebsmodi unterstützt — von vollautomatisch bis vollständig host-gesteuert:

```
Host-Applikation / CLI
  ├→ extract(request, runtime)            ← Standard: ein Aufruf, alles drin
  │    ExtractRequest                     ← fachliche Parameter
  │    RuntimeOptions                     ← technische Laufzeitoptionen (optional)
  │    ExtractResult                      ← strukturiertes Ergebnis
  │    on_event / on_output               ← Fortschritts-Callbacks (optional)
  │
  └→ Fortgeschrittener Workflow (schrittweise Steuerung durch Host)
       get_video_properties() → VideoProperties       (Metadaten, HDR, Auflösung)
       create_mosaic()        → CreateMosaicResult    (Mosaik für manuelle/KI-Auswahl)
       suggest_frame()        → SuggestFrameResult    (KI-Frame-Vorschlag, optional)
       extract_frame()        → ExtractFrameResult    (Frame in hoher Auflösung)
       suggest_crop()         → SuggestCropResult     (KI-Ausschnittsvorschlag, optional)
       render_poster()        → RenderPosterResult    (finales Poster)
```

**Betriebsmodi:**
- **Vollautomatisch**: `extract()` – ein Aufruf, KI entscheidet alles
- **Semi-automatisch**: Einzelschritte via Advanced Workflow API, KI gibt Vorschläge
- **Manuell / host-gesteuert**: Einzelschritte via Advanced Workflow API, Host entscheidet

Interne Module (`core/`) und `ThumbnailSession` sind kein Teil der öffentlichen API.
Host-Applikationen müssen ausschließlich die öffentliche API verwenden — keine internen
Module importieren, keine CLI-Ausgaben parsen.

---

#### `extract()` mit Request/Result

```python
from vorschaubild_manager.api import ExtractRequest, RuntimeOptions, ExtractResult, extract

request = ExtractRequest(
    input_path="/pfad/zum/video.mp4",
    overlay_title="Mein Film",
    overlay_category="Dokumentation",
    overlay_note="2026",
    output_dir="/ausgabe/",
    fanart=True,
)

# RuntimeOptions ist optional – Standardwerte werden verwendet wenn weggelassen
runtime = RuntimeOptions(
    ffmpeg="/usr/local/bin/ffmpeg",   # Standard: "ffmpeg"
    ffprobe="/usr/local/bin/ffprobe", # Standard: "ffprobe"
)

result: ExtractResult = extract(request, runtime)
print(result.poster_path)   # absoluter Pfad zum Poster-JPEG
print(result.fanart_path)   # absoluter Pfad zum Fanart-JPEG (oder None)
print(result.reasoning)     # KI-Begründung
print(result.frame_index)   # Verwendeter Frame (–1 bei Bildeingabe)
print(result.crop_position) # Verwendete Crop-Position
```

Felder von `ExtractRequest` (fachliche Parameter):

| Parameter | Typ | Beschreibung | Standard |
|-----------|-----|--------------|---------|
| `input_path` | `str` | Pfad zur Eingabedatei (Video oder Bild) | *(Pflicht)* |
| `mode` | `str` | Auswahlmodus (`"auto"`) | `"auto"` |
| `format` | `str` | Ausgabeformat (`"poster"` oder `"landscape"`) | `"poster"` |
| `crop_position` | `str \| None` | Explizite Crop-Position; `None` = KI bestimmt | `None` |
| `overlay_title` | `str \| None` | Titeltext auf dem Bild | `None` |
| `overlay_category` | `str \| None` | Kategoriebeschriftung (nur Poster) | `None` |
| `overlay_category_logo_path` | `str \| None` | Pfad zu PNG-Logo | `None` |
| `overlay_note` | `str \| None` | Kleiner Hinweistext, zentriert am unteren Rand des Textbereichs | `None` |
| `output_dir` | `str \| None` | Ausgabeverzeichnis | Verzeichnis der Eingabedatei |
| `output_path` | `str \| None` | Expliziter Ausgabepfad | `None` |
| `output_name_suffix` | `str` | Suffix für den Ausgabedateinamen | `"-poster"` |
| `fanart` | `bool` | Zusätzliches 16:9-Fanart-Bild erstellen | `False` |
| `no_badges` | `bool` | Keine automatischen Badges | `False` |
| `template` | `dict \| None` | Optionales Design-Template | `None` |
| `description` | `str \| None` | Inhaltliche Beschreibung als KI-Kontext | `None` |

Felder von `RuntimeOptions` (technische Laufzeitoptionen):

| Parameter | Typ | Beschreibung | Standard |
|-----------|-----|--------------|---------|
| `ffmpeg` | `str` | `ffmpeg`-Programmdatei | `"ffmpeg"` |
| `ffprobe` | `str` | `ffprobe`-Programmdatei | `"ffprobe"` |

Felder des `ExtractResult`:

| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `poster_path` | `str` | Absoluter Pfad zum Poster-JPEG |
| `fanart_path` | `str \| None` | Absoluter Pfad zum Fanart-JPEG (oder `None`) |
| `frame_index` | `int` | Index des verwendeten Frames (–1 bei Bildeingabe) |
| `crop_position` | `str` | Verwendete Crop-Position |
| `reasoning` | `str` | KI-Begründung (leer bei manueller Auswahl) |
| `format` | `str` | Verwendetes Ausgabeformat |
| `source` | `str` | Quelle: `"frame"`, `"image"`, `"embedded"`, `"sidecar"` |

---

#### Fortgeschrittener Workflow (Advanced Workflow API)

Für Host-Applikationen, die interaktive oder mehrstufige Workflows implementieren möchten,
bietet `vorschaubild-manager` fünf zusätzliche öffentliche API-Funktionen. Jede Operation
ist **in sich abgeschlossen** – kein Session-Objekt, kein impliziter Zustand.

Der Workflow unterstützt drei Betriebsmodi:
- **Vollautomatisch**: nur `extract()` – ein Aufruf, KI entscheidet alles
- **Semi-automatisch**: `create_mosaic()` → `suggest_frame()` (KI-Vorschlag) → `extract_frame()` → `suggest_crop()` → `render_poster()`
- **Manuell**: `create_mosaic()` → Host wählt Frame → `extract_frame()` → Host wählt Crop → `render_poster()`

```python
from vorschaubild_manager.api import (
    GetVideoPropertiesRequest, get_video_properties,
    CreateMosaicRequest, create_mosaic,
    SuggestFrameRequest, suggest_frame,
    ExtractFrameRequest, extract_frame,
    SuggestCropRequest, suggest_crop,
    RenderPosterRequest, render_poster,
    RuntimeOptions,
    VorschaubildManagerEvent, VorschaubildManagerOutput,
)

runtime = RuntimeOptions(ffmpeg="/usr/local/bin/ffmpeg")

# Schritt 0 (optional): Videoeigenschaften für Badge-Erkennung abfragen
# Ergebnis kann direkt als video_properties in render_poster() übergeben werden
video_props = get_video_properties(
    GetVideoPropertiesRequest(input_path="/pfad/zum/video.mp4"),
    runtime=runtime,
)
# video_props.is_4k, video_props.is_hdr, video_props.fps etc.

# Schritt 1: Kontaktabzug (Mosaik) erstellen
# create_mosaic() unterstützt on_event/on_output für Fortschrittsanzeige
events = []
mosaic = create_mosaic(
    CreateMosaicRequest(
        input_path="/pfad/zum/video.mp4",
        output_path="/tmp/video-mosaic.jpg",
    ),
    runtime=runtime,
    on_event=events.append,
)
# mosaic.mosaic_path  → Pfad zum Mosaik-JPEG (20 Frames, beschriftet)
# mosaic.frames       → Liste von MosaicFrame(index=0..19, timestamp_seconds=...)

# Schritt 2a (semi-automatisch): KI schlägt besten Frame vor
suggestion = suggest_frame(
    SuggestFrameRequest(
        mosaic_path=mosaic.mosaic_path,
        overlay_title="Mein Film",
    )
)
# suggestion.frame_index  → z. B. 12 (KI-Vorschlag)
# suggestion.reasoning    → KI-Begründung
chosen_index = suggestion.frame_index  # oder manuell übersteuern

# Schritt 2b (manuell): Host wählt Frame direkt
# chosen_index = 7  # z. B. nach Anzeige des Mosaiks

# Schritt 3: Gewählten Frame in hoher Auflösung extrahieren
# extract_frame() unterstützt on_event/on_output für Fortschrittsanzeige
frame = extract_frame(
    ExtractFrameRequest(
        input_path="/pfad/zum/video.mp4",
        frame_index=chosen_index,
        output_path="/tmp/video-frame.jpg",
    ),
    runtime=runtime,
)
# frame.frame_path   → Pfad zur extrahierten Frame-Datei
# frame.frame_index  → chosen_index

# Schritt 4: KI-Ausschnittsvorschlag einholen (oder manuell wählen)
crop = suggest_crop(
    SuggestCropRequest(image_path=frame.frame_path)
)
# crop.crop_position  → z. B. "center-left"
# crop.reasoning      → KI-Begründung

# Schritt 5: Poster rendern (mit Badges aus Videoeigenschaften)
poster = render_poster(
    RenderPosterRequest(
        image_path=frame.frame_path,
        crop_position=crop.crop_position,
        overlay_title="Mein Film",
        overlay_category="Dokumentation",
        output_path="/ausgabe/video-poster.jpg",
        video_properties=video_props,  # für automatische Badge-Erkennung (4K, HDR, HD)
    )
)
# poster.poster_path  → absoluter Pfad zum Poster-JPEG
```

##### `create_mosaic()` — Kontaktabzug erstellen

Unterstützt `on_event` und `on_output` Callbacks (siehe [Ereignis- und Ausgabe-Callbacks](#ereignis--und-ausgabe-callbacks)).

| Parameter (`CreateMosaicRequest`) | Typ | Beschreibung | Standard |
|-----------------------------------|-----|--------------|---------|
| `input_path` | `str` | Pfad zur Eingabe-**Videodatei** | *(Pflicht)* |
| `output_path` | `str \| None` | Pfad zur Ausgabe-Mosaik-Datei | neben Eingabedatei (`-mosaic.jpg`) |

| Feld (`CreateMosaicResult`) | Typ | Beschreibung |
|-----------------------------|-----|--------------|
| `mosaic_path` | `str` | Absoluter Pfad zum Mosaik-JPEG |
| `frames` | `list[MosaicFrame]` | Liste der 20 Frames mit Index und Zeitstempel |

`MosaicFrame`-Felder: `index` (0–19), `timestamp_seconds` (Zeitstempel in Sekunden).

##### `suggest_frame()` — KI-Frame-Vorschlag

Analysiert ein Mosaik-Bild mit KI und schlägt den besten Frame für ein Poster vor.
Der Host kann den Vorschlag übernehmen oder manuell übersteuern.
Erfordert einen konfigurierten Claude-API-Schlüssel.

| Parameter (`SuggestFrameRequest`) | Typ | Beschreibung | Standard |
|-----------------------------------|-----|--------------|---------|
| `mosaic_path` | `str` | Pfad zur Mosaik-Datei (Ergebnis von `create_mosaic()`) | *(Pflicht)* |
| `overlay_title` | `str \| None` | Titel des Videos als KI-Kontext | `None` |
| `description` | `str \| None` | Beschreibung des Videoinhalts als KI-Kontext | `None` |

| Feld (`SuggestFrameResult`) | Typ | Beschreibung |
|-----------------------------|-----|--------------|
| `frame_index` | `int` | Vorgeschlagener Frame-Index (0–19) |
| `reasoning` | `str` | KI-Begründung für den Vorschlag |

##### `extract_frame()` — Frame extrahieren

Unterstützt `on_event` und `on_output` Callbacks (siehe [Ereignis- und Ausgabe-Callbacks](#ereignis--und-ausgabe-callbacks)).

| Parameter (`ExtractFrameRequest`) | Typ | Beschreibung | Standard |
|-----------------------------------|-----|--------------|---------|
| `input_path` | `str` | Pfad zur Eingabe-**Videodatei** | *(Pflicht)* |
| `frame_index` | `int` | Frame-Index (0–19) | *(Pflicht)* |
| `output_path` | `str \| None` | Pfad zur Ausgabedatei | neben Eingabedatei (`-frame-{n}.jpg`) |
| `profile` | `str` | Ausgabeprofil (`"poster"`) | `"poster"` |

| Feld (`ExtractFrameResult`) | Typ | Beschreibung |
|-----------------------------|-----|--------------|
| `frame_path` | `str` | Absoluter Pfad zur extrahierten Frame-Datei |
| `frame_index` | `int` | Index des extrahierten Frames |

Bei HDR-Quellen (PQ/HLG) wird automatisch ein sRGB-Tonemapping durchgeführt.

##### `suggest_crop()` — KI-Ausschnittsvorschlag

| Parameter (`SuggestCropRequest`) | Typ | Beschreibung | Standard |
|----------------------------------|-----|--------------|---------|
| `image_path` | `str` | Pfad zum Quellbild | *(Pflicht)* |
| `overlay_title` | `str \| None` | Titel (für künftige KI-Nutzung vorgesehen) | `None` |
| `description` | `str \| None` | Beschreibung (für künftige KI-Nutzung vorgesehen) | `None` |

| Feld (`SuggestCropResult`) | Typ | Beschreibung |
|----------------------------|-----|--------------|
| `crop_position` | `str` | Vorgeschlagene Position: `left`, `center-left`, `center`, `center-right`, `right` |
| `reasoning` | `str` | KI-Begründung |

##### `render_poster()` — Poster aus vorbereittem Bild rendern

| Parameter (`RenderPosterRequest`) | Typ | Beschreibung | Standard |
|-----------------------------------|-----|--------------|---------|
| `image_path` | `str` | Pfad zum vorbereiteten Quellbild | *(Pflicht)* |
| `crop_position` | `str` | Ausschnittsposition | `"center"` |
| `format` | `str` | Ausgabeformat (`"poster"` oder `"landscape"`) | `"poster"` |
| `overlay_title` | `str \| None` | Titeltext | `None` |
| `overlay_category` | `str \| None` | Kategoriebeschriftung (nur Poster) | `None` |
| `overlay_category_logo_path` | `str \| None` | Pfad zu PNG-Logo | `None` |
| `overlay_note` | `str \| None` | Kleiner Hinweistext, zentriert am unteren Rand des Textbereichs | `None` |
| `output_dir` | `str \| None` | Ausgabeverzeichnis | Verzeichnis von `image_path` |
| `output_path` | `str \| None` | Expliziter Ausgabepfad | `None` |
| `output_name_suffix` | `str` | Suffix für Ausgabedateinamen | `"-poster"` |
| `fanart` | `bool` | 16:9-Fanart-Bild erstellen | `False` |
| `no_badges` | `bool` | Keine Badges | `False` |
| `template` | `dict \| None` | Design-Template | `None` |
| `video_properties` | `VideoProperties \| None` | Video-Eigenschaften für Badge-Erkennung (Ergebnis von `get_video_properties()`) | `None` |

| Feld (`RenderPosterResult`) | Typ | Beschreibung |
|-----------------------------|-----|--------------|
| `poster_path` | `str` | Absoluter Pfad zum Poster-JPEG |
| `fanart_path` | `str \| None` | Absoluter Pfad zum Fanart-JPEG (oder `None`) |
| `crop_position` | `str` | Verwendete Ausschnittsposition |
| `format` | `str` | Verwendetes Ausgabeformat |

##### `get_video_properties()` — Videoeigenschaften abfragen

Liest Videoeigenschaften (Auflösung, HDR-Status, Framerate) aus einer Videodatei und gibt
ein :class:`VideoProperties`-Objekt zurück. Dieses kann direkt als ``video_properties``-Parameter
in :class:`RenderPosterRequest` verwendet werden, um automatische Badge-Erkennung (4K, HDR, HD) zu aktivieren.

| Parameter (`GetVideoPropertiesRequest`) | Typ | Beschreibung | Standard |
|-----------------------------------------|-----|--------------|---------|
| `input_path` | `str` | Pfad zur Eingabe-**Videodatei** | *(Pflicht)* |

| Feld (`VideoProperties`) | Typ | Beschreibung |
|--------------------------|-----|--------------|
| `width` | `int` | Videobreite in Pixeln |
| `height` | `int` | Videohöhe in Pixeln |
| `is_4k` | `bool` | `True` wenn Breite ≥ 3840 oder Höhe ≥ 2160 |
| `is_hd` | `bool` | `True` wenn Breite ≥ 1920 oder Höhe ≥ 1080 (und nicht 4K) |
| `is_hdr` | `bool` | `True` bei HDR-Transferfunktion (PQ/HLG) |
| `fps` | `float` | Durchschnittliche Bildrate (0.0 wenn unbekannt) |
| `is_hfr` | `bool` | `True` wenn fps ≥ 48 (High Frame Rate) |

`VideoProperties` kann auch direkt ohne einen API-Aufruf instanziiert werden, wenn die Eigenschaften
im Host bereits bekannt sind — z. B. wenn ein anderes System die Video-Metadaten schon ermittelt hat:

```python
from vorschaubild_manager.api import VideoProperties, RenderPosterRequest, render_poster

# Manuell bekannte Eigenschaften übergeben
props = VideoProperties(is_4k=True, is_hdr=True)
poster = render_poster(
    RenderPosterRequest(
        image_path="/pfad/zum/frame.jpg",
        video_properties=props,
    )
)
```

---



Lang laufende API-Funktionen (`extract`, `create_mosaic`, `extract_frame`) unterstützen
zwei optionale Callback-Parameter für Fortschrittsüberwachung und Diagnose:

```python
import sys
from vorschaubild_manager.api import (
    VorschaubildManagerEvent,
    VorschaubildManagerOutput,
    CreateMosaicRequest, create_mosaic,
    RuntimeOptions,
)

def on_event(event: VorschaubildManagerEvent) -> None:
    print(f"[{event.stage_id}] {event.message}", file=sys.stderr)
    if event.current is not None:
        print(f"  Fortschritt: {event.current}/{event.total}", file=sys.stderr)

def on_output(output: VorschaubildManagerOutput) -> None:
    print(f"[{output.source}/{output.stream}] {output.text}", file=sys.stderr)

mosaic = create_mosaic(
    CreateMosaicRequest(input_path="/pfad/zum/video.mp4"),
    runtime=RuntimeOptions(),
    on_event=on_event,
    on_output=on_output,
)
```

**`VorschaubildManagerEvent`-Felder:**

| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `stage_id` | `str` | Technische ID der Arbeitsphase (stabil, maschinenlesbar) |
| `message` | `str` | Deutschsprachige Beschreibung der Phase |
| `current` | `int \| None` | Aktueller Fortschritt im Loop (nur wenn sinnvoll gesetzt) |
| `total` | `int \| None` | Gesamtanzahl im Loop (nur wenn bekannt) |

Stabile `stage_id`-Werte für `extract()`:

| `stage_id` | Bedeutung |
|-----------|-----------|
| `starting` | Workflow startet |
| `extracting_frames` | Frames werden aus Video extrahiert |
| `analyzing_images` | Bilder werden durch KI analysiert |
| `selecting_frame` | Frame wird ausgewählt |
| `selecting_crop` | Bildausschnitt wird bestimmt |
| `completed` | Workflow erfolgreich abgeschlossen |

**`VorschaubildManagerOutput`-Felder:**

| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `source` | `str` | Quelle: `"ffmpeg"`, `"claude"`, `"app"` |
| `stream` | `str` | Ausgabe-Stream: `"stdout"` oder `"stderr"` |
| `text` | `str` | Der tatsächliche Ausgabe-Text |

> **Hinweis:** Events und Outputs dienen der Beobachtung (Logging, UI-Fortschritt).
> Fehler werden weiterhin ausschließlich über Exceptions signalisiert, nicht über Events.

---

### CLI-Integration (Subprocess)

Da stdout nur den Dateipfad (oder sauberes JSON) enthält, lässt sich das Tool auch
einfach via `subprocess` integrieren, wenn ein direkter Import nicht möglich ist:

```python
import subprocess, json, os

result = subprocess.run(
    ["vorschaubild-manager", "extract", video_path, "--mode", "auto", "--json"],
    capture_output=True, text=True, check=True,
    env={**os.environ, "ANTHROPIC_API_KEY": "sk-ant-..."},
)
data = json.loads(result.stdout)
poster_path = data["poster_path"]
```

---

## Versioning

Dieses Paket folgt [Semantic Versioning](https://semver.org/) mit Pre-Release-Stufen
gemäß PEP 440:

```
2.0.0.devN  →  2.0.0rc1  →  2.0.0
```

- **devN**: Entwicklungsbuilds zum lokalen Testen und zur Integration in Konsumenten-Projekte
- **rcN**: Release-Kandidaten für Abnahme
- **final**: Stabiles Release für den normalen Einsatz

Ältere Versionen wurden unter dem Distributionsnamen `video-thumbnail-creator` veröffentlicht.
Dieses Paket (`kurmann-vorschaubild-manager`) ist die Nachfolgedistribution mit vollständigem Rebranding.

---

## Änderungsverlauf

> **Hinweis:** Version 2.0.0 ist die erste architekturkonforme Veröffentlichung nach dem
> Namenswandel (`video-thumbnail-creator` → `kurmann-vorschaubild-manager`). CLI und API sind
> nicht rückwärtskompatibel zur Vorgängerversion (**Breaking Change**).

| Version | Datum | Highlights |
|---------|-------|-----------|
| [2.1.1] | 2026-03-29 | Hotfix: Gamma-Korrektur nach HDR-Tonemapping — SIPS-Ergebnisse für PQ-Quellen sind nicht mehr zu dunkel |
| [2.1.0] | 2026-03-29 | `--frame N` und `--at TIMESTAMP` für direkte Frame-Extraktion; `timestamp_seconds` in EXIF-Metadaten |
| [2.0.2] | 2026-03-28 | Hotfix: `render_poster()` API konvertiert externe Bilder (TIFF, HEIC, 16-bit) nun via SIPS zu sRGB JPEG |
| [2.0.1] | 2026-03-27 | Hotfix: Zwei-Schritt-SIPS-Konvertierung für externe und eingebettete Bilder in CLI/Session |
| [2.0.0] | 2026-03-13 | Erste konforme Veröffentlichung nach Namenswandel; API-Fassade; CLI nur noch via API; Exit-Code 0 bei Leeraufruf |

Die vollständige Änderungshistorie ist in [CHANGELOG.md](CHANGELOG.md) dokumentiert.

[2.1.1]: https://github.com/kurmann/vorschaubild-manager/releases/tag/v2.1.1
[2.1.0]: https://github.com/kurmann/vorschaubild-manager/releases/tag/v2.1.0
[2.0.2]: https://github.com/kurmann/vorschaubild-manager/releases/tag/v2.0.2
[2.0.1]: https://github.com/kurmann/vorschaubild-manager/releases/tag/v2.0.1
[2.0.0]: https://github.com/kurmann/vorschaubild-manager/releases/tag/v2.0.0

---

## Paketstruktur

```
src/vorschaubild_manager/
  __init__.py          ← Top-Level-Export der öffentlichen API
  api.py               ← Öffentliche API: ExtractRequest, RuntimeOptions, ExtractResult, extract()
  cli.py               ← CLI-Einstiegspunkt (dünner Adapter auf die öffentliche API)
  cli_helpers.py       ← CLI-Hilfsfunktionen (intern)
  cli_modes.py         ← CLI-Modusse: manual, auto, suggest (intern)
  cli_image_handlers.py← CLI-Handler für Bildeingabe (intern)
  cli_parser.py        ← Argument-Parser (intern)
  core/
    __init__.py
    extractor.py       ← Frame-Extraktion (ffmpeg)
    mosaic.py          ← Mosaik-Erstellung
    poster_composer.py ← Poster- und Landscape-Komposition
    poster_template.py ← Design-Templates und Stile
    fanart_composer.py ← Fanart-Komposition
    ai_selector.py     ← KI-Frame-Auswahl (Claude Vision)
    ai_utils.py        ← KI-Hilfsfunktionen
    crop_selector.py   ← KI-Crop-Positionsauswahl
    embedded_image.py  ← Eingebettetes Cover-Artwork (ffprobe/ffmpeg)
    sidecar_image.py   ← Sidecar-Bilderkennung
    image_converter.py ← Bildformat-Konvertierung (sRGB)
    utils.py           ← Allgemeine Hilfsfunktionen (ffprobe, Bildöffnung)
    badges.py          ← Badge-Erkennung (4K, HDR, FHD)
    fonts.py           ← Schriftartenverwaltung
    metadata.py        ← EXIF-Metadaten (einbetten / lesen)
    config.py          ← Konfigurationsverwaltung (~/.config/vorschaubild-manager/)
    session.py         ← ThumbnailSession und ThumbnailResult (intern)
```

**Importregel:**
- Öffentliche API: `from vorschaubild_manager.api import ExtractRequest, RuntimeOptions, ExtractResult, extract`
- Top-Level-Import: `from vorschaubild_manager import ExtractRequest, RuntimeOptions, ExtractResult, extract`
- Interne Core-Module (kein Teil der öffentlichen API): `from vorschaubild_manager.core.extractor import extract_frames`
- CLI bleibt unverändert: `vorschaubild-manager extract …`
