KI-generierte Bilder: eigener Lizenztyp, Anbieter-Bestätigung, Kennzeichnung

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kevin Adametz 2026-06-12 16:04:12 +00:00
parent 6e0b2b1814
commit cc7b3c3379
9 changed files with 255 additions and 31 deletions

View file

@ -15,6 +15,7 @@ enum ImageLicenseType: string
case Consent = 'consent'; case Consent = 'consent';
case PressPr = 'press_pr'; case PressPr = 'press_pr';
case PublicDomain = 'public_domain'; case PublicDomain = 'public_domain';
case AiGenerated = 'ai_generated';
case Other = 'other'; case Other = 'other';
/** /**
@ -29,6 +30,7 @@ enum ImageLicenseType: string
self::CreativeCommons => 'Creative-Commons-Lizenz', self::CreativeCommons => 'Creative-Commons-Lizenz',
self::PressPr => 'Presse-/PR-Bild mit Nutzungsfreigabe', self::PressPr => 'Presse-/PR-Bild mit Nutzungsfreigabe',
self::PublicDomain => 'Gemeinfrei / Public Domain / CC0', self::PublicDomain => 'Gemeinfrei / Public Domain / CC0',
self::AiGenerated => 'KI-generiert (z. B. Midjourney, DALL·E, Firefly)',
self::Other => 'Sonstige Lizenz / Sondervereinbarung', self::Other => 'Sonstige Lizenz / Sondervereinbarung',
}; };
} }
@ -43,10 +45,21 @@ enum ImageLicenseType: string
/** /**
* Ob zusaetzliche Lizenzdetails verpflichtend sind. * Ob zusaetzliche Lizenzdetails verpflichtend sind.
* Bei KI-Bildern ist das Detail das verwendete Tool (AI-Act-Kennzeichnung).
*/ */
public function requiresLicenseDetail(): bool 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;
} }
/** /**

View file

@ -30,6 +30,7 @@ class PressReleaseImage extends Model
'property_rights_status', 'property_rights_status',
'rights_notes', 'rights_notes',
'rights_confirmed_at', 'rights_confirmed_at',
'is_ai_generated',
'is_preview', 'is_preview',
'sort_order', 'sort_order',
'width', 'width',
@ -46,6 +47,7 @@ class PressReleaseImage extends Model
'license_type' => ImageLicenseType::class, 'license_type' => ImageLicenseType::class,
'persons_consent' => 'boolean', 'persons_consent' => 'boolean',
'rights_confirmed_at' => 'datetime', 'rights_confirmed_at' => 'datetime',
'is_ai_generated' => 'boolean',
'is_preview' => 'boolean', 'is_preview' => 'boolean',
'sort_order' => 'integer', 'sort_order' => 'integer',
'width' => 'integer', 'width' => 'integer',

View file

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* KI-Kennzeichnung für PM-Bilder (12.06.2026): Flag für KI-generierte
* Titelbilder Grundlage für das öffentliche Label auf den Portal-Seiten
* (Art. 50 EU AI Act, Transparenzpflicht ab 02.08.2026) und für die
* redaktionelle Sicht im Admin.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('press_release_images', function (Blueprint $table): void {
$table->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');
});
}
};

View file

@ -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 ✅ ## 2026-06-12 · Titelbild-Upload: Struktur + große Vorschau ✅
- **Was**: Das Bildrechte-Formular im Titelbild-Upload (gemeinsame - **Was**: Das Bildrechte-Formular im Titelbild-Upload (gemeinsame

View file

@ -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. 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 ## 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 - **Was:** bezahlte Hervorhebung einer PM Platzierung auf Startseite und Branchen-/Kategorieseite
- **Gate:** nur **grüne** PMs sind boostbar; gelb/rot nicht - **Gate:** nur **grüne** PMs sind boostbar; gelb/rot nicht
- **Mechanik:** nachträglich kaufbar, sobald die PM live und grün ist - **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) - **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 ### 2.2 Veröffentlichungsnachweis / PDF
- **Was:** generiertes PDF „PM XY wurde am … auf … veröffentlicht" inkl. URL, Datum, Vorschau - **Was:** generiertes PDF „PM XY wurde am … auf … veröffentlicht" inkl. URL, Datum, Vorschau
- **Zweck:** Reporting an Vorgesetzte/Kunden klassische PR-Mitnahme - **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 - **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). Kein wahlloses Ändern/Löschen Friction nach Anliegen. Alle Pfade laufen über dieselbe Verwaltung (Login **oder** Magic-Link).
|Pfad|Anliegen|Kosten|Phase|Public Hint| |Pfad|Anliegen|KI / Admin|Kosten|Bremse|Phase|Public Hint|
|---|---|---|---|---| |---|---|---|---|---|---|---|
|**A**|Tippfehler/Grammatik|kostenfrei|Launch*|nein| |**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|kostenfrei|Launch*|nein| |**B**|Pressekontakt-Daten ändern|keine|kostenfrei|normales Rate-Limit|**Launch**|nein|
|**C**|Inhaltliche Korrektur (sachlicher Fehler)|kostenpflichtig|**Phase 2**|ja| |**C**|Inhaltliche Korrektur (sachlicher Fehler)|KI|kostenpflichtig|Bezahlung = Bremse|**Phase 2**|ja|
|**D**|Update/Ergänzung (neue Information)|kostenpflichtig|**Phase 2**|ja| |**D**|Update/Ergänzung (neue Information)|KI|kostenpflichtig|Bezahlung = Bremse|**Phase 2**|ja|
|**E**|DSGVO-Anonymisierung|kostenfrei|**Launch (Pflicht)**|nein| |**E**|DSGVO-Anonymisierung|**Admin-Panel** (keine KI)|kostenfrei|1 offene Anfrage/PM gleichzeitig|**Launch (Pflicht)**|nein|
|**F**|Persönlichkeitsrechtsverletzung|kostenfrei|**Launch (Pflicht)**|je nach Outcome| |**F**|Persönlichkeitsrechtsverletzung|**Admin-Panel** (keine KI)|kostenfrei|1 offene Anfrage/PM gleichzeitig|**Launch (Pflicht)**|je nach Outcome|
|**G**|Depublizieren|kostenpflichtig + Bedenkzeit|**Phase 2**|Tombstone| |**G**|Depublizieren|KI|25 Credits + Bedenkzeit|Bezahlung + 2448 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):** **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 - **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 - **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 - **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 - **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) → Review-Queue mit KI-Vorklassifikation; Outcomes: Anonymisierung, Anpassung, Tombstone, Ablehnung mit Begründung - **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 → 2448 h Bedenkzeit mit Widerrufslink → Tombstone (`noindex`, raus aus Listen/Suche, URL bleibt) - **G Depublizieren:** Aufklärungsseite → Begründungspflicht (KI lenkt „veraltet" → D, „falsch/peinlich" → C) → kostenpflichtige Bestätigung → 2448 h Bedenkzeit mit Widerrufslink → Tombstone (`noindex`, raus aus Listen/Suche, URL bleibt)
### 3.3 Compliance-Minimum zum Launch (E & F) ### 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. **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 ### 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 - **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 - **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 - **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| |Pfad|Aktion|Anker|
|---|---|---| |---|---|---|
|C|Inhaltliche Korrektur|≈ 8 Credits| |C|Inhaltliche Korrektur|≈ 8 Credits (zu bestätigen)|
|D|Update/Ergänzung|≈ 4 Credits| |D|Update/Ergänzung|≈ 4 Credits (zu bestätigen)|
|G|Depublizieren|≈ 1929 Credits + 2448 h Bedenkzeit| |G|Depublizieren|**25 Credits** + 2448 h Bedenkzeit (festgelegt)|
Depublizieren bewusst am teuersten und mit Bedenkzeit, weil irreversibelste Aktion. 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) ## 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 - ✅ 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 - ✅ Eine Verwaltung, zwei Eintrittswege keine künstliche Trennung registrierter/unregistrierter Nutzer
- ✅ Prüf-Kontingent großzügig genug, dass der Normalfall nie ansteht - ✅ 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 - ✅ 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 ## 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** final bestätigen, sobald das Credit-/Prüf-System gebaut wird (aktuell ≈ 8 / ≈ 4 Credits).
- **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. **In Rev. 3 abgeschlossen:** A/B-Launch ✓ · Boost-Staffel 12/20/35 ✓ · PDF 3 Credits ✓ · G Depublizieren 25 Credits ✓
- **PDF-Preis** in Credits festlegen.

View file

@ -204,6 +204,19 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
<flux:icon.photo variant="micro" class="size-3.5" /> <flux:icon.photo variant="micro" class="size-3.5" />
<span>{{ __('Platzhalter-Titelbild (kein eigenes Bild hochgeladen).') }}</span> <span>{{ __('Platzhalter-Titelbild (kein eigenes Bild hochgeladen).') }}</span>
</div> </div>
@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) --}}
<div class="flex flex-wrap items-center gap-2 border-t border-[color:var(--color-bg-rule)] px-5 py-2.5 text-[12px] text-[color:var(--color-ink-3)]">
@if ($adminTitleImage->is_ai_generated)
<span class="badge hub">{{ __('KI-generiert') }}</span>
@endif
<span>{{ $adminTitleImage->copyright ?? __('Bild: KI-generiert') }}</span>
</div>
@endif
@endif @endif
</article> </article>

View file

@ -47,6 +47,8 @@ new class extends Component {
public bool $newRightsConfirmed = false; public bool $newRightsConfirmed = false;
public bool $newAiTermsConfirmed = false;
public bool $isUploadFormOpen = false; public bool $isUploadFormOpen = false;
public function mount(int $pressReleaseId): void public function mount(int $pressReleaseId): void
@ -62,12 +64,40 @@ new class extends Component {
/** /**
* Beim Wechsel des Lizenztyps das Detail-Feld leeren sonst klebt * Beim Wechsel des Lizenztyps das Detail-Feld leeren sonst klebt
* z. B. der zuvor gewählte CC-Wert (cc_by) im Freitextfeld * 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 public function updatedNewLicenseType(): void
{ {
$this->newLicenseDetail = ''; $this->newLicenseDetail = '';
$this->newAiTermsConfirmed = false;
$this->resetErrorBag('newLicenseDetail'); $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 public function closeUploadForm(): void
@ -98,6 +128,7 @@ new class extends Component {
$licenseType = ImageLicenseType::tryFrom($this->newLicenseType); $licenseType = ImageLicenseType::tryFrom($this->newLicenseType);
$requiresLicenseUrl = $licenseType?->requiresLicenseUrl() ?? false; $requiresLicenseUrl = $licenseType?->requiresLicenseUrl() ?? false;
$requiresLicenseDetail = $licenseType?->requiresLicenseDetail() ?? false; $requiresLicenseDetail = $licenseType?->requiresLicenseDetail() ?? false;
$isAiGenerated = $licenseType?->isAiGenerated() ?? false;
$this->validate( $this->validate(
[ [
@ -113,16 +144,22 @@ new class extends Component {
'newPropertyRightsStatus' => ['required', Rule::in(array_keys($this->propertyRightsOptions()))], 'newPropertyRightsStatus' => ['required', Rule::in(array_keys($this->propertyRightsOptions()))],
'newRightsNotes' => ['nullable', 'string', 'max:1000'], 'newRightsNotes' => ['nullable', 'string', 'max:1000'],
'newRightsConfirmed' => ['accepted'], 'newRightsConfirmed' => ['accepted'],
'newAiTermsConfirmed' => [$isAiGenerated ? 'accepted' : 'nullable'],
], ],
[ [
'newCopyright.required' => __('Bitte einen öffentlichen Bildnachweis angeben, z. B. Foto: Max Mustermann / Beispiel GmbH.'), '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.'), '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.'), 'newLicenseUrl.required' => __('Für diesen Lizenztyp ist eine Nachweis-URL erforderlich.'),
'newPeopleRightsStatus.required' => __('Bitte angeben, ob erkennbare Personen abgebildet sind.'), 'newPeopleRightsStatus.required' => __('Bitte angeben, ob erkennbare Personen abgebildet sind.'),
'newPropertyRightsStatus.required' => __('Bitte angeben, ob Marken, Kunstwerke oder private Orte sichtbar sind.'), 'newPropertyRightsStatus.required' => __('Bitte angeben, ob Marken, Kunstwerke oder private Orte sichtbar sind.'),
'newRightsConfirmed.accepted' => __('Bitte bestätigen, dass die Bildrechte geklärt 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, 'property_rights_status' => $this->newPropertyRightsStatus,
'rights_notes' => $this->newRightsNotes ?: null, 'rights_notes' => $this->newRightsNotes ?: null,
'rights_confirmed_at' => now(), 'rights_confirmed_at' => now(),
'is_ai_generated' => $isAiGenerated,
'is_preview' => true, 'is_preview' => true,
'sort_order' => ((int) $pressRelease->images()->max('sort_order')) + 1, 'sort_order' => ((int) $pressRelease->images()->max('sort_order')) + 1,
'width' => $stored['width'], 'width' => $stored['width'],
@ -217,6 +255,7 @@ new class extends Component {
'licenseUrlRequired' => ImageLicenseType::tryFrom($this->newLicenseType)?->requiresLicenseUrl() ?? false, 'licenseUrlRequired' => ImageLicenseType::tryFrom($this->newLicenseType)?->requiresLicenseUrl() ?? false,
'licenseDetailRequired' => ImageLicenseType::tryFrom($this->newLicenseType)?->requiresLicenseDetail() ?? false, 'licenseDetailRequired' => ImageLicenseType::tryFrom($this->newLicenseType)?->requiresLicenseDetail() ?? false,
'showsCcWarning' => $this->newLicenseType === ImageLicenseType::CreativeCommons->value, 'showsCcWarning' => $this->newLicenseType === ImageLicenseType::CreativeCommons->value,
'showsAiSection' => $this->newLicenseType === ImageLicenseType::AiGenerated->value,
'showsRightsWarning' => $this->shouldShowRightsWarning(), 'showsRightsWarning' => $this->shouldShowRightsWarning(),
]; ];
} }
@ -242,7 +281,7 @@ new class extends Component {
private function resetUploadForm(): void 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 {
</p> </p>
@endif @endif
<div class="flex flex-wrap items-center gap-1 text-xs text-zinc-400"> <div class="flex flex-wrap items-center gap-1 text-xs text-zinc-400">
@if ($titleImage->is_ai_generated)
<flux:badge color="purple" size="xs">{{ __('KI-generiert') }}</flux:badge>
@endif
@if ($titleImage->license_type) @if ($titleImage->license_type)
<flux:badge color="zinc" size="xs">{{ $titleImage->license_type->label() }} <flux:badge color="zinc" size="xs">{{ $titleImage->license_type->label() }}
</flux:badge> </flux:badge>
@ -449,7 +491,8 @@ new class extends Component {
class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Herkunft & Lizenz') }}</span> class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Herkunft & Lizenz') }}</span>
</div> </div>
<div class="grid gap-3 sm:grid-cols-2"> <div class="grid gap-3 sm:grid-cols-2">
<flux:input wire:model="newAuthor" :label="__('Urheber / Fotograf / Rechteinhaber')" <flux:input wire:model="newAuthor"
:label="$showsAiSection ? __('Verantwortlich für die Erstellung (Person/Firma)') : __('Urheber / Fotograf / Rechteinhaber')"
:badge="__('Pflicht')" required /> :badge="__('Pflicht')" required />
<flux:select wire:model.live="newLicenseType" :label="__('Lizenztyp')" :badge="__('Pflicht')" <flux:select wire:model.live="newLicenseType" :label="__('Lizenztyp')" :badge="__('Pflicht')"
required> required>
@ -460,7 +503,20 @@ new class extends Component {
</flux:select> </flux:select>
</div> </div>
@if ($newLicenseType === \App\Enums\ImageLicenseType::CreativeCommons->value) @if ($showsAiSection)
<flux:input wire:model.live.debounce.400ms="newLicenseDetail" :label="__('Verwendetes KI-Tool')"
:badge="__('Pflicht')" placeholder="{{ __('z. B. Midjourney v7, DALL·E 3, Adobe Firefly') }}"
required />
<div
class="rounded-md border border-amber-200 bg-amber-50 p-3 text-xs leading-relaxed text-amber-900 dark:border-amber-500/30 dark:bg-amber-500/10 dark:text-amber-100">
{{ __('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.') }}
</div>
<flux:switch wire:model="newAiTermsConfirmed" align="right"
:label="__('Anbieter-Bedingungen geprüft')"
:description="__('Ich bestätige, dass die Nutzungsbedingungen des KI-Anbieters die kommerzielle Nutzung und Veröffentlichung dieses Bildes erlauben.')" />
@elseif ($newLicenseType === \App\Enums\ImageLicenseType::CreativeCommons->value)
<flux:select wire:model.live="newLicenseDetail" :label="__('Creative-Commons-Lizenz')" <flux:select wire:model.live="newLicenseDetail" :label="__('Creative-Commons-Lizenz')"
required> required>
<flux:select.option value="" disabled>{{ __('Bitte wählen…') }}</flux:select.option> <flux:select.option value="" disabled>{{ __('Bitte wählen…') }}</flux:select.option>

View file

@ -104,6 +104,7 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
'categoryName' => $categoryName, 'categoryName' => $categoryName,
'coverUrl' => $cover->coverUrl($pr, 'cover'), 'coverUrl' => $cover->coverUrl($pr, 'cover'),
'coverIsPlaceholder' => $cover->coverIsPlaceholder($pr), 'coverIsPlaceholder' => $cover->coverIsPlaceholder($pr),
'titleImage' => $pr->images()->orderByDesc('is_preview')->orderBy('sort_order')->orderBy('id')->first(),
'quotaTotal' => $user->pressReleaseQuotaTotal(), 'quotaTotal' => $user->pressReleaseQuotaTotal(),
'quotaRemaining' => $user->pressReleaseQuotaRemaining(), 'quotaRemaining' => $user->pressReleaseQuotaRemaining(),
'canEdit' => auth()->user()->can('update', $pr) 'canEdit' => auth()->user()->can('update', $pr)
@ -505,6 +506,14 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
<flux:icon.photo variant="micro" class="size-3.5" /> <flux:icon.photo variant="micro" class="size-3.5" />
<span>{{ __('Platzhalter-Titelbild — laden Sie im Editor ein eigenes Bild hoch.') }}</span> <span>{{ __('Platzhalter-Titelbild — laden Sie im Editor ein eigenes Bild hoch.') }}</span>
</div> </div>
@elseif ($titleImage && ($titleImage->copyright || $titleImage->is_ai_generated))
{{-- Bildnachweis + KI-Kennzeichnung (Art. 50 EU AI Act) --}}
<div class="flex flex-wrap items-center gap-2 border-t border-[color:var(--color-bg-rule)] px-5 py-2.5 text-[12px] text-[color:var(--color-ink-3)]">
@if ($titleImage->is_ai_generated)
<span class="badge hub">{{ __('KI-generiert') }}</span>
@endif
<span>{{ $titleImage->copyright ?? __('Bild: KI-generiert') }}</span>
</div>
@endif @endif
</article> </article>

View file

@ -272,6 +272,63 @@ test('switching the license type clears the stale license detail', function () {
->assertSet('newLicenseDetail', ''); ->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 () { test('existing title image hides upload form and can be removed', function () {
/** @var TestCase $this */ /** @var TestCase $this */
['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner(); ['owner' => $owner, 'pr' => $pr] = makeImageDraftOwner();