Repository navigation
feat: gifs integration with your favorite provider (giphy|klipy|any) #367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: production
Are you sure you want to change the base?
Changes from all commits
3be4c8d
42c0907
ebf77cf
4bdf44c
11051f0
9d05d4d
c98cc7e
d4a4b86
2781173
9f2f132
106d03f
96d7a7e
ee40cff
92a6696
f9687a2
1a15f58
3db009c
7338b89
9336e16
d47e218
a8c076c
c39cab2
5ee0a25
82a7ce3
9cbd0c4
f239a13
2ecb76e
f26161b
db38a05
dd1236d
8d33ec4
cd0c099
7fb3512
0eb26f6
7fcd017
c054773
084ef71
cec8d9d
1fcc682
db145ac
4ebc0aa
1039690
ac31660
3043015
508c878
a762dd1
962fcbd
a8cc08a
cfe9338
402a38f
68cf0fa
4fed065
502ba88
3a2c641
ab43248
09e3e96
398686a
8482063
e09a84c
a96a8af
a2b9c22
f7f70c8
114ee7f
e39c33f
45960b2
99ef810
2273961
f9bfd86
8966c6b
2dd221e
13bd2a5
ef5ed56
f65661e
9d87640
ee0af0c
99fa27c
578a0a1
52f4f37
32f1265
f51af72
80c53bf
0d7fa7c
9a6d127
8e75f1b
d9ee564
7f786a1
e4e6451
742f6ac
7b9f8b2
786e30b
8c3bb64
4491f28
83486d3
6de8cd6
6893133
2ebe9ba
ecd4ce7
bfa267a
71fc053
6c14b1e
a27bd86
10020c2
c34f73a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,11 @@ | |
| use App\Models\Commit; | ||
| use App\Models\CommitImage; | ||
| use App\Services\OpenAI; | ||
| use Illuminate\Support\Facades\Http; | ||
| use Illuminate\Support\Facades\Image; | ||
| use Illuminate\Support\Facades\Storage; | ||
| use RuntimeException; | ||
| use Throwable; | ||
|
|
||
| final readonly class UpdateCommitAiDecision | ||
| { | ||
|
|
@@ -17,6 +21,10 @@ | |
| */ | ||
| private const float AUTO_REJECT_THRESHOLD = 0.95; | ||
|
|
||
| private const int GIF_MAX_BYTES = 20 * 1024 * 1024; | ||
|
|
||
| private const int GIF_MAX_DIMENSION = 1600; | ||
|
|
||
| public function __construct( | ||
| private OpenAI $openAI, | ||
| private UpdateCommitStatuses $updateCommitStatuses, | ||
|
|
@@ -25,7 +33,7 @@ public function __construct( | |
| } | ||
|
|
||
| /** | ||
| * Asks the AI whether the commit, images included, should be approved. The decision is mostly a hint for | ||
| * Asks the AI whether the commit, images and GIF included, should be approved. The decision is mostly a hint for | ||
| * moderators: a clean commit is approved, and it is rejected, without a reason, when the AI is all but certain. | ||
| * Failures, such as timeouts or rate limits, are thrown, so the caller can try again later. | ||
| */ | ||
|
|
@@ -35,6 +43,10 @@ public function handle(Commit $commit): void | |
| fn (CommitImage $image): string => 'data:image/webp;base64,'.base64_encode((string) Storage::get($image->path)), | ||
| )->all()); | ||
|
|
||
| if ($commit->gif !== null) { | ||
| $images[] = $this->gifForModeration($commit->gif->url); | ||
| } | ||
|
|
||
| $scores = $this->openAI->moderate($commit->body, $images); | ||
|
|
||
| $categories = array_keys($scores); | ||
|
|
@@ -56,4 +68,30 @@ public function handle(Commit $commit): void | |
| $this->updateCommitStatuses->handle([$commit->id], CommitStatus::Approved, onlyPending: true); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * The moderation endpoint only accepts non-animated GIFs of at most 20 MB, while providers serve animated ones. | ||
| * Sending their first frame as WebP keeps the check on the image itself instead of relying on the browser filter. | ||
| */ | ||
| private function gifForModeration(string $url): string | ||
| { | ||
| $response = Http::timeout(10)->withoutRedirecting()->get($url)->throw(); | ||
| $body = $response->body(); | ||
|
Comment on lines
+78
to
+79
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| if (mb_strlen($body) > self::GIF_MAX_BYTES) { | ||
| throw new RuntimeException('The GIF is too large to moderate.'); | ||
| } | ||
|
|
||
| try { | ||
| $webp = Image::fromBytes($body) | ||
| ->usingGd() | ||
| ->scale(self::GIF_MAX_DIMENSION, self::GIF_MAX_DIMENSION) | ||
|
Comment on lines
+86
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Limit decoded GIF frames before scaling. The 20 MiB check limits compressed bytes, but
|
||
| ->optimize('webp', 60) | ||
| ->toBytes(); | ||
| } catch (Throwable $throwable) { | ||
| throw new RuntimeException('The GIF could not be prepared for moderation.', $throwable->getCode(), previous: $throwable); | ||
| } | ||
|
|
||
| return 'data:image/webp;base64,'.base64_encode($webp); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Dto; | ||
|
|
||
| use App\Enums\GifProvider; | ||
|
|
||
| /** A GIF picked from a provider; URLs kept exactly as its API returned them. */ | ||
| final readonly class Gif | ||
| { | ||
| public function __construct( | ||
| public GifProvider $provider, | ||
| public string $id, | ||
| public string $url, | ||
| public string $pageUrl, | ||
| public ?string $title, | ||
| public ?string $username, | ||
| public int $width, | ||
| public int $height, | ||
| ) { | ||
| // | ||
| } | ||
|
|
||
| /** | ||
| * @return array<string, string|int|null> | ||
| */ | ||
| public function toColumns(): array | ||
| { | ||
| return [ | ||
| 'provider' => $this->provider->value, | ||
| 'provider_gif_id' => $this->id, | ||
| 'url' => $this->url, | ||
| 'page_url' => $this->pageUrl, | ||
| 'title' => $this->title, | ||
| 'username' => $this->username, | ||
| 'width' => $this->width, | ||
| 'height' => $this->height, | ||
| ]; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace App\Enums; | ||
|
|
||
| enum GifProvider: string | ||
| { | ||
| case Giphy = 'giphy'; | ||
| case Klipy = 'klipy'; | ||
|
|
||
| public function label(): string | ||
| { | ||
| return match ($this) { | ||
| self::Giphy => 'GIPHY', | ||
| self::Klipy => 'KLIPY', | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * @return list<string> | ||
| */ | ||
| public function mediaHostPatterns(): array | ||
| { | ||
| return match ($this) { | ||
| self::Giphy => ['/^media\d*\.giphy\.com$/'], | ||
| self::Klipy => ['/^static\d*\.klipy\.com$/'], | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * @return list<string> | ||
| */ | ||
| public function pageHostPatterns(): array | ||
| { | ||
| return match ($this) { | ||
| self::Giphy => ['/^(www\.)?giphy\.com$/'], | ||
| self::Klipy => ['/^(www\.)?klipy\.com$/'], | ||
| }; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.