feat: moderation
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Throwable;
|
||||
|
||||
class ContentModerationService
|
||||
{
|
||||
/**
|
||||
* @param array<int, mixed> $texts
|
||||
* @param array<int, mixed> $images
|
||||
*/
|
||||
public function ensureAcceptable(array $texts = [], array $images = []): void
|
||||
{
|
||||
$normalizedTexts = collect($texts)
|
||||
->flatten()
|
||||
->filter(fn (mixed $text): bool => is_string($text) && filled(trim($text)))
|
||||
->map(fn (string $text): string => trim($text))
|
||||
->values()
|
||||
->all();
|
||||
$normalizedImages = collect($images)
|
||||
->filter(fn (mixed $image): bool => $image instanceof UploadedFile || $this->isRemoteImageUrl($image))
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($normalizedTexts === [] && $normalizedImages === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->containsExplicitlyBlockedText($normalizedTexts)) {
|
||||
$this->rejectContent();
|
||||
}
|
||||
|
||||
if (! config('moderation.content.enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$apiKey = trim((string) config('moderation.content.openai_key'));
|
||||
|
||||
if ($apiKey === '') {
|
||||
throw new HttpException(503, __('api.moderation.unavailable'));
|
||||
}
|
||||
|
||||
$input = [];
|
||||
|
||||
if ($normalizedTexts !== []) {
|
||||
$input[] = [
|
||||
'type' => 'text',
|
||||
'text' => implode("\n", $normalizedTexts),
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($normalizedImages as $image) {
|
||||
$input[] = [
|
||||
'type' => 'image_url',
|
||||
'image_url' => [
|
||||
'url' => $image instanceof UploadedFile
|
||||
? $this->uploadedFileDataUrl($image)
|
||||
: $image,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
try {
|
||||
$response = Http::baseUrl(rtrim((string) config('moderation.content.openai_url'), '/'))
|
||||
->withToken($apiKey)
|
||||
->acceptJson()
|
||||
->timeout((int) config('moderation.content.timeout', 12))
|
||||
->retry(2, 250)
|
||||
->post('/moderations', [
|
||||
'model' => config('moderation.content.model'),
|
||||
'input' => $input,
|
||||
])
|
||||
->throw();
|
||||
} catch (Throwable $exception) {
|
||||
report($exception);
|
||||
|
||||
throw new HttpException(503, __('api.moderation.unavailable'), $exception);
|
||||
}
|
||||
|
||||
if (collect($response->json('results', []))->contains(
|
||||
fn (mixed $result): bool => is_array($result) && ($result['flagged'] ?? false) === true
|
||||
)) {
|
||||
$this->rejectContent();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $texts
|
||||
*/
|
||||
private function containsExplicitlyBlockedText(array $texts): bool
|
||||
{
|
||||
$content = Str::lower(Str::ascii(implode("\n", $texts)));
|
||||
|
||||
return collect(config('moderation.content.blocked_phrases', []))
|
||||
->filter(fn (mixed $phrase): bool => is_string($phrase) && filled($phrase))
|
||||
->contains(fn (string $phrase): bool => Str::contains($content, Str::lower(Str::ascii($phrase))));
|
||||
}
|
||||
|
||||
private function uploadedFileDataUrl(UploadedFile $image): string
|
||||
{
|
||||
$contents = file_get_contents($image->getRealPath());
|
||||
|
||||
if ($contents === false) {
|
||||
throw new HttpException(503, __('api.moderation.unavailable'));
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
'data:%s;base64,%s',
|
||||
$image->getMimeType() ?: 'application/octet-stream',
|
||||
base64_encode($contents),
|
||||
);
|
||||
}
|
||||
|
||||
private function isRemoteImageUrl(mixed $image): bool
|
||||
{
|
||||
return is_string($image)
|
||||
&& Str::startsWith($image, ['https://', 'http://']);
|
||||
}
|
||||
|
||||
private function rejectContent(): never
|
||||
{
|
||||
throw ValidationException::withMessages([
|
||||
'content' => [__('api.moderation.rejected')],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user