Skip to main content

Read-only-Bibliothek zur Introspektion von Videoschnitt-Projekten (FCPXML).

Project description

kurmann-schnittprojekt-leser

Read-only-Bibliothek zur Introspektion von Videoschnitt-Projekten. Liest FCPXML (Final Cut Pro und LumaFusion) und beantwortet strukturierte Fragen zum Projekt – primär: Aufnahmedatum des ersten Clips für die automatische Datei-Naming- und Text-Overlay-Vorbereitung im familienfilm-manager.

Designprinzip: Der Leser schreibt niemals in Schnittprojekte. Er ist ausschliesslich Beobachter.

Voraussetzungen

  • Python ≥ 3.11 (nutzt StrEnum, tomllib, zoneinfo aus der Standardlib).
  • exiftool – Pflicht. Die Bibliothek liest Aufnahmedaten aus den referenzierten Quelldateien (MOV/MP4/HEIC/JPG) via exiftool-Subprozess.
    • macOS: brew install exiftool
    • Linux: sudo apt install libimage-exiftool-perl
  • rclone – optional. Nur nötig, wenn RuntimeOptions.source_search_roots rclone-Remotes enthält (z. B. lyssach-nas:/Videoschnitt/...).

Installation

uv pip install kurmann-schnittprojekt-leser
# oder klassisch
pip install kurmann-schnittprojekt-leser

Lokale Entwicklung (Repo geklont):

uv venv
uv pip install -e ".[dev]"
uv run pytest

Verwendung

CLI

Zwei Subcommands unter inspect:

schnittprojekt-leser inspect first-clip-date <pfad> [--format value|json|iso-datetime]
schnittprojekt-leser inspect summary           <pfad> [--format text|json]

Beispiel: Datum des ersten Clips als ISO-Datum (Pipeline-Default):

$ schnittprojekt-leser inspect first-clip-date "Mein Schnitt.fcpxmld" --timezone Europe/Zurich
2026-03-29

Beispiel: Vollzeitstempel:

$ schnittprojekt-leser inspect first-clip-date "Mein Schnitt.fcpxmld" \
    --timezone Europe/Zurich --format iso-datetime
2026-03-29T08:20:02+02:00

Beispiel: Vollständiges Result als JSON (für familienfilm-manager):

$ schnittprojekt-leser inspect first-clip-date "Mein Schnitt.fcpxmld" \
    --timezone Europe/Zurich --format json
{
  "success": true,
  "iso_date": "2026-03-29",
  "iso_datetime": "2026-03-29T08:20:02+02:00",
  "source_label": "exif:Keys:CreationDate",
  "confidence": "high",
  "project_format": "fcpxml_bundle",
  "resolved_source_file": "/.../Mein Schnitt.fcpxmld/A001_03290820_D498.mov",
  "error_message": null,
  "warnings": []
}

Beispiel: Projekt-Übersicht:

$ schnittprojekt-leser inspect summary "Mein Schnitt.fcpxml"
Projekt: Mein Schnitt
Format: fcpxml_file (Schema 1.8)
Auflösung: 3840×2160
Farbraum: 9-16-9 (Rec. 2020 PQ)
Audio: stereo
Timeline-Dauer: 164.1s
Clips primär: 4
Clips total (inkl. sekundärer Spuren): 5
Projektdatei mtime: 2026-04-06T21:32:08+02:00

Beispiel: Quelldateien auf einem rclone-Remote suchen lassen:

schnittprojekt-leser inspect first-clip-date "Mein Schnitt.fcpxml" \
    --source-root "lyssach-nas:/Videoschnitt/Luma Fusion-Export" \
    --source-depth 2 \
    --timezone Europe/Zurich

stdout/stderr-Disziplin:

  • stdout – nur Ergebnisdaten (Datum, JSON, Übersicht). Pipeline-fähig.
  • stderr – Events, Warnungen, Fehlermeldung, Diagnose; rohen exiftool/rclone-Output nur mit --verbose.
  • --verbose ändert nie stdout.

Exit-Codes: 0 Erfolg, 1 Laufzeitfehler, 2 Argument-/Config-Fehler.

Als Bibliothek

from pathlib import Path

from schnittprojekt_leser.api import read_first_clip_date, read_project_summary
from schnittprojekt_leser.models import (
    ReadFirstClipDateRequest,
    ReadProjectSummaryRequest,
    RuntimeOptions,
    RcloneSource,
)

