images: KI-Vorab-Check für Titelbilder — Vision-Sichtung mit definiertem Ausgang je Befund, Admin-Queue, Protokoll und Archiv-Snapshot (Bildkennzeichnung Block K)
Decision-Update Abschnitt 12 (E15, S24): Sichtung auf offensichtliche Probleme, keine Rechteprüfung, öffentlich weiterhin „automatisierte Prüfung". - ImagePrecheckManager mit openai- und deterministic-Treiber (Muster der Text-Prüfstrecke), Bild auf 1024 px, Metadaten gestrippt, JPEG als Data-URI, Antwortschema 12.4; Fehler → Ausgang `error`, durchlassen mit Vermerk. - Auswertung 12.3: reject (Stock-Wasserzeichen, Nacktheit/Gewalt, Dokument ab 0,85), queue (Screenshot, fremdes Logo, Kennzeichen ab 0,6 sowie Ablehnungskategorien 0,6–0,85), hint (Personen, „wirkt KI" nur bei „ohne KI", technischer Mangel). Stufe 1 wertet nur das Wasserzeichen; die KI setzt nie die Kennzeichnung (E9). Herabstufung per Config (12.8). - Uploader: Check vor jedem Schreiben, Ablehnung speichert nichts und landet als ki_audits `image_precheck` an der PM; Hash-Bindung zwischen Vorprüfung im Formular und Speichern; Spalten precheck*, Snapshot `precheck_snapshot` im Bestätigungsprotokoll, Archiv-Payload erweitert. - Queue: Bild zurückgehalten (withheld_at), Meldung läuft weiter; Admin entscheidet mit Vermerk (Freigabe/Ablehnung → ki_audits `image_precheck_review`); Ansicht „Bild-Sichtung" in der PM-Liste, Sichtungsblock auf der PM-Seite, Systemstatus-Zähler. - Formular v2: Ablehnungsgrund unter dem Dateifeld, Hinweise nach der Dateiauswahl, „wird kurz gesichtet" an der Karte; API 422 mit Grund, Warnung `image_under_review`. - Config images.precheck (IMAGE_PRECHECK aus, STAGE 1); 17 Tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
3b613fadf4
commit
0e5cd20626
26 changed files with 1738 additions and 10 deletions
|
|
@ -8,6 +8,8 @@ use App\Models\PressReleaseImage;
|
|||
use App\Scopes\PortalScope;
|
||||
use App\Services\Image\ImageRightsTextRenderer;
|
||||
use App\Services\Image\ImageService;
|
||||
use App\Services\Image\Precheck\ImagePrecheckResult;
|
||||
use App\Services\Image\Precheck\ImagePrecheckService;
|
||||
use App\Services\PressRelease\Images\CustomerImageConfirmation;
|
||||
use App\Services\PressRelease\Images\ImageSubmission;
|
||||
use App\Services\PressRelease\Images\ImageUploadActor;
|
||||
|
|
@ -79,6 +81,10 @@ new class extends Component {
|
|||
#[Locked]
|
||||
public bool $enhancementHint = false;
|
||||
|
||||
/** Ergebnis des KI-Vorab-Checks (Block K) mit Hash-Bindung; null = Check aus. */
|
||||
#[Locked]
|
||||
public ?array $precheck = null;
|
||||
|
||||
public function mount(int $pressReleaseId): void
|
||||
{
|
||||
$this->pressReleaseId = $pressReleaseId;
|
||||
|
|
@ -128,6 +134,18 @@ new class extends Component {
|
|||
$this->aiSuggestion = $result->suggestedAiDeclaration()?->value;
|
||||
$this->enhancementHint = (bool) $result->derived['enhancement_hint'];
|
||||
|
||||
// Vorab-Check (K.3): Ablehnung zeigt den Grund unter dem Dateifeld,
|
||||
// alles andere bleibt als Hinweis oder wird beim Speichern übernommen.
|
||||
try {
|
||||
$this->precheck = $uploader->precheck($this->newImage, $this->getPressRelease())?->toArray();
|
||||
} catch (ImageUploadException $exception) {
|
||||
$this->addError('newImage', $exception->getMessage());
|
||||
$this->resetProvenance();
|
||||
$this->reset('newImage');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Nachweis vorbelegen: aus den Metadaten, sonst Firmenname — nur,
|
||||
// solange der Nutzer nichts Eigenes eingetragen hat.
|
||||
$suggested = $this->creditSuggestion ?? $this->companyName();
|
||||
|
|
@ -239,6 +257,7 @@ new class extends Component {
|
|||
$actor,
|
||||
expectedSha256: $this->provenanceSha,
|
||||
agbUrl: $this->agbUrl(),
|
||||
precheck: $this->precheck !== null ? ImagePrecheckResult::fromArray($this->precheck) : null,
|
||||
);
|
||||
} catch (ImageUploadException $exception) {
|
||||
$this->addError($exception->field === 'rights_confirmed' ? 'newRightsConfirmed' : 'newImage', $exception->getMessage());
|
||||
|
|
@ -318,6 +337,8 @@ new class extends Component {
|
|||
'titleImage' => $titleImage,
|
||||
'latestConfirmation' => $titleImage?->latestRightsConfirmation,
|
||||
'awaitsCustomerConfirmation' => $titleImage?->awaitsCustomerConfirmation() ?? false,
|
||||
'awaitsPrecheckReview' => $titleImage?->awaitsPrecheckReview() ?? false,
|
||||
'precheckHints' => $this->precheckHints(),
|
||||
'canEdit' => auth()->user()?->can('update', $pressRelease) === true && $this->canChangeImages($pressRelease),
|
||||
'isAdminActor' => $this->actsAsAdmin(),
|
||||
'companyName' => $this->companyName(),
|
||||
|
|
@ -443,8 +464,32 @@ new class extends Component {
|
|||
return $pressRelease->images()->with('latestRightsConfirmation')->orderByDesc('is_preview')->orderBy('sort_order')->orderBy('id')->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Hinweise aus dem Vorab-Check (DU 12.3), abhängig von der aktuellen
|
||||
* KI-Angabe: „wirkt KI-generiert" erscheint nur bei „ohne KI".
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function precheckHints(): array
|
||||
{
|
||||
if ($this->precheck === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = ImagePrecheckResult::fromArray($this->precheck);
|
||||
|
||||
if ($result === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$choice = $this->newAiDeclaration !== '' ? AiImageDeclaration::tryFrom($this->newAiDeclaration) : null;
|
||||
|
||||
return app(ImagePrecheckService::class)->evaluate($result, $choice)->hints();
|
||||
}
|
||||
|
||||
private function resetProvenance(): void
|
||||
{
|
||||
$this->precheck = null;
|
||||
$this->provenanceStatus = null;
|
||||
$this->provenanceSha = null;
|
||||
$this->creditSuggestion = null;
|
||||
|
|
@ -462,8 +507,8 @@ new class extends Component {
|
|||
<flux:card>
|
||||
<div class="flex items-center justify-between">
|
||||
<flux:heading size="md">{{ __('Titelbild') }}</flux:heading>
|
||||
<flux:badge color="{{ $titleImage ? ($awaitsCustomerConfirmation ? 'amber' : 'green') : 'zinc' }}" size="sm">
|
||||
{{ $titleImage ? ($awaitsCustomerConfirmation ? __('wartet auf Kundenbestätigung') : __('gesetzt')) : __('Platzhalter aktiv') }}
|
||||
<flux:badge color="{{ $titleImage ? (($awaitsCustomerConfirmation || $awaitsPrecheckReview) ? 'amber' : 'green') : 'zinc' }}" size="sm">
|
||||
{{ $titleImage ? ($awaitsCustomerConfirmation ? __('wartet auf Kundenbestätigung') : ($awaitsPrecheckReview ? __('wird kurz gesichtet') : __('gesetzt'))) : __('Platzhalter aktiv') }}
|
||||
</flux:badge>
|
||||
</div>
|
||||
|
||||
|
|
@ -530,6 +575,12 @@ new class extends Component {
|
|||
@endif
|
||||
</div>
|
||||
|
||||
@if ($awaitsPrecheckReview)
|
||||
<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" data-precheck-review>
|
||||
{{ __('Wird kurz gesichtet. Ihre Meldung läuft normal weiter; das Bild erscheint, sobald die Sichtung abgeschlossen ist.') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if ($awaitsCustomerConfirmation)
|
||||
<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">
|
||||
{{ __('Dieses Bild wurde im Auftrag hochgeladen. Es wird erst veröffentlicht, wenn der Kontoinhaber die Bildrechte bestätigt hat.') }}
|
||||
|
|
@ -624,6 +675,12 @@ new class extends Component {
|
|||
</div>
|
||||
@endif
|
||||
|
||||
@foreach ($precheckHints as $precheckHint)
|
||||
<div class="rounded-md border border-sky-200 bg-sky-50 p-3 text-xs leading-relaxed text-sky-900 dark:border-sky-500/30 dark:bg-sky-500/10 dark:text-sky-100" data-precheck-hint>
|
||||
{{ $precheckHint }}
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-2">
|
||||
<flux:select wire:model.live="focus" :label="__('Bildausschnitt: wichtiger Bereich')"
|
||||
:description="__('Der Zuschnitt für Vorschauen hält diesen Bereich im Bild.')">
|
||||
|
|
@ -652,7 +709,7 @@ new class extends Component {
|
|||
: __('Vorschlag: Ihr Firmenname — bitte prüfen. Bei Stock- oder Agenturbildern den Nachweis so eintragen, wie die Lizenz ihn verlangt.')"
|
||||
:placeholder="$companyName ?: __('z. B. Max Mustermann / Beispiel GmbH')" required />
|
||||
|
||||
<flux:radio.group wire:model="newAiDeclaration" :label="__('KI-Kennzeichnung')" :badge="__('Pflicht')"
|
||||
<flux:radio.group wire:model.live="newAiDeclaration" :label="__('KI-Kennzeichnung')" :badge="__('Pflicht')"
|
||||
:description="__('KI-generierte oder KI-bearbeitete Bilder erhalten öffentlich einen neutralen Hinweis. Übliche Bearbeitungen wie Zuschnitt, Farbkorrektur oder Schärfung gelten nicht als KI-Bearbeitung.')"
|
||||
required>
|
||||
@foreach ($aiOptions as $aiImageOption)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue