images: Datenmodell, Textarchiv, Herkunftsprüfung und gemeinsamer Uploader für Formular v2 (Bildkennzeichnung Block A)

- press_release_images: Kundenangabe/Portalkennzeichnung getrennt, Herkunftsbefund, Beweisoriginal, Snapshot-Pfade, published_ever_at, rules_version, Fokus/Letterbox; Bestand unangetastet
- legal_texts (insert-only) mit img-confirm-v1 (alter Wortlaut), v2 (AGB 0.6), v3 (AGB 0.7, noch ohne Datum); image-rights:backfill-confirmations
- image_rights_confirmations polymorph, insert-only, IP als versionierter HMAC; Löschung nur für nie veröffentlichte Uploads
- ImageProvenanceInspector (Imagick/exif): IPTC, XMP, EXIF; algorithmicallyEnhanced nur Vermerk; unlesbare Datei = error
- PressReleaseImageUploader: Original vor Derivaten, Hash-Bindung, Titelbild-Regel unter Sperre, Dateien bei Fehlern zurückgerollt; Admin-Upload ohne Bestätigung
- ImageService: kein Rückfall auf die Quelldatei, og-Variante, Fokuspunkt und no_crop

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-09-22 09:22:05 +00:00
parent b624f80d7f
commit e9bdff3ad3
36 changed files with 2631 additions and 30 deletions

View file

@ -0,0 +1,27 @@
<?php
namespace App\Enums;
/**
* Woher die effektive KI-Kennzeichnung eines Bildes stammt (Decision-Update
* S6): Angabe des Kunden, Vorbelegung aus ungesicherten Metadaten, geprüfte
* Content Credentials oder eine Admin-Korrektur. Wird öffentlich im
* Detailfenster genannt („Angabe des Einreichers" usw.).
*/
enum AiDeclarationSource: string
{
case Customer = 'customer';
case IptcPrefill = 'iptc_prefill';
case C2paVerified = 'c2pa_verified';
case AdminCorrection = 'admin_correction';
public function publicLabel(): string
{
return match ($this) {
self::Customer => __('Angabe des Einreichers'),
self::IptcPrefill => __('aus Metadaten der Datei vorbelegt, vom Einreicher bestätigt'),
self::C2paVerified => __('aus geprüften Herkunftsdaten der Datei'),
self::AdminCorrection => __('vom Portal nach Prüfung korrigiert'),
};
}
}

View file

@ -8,6 +8,11 @@ namespace App\Enums;
* Pflichtauswahl beim Bild-Upload (Decision-Update KI-Kennzeichnung,
* 08.07.2026). Bewusste Über-Erfüllung: gelabelt wird jedes deklarierte
* KI-Bild — auch stilisiert/abstrakt, nicht nur fotorealistische Deepfakes.
*
* Seit Formular v2 (Bildkennzeichnung, 22.09.2026) heißt die erste Option
* „Ohne generative KI erstellt oder bearbeitet": Sie gilt auch für Logos,
* Grafiken und Screenshots, nicht nur für Fotos (Feedback F11). Der
* gespeicherte Wert `original` bleibt aus Kompatibilitätsgründen.
*/
enum AiImageDeclaration: string
{
@ -21,9 +26,9 @@ enum AiImageDeclaration: string
public function label(): string
{
return match ($this) {
self::Original => __('Originalfoto'),
self::AiGenerated => __('KI-generiertes Bild'),
self::AiEdited => __('KI-bearbeitetes Bild'),
self::Original => __('Ohne generative KI erstellt oder bearbeitet'),
self::AiGenerated => __('KI-generiert'),
self::AiEdited => __('KI-bearbeitet (z. B. generative Füllung, KI-Retusche)'),
};
}
@ -41,6 +46,40 @@ enum AiImageDeclaration: string
};
}
/**
* Rangfolge für die Portalregel „Kennzeichnung = max(geprüfter Befund,
* Kundenangabe)" (Bildkennzeichnung A.4). Das ist eine bewusste
* Portalregel und keine Aussage über den Wahrheitsgehalt: Ein
* KI-erzeugter Bestandteil macht ein Bild nicht automatisch vollständig
* KI-generiert (Feedback F11) — der Befund wird vorher fachlich
* zugeordnet, erst dann greift die Mindestkennzeichnung.
*/
public function rank(): int
{
return match ($this) {
self::Original => 0,
self::AiEdited => 1,
self::AiGenerated => 2,
};
}
/**
* Die strengere von zwei Kennzeichnungen nach rank(); null zählt als
* „keine Angabe".
*/
public static function strictest(?self $a, ?self $b): ?self
{
if ($a === null) {
return $b;
}
if ($b === null) {
return $a;
}
return $b->rank() > $a->rank() ? $b : $a;
}
/**
* Optionsliste für Radios/Selects: value => label.
*

View file

@ -0,0 +1,15 @@
<?php
namespace App\Enums;
/**
* Über welchen Weg eine Bildrechte-Bestätigung abgegeben wurde
* (Bildkennzeichnung A.2). `admin` ist kein Bestätigungskanal: Ein
* Admin-Upload wartet auf die Kundenbestätigung per Magic-Link (E10).
*/
enum ConfirmationChannel: string
{
case Form = 'form';
case Api = 'api';
case MagicLink = 'magic_link';
}

View file

@ -0,0 +1,14 @@
<?php
namespace App\Enums;
/**
* Woher der Vorschlag für den Bildnachweis kam, den der Einreicher
* übernommen oder geändert hat (Bildkennzeichnung A.1).
*/
enum CreditSource: string
{
case Customer = 'customer';
case Metadata = 'metadata';
case Api = 'api';
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Enums;
/**
* Ergebnisklasse der Herkunftsprüfung eines Bild-Uploads (Bildkennzeichnung
* A.4, Feedback F02). Phase 1 liest nur ungesicherte IPTC/XMP/EXIF-Angaben,
* kann also höchstens `unsigned_metadata` liefern. Die `validated_*`-Stufen
* entstehen erst mit der C2PA-Signaturprüfung (Phase 2, Block I).
*/
enum ImageProvenanceStatus: string
{
/** Keine unterstützten Herkunftsdaten in der Datei. */
case Absent = 'absent';
/** IPTC/XMP/EXIF gefunden — frei editierbar, nicht signiert. */
case UnsignedMetadata = 'unsigned_metadata';
/** C2PA-Manifest, Signatur und Dateibindung geprüft, Aussteller in der Vertrauensliste. */
case ValidatedTrusted = 'validated_trusted';
/** C2PA technisch geprüft, Aussteller nicht in der Vertrauensliste. */
case ValidatedUntrusted = 'validated_untrusted';
/** C2PA vorhanden, Prüfung fehlgeschlagen (Signatur oder Bindung). */
case Invalid = 'invalid';
/** Format oder Manifestversion nicht unterstützt. */
case Unsupported = 'unsupported';
/** Technischer Fehler oder Zeitüberschreitung — Prüfung nicht abgeschlossen. */
case Error = 'error';
/**
* Nur ein vertrauenswürdig geprüfter Befund darf eine Kennzeichnung
* erzwingen (Decision-Update E9).
*/
public function allowsLock(): bool
{
return $this === self::ValidatedTrusted;
}
/**
* Ob überhaupt Herkunftsangaben vorliegen, aus denen das Formular
* vorbelegt werden kann.
*/
public function hasMetadata(): bool
{
return in_array($this, [self::UnsignedMetadata, self::ValidatedTrusted, self::ValidatedUntrusted], true);
}
}