# 1. Datum des ersten Clips
result = read_first_clip_date(
    ReadFirstClipDateRequest(
        project_path=Path("Mein Schnitt.fcpxmld"),
        timezone_assumption="Europe/Zurich",
    ),
    RuntimeOptions(
        source_search_roots=[
            RcloneSource(remote="lyssach-nas", path="/Videoschnitt/Luma Fusion-Export"),
            Path("/Volumes/Aufnahme-Archiv"),
        ],
        source_search_max_depth=2,
    ),
)
if result.success:
    print(result.iso_date, result.iso_datetime, result.confidence)
else:
    print("Fehler:", result.error_message)

# 2. Projekt-Übersicht (rein FCPXML-intern, kein Quelldatei-Zugriff)
summary = read_project_summary(
    ReadProjectSummaryRequest(project_path=Path("Mein Schnitt.fcpxml")),
)
print(summary.project_name, summary.video_resolution, summary.clip_count_primary)

Konfiguration

RuntimeOptions enthält ausschliesslich technische Parameter (Werkzeug-Pfade, Timeouts, Suchverzeichnisse). Fachliche Parameter wie project_path oder timezone_assumption gehören in den jeweiligen Request.

Feld Default Bedeutung
exiftool_path "exiftool" Pfad zum exiftool-Binary (PATH-Auflösung).
exiftool_timeout_seconds 30.0 Timeout pro exiftool-Aufruf.
rclone_path "rclone" Pfad zum rclone-Binary (PATH-Auflösung). Nur für rclone-Quellen.
rclone_timeout_seconds 60.0 Timeout pro rclone-Aufruf.
source_search_roots [] Zusätzliche Suchverzeichnisse für Quelldateien (Path lokal oder RcloneSource).
source_search_max_depth 2 Rekursionstiefe pro Search-Root. 0 = nur Wurzel, 1 = +eine Ebene, etc.
temp_dir None Temp-Verzeichnis für rclone-Downloads. None = System-Temp.

Datenebenen-Modell

Jede Antwort sitzt auf genau einer dieser vier Ebenen oder ist eine bewusste Komposition daraus:

  1. Projekt – Name, Schema-Version, Auflösung, Audio-Layout, Timeline-Dauer, Anzahl Clips, Projektdatei-mtime.
  2. Timeline – geordnete Spine-Clip-Sequenz, Lane-Sub-Spines.
  3. Asset (Resource) – Eintrag aus dem <resources>-Pool: src-Verweis, Format, Audio-Charakteristik.
  4. Quelldatei – die tatsächliche .mov/.heic/.jpg. Hier wohnen QuickTime/EXIF-Metadaten, GPS, Filesystem-mtime/birthtime.

read_project_summary operiert rein auf Projekt + Timeline. read_first_clip_date ist eine Komposition Timeline → Asset → Quelldatei.

Hierarchie der Datums-Quellen

Spiegelt das Vorbild aus kamera-einleser (siehe Hinweis zur Code-Spiegelung am Ende). Erste Quelle mit nicht-leerem Wert gewinnt:

Position Quelle Source-Label Konfidenz
1 Keys:CreationDate (Apple Item-List, mit TZ) exif:Keys:CreationDate HIGH
2 QuickTime:CreationDate (UDTA, mit TZ) exif:CreationDate HIGH
3 Blackmagic-design:CameraDateRecorded exif:Blackmagic-design:CameraDateRecorded HIGH
4 DateTimeOriginal (klassisches EXIF) exif:DateTimeOriginal HIGH
5 CreateDate (QuickTime MVHD, oft UTC ohne TZ) exif:CreateDate (ggf. +tz_from_mtime) MEDIUM
6 Filesystem st_birthtime (macOS APFS/HFS+) file_birthtime MEDIUM
7 Filesystem st_mtime file_mtime LOW
Notnagel Projektdatei-mtime, wenn Quelldatei nicht auflösbar project_fallback LOW

Konflikt-Strategie: die hierarchisch höchste Quelle gewinnt. Abweichende Sekundär-Werte werden als warnings mitgegeben. confidence und source_label machen jederzeit nachvollziehbar, woher der Wert kam.

Final-Cut-Camera-Sonderfall

