diff --git a/.gitignore b/.gitignore index 8f26127..f582e1a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,9 @@ DerivedData/ *.xcuserstate xcuserdata/ + +# Local app builds and render projects +build/ +*.growthlapse-cache/ +*.growthlapse-project.json +GrowthLapse-*/ diff --git a/Package.swift b/Package.swift index 4ea0733..2ad1e67 100644 --- a/Package.swift +++ b/Package.swift @@ -17,6 +17,7 @@ let package = Package( linkerSettings: [ .linkedFramework("AVKit") ] - ) + ), + .testTarget(name: "GrowthLapseTests", dependencies: ["GrowthLapse"]) ] ) diff --git a/README.md b/README.md new file mode 100644 index 0000000..c0316d3 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# GrowthLapse + +GrowthLapse ist eine native macOS-App in SwiftUI, die aus mehreren Videos einen Wachstums-Zeitraffer erstellt. Aus jedem Video wird ein Ausschnitt gewählt, optional stabilisiert und am Gesicht ausgerichtet, beschleunigt und mit den übrigen Clips verbunden. Die Verarbeitung läuft lokal mit Apple Vision/AVFoundation und externem FFmpeg. + +## Voraussetzungen + +- macOS 13 oder neuer. +- Swift 5.9 oder neuer mit macOS SDK, etwa über Xcode bzw. die Xcode Command Line Tools. +- `ffmpeg` und `ffprobe` als ausführbare Dateien. Die App enthält diese Werkzeuge nicht. +- Für VidStab: FFmpeg mit `vidstabdetect` und `vidstabtransform`; alternativ `deshake`. Für eingeblendete Clipnummern ist `drawtext` erforderlich. + +Eine Installation über Homebrew ist mit `brew install ffmpeg` möglich. Welche optionalen Filter dein Build enthält, zeigt `ffmpeg -hide_banner -filters`. + +GrowthLapse sucht Werkzeuge zuerst in `/opt/homebrew/bin`, `/usr/local/bin` und `/usr/bin`, danach im `PATH`. In der Oberfläche lassen sich eigene ausführbare Dateien auswählen. Bei alten gespeicherten Pfaden hilft „Automatische Suche verwenden“. + +## Bauen und starten + +```sh +git clone https://git.casaderoll.de/michael/GrowthLapse.git +cd GrowthLapse +swift build +swift run GrowthLapse +``` + +Für eine lokal startbare App mit Icon: + +```sh +bash scripts/build-app.sh +open build/GrowthLapse.app +``` + +Das Skript baut im Release-Modus für die Architektur des ausführenden Macs und signiert die App ad hoc. Es erzeugt keine notarisiert veröffentlichte oder universelle App. Die bereits eingecheckte App in `dist/` ist ein historisches Build-Artefakt; Änderungen am Quellcode werden erst durch einen neuen Build wirksam. + +## Bedienung + +1. Input-Ordner und Output-MP4 auswählen. Unterstützt werden `mp4`, `mov`, `m4v`, `avi`, `mkv` und `webm` direkt im gewählten Ordner; Unterordner werden nicht durchsucht. +2. Sortierung und gegebenenfalls Variantenwahl einstellen. Die Wochen-Sortierung erkennt beispielsweise `12 Wochen alt V1.mov`. Dateien ohne erkennbares Wochenalter stehen dahinter. Varianten werden anhand eines abschließenden Leerzeichens mit Zahl oder `V` und Zahl gruppiert, etwa `12 Wochen alt V1.mov` und `12 Wochen alt V2.mov`. +3. Ausschnittlänge, Ziellänge und Übergang festlegen. Standard: 8 Sekunden Quelle, 1,5 Sekunden pro Ergebnisclip, 0,6 Sekunden Überblendung und 30 FPS. Die Übergangslänge muss kleiner als die Ziellänge sein; 0 bedeutet einen harten Schnitt. +4. Ausgabeformat, Encoder und optionale Stabilisierung/Gesichtsausrichtung wählen. Unterstützt sind 1920×1080, 1080×1920 oder die Abmessungen des ersten Videos. Für H.264 stehen CPU (`libx264`) und VideoToolbox zur Auswahl. Audio wird standardmäßig entfernt. +5. „Render starten“ wählen. Fortschritt und Log erscheinen in der App; „STOP“ bricht die Verarbeitung ab. +6. In der Clip-Nachbearbeitung Segmentstarts oder Varianten ändern und „Änderungen neu rendern“ wählen. „Projekt laden“ öffnet eine gespeicherte Projektdatei. + +Die automatische Segmentwahl sucht nach geeigneten Gesicht-/Augenaufnahmen. Gesichtsausrichtung und Stabilisierung sind heuristisch und sollten anhand des Ergebnisses geprüft werden. + +## Dateien und Einstellungen + +Neben `Ergebnis.mp4` können folgende Dateien entstehen: + +| Datei | Zweck | +| --- | --- | +| `Ergebnis.growthlapse-project.json` | Quellen, Ausschnitte, Varianten und Cache-Verweise | +| `Ergebnis.growthlapse-cache/` | Zwischenclips für die Nachbearbeitung | +| `Ergebnis.growthlapse-report.txt` | Render-Protokoll | + +„Ergebnis bestätigen“ löscht den Projekt-Cache. Fehlende Zwischenclips werden beim erneuten Rendern neu erzeugt; die Quelldateien müssen weiterhin vorhanden sein. Ein neuer Render mit demselben Ausgabeziel setzt dessen Review-Cache zurück und kann die vorhandene Ausgabe überschreiben. + +Projekte verwenden absolute Dateipfade und sind daher nicht ohne Anpassung zwischen Rechnern verschiebbar. Die globalen Einstellungen liegen unter `~/Library/Application Support/GrowthLapse/settings.json`. „Defaults“ setzt sie zurück. Projektdateien ersetzen keine vollständige Sicherung der globalen Render-Einstellungen. + +## Entwicklung und Prüfung + +Für die XCTest-Tests ist eine vollständige Xcode-Installation mit ausgewählter Xcode-Entwicklerumgebung erforderlich; die Command Line Tools allein enthalten hier kein `XCTest`. + +```sh +swift test +``` + +Die Tests prüfen neutrale Startwerte, die JSON-Speicherung und die Kompatibilität älterer Einstellungen. Sie ersetzen keine Prüfung der Videoausgabe. + +Für einen manuellen Funktionstest zwei kurze Videos in einen separaten Testordner legen und ein neues Ausgabeziel wählen. Zunächst ohne Stabilisierung und ohne Übergang rendern, danach mit Überblendung und den gewünschten Filtern. Ergebnis abspielen, einen Segmentstart ändern, erneut rendern, Projekt laden und Abbruch während eines weiteren Renders prüfen. + +| Quellcode | Aufgabe | +| --- | --- | +| `GrowthLapseApp.swift` | App-Einstieg und Fensterlebenszyklus | +| `ContentView.swift` | Einstellungen, Vorschau und Clip-Nachbearbeitung | +| `RenderSettings.swift` | Datenmodelle und persistierte Einstellungen | +| `FFmpegService.swift` | Werkzeugsuche und asynchrone Prozessausführung | +| `VideoProcessor.swift` | Videoanalyse, Render-Pipeline, Cache und Gesichtserkennung | + +Alle genannten Swift-Dateien liegen unter `Sources/GrowthLapse/`. + +## Bekannte Grenzen + +- Foto-Morphing ist im Quellcode enthalten, aber derzeit deaktiviert. +- Verfügbare FFmpeg-Filter und Encoder hängen von der lokalen Installation ab. Wenn VidStab fehlt, wird Deshake versucht; ohne passenden Filter Stabilisierung deaktivieren oder einen geeigneten Build verwenden. +- Gesichtserkennung garantiert keine passende Ausrichtung für jede Aufnahme. +- Automatische Tests für die vollständige Render-Pipeline fehlen bislang. +- Im Repository ist bisher keine Lizenz festgelegt. + +Weitere technische Befunde stehen in [docs/ANALYSE.md](docs/ANALYSE.md). diff --git a/Sources/GrowthLapse/RenderSettings.swift b/Sources/GrowthLapse/RenderSettings.swift index 3adc8d9..2dac155 100644 --- a/Sources/GrowthLapse/RenderSettings.swift +++ b/Sources/GrowthLapse/RenderSettings.swift @@ -123,10 +123,10 @@ struct FFmpegFilterAvailability: Equatable { } struct RenderSettings: Equatable { - var inputFolder: URL? = URL(fileURLWithPath: "/Users/mike_i386/Not To Sync/TESTIN", isDirectory: true) - var outputFile: URL? = URL(fileURLWithPath: "/Users/mike_i386/Desktop/Joshua_GrowthLapse.mp4") - var ffmpegExecutable: URL? = URL(fileURLWithPath: "/Users/mike_i386/Downloads/ffmpeg") - var ffprobeExecutable: URL? = URL(fileURLWithPath: "/Users/mike_i386/Downloads/ffprobe") + var inputFolder: URL? = nil + var outputFile: URL? = nil + var ffmpegExecutable: URL? = nil + var ffprobeExecutable: URL? = nil var renderMode: RenderMode = .videoGrowthLapse var segmentLength: Double = 8 var targetClipLength: Double = 1.5 diff --git a/Tests/GrowthLapseTests/SettingsTests.swift b/Tests/GrowthLapseTests/SettingsTests.swift new file mode 100644 index 0000000..c4a7d93 --- /dev/null +++ b/Tests/GrowthLapseTests/SettingsTests.swift @@ -0,0 +1,41 @@ +import XCTest +@testable import GrowthLapse + +final class SettingsTests: XCTestCase { + func testFreshSettingsDoNotDependOnDeveloperMachine() { + let settings = RenderSettings() + XCTAssertNil(settings.inputFolder) + XCTAssertNil(settings.outputFile) + XCTAssertNil(settings.ffmpegExecutable) + XCTAssertNil(settings.ffprobeExecutable) + } + + func testCustomSettingsSurviveJSONRoundTrip() throws { + var settings = RenderSettings() + settings.inputFolder = URL(fileURLWithPath: "/tmp/Meine Videos") + settings.outputFile = URL(fileURLWithPath: "/tmp/Ergebnis.mp4") + settings.ffmpegExecutable = URL(fileURLWithPath: "/tmp/tools/ffmpeg") + settings.segmentLength = 12 + settings.sortMode = .filenameAscending + settings.keepRenderCacheForReview = false + let data = try JSONEncoder().encode(PersistedRenderSettings(settings: settings)) + let restored = try JSONDecoder().decode(PersistedRenderSettings.self, from: data) + XCTAssertEqual(restored.renderSettings, settings) + } + + func testOlderSettingsReceiveCompatibilityDefaults() throws { + let data = try JSONEncoder().encode(PersistedRenderSettings(settings: RenderSettings())) + var json = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any]) + for key in ["renderMode", "duplicateClipSelection", "keepRenderCacheForReview", + "burnInClipNumbers", "bestSegmentDetectionEnabled"] { + json.removeValue(forKey: key) + } + let restored = try JSONDecoder().decode(PersistedRenderSettings.self, + from: JSONSerialization.data(withJSONObject: json)).renderSettings + XCTAssertEqual(restored.renderMode, .videoGrowthLapse) + XCTAssertEqual(restored.duplicateClipSelection, .first) + XCTAssertTrue(restored.keepRenderCacheForReview) + XCTAssertFalse(restored.burnInClipNumbers) + XCTAssertFalse(restored.bestSegmentDetectionEnabled) + } +} diff --git a/docs/ANALYSE.md b/docs/ANALYSE.md new file mode 100644 index 0000000..fbdbc95 --- /dev/null +++ b/docs/ANALYSE.md @@ -0,0 +1,37 @@ +# Technische Bestandsaufnahme + +Stand: 13. September 2026, Ausgangscommit `0b85f7d73a8ae09a8711038a4d08ae34642c6243`. + +## Bestand + +Das Projekt ist ein Swift-Package mit einer macOS-Anwendung und ohne externe Swift-Package-Abhängigkeiten. Die Oberfläche bietet Video-Zeitraffer, lokale Gesichts-/Augenanalyse, Stabilisierung, Variantenwahl und nachträgliche Ausschnittkorrekturen mit Cache. Eine kompilierte App samt Icon ist eingecheckt. Foto-Morphing ist implementiert, aber ausdrücklich gesperrt. + +## Ergänzungen dieser Überarbeitung + +- README mit Voraussetzungen, Start, Bedienung, Dateiformaten und Grenzen. +- Persönliche Standardpfade entfernt: Eingabe/Ausgabe beginnen ohne Auswahl; FFmpeg und FFprobe werden automatisch gesucht. Bereits gespeicherte Einstellungen bleiben erhalten. +- Wiederholbarer Release-App-Build unter `scripts/build-app.sh`, Ausgabe in `build/`. +- Tests für Einstellungs-Persistenz und ältere JSON-Daten ergänzt. +- Lokale Build-Ausgaben, Projektdateien und Render-Caches in `.gitignore` ergänzt. + +## Offene technische Arbeit + +Diese Befunde stammen aus der Quellcodeprüfung; sie sind keine vollständig reproduzierten Fehlerberichte: + +1. `FFmpegService.run` deaktiviert beim Prozessende den Pipe-Reader, ohne ausdrücklich bis EOF zu lesen. Bei kurzen Prozessen könnte die letzte Ausgabe fehlen. Prozessausgabe und Abbruch sollten mit gezielten Integrationstests abgesichert werden. +2. Geladene Projekte enthalten absolute Pfade. Vor Rendern und Cache-Löschen sollten Projektstruktur, leere Cliplisten und zulässige Cache-Verzeichnisse validiert werden. Der Renderpfad greift auf den ersten Clip zu und setzt eine nicht leere Liste voraus. +3. Ein neuer Render setzt den Review-Cache früh zurück; Ausgabeoperationen überschreiben das Ziel. Ein späterer Ausbau sollte temporäre Ausgabe und atomaren Austausch nach Erfolg verwenden. +4. Fehlender VidStab führt zu Deshake, auch wenn dieser Filter ebenfalls fehlt. Eine frühzeitige klare Fehlermeldung wäre besser als ein späterer FFmpeg-Fehler. +5. `VideoProcessor.swift` bündelt rund 2.900 Zeilen verschiedener Verantwortlichkeiten. Renderplanung, Projektverwaltung und Bildanalyse lassen sich für gezielte Tests trennen. +6. Es fehlen Render-Integrationstests mit synthetischen Videos für Audio, Überblendungen, HDR, Abbruch und Cache-Wiederverwendung sowie CI auf macOS. +7. Release-Versionierung, Developer-ID-Signierung, Notarisierung und Lizenzentscheidung sind noch nicht als Veröffentlichungsprozess beschrieben. Das neue Build-Skript dient lokalen Builds. + +Eine vollständige Produktfreigabe erfordert zusätzlich manuelle Prüfung der Oberfläche und repräsentativer Videoausgaben. + +## Lokale Verifikation + +- Debug-Build erfolgreich. +- Release-App über `bash scripts/build-app.sh` erfolgreich gebaut und ad hoc signiert. +- `swift test` durch fehlendes `XCTest` in der installierten Command-Line-Tools-Umgebung blockiert; die Tests sind daher noch nicht als bestanden verifiziert. +- Shell-Syntax des Build-Skripts und `git diff --check` ohne Befund. +- Keine vollständige Prüfung von Oberfläche oder Videoausgabe durchgeführt. diff --git a/scripts/build-app.sh b/scripts/build-app.sh new file mode 100644 index 0000000..cadb8ee --- /dev/null +++ b/scripts/build-app.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")/.." +swift build -c release +bin_dir="$(swift build -c release --show-bin-path)" +app="build/GrowthLapse.app" +mkdir -p "$app/Contents/MacOS" "$app/Contents/Resources" +cp "$bin_dir/GrowthLapse" "$app/Contents/MacOS/GrowthLapse" +cp dist/GrowthLapse.app/Contents/Info.plist "$app/Contents/Info.plist" +cp dist/GrowthLapse.app/Contents/Resources/GrowthLapse.icns "$app/Contents/Resources/GrowthLapse.icns" +codesign --force --deep --sign - "$app" +printf 'App erstellt: %s/%s\n' "$PWD" "$app"