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

@ -204,6 +204,19 @@ new #[Layout('components.layouts.app'), Title('Pressemitteilung')] class extends
<flux:icon.photo variant="micro" class="size-3.5" />
<span>{{ __('Platzhalter-Titelbild (kein eigenes Bild hochgeladen).') }}</span>
</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
</article>

View file

@ -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 {
</p>
@endif
<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)
<flux:badge color="zinc" size="xs">{{ $titleImage->license_type->label() }}
</flux:badge>
@ -449,7 +491,8 @@ new class extends Component {
class="text-[11px] font-semibold uppercase tracking-wider text-zinc-500">{{ __('Herkunft & Lizenz') }}</span>
</div>
<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 />
<flux:select wire:model.live="newLicenseType" :label="__('Lizenztyp')" :badge="__('Pflicht')"
required>
@ -460,7 +503,20 @@ new class extends Component {
</flux:select>
</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')"
required>
<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,
'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
<flux:icon.photo variant="micro" class="size-3.5" />
<span>{{ __('Platzhalter-Titelbild — laden Sie im Editor ein eigenes Bild hoch.') }}</span>
</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
</article>