Apples Final Cut Camera (iOS 18+) schreibt CreateDate in UTC ohne TZ-Marker. Wird der Wert als-ist angezeigt, missverstehen Konsumenten ihn als lokale Zeit (1–2 Stunden falsch). Die Bibliothek erkennt diesen Fall: wenn exif_create_date ohne TZ-Marker ist und file_mtime eine TZ trägt, wird die TZ inferiert und auf den UTC-Wert angewendet. Das Source-Label bekommt das Suffix +tz_from_mtime – Konsumenten sehen sofort, dass die TZ abgeleitet ist.

Timezone-Verhalten

request.timezone_assumption ist die intendierte lokale Zeitzone des Konsumenten (IANA-Name, z. B. "Europe/Zurich"). Sie steuert ausschliesslich, in welcher Zone das iso_date (Wallclock-Tag) ausgewiesen wird, und wie naive Zeitstempel interpretiert werden. iso_datetime bleibt der Originalwert mit Offset, ohne Konvertierung.

Stempel Assumption Verhalten
Aware (z. B. …+02:00) gesetzt Stempel in Assumption-TZ konvertieren, Datum daraus ableiten
Aware None Datum aus der Eigen-TZ des Stempels
Naive (kein Offset) gesetzt Stempel als Lokalzeit der Assumption interpretieren
Naive None Stempel als UTC interpretieren

Für Familienfilme zählt der Wallclock-Tag. iPhone-Aufnahmen werden meist mit com.apple.quicktime.creationdate plus Local-TZ-Offset gespeichert; ohne Assumption führt Z-stamps in der Schweiz (CEST) bei späten Abend-Aufnahmen zu Datumsverschiebungen um einen Tag. Für familienfilm-manager gilt: immer timezone_assumption="Europe/Zurich" setzen.

„Erster Clip" – formatkonkret

  • FCPXML: erster <asset-clip> direkt unter <spine> (nicht in lane-Sub-Spines), der ein Asset mit is_real_source=True referenziert. Generator/Title-Refs (z. B. *.titleData) werden übersprungen.
  • Edge Case: leere Spine oder nur Generators → success=False, Fehlermeldung „Schnittprojekt enthält keinen primären Video-Clip mit Quelldatei".
  • Edge Case: erste echte Quelldatei nur auf lane > 0 (sekundäre Spur) → Phase 1 sucht nicht dort. Konvention: Familienfilm-Schnitte folgen der Spine-Konvention; Sonderfälle sollen auffällig fehlschlagen statt still zu raten.

rclone-Quellen und Umlaute

Quellverzeichnisse können lokal oder rclone-Remotes sein:

from schnittprojekt_leser.models import RcloneSource, parse_source_spec
from pathlib import Path

# Direkt:
RcloneSource(remote="lyssach-nas", path="/Videoschnitt/Luma Fusion-Export")

# Aus Config-String (gleiches Format wie kamera-einleser):
parse_source_spec("lyssach-nas:/Videoschnitt/Luma Fusion-Export")  # → RcloneSource
parse_source_spec("/Volumes/Card")                                  # → Path
parse_source_spec("~/Schnittprojekte")                              # → Path (expandiert)

NFC-Normalisierung: macOS speichert Dateinamen typisch als NFD (ä als a + Combining Diaeresis), Linux/SMB-NAS als NFC (precomposed ä). Beim Filename-Match in source_search_roots werden Namen via unicodedata.normalize("NFC", name) normalisiert, damit der Match auch bei gemischten Setups (macOS-Schnitt + Synology-NAS) funktioniert.

Unterstützte Formate

Format Phase Anmerkung
*.fcpxml (Single-File) 1 Final Cut Pro und LumaFusion
*.fcpxmld/ (Bundle) 1 FCP-Konvention; Quelldateien direkt im Bundle
*.iMovieMobile (ZIP) 2 Sample-Analyse als Anhang K in Specs.md; Implementierung folgt
DaVinci, Premiere, Avid, native FCP-Library nicht unterstützt

Architektur

Schichten gemäss kurmann-python-api-Skill. Importrichtung strikt: cliapicoreservices. Services werden über Funktionsargumente injiziert, nicht direkt aus core importiert.

