enabled()) { return null; } $provider = (string) config('images.precheck.provider', 'deterministic'); try { return $this->manager->driver($provider)->check($absolutePath)->withFileSha256($fileSha256); } catch (Throwable $exception) { Log::channel('ki')->warning('KI-Vorab-Check für ein Bild fehlgeschlagen; das Bild wird durchgelassen.', [ 'provider' => $provider, 'error' => $exception->getMessage(), ]); return ImagePrecheckResult::error( mb_substr($exception->getMessage(), 0, 200), $provider, config('images.precheck.model') ?: null, $fileSha256, ); } } /** * Vorhandenes Ergebnis wiederverwenden, wenn es zur Datei passt * (Hash-Bindung zwischen Vorprüfung und Speichern), sonst neu rechnen. */ public function resolve(string $absolutePath, string $fileSha256, ?ImagePrecheckResult $existing): ?ImagePrecheckResult { if (! $this->enabled()) { return null; } if ($existing !== null && $existing->fileSha256 !== null && hash_equals($existing->fileSha256, $fileSha256)) { return $existing; } return $this->run($absolutePath, $fileSha256); } /** * Ausgang nach DU 12.3. Stufe 1 wertet nur `stock_watermark`; die * Kundenangabe entscheidet nur darüber, ob „wirkt KI-generiert" als * Hinweis erscheint. */ public function evaluate(ImagePrecheckResult $result, ?AiImageDeclaration $userChoice): ImagePrecheckEvaluation { $stage = $this->stage(); if ($result->isError()) { return new ImagePrecheckEvaluation(ImagePrecheckOutcome::Error, [], $stage); } $reject = (float) config('images.precheck.reject_threshold', 0.85); $queue = (float) config('images.precheck.queue_threshold', 0.6); $downgraded = (array) config('images.precheck.downgrade_to_queue', []); $triggers = []; foreach (ImagePrecheckCategory::cases() as $category) { if (! $category->activeInStage($stage)) { continue; } $confidence = $result->confidence($category); $default = $category->defaultOutcome(); if ($default === ImagePrecheckOutcome::Reject && in_array($category->value, $downgraded, true)) { $default = ImagePrecheckOutcome::Queue; } $outcome = match ($default) { ImagePrecheckOutcome::Reject => $confidence >= $reject ? ImagePrecheckOutcome::Reject : ($confidence >= $queue ? ImagePrecheckOutcome::Queue : null), ImagePrecheckOutcome::Queue => $confidence >= $queue ? ImagePrecheckOutcome::Queue : null, ImagePrecheckOutcome::Hint => $this->hintApplies($category, $confidence, $queue, $userChoice) ? ImagePrecheckOutcome::Hint : null, default => null, }; if ($outcome === null) { continue; } $triggers[] = [ 'category' => $category, 'confidence' => $confidence, 'outcome' => $outcome, 'note' => $category === ImagePrecheckCategory::TechnicalDefect ? $result->note($category) : null, ]; } $overall = ImagePrecheckOutcome::Pass; foreach ($triggers as $trigger) { if ($trigger['outcome']->rank() > $overall->rank()) { $overall = $trigger['outcome']; } } return new ImagePrecheckEvaluation($overall, $triggers, $stage); } /** * Ablehnung vor dem Speichern (DU 12.3): kein Bild, aber ein Eintrag im * unveränderlichen KI-Protokoll an der Meldung — der Beleg eigener * Sorgfalt, ohne die Datei. */ public function auditRejected(PressRelease $pressRelease, ImagePrecheckResult $result, ImagePrecheckEvaluation $evaluation): KiAudit { return $this->audit($pressRelease->getKey(), null, $result, $evaluation); } public function auditStored(PressReleaseImage $image, ImagePrecheckResult $result, ImagePrecheckEvaluation $evaluation): KiAudit { return $this->audit((int) $image->press_release_id, (int) $image->getKey(), $result, $evaluation); } /** * Wert für die Spalte `precheck` am Bild und den Snapshot im * Bestätigungsprotokoll: Rohbefund plus Auswertung. * * @return array */ public function storedPayload(ImagePrecheckResult $result, ImagePrecheckEvaluation $evaluation): array { return $result->toArray() + ['evaluation' => $evaluation->toArray(), 'prompt_version' => $result->raw['prompt_version'] ?? config('images.precheck.prompt_version')]; } private function hintApplies(ImagePrecheckCategory $category, float $confidence, float $queueThreshold, ?AiImageDeclaration $userChoice): bool { if ($confidence < $queueThreshold) { return false; } if ($category === ImagePrecheckCategory::LooksAiGenerated) { return $userChoice === null || $userChoice === AiImageDeclaration::Original; } return true; } private function audit(int $pressReleaseId, ?int $imageId, ImagePrecheckResult $result, ImagePrecheckEvaluation $evaluation): KiAudit { return KiAudit::query()->create([ 'press_release_id' => $pressReleaseId, 'type' => KiAudit::TYPE_IMAGE_PRECHECK, 'provider' => $result->provider, 'model' => $result->model, 'result' => $evaluation->outcome->value, 'reason' => $evaluation->summary(), 'raw_response' => $this->storedPayload($result, $evaluation) + ['image_id' => $imageId], 'created_at' => now(), ]); } }