From cc7b3c3379d6d7f8a0e0f6ebe220c8414eabd803 Mon Sep 17 00:00:00 2001
From: Kevin Adametz
Date: Fri, 12 Jun 2026 16:04:12 +0000
Subject: [PATCH] =?UTF-8?q?KI-generierte=20Bilder:=20eigener=20Lizenztyp,?=
=?UTF-8?q?=20Anbieter-Best=C3=A4tigung,=20Kennzeichnung?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-Authored-By: Claude Fable 5
---
app/Enums/ImageLicenseType.php | 15 +++-
app/Models/PressReleaseImage.php | 2 +
...s_ai_generated_to_press_release_images.php | 28 ++++++++
dev/frontend/hub-flux/PROGRESS.md | 25 +++++++
...unktionen & Magic-Link-Änderungsprozess.md | 69 ++++++++++++-------
.../admin/press-releases/show.blade.php | 13 ++++
.../press-release-images-manager.blade.php | 68 ++++++++++++++++--
.../customer/press-releases/show.blade.php | 9 +++
.../Feature/PressReleaseImageLicenseTest.php | 57 +++++++++++++++
9 files changed, 255 insertions(+), 31 deletions(-)
create mode 100644 database/migrations/2026_06_12_160000_add_is_ai_generated_to_press_release_images.php
diff --git a/app/Enums/ImageLicenseType.php b/app/Enums/ImageLicenseType.php
index 34dbcf8..61d4cf8 100644
--- a/app/Enums/ImageLicenseType.php
+++ b/app/Enums/ImageLicenseType.php
@@ -15,6 +15,7 @@ enum ImageLicenseType: string
case Consent = 'consent';
case PressPr = 'press_pr';
case PublicDomain = 'public_domain';
+ case AiGenerated = 'ai_generated';
case Other = 'other';
/**
@@ -29,6 +30,7 @@ enum ImageLicenseType: string
self::CreativeCommons => 'Creative-Commons-Lizenz',
self::PressPr => 'Presse-/PR-Bild mit Nutzungsfreigabe',
self::PublicDomain => 'Gemeinfrei / Public Domain / CC0',
+ self::AiGenerated => 'KI-generiert (z. B. Midjourney, DALL·E, Firefly)',
self::Other => 'Sonstige Lizenz / Sondervereinbarung',
};
}
@@ -43,10 +45,21 @@ enum ImageLicenseType: string
/**
* Ob zusaetzliche Lizenzdetails verpflichtend sind.
+ * Bei KI-Bildern ist das Detail das verwendete Tool (AI-Act-Kennzeichnung).
*/
public function requiresLicenseDetail(): bool
{
- return in_array($this, [self::CreativeCommons, self::Other], true);
+ return in_array($this, [self::CreativeCommons, self::AiGenerated, self::Other], true);
+ }
+
+ /**
+ * Rein KI-generierte Bilder haben keinen menschlichen Urheber (§ 2 UrhG);
+ * maßgeblich sind die Anbieter-Bedingungen und die Kennzeichnungspflicht
+ * aus Art. 50 EU AI Act (ab 02.08.2026).
+ */
+ public function isAiGenerated(): bool
+ {
+ return $this === self::AiGenerated;
}
/**
diff --git a/app/Models/PressReleaseImage.php b/app/Models/PressReleaseImage.php
index 6b645b0..391dae7 100644
--- a/app/Models/PressReleaseImage.php
+++ b/app/Models/PressReleaseImage.php
@@ -30,6 +30,7 @@ class PressReleaseImage extends Model
'property_rights_status',
'rights_notes',
'rights_confirmed_at',
+ 'is_ai_generated',
'is_preview',
'sort_order',
'width',
@@ -46,6 +47,7 @@ class PressReleaseImage extends Model
'license_type' => ImageLicenseType::class,
'persons_consent' => 'boolean',
'rights_confirmed_at' => 'datetime',
+ 'is_ai_generated' => 'boolean',
'is_preview' => 'boolean',
'sort_order' => 'integer',
'width' => 'integer',
diff --git a/database/migrations/2026_06_12_160000_add_is_ai_generated_to_press_release_images.php b/database/migrations/2026_06_12_160000_add_is_ai_generated_to_press_release_images.php
new file mode 100644
index 0000000..020ebe8
--- /dev/null
+++ b/database/migrations/2026_06_12_160000_add_is_ai_generated_to_press_release_images.php
@@ -0,0 +1,28 @@
+boolean('is_ai_generated')->default(false)->after('rights_confirmed_at');
+ });
+ }
+
+ public function down(): void
+ {
+ Schema::table('press_release_images', function (Blueprint $table): void {
+ $table->dropColumn('is_ai_generated');
+ });
+ }
+};
diff --git a/dev/frontend/hub-flux/PROGRESS.md b/dev/frontend/hub-flux/PROGRESS.md
index e5bc42a..dbfd56e 100644
--- a/dev/frontend/hub-flux/PROGRESS.md
+++ b/dev/frontend/hub-flux/PROGRESS.md
@@ -5,6 +5,31 @@
---
+## 2026-06-12 · KI-generierte Bilder: Lizenztyp + Kennzeichnung ✅
+
+- **Was**: Neuer Lizenztyp „KI-generiert" im Titelbild-Upload
+ (Entscheidung nach Kevins Frage): KI-Bilder haben keinen menschlichen
+ Urheber (§ 2 UrhG) — maßgeblich sind die Anbieter-Bedingungen, und ab
+ 02.08.2026 greift die Kennzeichnungspflicht aus Art. 50 EU AI Act.
+ Formular-Schaltung bei Auswahl: Pflichtfeld „Verwendetes KI-Tool"
+ (statt CC-/Sonstiges-Detail), Urheber-Label wird „Verantwortlich für
+ die Erstellung", Hinweis-Box (kein Urheberrecht, Anbieter-Bedingungen,
+ öffentliche Kennzeichnung, keine realen Personen/Marken), zusätzlicher
+ Pflicht-Switch „Anbieter-Bedingungen geprüft". Der öffentliche
+ Bildnachweis wird automatisch vorgeschlagen („Bild: KI-generiert
+ (Tool)"), manuelle Eingaben werden nicht überschrieben. Neues Flag
+ `press_release_images.is_ai_generated` (Migration); Kennzeichnung
+ sichtbar als Badge im Upload-Manager und als Bildnachweis-Zeile unter
+ dem Titelbild auf Customer- und Admin-Detailseite — die öffentlichen
+ Portal-Seiten übernehmen das Label beim Web-Relaunch.
+- **Dateien**: `app/Enums/ImageLicenseType.php` (AiGenerated),
+ `press-release-images-manager.blade.php`, `PressReleaseImage`-Model,
+ Migration, Customer-/Admin-Show (Bildnachweis-Zeile).
+- **Build/Test**: 3 neue Tests (Pflichtfelder, Speichern inkl. Flag,
+ Nachweis-Vorschlag), Suite 557 passed / 4 skipped, Pint clean.
+- **Offene Fragen**: Label-Ausspielung auf den öffentlichen Web-Seiten
+ beim Relaunch (Flag + copyright sind vorhanden).
+
## 2026-06-12 · Titelbild-Upload: Struktur + große Vorschau ✅
- **Was**: Das Bildrechte-Formular im Titelbild-Upload (gemeinsame
diff --git a/docs/weiteres/Decision-Update Phase-2-Funktionen & Magic-Link-Änderungsprozess.md b/docs/weiteres/Decision-Update Phase-2-Funktionen & Magic-Link-Änderungsprozess.md
index f06ed10..fb1d299 100644
--- a/docs/weiteres/Decision-Update Phase-2-Funktionen & Magic-Link-Änderungsprozess.md
+++ b/docs/weiteres/Decision-Update Phase-2-Funktionen & Magic-Link-Änderungsprozess.md
@@ -1,6 +1,6 @@
-**Version:** Juni 2026 **Datum:** 11.06.2026 **Status:** Abgestimmt – bereit zur Integration ins Konzept-/Decision-Log **Scope:** Definition von Boost und Veröffentlichungsnachweis (Launch), des Magic-Link-Zugangs- und Änderungsprozesses sowie der Phase-2-Funktionen (Vorab-Prüfung/Redigieren, Prüfzähler, höheres Prüfkontingent, kostenpflichtige Änderungspfade).
+**Version:** Juni 2026 (Rev. 2) **Datum:** 12.06.2026 **Status:** Abgestimmt – bereit zur Integration ins Konzept-/Decision-Log **Scope:** Definition von Boost und Veröffentlichungsnachweis (Launch), des Magic-Link-Zugangs- und Änderungsprozesses sowie der Phase-2-Funktionen (Vorab-Prüfung/Redigieren, Prüfzähler, höheres Prüfkontingent, kostenpflichtige Änderungspfade). **Änderungen Rev. 2:** KI bei E/F entfernt (Admin-Panel statt KI-Check); A nutzt Algorithmus-Diff statt KI; Bremsen-/Limit-Spalte für alle Pfade ergänzt. **Änderungen Rev. 3:** A & B als Launch bestätigt; Boost-Preisstaffel (12/20/35) und PDF-Preis (3 Credits) festgelegt; G Depublizieren = 25 Credits.
---
@@ -8,6 +8,11 @@
Dieses Update definiert die credit-nahen Funktionen und den Änderungsprozess für bestehende Pressemitteilungen. Leitlinie: kleiner, ehrlicher Launch-Umfang; alles, was an die volle Credit-/Prüf-Mechanik gekoppelt ist, wandert kontrolliert in Phase 2. Gesetzlich verpflichtende Pfade bleiben unabhängig davon ab Launch erreichbar.
+**Grundprinzip Kostenkontrolle:** Jeder Vorgang, der einen KI-Call oder Admin-Arbeit auslöst, braucht ein Limit. Daraus folgen zwei Strategien:
+
+1. **Kosten vermeiden statt deckeln** – wo ein billiger Algorithmus die Arbeit erledigt (z. B. Tippfehler-Diff), wird kein KI-Call ausgelöst. Das Limit schützt dann nur noch gegen Missbrauch, nicht gegen Kosten.
+2. **Bezahlung als Selbstbremse** – kostenpflichtige Pfade (C/D/G) sind selbstbegrenzend: Wer zahlt, missbraucht nicht. Harte Limits braucht es nur bei den kostenlosen Pfaden (A/B/E/F).
+
---
## 2. Launch-Funktionen
@@ -17,15 +22,26 @@ Dieses Update definiert die credit-nahen Funktionen und den Änderungsprozess f
- **Was:** bezahlte Hervorhebung einer PM – Platzierung auf Startseite und Branchen-/Kategorieseite
- **Gate:** nur **grüne** PMs sind boostbar; gelb/rot nicht
- **Mechanik:** nachträglich kaufbar, sobald die PM live und grün ist
-- **Dauer:** Featured-Zeitraum in Stufen (z. B. 7 / 14 / 30 Tage), Preis gestaffelt
+- **Umfang:** ein Boost = Featured auf Startseite **und** Branchenseite (eine Stufe, nur die Dauer variiert – bewusst kein getrenntes Pricing pro Platzierung)
- **Bezahlung:** über die Credit-Wallet (kleine Wallet ist Launch-Bestandteil)
+**Preisstaffel (1 Credit = 1 €):**
+
+|Dauer|Credits|Pro Tag|
+|---|---|---|
+|7 Tage|12|1,71 €|
+|14 Tage|20|1,43 €|
+|30 Tage|35|1,17 €|
+
+Moderate Staffel: Pro-Tag-Preis sinkt mit der Dauer, der Einstieg (7 Tage / 12) bleibt unter dem PM-Preis von 19 – ein Boost wirkt nie teurer als das Veröffentlichen selbst. Passt zur „Nische besetzen statt abschöpfen"-Linie (Volumen statt Einzelmarge).
+
### 2.2 Veröffentlichungsnachweis / PDF
- **Was:** generiertes PDF „PM XY wurde am … auf … veröffentlicht" inkl. URL, Datum, Vorschau
- **Zweck:** Reporting an Vorgesetzte/Kunden – klassische PR-Mitnahme
-- **Bezahlung:** kleiner Credit-Betrag, keine KI nötig
+- **Preis:** **3 Credits**, pauschal pro PM – Impulskauf, keine Abwägung (kostet die Plattform praktisch nichts: keine KI, on-demand aus vorhandenen Daten)
- **Generierung:** on-demand aus vorhandenen PM-Daten
+- **Phase-2-Option:** in Business/Pro/Agency später ggf. **inklusive** als kleiner Tarif-Perk; zum Launch einheitlich 3 Credits
---
@@ -53,34 +69,39 @@ Der Magic-Link ist also **kein eigenes System**, sondern die Zugangsbrücke für
Kein wahlloses Ändern/Löschen – Friction nach Anliegen. Alle Pfade laufen über dieselbe Verwaltung (Login **oder** Magic-Link).
-|Pfad|Anliegen|Kosten|Phase|Public Hint|
-|---|---|---|---|---|
-|**A**|Tippfehler/Grammatik|kostenfrei|Launch*|nein|
-|**B**|Pressekontakt-Daten ändern|kostenfrei|Launch*|nein|
-|**C**|Inhaltliche Korrektur (sachlicher Fehler)|kostenpflichtig|**Phase 2**|ja|
-|**D**|Update/Ergänzung (neue Information)|kostenpflichtig|**Phase 2**|ja|
-|**E**|DSGVO-Anonymisierung|kostenfrei|**Launch (Pflicht)**|nein|
-|**F**|Persönlichkeitsrechtsverletzung|kostenfrei|**Launch (Pflicht)**|je nach Outcome|
-|**G**|Depublizieren|kostenpflichtig + Bedenkzeit|**Phase 2**|Tombstone|
+|Pfad|Anliegen|KI / Admin|Kosten|Bremse|Phase|Public Hint|
+|---|---|---|---|---|---|---|
+|**A**|Tippfehler/Grammatik|Algorithmus-Diff, KI nur im Graubereich|kostenfrei|1 Einreichung/PM/24 h, gebündelt + Account-Tages-Cap|**Launch**|nein|
+|**B**|Pressekontakt-Daten ändern|keine|kostenfrei|normales Rate-Limit|**Launch**|nein|
+|**C**|Inhaltliche Korrektur (sachlicher Fehler)|KI|kostenpflichtig|Bezahlung = Bremse|**Phase 2**|ja|
+|**D**|Update/Ergänzung (neue Information)|KI|kostenpflichtig|Bezahlung = Bremse|**Phase 2**|ja|
+|**E**|DSGVO-Anonymisierung|**Admin-Panel** (keine KI)|kostenfrei|1 offene Anfrage/PM gleichzeitig|**Launch (Pflicht)**|nein|
+|**F**|Persönlichkeitsrechtsverletzung|**Admin-Panel** (keine KI)|kostenfrei|1 offene Anfrage/PM gleichzeitig|**Launch (Pflicht)**|je nach Outcome|
+|**G**|Depublizieren|KI|25 Credits + Bedenkzeit|Bezahlung + 24–48 h Bedenkzeit|**Phase 2**|Tombstone|
-* A/B sind kostenfrei und credit-unabhängig – technisch Launch-fähig, aber kein Muss (siehe offene Punkte).
+A & B sind als Launch-Bestandteil bestätigt: kostenfrei, credit-unabhängig und sie senken sofort das unautorisierte Mail-Aufkommen. C/D/G folgen mit dem Credit-/Prüf-System in Phase 2.
**Pfad-Details (Kurzfassung):**
-- **A Tippfehler:** Inline-Editor mit Diff; KI prüft, ob nur kosmetisch → ja: übernommen ohne Hinweis; nein: Umleitung zu C
+- **A Tippfehler:** Inline-Editor mit Diff. **Kein KI-Call im Normalfall** – ein lokaler Zeichen-Diff (Levenshtein) entscheidet: Änderung klein **und** keine Zahlen/Namen/Eigennamen berührt → automatisch übernommen (0 KI, 0 Admin). Nur im Graubereich (größere Änderung oder sensible Tokens berührt) → KI-Check **oder** Umleitung zu Pfad C. Nutzer korrigiert idealerweise alle Tippfehler gebündelt und reicht **einmal** ein = ein Vorgang.
- **B Kontaktdaten:** Formular, direkt übernommen, Versionierung im Hintergrund
- **C Korrektur:** Editor + Pflichtfeld „Was war falsch / was ist korrekt?"; KI erlaubt Fakt-Korrektur (Zahl/Datum/Name), blockiert Umschreibung der Aussage; PM erhält Korrektur-Hinweis
- **D Update:** Ergänzung wird unten mit Datum angehängt, Original bleibt unverändert; KI-Check auf Spam/Werbung
-- **E DSGVO:** Aufklärung (Art. 85 DSGVO, Medienprivileg → keine Volllöschung, aber kostenfreie Entfernung personenbezogener Daten); Checkbox-Auswahl (Name, Durchwahl, persönliche E-Mail, Freitext); KI-Plausibilitätscheck; sofort umgesetzt
-- **F Persönlichkeitsrecht:** Pflichtfelder (betroffene Stelle, Art, Begründung, Belege) → Review-Queue mit KI-Vorklassifikation; Outcomes: Anonymisierung, Anpassung, Tombstone, Ablehnung mit Begründung
+- **E DSGVO:** Aufklärung (Art. 85 DSGVO, Medienprivileg → keine Volllöschung, aber kostenfreie Entfernung personenbezogener Daten); Checkbox-Auswahl (Name, Durchwahl, persönliche E-Mail, Freitext). **Keine KI** – die Anfrage geht als Benachrichtigung ins Admin-Panel, manuelle Sichtung. In Phase 2/3 automatisierbar.
+- **F Persönlichkeitsrecht:** Pflichtfelder (betroffene Stelle, Art, Begründung, Belege). **Keine KI** – Benachrichtigung ins Admin-Panel, manuelle Sichtung; Outcomes: Anonymisierung, Anpassung, Tombstone, Ablehnung mit Begründung. In Phase 2/3 automatisierbar.
- **G Depublizieren:** Aufklärungsseite → Begründungspflicht (KI lenkt „veraltet" → D, „falsch/peinlich" → C) → kostenpflichtige Bestätigung → 24–48 h Bedenkzeit mit Widerrufslink → Tombstone (`noindex`, raus aus Listen/Suche, URL bleibt)
### 3.3 Compliance-Minimum zum Launch (E & F)
**E und F können nicht auf Phase 2 warten.** Sobald PMs live sind, besteht ein gesetzliches Recht auf Anonymisierung personenbezogener Daten (E) und auf Meldung von Persönlichkeitsrechtsverletzungen (F). Beide müssen ab Tag 1 erreichbar sein – zur Not als einfaches Formular/manueller Prozess statt vollem Wizard. Beide sind **kostenfrei** (eine legitime Rechtsanfrage darf nie hinter einer Gebühr stehen) und damit unabhängig vom Phase-2-Credit-Build.
+**Abwicklung über Admin-Panel (keine KI):** E und F lösen keinen KI-Call aus. Die Anfrage erzeugt eine Benachrichtigung im Admin-Panel, die Sichtung erfolgt manuell. Das spart KI-Kosten und gibt bei Rechtsthemen die bessere Kontrolle. Eine Automatisierung ist für Phase 2/3 vorgesehen.
+
+**Limit – nicht „einmalig", sondern „eine offene Anfrage pro PM":** Eine gesetzliche Anfrage darf nie hart gesperrt werden (jemand kann legitim ein zweites Mal eine Anonymisierung brauchen, etwa wenn über ein Update neue personenbezogene Daten hinzukommen). Die saubere Bremse: Solange für eine PM eine Anfrage offen ist, kann keine neue gestellt werden (verhindert Spam-Duplikate); nach Abschluss geht wieder eine. Das deckelt Missbrauch ohne Rechtsrisiko.
+
### 3.4 Missbrauchsschutz, Edge Cases & Standard-Antwort
+- **Pfad A (Tippfehler):** Algorithmus-Diff spart im Normalfall jeden KI-Call → Limit schützt nur gegen Missbrauch, nicht gegen Kosten. Bremse: 1 Korrektur-Einreichung pro PM / 24 h (gebündelt), plus Account-Tages-Cap gegen den pathologischen Fall (ein Account bearbeitet hunderte PMs)
- **Rate-Limit** auf Magic-Link-Anfragen pro E-Mail/IP; Cooldown nach Depublizierung (Widerruf-Fenster); Audit-Log mit IP/User-Agent je Edit-Aktion
- **Keine valide E-Mail** (alte connektar-PMs): Fallback „Verifikation per Domain-Inhaberschaft / Impressums-Match", manuelle Prüfung
- **E-Mail geändert / Person verlässt Firma:** manuelle Anfrage mit Bestätigung über `info@`/Impressum
@@ -133,9 +154,9 @@ Kosten-Anker bei 1 Credit = 1 € (zu bestätigen):
|Pfad|Aktion|Anker|
|---|---|---|
-|C|Inhaltliche Korrektur|≈ 8 Credits|
-|D|Update/Ergänzung|≈ 4 Credits|
-|G|Depublizieren|≈ 19–29 Credits + 24–48 h Bedenkzeit|
+|C|Inhaltliche Korrektur|≈ 8 Credits (zu bestätigen)|
+|D|Update/Ergänzung|≈ 4 Credits (zu bestätigen)|
+|G|Depublizieren|**25 Credits** + 24–48 h Bedenkzeit (festgelegt)|
Depublizieren bewusst am teuersten und mit Bedenkzeit, weil irreversibelste Aktion.
@@ -143,8 +164,9 @@ Depublizieren bewusst am teuersten und mit Bedenkzeit, weil irreversibelste Akti
## 5. Anti-Zombie-Check (dieser Stand)
-- ✅ Gesetzliche Anfragen (E/F) immer kostenfrei und ab Launch erreichbar
+- ✅ Gesetzliche Anfragen (E/F) immer kostenfrei, ab Launch erreichbar und nie hart gesperrt (1 offene Anfrage/PM statt „einmalig")
- ✅ Kosten nur bei echtem Mehraufwand (Korrektur, Update, Depublizierung), nicht bei Pflicht-Rechten
+- ✅ Kostenlose Pfade bleiben echt kostenlos: Tippfehler laufen über Algorithmus-Diff statt KI, Limits schützen gegen Missbrauch – nicht als versteckte Kostenbremse
- ✅ Eine Verwaltung, zwei Eintrittswege – keine künstliche Trennung registrierter/unregistrierter Nutzer
- ✅ Prüf-Kontingent großzügig genug, dass der Normalfall nie ansteht
- ✅ Depublizierung mit Aufklärung + Bedenkzeit statt Hard-Delete – schützt den Kunden vor sich selbst
@@ -153,7 +175,6 @@ Depublizieren bewusst am teuersten und mit Bedenkzeit, weil irreversibelste Akti
## 6. Offene Punkte
-- **A/B im Launch?** Kostenfrei und credit-unabhängig → könnten den Mail-Aufwand sofort senken. Entscheidung: A/B mit in den Launch nehmen oder kompletten Änderungs-Wizard (inkl. A/B) erst in Phase 2, zum Launch nur E/F als Pflicht-Minimum.
-- **Kosten-Anker C/D/G** final bestätigen, sobald das Credit-/Prüf-System gebaut wird.
-- **Boost-Preisstaffel** (7/14/30 Tage) in Credits festlegen.
-- **PDF-Preis** in Credits festlegen.
\ No newline at end of file
+- **Kosten-Anker C/D** final bestätigen, sobald das Credit-/Prüf-System gebaut wird (aktuell ≈ 8 / ≈ 4 Credits).
+
+**In Rev. 3 abgeschlossen:** A/B-Launch ✓ · Boost-Staffel 12/20/35 ✓ · PDF 3 Credits ✓ · G Depublizieren 25 Credits ✓
\ No newline at end of file
diff --git a/resources/views/livewire/admin/press-releases/show.blade.php b/resources/views/livewire/admin/press-releases/show.blade.php
index b51014f..86db104 100644
--- a/resources/views/livewire/admin/press-releases/show.blade.php
+++ b/resources/views/livewire/admin/press-releases/show.blade.php
@@ -204,6 +204,19 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
{{ __('Platzhalter-Titelbild (kein eigenes Bild hochgeladen).') }}
+ @else
+ @php
+ $adminTitleImage = $pr->images->sortByDesc('is_preview')->first();
+ @endphp
+ @if ($adminTitleImage && ($adminTitleImage->copyright || $adminTitleImage->is_ai_generated))
+ {{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act) --}}
+
+ @if ($adminTitleImage->is_ai_generated)
+ {{ __('KI-generiert') }}
+ @endif
+ {{ $adminTitleImage->copyright ?? __('Bild: KI-generiert') }}
+
+ @endif
@endif
diff --git a/resources/views/livewire/components/press-release-images-manager.blade.php b/resources/views/livewire/components/press-release-images-manager.blade.php
index 2504c24..b5d1120 100644
--- a/resources/views/livewire/components/press-release-images-manager.blade.php
+++ b/resources/views/livewire/components/press-release-images-manager.blade.php
@@ -47,6 +47,8 @@ new class extends Component {
public bool $newRightsConfirmed = false;
+ public bool $newAiTermsConfirmed = false;
+
public bool $isUploadFormOpen = false;
public function mount(int $pressReleaseId): void
@@ -62,12 +64,40 @@ new class extends Component {
/**
* Beim Wechsel des Lizenztyps das Detail-Feld leeren — sonst klebt
* z. B. der zuvor gewählte CC-Wert (cc_by) im Freitextfeld
- * „Lizenzdetails / Begründung" von „Sonstiges".
+ * „Lizenzdetails / Begründung" von „Sonstiges". Für KI-Bilder wird der
+ * öffentliche Bildnachweis vorgeschlagen (AI-Act-Kennzeichnung).
*/
public function updatedNewLicenseType(): void
{
$this->newLicenseDetail = '';
+ $this->newAiTermsConfirmed = false;
$this->resetErrorBag('newLicenseDetail');
+
+ $this->suggestAiCopyright();
+ }
+
+ /**
+ * Tool-Angabe in den Bildnachweis-Vorschlag übernehmen, solange der
+ * Nutzer den Nachweis nicht selbst überschrieben hat.
+ */
+ public function updatedNewLicenseDetail(): void
+ {
+ $this->suggestAiCopyright();
+ }
+
+ private function suggestAiCopyright(): void
+ {
+ if ($this->newLicenseType !== ImageLicenseType::AiGenerated->value) {
+ return;
+ }
+
+ if (filled($this->newCopyright) && ! str_starts_with($this->newCopyright, __('Bild: KI-generiert'))) {
+ return;
+ }
+
+ $this->newCopyright = filled($this->newLicenseDetail)
+ ? __('Bild: KI-generiert (:tool)', ['tool' => trim($this->newLicenseDetail)])
+ : __('Bild: KI-generiert');
}
public function closeUploadForm(): void
@@ -98,6 +128,7 @@ new class extends Component {
$licenseType = ImageLicenseType::tryFrom($this->newLicenseType);
$requiresLicenseUrl = $licenseType?->requiresLicenseUrl() ?? false;
$requiresLicenseDetail = $licenseType?->requiresLicenseDetail() ?? false;
+ $isAiGenerated = $licenseType?->isAiGenerated() ?? false;
$this->validate(
[
@@ -113,16 +144,22 @@ new class extends Component {
'newPropertyRightsStatus' => ['required', Rule::in(array_keys($this->propertyRightsOptions()))],
'newRightsNotes' => ['nullable', 'string', 'max:1000'],
'newRightsConfirmed' => ['accepted'],
+ 'newAiTermsConfirmed' => [$isAiGenerated ? 'accepted' : 'nullable'],
],
[
'newCopyright.required' => __('Bitte einen öffentlichen Bildnachweis angeben, z. B. Foto: Max Mustermann / Beispiel GmbH.'),
- 'newAuthor.required' => __('Bitte Urheber, Fotograf oder Rechteinhaber angeben.'),
+ 'newAuthor.required' => $isAiGenerated
+ ? __('Bitte angeben, wer für die Erstellung verantwortlich ist (Person oder Firma).')
+ : __('Bitte Urheber, Fotograf oder Rechteinhaber angeben.'),
'newLicenseType.required' => __('Bitte einen Lizenztyp wählen.'),
- 'newLicenseDetail.required' => __('Bitte die Lizenz genauer angeben.'),
+ 'newLicenseDetail.required' => $isAiGenerated
+ ? __('Bitte das verwendete KI-Tool angeben, z. B. Midjourney v7.')
+ : __('Bitte die Lizenz genauer angeben.'),
'newLicenseUrl.required' => __('Für diesen Lizenztyp ist eine Nachweis-URL erforderlich.'),
'newPeopleRightsStatus.required' => __('Bitte angeben, ob erkennbare Personen abgebildet sind.'),
'newPropertyRightsStatus.required' => __('Bitte angeben, ob Marken, Kunstwerke oder private Orte sichtbar sind.'),
'newRightsConfirmed.accepted' => __('Bitte bestätigen, dass die Bildrechte geklärt sind.'),
+ 'newAiTermsConfirmed.accepted' => __('Bitte bestätigen, dass die Anbieter-Bedingungen die kommerzielle Nutzung erlauben.'),
],
);
@@ -146,6 +183,7 @@ new class extends Component {
'property_rights_status' => $this->newPropertyRightsStatus,
'rights_notes' => $this->newRightsNotes ?: null,
'rights_confirmed_at' => now(),
+ 'is_ai_generated' => $isAiGenerated,
'is_preview' => true,
'sort_order' => ((int) $pressRelease->images()->max('sort_order')) + 1,
'width' => $stored['width'],
@@ -217,6 +255,7 @@ new class extends Component {
'licenseUrlRequired' => ImageLicenseType::tryFrom($this->newLicenseType)?->requiresLicenseUrl() ?? false,
'licenseDetailRequired' => ImageLicenseType::tryFrom($this->newLicenseType)?->requiresLicenseDetail() ?? false,
'showsCcWarning' => $this->newLicenseType === ImageLicenseType::CreativeCommons->value,
+ 'showsAiSection' => $this->newLicenseType === ImageLicenseType::AiGenerated->value,
'showsRightsWarning' => $this->shouldShowRightsWarning(),
];
}
@@ -242,7 +281,7 @@ new class extends Component {
private function resetUploadForm(): void
{
- $this->reset(['newImage', 'newTitle', 'newCopyright', 'newAuthor', 'newLicenseType', 'newLicenseDetail', 'newLicenseUrl', 'newSourceUrl', 'newPeopleRightsStatus', 'newPropertyRightsStatus', 'newRightsNotes', 'newRightsConfirmed']);
+ $this->reset(['newImage', 'newTitle', 'newCopyright', 'newAuthor', 'newLicenseType', 'newLicenseDetail', 'newLicenseUrl', 'newSourceUrl', 'newPeopleRightsStatus', 'newPropertyRightsStatus', 'newRightsNotes', 'newRightsConfirmed', 'newAiTermsConfirmed']);
}
/**
@@ -331,6 +370,9 @@ new class extends Component {
@endif
+ @if ($titleImage->is_ai_generated)
+ {{ __('KI-generiert') }}
+ @endif
@if ($titleImage->license_type)
{{ $titleImage->license_type->label() }}
@@ -449,7 +491,8 @@ new class extends Component {
class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Herkunft & Lizenz') }}
-
@@ -460,7 +503,20 @@ new class extends Component {
- @if ($newLicenseType === \App\Enums\ImageLicenseType::CreativeCommons->value)
+ @if ($showsAiSection)
+
+
+
+ {{ __('KI-generierte Bilder haben keinen menschlichen Urheber — maßgeblich sind die Nutzungsbedingungen des KI-Anbieters. Das Bild wird öffentlich als KI-generiert gekennzeichnet (Transparenzpflicht, EU AI Act). Achten Sie darauf, dass keine realen Personen, Marken oder geschützten Werke erkennbar nachgebildet werden.') }}
+
+
+
+ @elseif ($newLicenseType === \App\Enums\ImageLicenseType::CreativeCommons->value)
{{ __('Bitte wählen…') }}
diff --git a/resources/views/livewire/customer/press-releases/show.blade.php b/resources/views/livewire/customer/press-releases/show.blade.php
index 264cbef..7361b41 100644
--- a/resources/views/livewire/customer/press-releases/show.blade.php
+++ b/resources/views/livewire/customer/press-releases/show.blade.php
@@ -104,6 +104,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
'categoryName' => $categoryName,
'coverUrl' => $cover->coverUrl($pr, 'cover'),
'coverIsPlaceholder' => $cover->coverIsPlaceholder($pr),
+ 'titleImage' => $pr->images()->orderByDesc('is_preview')->orderBy('sort_order')->orderBy('id')->first(),
'quotaTotal' => $user->pressReleaseQuotaTotal(),
'quotaRemaining' => $user->pressReleaseQuotaRemaining(),
'canEdit' => auth()->user()->can('update', $pr)
@@ -505,6 +506,14 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
{{ __('Platzhalter-Titelbild — laden Sie im Editor ein eigenes Bild hoch.') }}
+ @elseif ($titleImage && ($titleImage->copyright || $titleImage->is_ai_generated))
+ {{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act) --}}
+
+ @if ($titleImage->is_ai_generated)
+ {{ __('KI-generiert') }}
+ @endif
+ {{ $titleImage->copyright ?? __('Bild: KI-generiert') }}
+
@endif
diff --git a/tests/Feature/PressReleaseImageLicenseTest.php b/tests/Feature/PressReleaseImageLicenseTest.php
index 4f5647f..d22024d 100644
--- a/tests/Feature/PressReleaseImageLicenseTest.php
+++ b/tests/Feature/PressReleaseImageLicenseTest.php
@@ -272,6 +272,63 @@ test('switching the license type clears the stale license detail', function () {
->assertSet('newLicenseDetail', '');
});
+test('ai generated images require tool and provider terms confirmation', function () {
+ /** @var TestCase $this */
+ ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
+ $this->actingAs($owner);
+
+ LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
+ ->set('newImage', UploadedFile::fake()->image('ki-bild.jpg', 1200, 800))
+ ->set('newAuthor', 'Beispiel GmbH')
+ ->set('newLicenseType', ImageLicenseType::AiGenerated->value)
+ ->set('newPeopleRightsStatus', 'none')
+ ->set('newPropertyRightsStatus', 'none')
+ ->set('newRightsConfirmed', true)
+ ->call('saveImage')
+ ->assertHasErrors(['newLicenseDetail', 'newAiTermsConfirmed']);
+});
+
+test('a valid ai generated upload stores tool and ai flag', function () {
+ /** @var TestCase $this */
+ ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
+ $this->actingAs($owner);
+
+ LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
+ ->set('newImage', UploadedFile::fake()->image('ki-bild.jpg', 1200, 800))
+ ->set('newAuthor', 'Beispiel GmbH')
+ ->set('newLicenseType', ImageLicenseType::AiGenerated->value)
+ ->set('newLicenseDetail', 'Midjourney v7')
+ ->set('newPeopleRightsStatus', 'none')
+ ->set('newPropertyRightsStatus', 'none')
+ ->set('newRightsConfirmed', true)
+ ->set('newAiTermsConfirmed', true)
+ ->call('saveImage')
+ ->assertHasNoErrors();
+
+ $image = $pr->images()->first();
+
+ expect($image)->not->toBeNull();
+ expect($image->license_type)->toBe(ImageLicenseType::AiGenerated);
+ expect($image->license_detail)->toBe('Midjourney v7');
+ expect($image->is_ai_generated)->toBeTrue();
+ expect($image->copyright)->toBe('Bild: KI-generiert (Midjourney v7)');
+});
+
+test('the ai copyright suggestion follows the tool but respects manual input', function () {
+ /** @var TestCase $this */
+ ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();
+ $this->actingAs($owner);
+
+ LivewireVolt::test('components.press-release-images-manager', ['pressReleaseId' => $pr->id])
+ ->set('newLicenseType', ImageLicenseType::AiGenerated->value)
+ ->assertSet('newCopyright', 'Bild: KI-generiert')
+ ->set('newLicenseDetail', 'DALL·E 3')
+ ->assertSet('newCopyright', 'Bild: KI-generiert (DALL·E 3)')
+ ->set('newCopyright', 'Eigener Nachweis')
+ ->set('newLicenseDetail', 'Midjourney v7')
+ ->assertSet('newCopyright', 'Eigener Nachweis');
+});
+
test('existing title image hides upload form and can be removed', function () {
/** @var TestCase $this */
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();