Metadata-Version: 2.4
Name: video-thumbnail-creator
Version: 1.5.0
Summary: CLI tool to extract and select thumbnail images from video files
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/video-thumbnail-creator
Project-URL: Source, https://github.com/kurmann/video-thumbnail-creator
Project-URL: Issues, https://github.com/kurmann/video-thumbnail-creator/issues
Project-URL: Changelog, https://github.com/kurmann/video-thumbnail-creator/releases
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"
Dynamic: license-file

# video-thumbnail-creator

CLI tool to extract and select a thumbnail (poster) image from a video or image file.
Supports manual selection, fully automatic AI selection (Claude Vision), and a
semi-automatic suggest mode where the AI proposes a frame and you confirm.

Every generated JPEG has creation metadata (EXIF) embedded so that the exact parameters
used during generation are preserved and can be read back with the `info` subcommand.

> **Note:** This tool extracts and saves a still image only. Embedding the
> image into the video file (e.g. with AtomicParsley) is out of scope and must
> be handled by the caller.

---

## Requirements

- Python 3.10+
- [ffmpeg](https://ffmpeg.org/) and `ffprobe` available in `$PATH` *(required for video input; not needed for image-only input)*
- **macOS only:** `sips` — built-in macOS image tool, used for wide-gamut color space conversion (TIFF/HEIC with Rec.2020 or Display P3).
  On Linux/Windows, Pillow is used as a fallback but may not handle wide-gamut images correctly.
- An [Anthropic API key](https://console.anthropic.com/) for `auto` / `suggest` modes
  (set via `CLAUDE_API_KEY` env variable or `video-thumbnail-creator config set claude.api_key <key>`)

---

## Installation

```bash
pip install video-thumbnail-creator
```

Or in editable/development mode:

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

---

## Usage

```
video-thumbnail-creator extract <input_path> [OPTIONS]
```

### Options

| Option | Description |
|--------|-------------|
| `--mode manual` | Interactive: open mosaic, enter frame number 0–19 |
| `--mode auto` | Fully automatic: AI selects the best frame |
| `--mode suggest` | AI suggests a frame; you confirm or override |
| `--format poster` | Output format 2:3 (1080×1620) with 1:1 crop + text area **(default)** |
| `--format landscape` | Output format 16:9 (1920×1080) — existing behaviour |
| `--embedded-image prefer` | Use embedded cover art or sidecar image if present; otherwise extract frames |
| `--embedded-image ignore` | Always extract frames (ignore any embedded cover art or sidecar images) |
| `--embedded-image ask` | Prompt user when embedded cover art or sidecar image is found **(default)** |
| `--crop-position POSITION` | Set crop position directly (`left`, `center-left`, `center`, `center-right`, `right`); skips interactive prompt and AI crop selection |
| `--overlay-title TEXT` | Title text to overlay on the output image |
| `--overlay-title-from-filename` | Use the input filename stem as overlay title |
| `--overlay-category TEXT` | Category label shown above the title (poster format only) |
| `--overlay-category-logo PATH` | PNG logo shown instead of category text (poster format only); wide logos centered above title, square/portrait logos to the left |
| `--overlay-note TEXT` | Small text centered at the bottom of the poster text area (poster format only) |
| `--style NAME` | Poster style to use (default: `default`). Run `video-thumbnail-creator styles` to list available options |
| `--description TEXT` | Optional video description for AI context (max 1000 chars) |
| `--output-dir PATH` | Output directory (default: same directory as the video) |
| `--output-name-suffix SUFFIX` | Suffix appended to the video filename stem (default: `-poster`) |
| `--json` | Emit machine-readable JSON to stdout |
| `--no-badges` | Disable automatic technical badges (4K, HD, HDR) on the poster |
| `--fanart` | Generate an additional clean 16:9 fanart image (for Infuse/Emby) with `-fanart` suffix |

---

## `info` Subcommand

Read and display the creation metadata embedded in a generated poster image:

```bash
video-thumbnail-creator info /path/to/poster.jpg
```

**Default output:**
```
Poster Metadata:
  Version            1.3.0
  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
  AI Reasoning       Sharp, well-lit frame with child running towards camera…
  Created            2026-02-26T14:30:00
```

**JSON output (`--json`):**
```bash
video-thumbnail-creator info --json /path/to/poster.jpg
```
```json
{
  "vtc_version": "1.3.0",
  "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",
  "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"
}
```

The embedded metadata enables future re-generation of posters (e.g. with a new template)
without needing AI calls or interactive prompts.

---

## Poster Format (2:3)

The default `poster` format produces a **1080×1620** image composed of two sections:

```
┌──────────────────┐
│                  │
│   1:1 crop of    │  ← 1080×1080 square crop (with subtle vignette)
│   selected frame │
│                  │
├──────────────────┤  ← 10px separator line (#2a2a2a)
│▓  [category]    ▓│  ← optional category text or logo above title
│▓                ▓│
│▓    Title       ▓│  ← bold, auto-sized (40–72px), centered, drop shadow
│▓                ▓│
│▓         note   ▓│  ← optional small note text at bottom-right
│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓│  ← solid dark background (#1a1a1a)
└──────────────────┘
        2:3
```

- **Top section (1080×1080)**: A 1:1 square crop of the selected high-res frame with a
  subtle radial vignette effect (15–20% edge darkening). The horizontal crop position
  (`left` / `center-left` / `center` / `center-right` / `right`) can be set directly
  with `--crop-position`, chosen by AI in `auto`/`suggest` mode, or prompted from the
  user in `manual` mode.
- **Bottom section**: The text area fills the entire bottom section edge-to-edge. Layout
  details depend on the active style (see [Poster Styles](#poster-styles)):
  - **Title** (optional `--overlay-title`): bold, auto-sized, word-wrapped, centered, white with drop shadow.
  - **Note** (optional `--overlay-note`): small text centered at the bottom of the text area.
  - **Category** / **Logo** (optional): placement varies by style — in the text area above the title, or as an overlay band at the top of the image.

---

## Supported Image Formats

When `<input_path>` points to an image file, the mosaic/frame-extraction pipeline
is skipped and the image is used directly as the high-res source for crop-position
selection and poster/landscape composition.

| Format | Notes |
|--------|-------|
| JPEG (`.jpg`, `.jpeg`) | Used directly (copied as-is for sRGB images) |
| PNG (`.png`) | Converted to JPEG via Pillow |
| TIFF (`.tiff`, `.tif`) | Wide-gamut (Rec.2020, P3) converted to sRGB via `sips` (macOS) |
| HEIC/HEIF (`.heic`, `.heif`) | Converted to sRGB JPEG via `sips` (macOS) |

> **Note:** Wide-gamut color space conversion (Rec.2020, Display P3) requires
> macOS with `sips`. On other systems, Pillow is used as a fallback but may
> not handle wide-gamut TIFF files correctly.

---

## Examples

### Manual mode

```bash
video-thumbnail-creator extract /path/to/video.mp4 --mode manual
```

The mosaic of 20 frames is opened in the system image viewer. Enter the frame
number (0–19) at the prompt. In poster format, you are also prompted to choose
a crop position. The result is saved next to the video.

### Manual mode — landscape with text overlay

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

### Automatic AI mode (poster, default format)

```bash
video-thumbnail-creator extract /path/to/video.mp4 \
  --mode auto \
  --description "Documentary about rocket launches" \
  --overlay-title "2025-05-15 – Starship IFT-7"
```

The AI selects the best frame from the 20-frame mosaic, then chooses the
optimal 1:1 crop position for the poster, and renders the text in the
blurred bottom area.

### Poster with category, logo, and note

```bash
# Category text above title, note at bottom-right
video-thumbnail-creator extract /path/to/video.mp4 \
  --mode auto \
  --overlay-title "Starship IFT-7" \
  --overlay-category "Space Exploration" \
  --overlay-note "2025-05-15"

# Category logo (wide, centered above title)
video-thumbnail-creator extract /path/to/video.mp4 \
  --mode auto \
  --overlay-title "Starship IFT-7" \
  --overlay-category-logo /path/to/channel-logo-wide.png \
  --overlay-note "Episode 7"

# Square/portrait logo (left of title)
video-thumbnail-creator extract /path/to/video.mp4 \
  --mode manual \
  --overlay-title "My Documentary" \
  --overlay-category-logo /path/to/icon-square.png
```

> **Note on `--overlay-category` vs `--overlay-category-logo`**: If both are provided, the logo
takes precedence and a warning is printed to stderr.

### Using embedded cover art (MP4/M4V/MOV)

```bash
# Use embedded artwork if present, fall back to frame extraction
video-thumbnail-creator extract /path/to/video.mp4 \
  --embedded-image prefer \
  --mode auto

# Always skip frame extraction and use embedded cover art
video-thumbnail-creator config set defaults.embedded_image prefer
```

The `--embedded-image` option controls how embedded cover art and sidecar images are handled:
- `prefer`: Use the embedded image or sidecar image if found; otherwise extract frames normally.
- `ignore`: Always extract frames, even if embedded cover art or sidecar images exist.
- `ask` *(default)*: Prompt the user when an embedded image or sidecar image is found.

> **Note:** Embedded image detection is only supported for MP4, M4V, and MOV
> containers. For other formats the tool falls through to sidecar detection and
> then frame extraction.
> The `--embedded-image` option is independent from `--mode`; the mode only
affects how the crop position is determined after the source image is resolved.

### Using sidecar thumbnail images

When extracting from a video file (e.g. `video.mp4`), the tool also checks for
existing thumbnail images ("sidecar" files) in the same directory with the same
filename stem:

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

The detection priority for video input is:
1. Embedded image (inside the video container)
2. Sidecar image (next to the video file)
3. Frame extraction (mosaic flow)

The `--embedded-image` option controls sidecar image handling the same way it
controls embedded image handling.

```bash
# Use sidecar image if present, fall back to frame extraction
video-thumbnail-creator extract /path/to/video.mp4 \
  --embedded-image prefer \
  --mode auto

# Always use frames, ignore any sidecar images
video-thumbnail-creator extract /path/to/video.mp4 \
  --embedded-image ignore \
  --mode manual
```

When a sidecar image is used, the JSON output has `"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"
}
```

### Using `--crop-position` to skip interactive and AI crop selection

```bash
# Set crop position directly — skips AI crop selection (saves API costs)
video-thumbnail-creator extract /path/to/video.mp4 \
  --mode auto \
  --crop-position center

# Useful for batch processing where crop position is already known
for f in /videos/*.mp4; do
  video-thumbnail-creator extract "$f" --mode auto --crop-position center-left
done
```

The `--crop-position` option accepts: `left`, `center-left`, `center`, `center-right`, `right`.
When provided with `--format poster`, it skips both the interactive crop prompt
(manual mode) and the AI crop selection call (auto/suggest modes).

### Image file input (JPEG, PNG, TIFF, HEIC)

```bash
# Create poster from a TIFF image (auto color space conversion on macOS)
video-thumbnail-creator extract /path/to/photo.tiff \
  --mode auto \
  --overlay-title "Herbst-Spaziergang"

# Create poster from JPEG with manual crop
video-thumbnail-creator extract /path/to/photo.jpg \
  --mode manual \
  --overlay-title "Mein Foto" \
  --overlay-category "Familie Kurmann"
```

For image input, ffmpeg/ffprobe are not required. The image itself is used as
the high-res source; only crop-position selection and poster composition run.

### Semi-automatic suggest mode

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

In poster format, after the frame is selected the AI also suggests a crop
position, which you can confirm or override at the prompt.

---

## Configuration

Settings can be stored in `~/.config/video-thumbnail-creator/config.toml` so you
don't have to pass them on every invocation.  The directory and file are created
automatically on the first `config set`.

### Priority order (highest to lowest)

1. Explicit CLI arguments
2. Config file values
3. Built-in defaults

### Commands

```bash
# Store a value
video-thumbnail-creator config set claude.api_key "sk-ant-..."

# Read a single value
video-thumbnail-creator config get claude.model

# Show all stored values
video-thumbnail-creator config list
```

### Allowed keys

| Key | Description | Default |
|-----|-------------|---------|
| `claude.api_key` | Anthropic Claude API key | *(none)* |
| `claude.model` | Claude model name | `claude-opus-4-5` |
| `tools.ffmpeg` | Path to `ffmpeg` binary | `ffmpeg` |
| `tools.ffprobe` | Path to `ffprobe` binary | `ffprobe` |
| `defaults.output_name_suffix` | Suffix for output filename | `-poster` |
| `defaults.mode` | Default selection mode | `manual` |
| `defaults.format` | Default output format (`poster` or `landscape`) | `poster` |
| `defaults.embedded_image` | Default embedded image handling (`prefer`, `ignore`, `ask`) | `ask` |

### Example `config.toml`

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

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

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

### Output filename

The output filename is formed by appending the suffix to the video file stem:
```
2025-05-15_Starship_IFT7.mkv  +  suffix "-poster"  →  2025-05-15_Starship_IFT7-poster.jpg
```

---

## Output

### Default (no `--json`)

stdout contains only the absolute path of the created image:
```
/path/to/poster.jpg
```

All status messages, progress info, and AI reasoning are written to **stderr**.

### JSON mode (`--json`)

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

The `overlay_category` field is only present when `--overlay-category` is provided (and `--overlay-category-logo`
is not used alongside it). The `overlay_note` field is only present when `--overlay-note` is provided.

When embedded cover art is used as the source:
```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"
}
```

When an image file is used as the source:
```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"
}
```

The `source` field is `"frame"` when a video frame was used, `"embedded"` when
embedded cover art was used, `"sidecar"` when a sidecar image file was used, and
`"image"` when an image file was used as input.
When `source` is `"embedded"`, `"sidecar"`, or `"image"`, `frame_index` is `-1`.

When `--fanart` is used, the JSON output includes an additional `fanart_path` field:
```json
{
  "poster_path": "/path/to/video-poster.jpg",
  "fanart_path": "/path/to/video-fanart.jpg",
  ...
}
```

---

## Fanart Image (`--fanart`)

The `--fanart` flag generates an **additional** clean 16:9 JPEG alongside the
normal poster or landscape output. This image has no text overlays, no badges,
and no gradients — just the pure source frame scaled to 16:9. It is intended for
media servers such as **Infuse** and **Emby** that look for a file with a
`-fanart` suffix.

```bash
# Generates both "My Video-poster.jpg" and "My Video-fanart.jpg"
videos-thumbnail-creator extract "My Video.mp4" --fanart
```

**Output resolution:**
- 4K source (width ≥ 3840 or height ≥ 2160): **3840 × 2160**
- Otherwise: **1920 × 1080**

**Non-16:9 sources:** A blurred background fill is applied automatically
(same visual approach as the existing frame extraction) so the output is always
exactly 16:9 without black bars or stretching.

---

## Poster Styles

All visual design constants (colors, fonts, layout, badge placement) are defined
as **built-in named styles**. Select a style with `--style <name>` (default: `default`).

### List available styles

```bash
video-thumbnail-creator styles
```

| Style | Description |
|-------|-------------|
| `default` | Classic blurred video background in text area, Helvetica Neue, note at bottom |
| `internet` | Category header overlaid at top of image, matte black text area, large bold fonts, badges on image — optimised for YouTube/web video posters |

### Examples

```bash
# Default style (classic blurred background)
video-thumbnail-creator extract video.mp4 \
  --overlay-title "My Title"

# Internet style (matte black, category at top, HD badge on image)
video-thumbnail-creator extract video.mp4 \
  --style internet \
  --overlay-category "My Channel" \
  --overlay-title "My Video Title" \
  --overlay-note "15. March 2025"
```

---

## Exit Codes

| Code | Meaning |
|------|---------|
| `0` | Success |
| `1` | General error (file not found, ffmpeg missing, etc.) |
| `2` | No selection made (user cancelled) |
| `3` | AI selection failed (no API key, timeout, invalid response) |

---

## Integration

### Library Integration (Python Import)

`video-thumbnail-creator` ships a full Python API at three levels — pick the
one that fits your use case:

```
CLI (Terminal user)
  └→ High-Level API: create_thumbnail()     – one call, everything automatic
       └→ Mid-Level API: ThumbnailSession    – multi-step, caller controls each step
            └→ Low-Level API: extract_frames(), compose_poster(), etc.  – individual building blocks
```

#### High-Level API – `create_thumbnail()`

One call for fully automatic thumbnail creation (AI selects frame and crop position):

```python
from video_thumbnail_creator import create_thumbnail

result = create_thumbnail(
    "/path/to/video.mp4",
    overlay_title="My Film",
    output_dir="/output/",
    fanart=True,
)
print(result.poster_path)   # absolute path to the poster JPEG
print(result.fanart_path)   # absolute path to the fanart JPEG (or None)
print(result.reasoning)     # AI explanation
```

Works with image files too (JPEG, PNG, TIFF, HEIC) — ffmpeg is not required:

```python
result = create_thumbnail("/path/to/cover.jpg", format="poster")
```

#### Mid-Level API – `ThumbnailSession`

`ThumbnailSession` gives you full control over each step. Use the context
manager for automatic cleanup of temporary files.

**Automatic** — AI decides everything, step by step:

```python
from video_thumbnail_creator import ThumbnailSession

with ThumbnailSession("/path/to/video.mp4") as session:
    suggestion = session.suggest_frame(title="My Film", description="A documentary")
    session.select_frame(suggestion["frame_index"])
    crop = session.suggest_crop()
    result = session.compose(
        crop_position=crop["crop_position"],
        overlay_title="My Film",
        output_dir="/output/",
    )
```

**Suggest** — AI suggests, caller confirms or overrides:

```python
with ThumbnailSession("/path/to/video.mp4") as session:
    print(session.mosaic_path)   # show the mosaic to the user
    suggestion = session.suggest_frame()
    # ... show suggestion to user, let them confirm or pick a different index ...
    chosen_index = int(input(f"Frame [{suggestion['frame_index']}]: ") or suggestion["frame_index"])
    session.select_frame(chosen_index)
    result = session.compose(crop_position="center", output_dir="/output/")
```

**Manual** — caller decides everything, mosaic is just a visual aid:

```python
with ThumbnailSession("/path/to/video.mp4") as session:
    # Display session.mosaic_path to the user, then:
    session.select_frame(7)
    result = session.compose(
        crop_position="center-left",
        overlay_title="My Film",
        format="poster",
        output_path="/output/my-film-poster.jpg",
        fanart=True,
    )
```

**Image input** — no frame extraction needed:

```python
with ThumbnailSession("/path/to/cover.jpg") as session:
    # session._frame_selected is already True; call compose() directly
    result = session.compose(format="poster", output_dir="/output/")
```

#### Low-Level API – Individual Functions

Use the building blocks directly when you need maximum control:

```python
from video_thumbnail_creator import (
    get_video_properties,
    extract_frames,
    create_mosaic,
    extract_single_frame_highres,
    compose_poster,
    compose_fanart,
    detect_badges,
)

props = get_video_properties("/path/to/video.mp4")
frame_paths = extract_frames("/path/to/video.mp4", "/tmp/frames/")
mosaic = create_mosaic(frame_paths, "/tmp/mosaic.jpg")
highres = extract_single_frame_highres("/path/to/video.mp4", 5, "/tmp/highres.jpg")
badges = detect_badges(props)
compose_poster(highres, "center", "My Title", "/output/poster.jpg", badges=badges)
compose_fanart(highres, "/output/fanart.jpg", is_4k=props["is_4k"])
```

---

### CLI Integration (Subprocess)

Because stdout contains only the file path (or clean JSON), this tool is also
easy to integrate via `subprocess` when you cannot import it directly:

```python
import subprocess, json

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

---

## Recent Changes

> This section lists the release notes for the three most recent versions.
> For older versions, see the [Releases](https://github.com/kurmann/video-thumbnail-creator/releases) page
> or the respective version on [PyPI](https://pypi.org/project/video-thumbnail-creator/#history).

### v1.4.6
- **Layout**: Increase overlay category font size in poster composer by @Copilot in #34
- **Layout**: Remove rounded frame border from poster layout; promote separator line as sole divider
- **Code cleanup**: Modularize cli.py, centralize fonts/AI-retry, remove dead code, optimize hot paths

### v1.4.2

- **Batches**: Rename FHD→HD, add HFR badge support (4K60/HD48), remove PNG logo config for technical badges
- **Thumbnail selection**: Prioritise recurring presenter over topic objects in thumbnail selection

### v1.4.1

- **Library API**: New `ThumbnailSession` class for step-by-step thumbnail creation with full control over each stage (frame selection, crop position, composition)
- **Convenience function**: New `create_thumbnail()` one-liner for fully automatic thumbnail generation from Python code
- **Three integration levels**: High-level (`create_thumbnail`), mid-level (`ThumbnailSession`), and low-level (individual functions like `extract_frames`, `compose_poster`, etc.)
