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:
Kevin Adametz 2026-09-22 13:27:31 +00:00
parent 70e15d5df5
commit 8bb7049bdc
26 changed files with 1738 additions and 10 deletions

View file

@ -6,6 +6,7 @@ use App\Enums\AiDeclarationSource;
use App\Enums\AiImageDeclaration;
use App\Enums\CreditSource;
use App\Enums\ImageLicenseType;
use App\Enums\ImagePrecheckOutcome;
use App\Enums\ImageProvenanceStatus;
use Database\Factories\PressReleaseImageFactory;
use Illuminate\Database\Eloquent\Builder;
@ -54,6 +55,12 @@ use Illuminate\Support\Facades\URL;
* @property array<string, mixed>|null $provenance
* @property Carbon|null $provenance_checked_at
* @property string|null $provenance_tool_version
* @property array<string, mixed>|null $precheck
* @property ImagePrecheckOutcome|null $precheck_outcome
* @property Carbon|null $precheck_review_required_at
* @property Carbon|null $precheck_reviewed_at
* @property int|null $precheck_reviewed_by
* @property string|null $precheck_review_note
* @property string|null $original_disk
* @property string|null $original_path
* @property string|null $original_sha256
@ -111,6 +118,12 @@ class PressReleaseImage extends Model
'provenance',
'provenance_checked_at',
'provenance_tool_version',
'precheck',
'precheck_outcome',
'precheck_review_required_at',
'precheck_reviewed_at',
'precheck_reviewed_by',
'precheck_review_note',
'original_disk',
'original_path',
'original_sha256',
@ -148,6 +161,10 @@ class PressReleaseImage extends Model
'provenance_status' => ImageProvenanceStatus::class,
'provenance' => 'array',
'provenance_checked_at' => 'datetime',
'precheck' => 'array',
'precheck_outcome' => ImagePrecheckOutcome::class,
'precheck_review_required_at' => 'datetime',
'precheck_reviewed_at' => 'datetime',
'published_ever_at' => 'datetime',
'customer_confirmation_requested_at' => 'datetime',
'withheld_at' => 'datetime',
@ -228,6 +245,28 @@ class PressReleaseImage extends Model
* Vorläufig zurückgehalten (Korrekturweg E.6): öffentlich greift der
* Platzhalter, im Panel bleibt das Bild sichtbar.
*/
/**
* Ausgang `queue` des Vorab-Checks (Block K): Bild ist zurückgehalten,
* bis ein Admin freigibt oder ablehnt.
*/
public function awaitsPrecheckReview(): bool
{
return $this->precheck_review_required_at !== null && $this->precheck_reviewed_at === null;
}
public function scopeAwaitingPrecheckReview(Builder $query): Builder
{
return $query->whereNotNull('precheck_review_required_at')->whereNull('precheck_reviewed_at');
}
/**
* @return BelongsTo<User, $this>
*/
public function precheckReviewer(): BelongsTo
{
return $this->belongsTo(User::class, 'precheck_reviewed_by');
}
public function isWithheld(): bool
{
return $this->withheld_at !== null;