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();