src/schnittprojekt_leser/
├── api/                       # Public-Fassaden, fangen Exceptions zu error_message
├── cli/                       # Typer-Adapter, stdout/stderr-Disziplin
├── core/
│   ├── format_detection.py    # detect_format(path) → ProjectFormat
│   ├── recorded_at.py         # pick_recorded_at + Hierarchie + FCC-Fallback
│   ├── first_clip_date.py     # Komposition Detection → FCPXML → Resolver → exiftool
│   └── project_summary.py     # Komposition Detection → FCPXML
├── services/
│   ├── fcpxml_reader.py       # lxml → FcpxmlProject
│   ├── exiftool_runner.py     # subprocess → DateSources
│   ├── rclone_source.py       # rclone-Subprocess + NFC
│   └── source_resolver.py     # Lokale + rclone-Suche, Temp-Cleanup
└── models/                    # Datenmodelle, Enums, Errors

Drei-Kanal-Events:

  • Result (Pflicht) – fachliches Endergebnis, Exceptions als error_message.
  • on_event – strukturierte Stages (SchnittprojektLeserEvent mit Englisch-Stage-IDs gemäss ADR-0009, Deutsch-message).
  • on_output – roher Subprozess-Text (exiftool/rclone). Nur ans Terminal bei --verbose.

Hinweis: Code-Spiegelung mit kamera-einleser

Die Datums-Extraktion (core/recorded_at.py, services/exiftool_runner.py, models/dates.py) und die rclone-Integration (services/rclone_source.py, RcloneSource-Dataclass, parse_source_spec) sind bewusst nahe an kamera-einleser gehalten: identische Tag-Liste, identische Hierarchie, identisches Source-Label-Format, identische rclone-Aufrufmuster, identische NFC-Normalisierung.

Aktuell als Redundanz akzeptiert. Bei einem dritten Konsumenten dieser Logik ist ein Extract zu einer gemeinsamen Bibliothek kurmann-recorded-at geplant – die API-Form ist exakt spiegelnd, damit der Extract mechanisch möglich ist.

Änderungsverlauf

Die letzten drei Versionen:

  • 0.2.0 (2026-05-08) – Erste Implementierung Phase 1: FCPXML-Reader (LumaFusion + FCP), exiftool-basierte Datums-Auslesung mit 7-stufiger Hierarchie, FCC-TZ-Fallback, rclone-Quellen, NFC-Normalisierung, CLI mit inspect first-clip-date und inspect summary.
  • 0.1.0 (2026-05-08) – Initiale Spezifikation Specs.md als Starthilfe (siehe ADR-0010).

Vollständige Historie: CHANGELOG.md.

Konsumenten

Lizenz

MIT. Siehe LICENSE.


Specs.md im Repo ist die ursprüngliche Spec als Starthilfe und wird zukünftig archiviert; die kanonische Doku der Fachlogik ist diese README.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

kurmann_schnittprojekt_leser-0.2.0.tar.gz (36.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

kurmann_schnittprojekt_leser-0.2.0-py3-none-any.whl (41.0 kB view details)

Uploaded Python 3

File details

Details for the file kurmann_schnittprojekt_leser-0.2.0.tar.gz.

File metadata

File hashes

Hashes for kurmann_schnittprojekt_leser-0.2.0.tar.gz
Algorithm Hash digest
SHA256 96e3ede9dbf68baec56c51da550ae034eda6487cfcc5ce495f7041615d8000ab
MD5 87c738eeee3af7b934900cc2e2fa1241
BLAKE2b-256 8425cb7a47c4ad6537ad614699699f051aebff2fa7b954d7a0b14c1bc2080197

See more details on using hashes here.

Provenance

The following attestation bundles were made for kurmann_schnittprojekt_leser-0.2.0.tar.gz:

Publisher: publish.yml on kurmann/schnittprojekt-leser

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file kurmann_schnittprojekt_leser-0.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for kurmann_schnittprojekt_leser-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6eb8307b6ca5b6f191f61c196cbabbab6ecb5ae34227f2bc827321c42093d9f5
MD5 c6863144a6bfe4245db1b5051b3648e6
BLAKE2b-256 c1a2e463db05c88a023a8f6db028a759ee4e0ec96e0eb34378b40d783d8965aa

See more details on using hashes here.

Provenance

The following attestation bundles were made for kurmann_schnittprojekt_leser-0.2.0-py3-none-any.whl:

Publisher: publish.yml on kurmann/schnittprojekt-leser

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page