Save workspace changes
This commit is contained in:
@@ -0,0 +1,562 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Maturity;
|
||||
|
||||
use App\Models\Artwork;
|
||||
use App\Models\User;
|
||||
use App\Services\ArtworkSearchIndexer;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
final class ArtworkMaturityService
|
||||
{
|
||||
public const LEVEL_SAFE = 'safe';
|
||||
public const LEVEL_MATURE = 'mature';
|
||||
|
||||
public const SOURCE_AI = 'ai';
|
||||
public const SOURCE_LEGACY = 'legacy';
|
||||
public const SOURCE_MODERATOR = 'moderator';
|
||||
public const SOURCE_USER = 'user';
|
||||
|
||||
public const STATUS_CLEAR = 'clear';
|
||||
public const STATUS_DECLARED = 'declared';
|
||||
public const STATUS_REVIEWED = 'reviewed';
|
||||
public const STATUS_SUSPECTED = 'suspected';
|
||||
|
||||
public const AI_ACTION_SAFE = 'safe';
|
||||
public const AI_ACTION_ALLOW = self::AI_ACTION_SAFE;
|
||||
public const AI_ACTION_REVIEW = 'review';
|
||||
public const AI_ACTION_FLAG_HIGH = 'flag_high';
|
||||
|
||||
public const AI_STATUS_FAILED = 'failed';
|
||||
public const AI_STATUS_NOT_REQUESTED = 'not_requested';
|
||||
public const AI_STATUS_PENDING = 'pending';
|
||||
public const AI_STATUS_SKIPPED = 'skipped';
|
||||
public const AI_STATUS_SUCCEEDED = 'succeeded';
|
||||
|
||||
public const VIEW_BLUR = 'blur';
|
||||
public const VIEW_HIDE = 'hide';
|
||||
public const VIEW_SHOW = 'show';
|
||||
|
||||
/**
|
||||
* @var array<int, array{visibility:string,warn_on_detail:bool,is_guest:bool}>
|
||||
*/
|
||||
private array $viewerPreferenceCache = [];
|
||||
|
||||
public function __construct(private readonly ArtworkSearchIndexer $searchIndexer)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{visibility:string,warn_on_detail:bool,is_guest:bool}
|
||||
*/
|
||||
public function viewerPreferences(?User $viewer): array
|
||||
{
|
||||
$defaultMode = $this->normalizeVisibilityPreference((string) config('maturity.viewer.default_mode', self::VIEW_BLUR));
|
||||
$defaultWarnOnDetail = (bool) config('maturity.viewer.default_warn_on_detail', true);
|
||||
|
||||
if (! $viewer) {
|
||||
return [
|
||||
'visibility' => $defaultMode,
|
||||
'warn_on_detail' => $defaultWarnOnDetail,
|
||||
'is_guest' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$viewerId = (int) $viewer->id;
|
||||
if (isset($this->viewerPreferenceCache[$viewerId])) {
|
||||
return $this->viewerPreferenceCache[$viewerId];
|
||||
}
|
||||
|
||||
$resolved = [
|
||||
'visibility' => $defaultMode,
|
||||
'warn_on_detail' => $defaultWarnOnDetail,
|
||||
'is_guest' => false,
|
||||
];
|
||||
|
||||
if (Schema::hasTable('user_profiles')) {
|
||||
$row = DB::table('user_profiles')
|
||||
->where('user_id', $viewerId)
|
||||
->first(['mature_content_visibility', 'mature_content_warning_enabled']);
|
||||
|
||||
if ($row !== null) {
|
||||
$resolved['visibility'] = $this->normalizeVisibilityPreference((string) ($row->mature_content_visibility ?? $defaultMode));
|
||||
$resolved['warn_on_detail'] = array_key_exists('mature_content_warning_enabled', (array) $row)
|
||||
? (bool) $row->mature_content_warning_enabled
|
||||
: $defaultWarnOnDetail;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->viewerPreferenceCache[$viewerId] = $resolved;
|
||||
}
|
||||
|
||||
public function applyViewerFilter(Builder $query, ?User $viewer): Builder
|
||||
{
|
||||
if ($this->viewerPreferences($viewer)['visibility'] !== self::VIEW_HIDE) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$table = $query->getModel()->getTable();
|
||||
|
||||
return $query
|
||||
->whereRaw('COALESCE(' . $table . '.is_mature, 0) = 0')
|
||||
->whereRaw("COALESCE(" . $table . ".maturity_status, '" . self::STATUS_CLEAR . "') != ?", [self::STATUS_SUSPECTED]);
|
||||
}
|
||||
|
||||
public function appendSearchFilter(string $filter, ?User $viewer): string
|
||||
{
|
||||
$filter = trim($filter);
|
||||
|
||||
if ($this->viewerPreferences($viewer)['visibility'] !== self::VIEW_HIDE) {
|
||||
return $filter;
|
||||
}
|
||||
|
||||
$hideClause = 'is_mature_effective = false';
|
||||
if ($filter === '') {
|
||||
return $hideClause;
|
||||
}
|
||||
|
||||
return $filter . ' AND ' . $hideClause;
|
||||
}
|
||||
|
||||
public function effectiveIsMature(mixed $artwork): bool
|
||||
{
|
||||
$level = Str::lower((string) $this->value($artwork, 'maturity_level', self::LEVEL_SAFE));
|
||||
$status = Str::lower((string) $this->value($artwork, 'maturity_status', self::STATUS_CLEAR));
|
||||
$isMature = (bool) $this->value($artwork, 'is_mature', false);
|
||||
|
||||
return $isMature || $level === self::LEVEL_MATURE || $status === self::STATUS_SUSPECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function presentation(mixed $artwork, ?User $viewer): array
|
||||
{
|
||||
$preferences = $this->viewerPreferences($viewer);
|
||||
$effectiveIsMature = $this->effectiveIsMature($artwork);
|
||||
$visibilityMode = $preferences['visibility'];
|
||||
$shouldHide = $effectiveIsMature && $visibilityMode === self::VIEW_HIDE;
|
||||
$shouldBlur = $effectiveIsMature && ! $shouldHide && $visibilityMode !== self::VIEW_SHOW;
|
||||
$requiresInterstitial = $effectiveIsMature && (bool) $preferences['warn_on_detail'];
|
||||
$status = Str::lower((string) $this->value($artwork, 'maturity_status', self::STATUS_CLEAR));
|
||||
$labels = $this->normalizeLabels($this->value($artwork, 'maturity_ai_labels', []));
|
||||
|
||||
return [
|
||||
'effective_level' => $effectiveIsMature ? self::LEVEL_MATURE : self::LEVEL_SAFE,
|
||||
'level' => Str::lower((string) $this->value($artwork, 'maturity_level', self::LEVEL_SAFE)),
|
||||
'source' => Str::lower((string) $this->value($artwork, 'maturity_source', self::SOURCE_LEGACY)),
|
||||
'status' => $status,
|
||||
'is_mature' => (bool) $this->value($artwork, 'is_mature', false),
|
||||
'is_mature_effective' => $effectiveIsMature,
|
||||
'ai_score' => $this->normalizeScore($this->value($artwork, 'maturity_ai_score')),
|
||||
'ai_confidence' => $this->normalizeScore($this->value($artwork, 'maturity_ai_confidence', $this->value($artwork, 'maturity_ai_score'))),
|
||||
'ai_label' => Str::lower((string) $this->value($artwork, 'maturity_ai_label', '')) ?: null,
|
||||
'ai_labels' => $labels,
|
||||
'ai_status' => Str::lower((string) $this->value($artwork, 'maturity_ai_status', self::AI_STATUS_NOT_REQUESTED)),
|
||||
'ai_action_hint' => Str::lower((string) $this->value($artwork, 'maturity_ai_action_hint', '')) ?: null,
|
||||
'ai_model' => $this->value($artwork, 'maturity_ai_model'),
|
||||
'ai_threshold_used' => $this->normalizeScore($this->value($artwork, 'maturity_ai_threshold_used')),
|
||||
'ai_analysis_time_ms' => is_numeric($this->value($artwork, 'maturity_ai_analysis_time_ms')) ? (int) $this->value($artwork, 'maturity_ai_analysis_time_ms') : null,
|
||||
'ai_advisory' => $this->value($artwork, 'maturity_ai_advisory'),
|
||||
'flag_reason' => $this->value($artwork, 'maturity_flag_reason'),
|
||||
'is_flagged' => $status === self::STATUS_SUSPECTED,
|
||||
'should_hide' => $shouldHide,
|
||||
'should_blur' => $shouldBlur,
|
||||
'requires_interstitial' => $requiresInterstitial,
|
||||
'viewer_preference' => $visibilityMode,
|
||||
'warning_title' => $effectiveIsMature ? 'Mature content warning' : null,
|
||||
'warning_message' => $effectiveIsMature
|
||||
? 'This artwork may contain mature material. Continue only if you want to view it.'
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function decoratePayload(array $payload, mixed $artwork, ?User $viewer): array
|
||||
{
|
||||
$payload['maturity'] = $this->presentation($artwork, $viewer);
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array<string, mixed>> $items
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public function filterPayloadItems(array $items, ?User $viewer): array
|
||||
{
|
||||
return array_values(array_filter($items, function (array $item) use ($viewer): bool {
|
||||
$maturity = Arr::get($item, 'maturity');
|
||||
if (! is_array($maturity)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ! (bool) ($maturity['should_hide'] ?? false);
|
||||
}));
|
||||
}
|
||||
|
||||
public function applyUploaderDeclaration(Artwork $artwork, bool $isMature): Artwork
|
||||
{
|
||||
$artwork->forceFill([
|
||||
'is_mature' => $isMature,
|
||||
'maturity_level' => $isMature ? self::LEVEL_MATURE : self::LEVEL_SAFE,
|
||||
'maturity_source' => self::SOURCE_USER,
|
||||
'maturity_status' => $isMature ? self::STATUS_DECLARED : self::STATUS_CLEAR,
|
||||
'maturity_declared_at' => now(),
|
||||
'maturity_flagged_at' => $isMature ? $artwork->maturity_flagged_at : null,
|
||||
'maturity_flag_reason' => $isMature ? $artwork->maturity_flag_reason : null,
|
||||
])->saveQuietly();
|
||||
|
||||
$this->searchIndexer->update($artwork);
|
||||
|
||||
return $artwork;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $analysis
|
||||
* @return array{score:float,labels:array<int,string>,flagged:bool}
|
||||
*/
|
||||
public function applyAiAssessment(Artwork $artwork, array $analysis): array
|
||||
{
|
||||
$assessment = $this->normalizeAiAssessment($analysis);
|
||||
$labels = $assessment['labels'];
|
||||
$aiStatus = $assessment['status'];
|
||||
$flagged = $this->shouldFlagAssessment($artwork, $assessment);
|
||||
$existingMismatchCount = (int) ($artwork->maturity_mismatch_count ?? 0);
|
||||
|
||||
$payload = [
|
||||
'maturity_ai_score' => $assessment['confidence'],
|
||||
'maturity_ai_labels' => $labels === [] ? null : $labels,
|
||||
'maturity_ai_label' => $assessment['maturity_label'],
|
||||
'maturity_ai_confidence' => $assessment['confidence'],
|
||||
'maturity_ai_model' => $assessment['model'],
|
||||
'maturity_ai_threshold_used' => $assessment['threshold_used'],
|
||||
'maturity_ai_analysis_time_ms' => $assessment['analysis_time_ms'],
|
||||
'maturity_ai_action_hint' => $assessment['action_hint'],
|
||||
'maturity_ai_advisory' => $assessment['advisory'],
|
||||
'maturity_ai_status' => $aiStatus,
|
||||
'maturity_ai_detected_at' => now(),
|
||||
];
|
||||
|
||||
if ($aiStatus !== self::AI_STATUS_SUCCEEDED) {
|
||||
$artwork->forceFill($payload)->saveQuietly();
|
||||
|
||||
$this->searchIndexer->update($artwork);
|
||||
|
||||
return $assessment;
|
||||
}
|
||||
|
||||
$artwork->forceFill(array_merge($payload, [
|
||||
'maturity_status' => $flagged ? self::STATUS_SUSPECTED : ($artwork->is_mature ? self::STATUS_DECLARED : ($artwork->maturity_status ?: self::STATUS_CLEAR)),
|
||||
'maturity_flagged_at' => $flagged ? now() : $artwork->maturity_flagged_at,
|
||||
'maturity_flag_reason' => $flagged
|
||||
? $this->buildAiFlagReason($assessment)
|
||||
: $artwork->maturity_flag_reason,
|
||||
'maturity_mismatch_count' => $flagged ? $existingMismatchCount + 1 : $existingMismatchCount,
|
||||
]))->saveQuietly();
|
||||
|
||||
$this->searchIndexer->update($artwork);
|
||||
|
||||
return $assessment;
|
||||
}
|
||||
|
||||
public function review(Artwork $artwork, string $action, Authenticatable $moderator, ?string $note = null): Artwork
|
||||
{
|
||||
$normalizedAction = Str::lower(trim($action));
|
||||
$isMature = $this->effectiveIsMature($artwork);
|
||||
$moderatorId = (int) $moderator->getAuthIdentifier();
|
||||
|
||||
if ($normalizedAction === 'mark_safe') {
|
||||
$isMature = false;
|
||||
}
|
||||
|
||||
if (in_array($normalizedAction, ['mark_mature', 'confirm'], true)) {
|
||||
$isMature = true;
|
||||
}
|
||||
|
||||
if ($normalizedAction === 'confirm_current') {
|
||||
$isMature = $this->effectiveIsMature($artwork);
|
||||
}
|
||||
|
||||
$artwork->forceFill([
|
||||
'is_mature' => $isMature,
|
||||
'maturity_level' => $isMature ? self::LEVEL_MATURE : self::LEVEL_SAFE,
|
||||
'maturity_source' => self::SOURCE_MODERATOR,
|
||||
'maturity_status' => self::STATUS_REVIEWED,
|
||||
'maturity_declared_at' => $isMature ? ($artwork->maturity_declared_at ?: now()) : $artwork->maturity_declared_at,
|
||||
'maturity_reviewed_by' => $moderatorId,
|
||||
'maturity_reviewed_at' => now(),
|
||||
'maturity_reviewer_note' => $note,
|
||||
'maturity_flag_reason' => $note ?: $artwork->maturity_flag_reason,
|
||||
])->saveQuietly();
|
||||
|
||||
$this->searchIndexer->update($artwork);
|
||||
|
||||
return $artwork->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $analysis
|
||||
* @return array{score:float,labels:array<int,string>,flagged:bool}
|
||||
*/
|
||||
public function assessAnalysis(array $analysis): array
|
||||
{
|
||||
$labels = [];
|
||||
$score = 0.0;
|
||||
$strong = collect((array) config('maturity.ai.strong_keywords', []))
|
||||
->map(static fn (mixed $keyword): string => Str::lower(trim((string) $keyword)))
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
$medium = collect((array) config('maturity.ai.medium_keywords', []))
|
||||
->map(static fn (mixed $keyword): string => Str::lower(trim((string) $keyword)))
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
$fragments = collect(array_merge(
|
||||
$this->analysisTextFragments($analysis['clip_tags'] ?? []),
|
||||
$this->analysisTextFragments($analysis['yolo_objects'] ?? []),
|
||||
[Str::lower(trim((string) ($analysis['blip_caption'] ?? '')))]
|
||||
))
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
foreach ($fragments as $fragment) {
|
||||
foreach ($strong as $keyword) {
|
||||
if (Str::contains($fragment, $keyword)) {
|
||||
$score += 0.42;
|
||||
$labels[] = $keyword;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($medium as $keyword) {
|
||||
if (Str::contains($fragment, $keyword)) {
|
||||
$score += 0.18;
|
||||
$labels[] = $keyword;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$labels = array_values(array_unique($labels));
|
||||
$score = min(1.0, round($score, 4));
|
||||
|
||||
return [
|
||||
'confidence' => $score,
|
||||
'score' => $score,
|
||||
'labels' => $labels,
|
||||
'flagged' => $score >= (float) config('maturity.ai.threshold', 0.68),
|
||||
'status' => self::AI_STATUS_SUCCEEDED,
|
||||
'maturity_label' => $score >= (float) config('maturity.ai.threshold', 0.68) ? self::LEVEL_MATURE : self::LEVEL_SAFE,
|
||||
'action_hint' => $score >= (float) config('maturity.ai.threshold', 0.68) ? self::AI_ACTION_REVIEW : self::AI_ACTION_SAFE,
|
||||
'model' => null,
|
||||
'threshold_used' => (float) config('maturity.ai.threshold', 0.68),
|
||||
'analysis_time_ms' => null,
|
||||
'advisory' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $analysis
|
||||
* @return array{status:string,maturity_label:?string,confidence:?float,labels:array<int,string>,action_hint:?string,model:?string,threshold_used:?float,analysis_time_ms:?int,advisory:?string,flagged:bool,score:?float}
|
||||
*/
|
||||
private function normalizeAiAssessment(array $analysis): array
|
||||
{
|
||||
if (! $this->looksLikeNormalizedAssessment($analysis)) {
|
||||
/** @var array{status:string,maturity_label:?string,confidence:?float,labels:array<int,string>,action_hint:?string,model:?string,threshold_used:?float,analysis_time_ms:?int,advisory:?string,flagged:bool,score:?float} $legacy */
|
||||
$legacy = $this->assessAnalysis($analysis);
|
||||
|
||||
return $legacy;
|
||||
}
|
||||
|
||||
$status = $this->normalizeAiStatus($analysis['status'] ?? null);
|
||||
$label = $this->normalizeAiLabel($analysis['maturity_label'] ?? ($analysis['label'] ?? null));
|
||||
$confidence = $this->normalizeScore($analysis['confidence'] ?? ($analysis['score'] ?? null));
|
||||
$labels = $this->normalizeLabels($analysis['labels'] ?? ($analysis['maturity_ai_labels'] ?? []));
|
||||
$actionHint = $this->normalizeAiActionHint($analysis['action_hint'] ?? null);
|
||||
$model = is_scalar($analysis['model'] ?? null) ? trim((string) $analysis['model']) : null;
|
||||
$thresholdUsed = $this->normalizeScore($analysis['threshold_used'] ?? null);
|
||||
$analysisTime = is_numeric($analysis['analysis_time_ms'] ?? null) ? (int) $analysis['analysis_time_ms'] : null;
|
||||
$advisory = is_scalar($analysis['advisory'] ?? null) ? trim((string) $analysis['advisory']) : null;
|
||||
|
||||
if ($labels === [] && $label !== null) {
|
||||
$labels[] = $label;
|
||||
}
|
||||
|
||||
$flagged = $status === self::AI_STATUS_SUCCEEDED
|
||||
&& in_array($actionHint, [self::AI_ACTION_REVIEW, self::AI_ACTION_FLAG_HIGH], true);
|
||||
|
||||
return [
|
||||
'status' => $status,
|
||||
'maturity_label' => $label,
|
||||
'confidence' => $confidence,
|
||||
'labels' => $labels,
|
||||
'action_hint' => $actionHint,
|
||||
'model' => $model !== '' ? $model : null,
|
||||
'threshold_used' => $thresholdUsed,
|
||||
'analysis_time_ms' => $analysisTime,
|
||||
'advisory' => $advisory !== '' ? $advisory : null,
|
||||
'flagged' => $flagged,
|
||||
'score' => $confidence,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $assessment
|
||||
*/
|
||||
private function shouldFlagAssessment(Artwork $artwork, array $assessment): bool
|
||||
{
|
||||
if (($assessment['status'] ?? self::AI_STATUS_FAILED) !== self::AI_STATUS_SUCCEEDED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((bool) $artwork->is_mature) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) ($assessment['flagged'] ?? false)
|
||||
|| in_array($assessment['action_hint'] ?? null, [self::AI_ACTION_REVIEW, self::AI_ACTION_FLAG_HIGH], true)
|
||||
|| (($assessment['maturity_label'] ?? null) === self::LEVEL_MATURE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $assessment
|
||||
*/
|
||||
private function buildAiFlagReason(array $assessment): string
|
||||
{
|
||||
$labels = array_slice($this->normalizeLabels($assessment['labels'] ?? []), 0, 5);
|
||||
$action = $this->normalizeAiActionHint($assessment['action_hint'] ?? null);
|
||||
$prefix = match ($action) {
|
||||
self::AI_ACTION_FLAG_HIGH => 'AI flagged high-confidence mature content',
|
||||
self::AI_ACTION_REVIEW => 'AI requested moderation review for mature content',
|
||||
default => 'AI suspected mature content',
|
||||
};
|
||||
|
||||
if ($labels === []) {
|
||||
return $prefix . '.';
|
||||
}
|
||||
|
||||
return $prefix . ' from: ' . implode(', ', $labels);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $rows
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function analysisTextFragments(array $rows): array
|
||||
{
|
||||
return collect($rows)
|
||||
->map(function (mixed $row): string {
|
||||
if (is_array($row)) {
|
||||
return Str::lower(trim((string) ($row['tag'] ?? $row['label'] ?? $row['name'] ?? '')));
|
||||
}
|
||||
|
||||
return Str::lower(trim((string) $row));
|
||||
})
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function normalizeVisibilityPreference(string $value): string
|
||||
{
|
||||
return match (Str::lower(trim($value))) {
|
||||
self::VIEW_HIDE => self::VIEW_HIDE,
|
||||
self::VIEW_SHOW => self::VIEW_SHOW,
|
||||
default => self::VIEW_BLUR,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private function normalizeLabels(mixed $labels): array
|
||||
{
|
||||
if (! is_array($labels)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect($labels)
|
||||
->map(static fn (mixed $label): string => trim((string) $label))
|
||||
->filter()
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
private function normalizeScore(mixed $value): ?float
|
||||
{
|
||||
return is_numeric($value) ? round((float) $value, 4) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $analysis
|
||||
*/
|
||||
private function looksLikeNormalizedAssessment(array $analysis): bool
|
||||
{
|
||||
return array_key_exists('maturity_label', $analysis)
|
||||
|| array_key_exists('action_hint', $analysis)
|
||||
|| array_key_exists('status', $analysis)
|
||||
|| array_key_exists('threshold_used', $analysis)
|
||||
|| array_key_exists('analysis_time_ms', $analysis);
|
||||
}
|
||||
|
||||
private function normalizeAiStatus(mixed $value): string
|
||||
{
|
||||
return match (Str::lower(trim((string) $value))) {
|
||||
self::AI_STATUS_PENDING => self::AI_STATUS_PENDING,
|
||||
self::AI_STATUS_SKIPPED => self::AI_STATUS_SKIPPED,
|
||||
self::AI_STATUS_SUCCEEDED => self::AI_STATUS_SUCCEEDED,
|
||||
self::AI_STATUS_NOT_REQUESTED => self::AI_STATUS_NOT_REQUESTED,
|
||||
default => self::AI_STATUS_FAILED,
|
||||
};
|
||||
}
|
||||
|
||||
private function normalizeAiLabel(mixed $value): ?string
|
||||
{
|
||||
return match (Str::lower(trim((string) $value))) {
|
||||
self::LEVEL_SAFE => self::LEVEL_SAFE,
|
||||
self::LEVEL_MATURE => self::LEVEL_MATURE,
|
||||
'adult', 'explicit', 'nsfw' => self::LEVEL_MATURE,
|
||||
'clear', 'sfw' => self::LEVEL_SAFE,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function normalizeAiActionHint(mixed $value): ?string
|
||||
{
|
||||
return match (Str::lower(trim((string) $value))) {
|
||||
self::AI_ACTION_SAFE, self::AI_ACTION_ALLOW, 'mark_safe', 'allow' => self::AI_ACTION_SAFE,
|
||||
self::AI_ACTION_REVIEW, 'queue', 'suspect' => self::AI_ACTION_REVIEW,
|
||||
self::AI_ACTION_FLAG_HIGH, 'block', 'mark_mature', 'mature' => self::AI_ACTION_FLAG_HIGH,
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
private function value(mixed $artwork, string $key, mixed $default = null): mixed
|
||||
{
|
||||
if ($artwork instanceof Artwork) {
|
||||
return $artwork->getAttribute($key) ?? $default;
|
||||
}
|
||||
|
||||
if (is_array($artwork)) {
|
||||
return $artwork[$key] ?? $default;
|
||||
}
|
||||
|
||||
if (! is_object($artwork)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return $artwork->{$key} ?? $default;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user