diff --git a/_ide_helper.php b/_ide_helper.php
index 626d906..ef3a00e 100644
--- a/_ide_helper.php
+++ b/_ide_helper.php
@@ -24337,6 +24337,103 @@ public static function inertiaPage()
}
}
+namespace Illuminate\Database\Schema {
+ /**
+ *
+ *
+ */
+ class Blueprint {
+ /**
+ *
+ *
+ * @see \Kalnoy\Nestedset\NestedSetServiceProvider::register()
+ * @static
+ */
+ public static function nestedSet()
+ {
+ return \Illuminate\Database\Schema\Blueprint::nestedSet();
+ }
+
+ /**
+ *
+ *
+ * @see \Kalnoy\Nestedset\NestedSetServiceProvider::register()
+ * @static
+ */
+ public static function dropNestedSet()
+ {
+ return \Illuminate\Database\Schema\Blueprint::dropNestedSet();
+ }
+
+ /**
+ *
+ *
+ * @see \Pgvector\Laravel\Schema::register()
+ * @param string $column
+ * @param mixed|null $dimensions
+ * @return \Illuminate\Database\Schema\ColumnDefinition
+ * @static
+ */
+ public static function halfvec($column, $dimensions = null)
+ {
+ return \Illuminate\Database\Schema\Blueprint::halfvec($column, $dimensions);
+ }
+
+ /**
+ *
+ *
+ * @see \Pgvector\Laravel\Schema::register()
+ * @param string $column
+ * @param mixed|null $length
+ * @return \Illuminate\Database\Schema\ColumnDefinition
+ * @static
+ */
+ public static function bit($column, $length = null)
+ {
+ return \Illuminate\Database\Schema\Blueprint::bit($column, $length);
+ }
+
+ /**
+ *
+ *
+ * @see \Pgvector\Laravel\Schema::register()
+ * @param string $column
+ * @param mixed|null $dimensions
+ * @return \Illuminate\Database\Schema\ColumnDefinition
+ * @static
+ */
+ public static function sparsevec($column, $dimensions = null)
+ {
+ return \Illuminate\Database\Schema\Blueprint::sparsevec($column, $dimensions);
+ }
+
+ }
+ }
+
+namespace Illuminate\Database\Eloquent\Factories {
+ /**
+ *
+ *
+ * @template TModel of \Illuminate\Database\Eloquent\Model
+ * @method $this trashed()
+ */
+ class Factory {
+ /**
+ *
+ *
+ * @see \Spatie\Translatable\TranslatableServiceProvider::packageRegistered()
+ * @param array|string $locales
+ * @param mixed|null $value
+ * @static
+ */
+ public static function translations($locales, $value)
+ {
+ return \Illuminate\Database\Eloquent\Factories\Factory::translations($locales, $value);
+ }
+
+ }
+ }
+
namespace Illuminate\Database\Schema\Grammars {
/**
*
@@ -24517,57 +24614,6 @@ public static function typeSparsevec($column)
}
}
-namespace Illuminate\Database\Schema {
- /**
- *
- *
- */
- class Blueprint {
- /**
- *
- *
- * @see \Pgvector\Laravel\Schema::register()
- * @param string $column
- * @param mixed|null $dimensions
- * @return \Illuminate\Database\Schema\ColumnDefinition
- * @static
- */
- public static function halfvec($column, $dimensions = null)
- {
- return \Illuminate\Database\Schema\Blueprint::halfvec($column, $dimensions);
- }
-
- /**
- *
- *
- * @see \Pgvector\Laravel\Schema::register()
- * @param string $column
- * @param mixed|null $length
- * @return \Illuminate\Database\Schema\ColumnDefinition
- * @static
- */
- public static function bit($column, $length = null)
- {
- return \Illuminate\Database\Schema\Blueprint::bit($column, $length);
- }
-
- /**
- *
- *
- * @see \Pgvector\Laravel\Schema::register()
- * @param string $column
- * @param mixed|null $dimensions
- * @return \Illuminate\Database\Schema\ColumnDefinition
- * @static
- */
- public static function sparsevec($column, $dimensions = null)
- {
- return \Illuminate\Database\Schema\Blueprint::sparsevec($column, $dimensions);
- }
-
- }
- }
-
namespace Illuminate\Database\Query\Grammars {
/**
*
diff --git a/app/Helpers/FirstParty/AI/OpenAI.php b/app/Helpers/FirstParty/AI/OpenAI.php
index aa8e269..bef53bc 100644
--- a/app/Helpers/FirstParty/AI/OpenAI.php
+++ b/app/Helpers/FirstParty/AI/OpenAI.php
@@ -6,6 +6,51 @@
class OpenAI
{
+ public static function getMemeKeywords(string $name, string $description)
+ {
+ $apiKey = config('services.openai.api_key'); // Make sure to set this in config/services.php
+
+ $response = Http::withHeaders([
+ 'Content-Type' => 'application/json',
+ 'Authorization' => 'Bearer ' . $apiKey,
+ ])->post('https://api.openai.com/v1/responses', [
+ 'model' => 'gpt-4.1-nano',
+ 'input' => [
+ [
+ 'role' => 'system',
+ 'content' => [
+ [
+ 'type' => 'input_text',
+ 'text' => "You are a meme annotation assistant. Given a meme name and description, you must populate three keyword categories: action_keywords: List specific actions being performed in the meme (e.g., wearing, singing, dancing) emotion_keywords: List feelings and emotions conveyed by the meme (e.g., joy, excitement, sadness) misc_keywords: List core identifying elements - keep this concise and focused on essential references only (e.g., main subjects, key objects, proper nouns) Format your response with clear category labels. Be precise and avoid over-elaborating, especially in misc_keywords. Example: Input: \"7th Element OIIA Cat, OIIA cat wearing 7th element head ban and sings 7th element song chorous with OIIA sounds\" Output: action_keywords: wearing, singing, performing emotion_keywords: excitement, joy, playfulness misc_keywords: cat, 7th element, headband, OIIA. the description may also have spelling and grammar issues, please fix it\n\nreturn in json:\n{\ndescription: \"\",\naction_keywords:[],\nemotion_keywords:[],\nmisc_keywords:[],\n}",
+ ],
+ ],
+ ],
+ [
+ 'role' => 'user',
+ 'content' => [
+ [
+ 'type' => 'input_text',
+ 'text' => "Name: $name\nDescription: $description",
+ ],
+ ],
+ ],
+ ],
+ 'text' => [
+ 'format' => [
+ 'type' => 'json_object',
+ ],
+ ],
+ 'reasoning' => new \stdClass,
+ 'tools' => [],
+ 'temperature' => 1,
+ 'max_output_tokens' => 2048,
+ 'top_p' => 1,
+ 'store' => true,
+ ]);
+
+ return $response->json();
+ }
+
public static function getSingleMemeGenerator($user_prompt)
{
@@ -17,7 +62,6 @@ public static function getSingleMemeGenerator($user_prompt)
'A humorous, funny one-liner with no punctuation that describes a vivid, realistic scenario or reaction in a clearly defined context',
];
-
$response = Http::withHeaders([
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . env('OPENAI_API_KEY'),
@@ -56,9 +100,27 @@ public static function getSingleMemeGenerator($user_prompt)
'type' => 'string',
'description' => $caption_descriptions[rand(0, count($caption_descriptions) - 1)],
],
- 'meme_keywords' => [
+ 'primary_keyword_type' => [
+ 'type' => 'string',
+ 'description' => "Primary keyword type, choose only between: (action|emotion|misc)",
+ ],
+ 'action_keywords' => [
'type' => 'array',
- 'description' => 'Array of 3–5 tags that describe only internal emotional states (e.g., "anxiety", "burnout", "imposter syndrome", "joy", "excitement", "confusion", "awkwardness"). Avoid external concepts, situations, or behaviors like "familylife" or "comparison". Only include pure feelings.',
+ 'description' => 'List specific actions being performed in the meme (e.g., wearing, singing, dancing)',
+ 'items' => [
+ 'type' => 'string',
+ ],
+ ],
+ 'emotion_keywords' => [
+ 'type' => 'array',
+ 'description' => 'List feelings and emotions conveyed by the meme (e.g., joy, excitement, sadness)',
+ 'items' => [
+ 'type' => 'string',
+ ],
+ ],
+ 'misc_keywords' => [
+ 'type' => 'array',
+ 'description' => 'List core identifying elements - keep this concise and focused on essential references only (e.g., main subjects, key objects, proper nouns)',
'items' => [
'type' => 'string',
],
@@ -78,7 +140,10 @@ public static function getSingleMemeGenerator($user_prompt)
],
'required' => [
'caption',
- 'meme_keywords',
+ 'primary_keyword_type',
+ 'action_keywords',
+ 'emotion_keywords',
+ 'misc_keywords',
'background',
'keywords',
],
@@ -109,7 +174,7 @@ public static function getSingleMemeGenerator($user_prompt)
public static function getOpenAIOutput($data)
{
- //dump($data);
+ // dump($data);
$output = null;
@@ -117,7 +182,7 @@ public static function getOpenAIOutput($data)
if ($response_type === 'output_text') {
$output = data_get($data, 'output.0.content.0.text', null);
- } else if ($response_type === 'refusal') {
+ } elseif ($response_type === 'refusal') {
$output = data_get($data, 'output.0.content.0.refusal', null);
}
diff --git a/app/Helpers/FirstParty/AI/RunwareAI.php b/app/Helpers/FirstParty/AI/RunwareAI.php
index cbc8ac5..1c04055 100644
--- a/app/Helpers/FirstParty/AI/RunwareAI.php
+++ b/app/Helpers/FirstParty/AI/RunwareAI.php
@@ -14,22 +14,22 @@ public static function generateSchnellImage($uuid, $prompt, $width = 1024, $heig
$response = Http::timeout(60)
->withHeaders([
'Content-Type' => 'application/json',
- 'Authorization' => 'Bearer ' . $api_key,
+ 'Authorization' => 'Bearer '.$api_key,
])
->post('https://api.runware.ai/v1', [
[
- "taskUUID" => $uuid,
- "taskType" => "imageInference",
- "width" => $width,
- "height" => $height,
- "numberResults" => 1,
- "outputFormat" => "WEBP",
- "outputType" => ["URL"],
- "includeCost" => true,
- "inputImages" => [],
- "positivePrompt" => $prompt,
- "model" => "runware:100@1"
- ]
+ 'taskUUID' => $uuid,
+ 'taskType' => 'imageInference',
+ 'width' => $width,
+ 'height' => $height,
+ 'numberResults' => 1,
+ 'outputFormat' => 'WEBP',
+ 'outputType' => ['URL'],
+ 'includeCost' => true,
+ 'inputImages' => [],
+ 'positivePrompt' => $prompt,
+ 'model' => 'runware:100@1',
+ ],
]);
// Check if the request was successful
@@ -50,10 +50,10 @@ public static function generateSchnellImage($uuid, $prompt, $width = 1024, $heig
throw new \Exception('Image URL not found in response');
}
- throw new \Exception('API request failed: ' . $response->status() . ' - ' . $response->body());
+ throw new \Exception('API request failed: '.$response->status().' - '.$response->body());
} catch (\Exception $e) {
// Log the error or handle as needed
- \Log::error('RunwareAI API Error: ' . $e->getMessage());
+ \Log::error('RunwareAI API Error: '.$e->getMessage());
throw $e;
}
}
diff --git a/app/Helpers/FirstParty/AspectRatio.php b/app/Helpers/FirstParty/AspectRatio.php
index 15bb71e..8c256f3 100644
--- a/app/Helpers/FirstParty/AspectRatio.php
+++ b/app/Helpers/FirstParty/AspectRatio.php
@@ -4,20 +4,19 @@
class AspectRatio
{
-
/**
* Get the aspect ratio for given width and height
* Returns common aspect ratios first, then computed ratios
*
- * @param int|float $width
- * @param int|float $height
+ * @param int|float $width
+ * @param int|float $height
* @return string
*/
public static function get($width, $height)
{
// Handle edge cases
if ($width <= 0 || $height <= 0) {
- return "Invalid dimensions";
+ return 'Invalid dimensions';
}
// Calculate the actual ratio
@@ -53,8 +52,8 @@ public static function get($width, $height)
/**
* Compute simplified aspect ratio using GCD
*
- * @param int|float $width
- * @param int|float $height
+ * @param int|float $width
+ * @param int|float $height
* @return string
*/
private static function computeSimplifiedRatio($width, $height)
@@ -70,14 +69,14 @@ private static function computeSimplifiedRatio($width, $height)
$simplifiedWidth = $intWidth / $gcd;
$simplifiedHeight = $intHeight / $gcd;
- return $simplifiedWidth . ':' . $simplifiedHeight;
+ return $simplifiedWidth.':'.$simplifiedHeight;
}
/**
* Calculate Greatest Common Divisor using Euclidean algorithm
*
- * @param int $a
- * @param int $b
+ * @param int $a
+ * @param int $b
* @return int
*/
private static function gcd($a, $b)
@@ -88,6 +87,7 @@ private static function gcd($a, $b)
$b = $a % $b;
$a = $temp;
}
+
return $a;
}
}
diff --git a/app/Helpers/FirstParty/Maintenance/MemeMediaMaintenance.php b/app/Helpers/FirstParty/Maintenance/MemeMediaMaintenance.php
index 7af4e27..2a610f1 100644
--- a/app/Helpers/FirstParty/Maintenance/MemeMediaMaintenance.php
+++ b/app/Helpers/FirstParty/Maintenance/MemeMediaMaintenance.php
@@ -2,11 +2,44 @@
namespace App\Helpers\FirstParty\Maintenance;
+use App\Helpers\FirstParty\AI\OpenAI;
use App\Models\MemeMedia;
use ProtoneMedia\LaravelFFMpeg\Support\FFMpeg;
class MemeMediaMaintenance
{
+ public static function patchMemeKeywords()
+ {
+ $meme_medias = MemeMedia::whereNull('action_keywords')->get();
+
+ foreach ($meme_medias as $key => $meme_media) {
+
+ dump('Processing '.$key + 1 .'/'.$meme_medias->count().': '.$meme_media->name);
+
+ $meme_keywords_response = OpenAI::getMemeKeywords($meme_media->name, $meme_media->description);
+
+ $meme_keywords_output = json_decode(OpenAI::getOpenAIOutput($meme_keywords_response));
+
+ $meme_media->description = $meme_keywords_output->description;
+ $meme_media->action_keywords = $meme_keywords_output->action_keywords;
+ $meme_media->emotion_keywords = $meme_keywords_output->emotion_keywords;
+ $meme_media->misc_keywords = $meme_keywords_output->misc_keywords;
+
+ $meme_media->save();
+ }
+ }
+
+ public static function addMemeKeywordsToTags()
+ {
+ $meme_medias = MemeMedia::all();
+
+ foreach ($meme_medias as $key => $meme_media) {
+ $meme_media->attachTags($meme_media->action_keywords, 'meme_media_action');
+ $meme_media->attachTags($meme_media->emotion_keywords, 'meme_media_emotion');
+ $meme_media->attachTags($meme_media->misc_keywords, 'meme_media_misc');
+ }
+ }
+
public static function populateDurations()
{
diff --git a/app/Helpers/FirstParty/Meme/MemeGenerator.php b/app/Helpers/FirstParty/Meme/MemeGenerator.php
index 273693a..685a29d 100644
--- a/app/Helpers/FirstParty/Meme/MemeGenerator.php
+++ b/app/Helpers/FirstParty/Meme/MemeGenerator.php
@@ -16,15 +16,21 @@
class MemeGenerator
{
-
const TYPE_SINGLE_CAPTION_MEME_BACKGROUND = 'single_caption_meme_background';
const STATUS_PENDING = 'pending';
+
const STATUS_COMPLETED = 'completed';
+ public static function getSuitableMeme(Meme $meme)
+ {
+ //dd($meme->toArray());
+ return MemeMedia::first();
+ }
+
public static function generateMemeByCategory(Category $category)
{
- $meme_output = self::getMemeOutputByCategory($category);
+ $meme_output = self::generateMemeOutputByCategory($category);
$meme = null;
@@ -38,18 +44,25 @@ public static function generateMemeByCategory(Category $category)
'background' => $meme_output->background,
'keywords' => $meme_output->keywords,
'is_system' => true,
- 'status' => self::STATUS_PENDING
+ 'status' => self::STATUS_PENDING,
+ 'primary_keyword_type' => $meme_output->primary_keyword_type,
+ 'action_keywords' => $meme_output->action_keywords,
+ 'emotion_keywords' => $meme_output->emotion_keywords,
+ 'misc_keywords' => $meme_output->misc_keywords,
]);
$meme->attachTags($meme_output->keywords, 'meme');
}
- if (!is_null($meme) && $meme->status == self::STATUS_PENDING) {
+ if (! is_null($meme) && $meme->status == self::STATUS_PENDING) {
// populate meme_id
- $meme->meme_id = self::getMemeMediaByKeywords($meme_output->keywords)->id;
+ $meme->meme_id = null; // self::getMemeMediaByKeywords($meme_output->keywords)->id;
$meme->background_id = self::generateBackgroundMediaWithRunware($meme_output->background)->id;
- if (!is_null($meme->meme_id) && !is_null($meme->background_id)) {
+ if (
+ //!is_null($meme->meme_id) &&
+ ! is_null($meme->background_id)
+ ) {
$meme->status = self::STATUS_COMPLETED;
}
@@ -59,21 +72,21 @@ public static function generateMemeByCategory(Category $category)
return $meme;
}
- public static function getMemeOutputByCategory(Category $category)
+ public static function generateMemeOutputByCategory(Category $category)
{
$retries = 3;
$attempt = 0;
$random_keyword = Str::lower($category->name);
- if (!is_null($category->parent_id)) {
- $random_keyword = $category->parent->name . " - " . $random_keyword;
+ if (! is_null($category->parent_id)) {
+ $random_keyword = $category->parent->name . ' - ' . $random_keyword;
}
- if (!is_null($category->meme_angles)) {
- $random_keyword .= " - " . collect($category->meme_angles)->random();
- } else if (!is_null($category->keywords)) {
- $random_keyword .= " - " . collect($category->keywords)->random();
+ if (! is_null($category->meme_angles)) {
+ $random_keyword .= ' - ' . collect($category->meme_angles)->random();
+ } elseif (! is_null($category->keywords)) {
+ $random_keyword .= ' - ' . collect($category->keywords)->random();
}
$prompt = "Write me 1 meme about {$random_keyword}";
@@ -87,12 +100,14 @@ public static function getMemeOutputByCategory(Category $category)
$output_is_valid = false;
- if (!is_null($meme_output)) {
+ if (! is_null($meme_output)) {
if (
isset($meme_output->caption) &&
- isset($meme_output->meme_keywords) &&
isset($meme_output->background) &&
- isset($meme_output->keywords)
+ isset($meme_output->keywords) &&
+ isset($meme_output->primary_keyword_type) &&
+ isset($meme_output->action_keywords) &&
+ isset($meme_output->emotion_keywords)
) {
$output_is_valid = true;
}
@@ -118,7 +133,7 @@ public static function getMemeOutputByCategory(Category $category)
$meme_output = (object) [
'success' => false,
'attempts' => $attempt, // Optional: track how many attempts were made
- 'error' => 'Failed to generate valid meme after ' . $retries . ' attempts'
+ 'error' => 'Failed to generate valid meme after ' . $retries . ' attempts',
];
}
@@ -131,7 +146,6 @@ public static function generateBackgroundMediaWithRunware($prompt)
$media_height = 1024;
$aspect_ratio = AspectRatio::get($media_width, $media_height);
-
$runware_output_url = RunwareAI::generateSchnellImage(Str::uuid(), $prompt, $media_width, $media_height);
$media = MediaEngine::addMedia(
diff --git a/app/Http/Controllers/FrontMediaController.php b/app/Http/Controllers/FrontMediaController.php
index 0c3746d..3ce72bf 100644
--- a/app/Http/Controllers/FrontMediaController.php
+++ b/app/Http/Controllers/FrontMediaController.php
@@ -2,7 +2,6 @@
namespace App\Http\Controllers;
-use App\Helpers\FirstParty\MediaEngine\MediaEngine;
use App\Helpers\FirstParty\Meme\MemeGenerator;
use App\Models\BackgroundMedia;
use App\Models\Meme;
@@ -15,12 +14,14 @@ public function init(Request $request)
{
$meme = Meme::with('meme_media', 'background_media')->where('status', MemeGenerator::STATUS_COMPLETED)->take(1)->latest()->first();
+ $meme_media = MemeGenerator::getSuitableMeme($meme);
+
return response()->json([
'success' => [
'data' => [
'init' => [
'caption' => $meme->caption,
- 'meme' => $meme->meme_media,
+ 'meme' => $meme_media,
'background' => $meme->background_media,
],
],
diff --git a/app/Http/Controllers/TestController.php b/app/Http/Controllers/TestController.php
index abc1d54..3f291d0 100644
--- a/app/Http/Controllers/TestController.php
+++ b/app/Http/Controllers/TestController.php
@@ -2,15 +2,12 @@
namespace App\Http\Controllers;
-use App\Helpers\FirstParty\AI\CloudflareAI;
use App\Helpers\FirstParty\AI\OpenAI;
use App\Helpers\FirstParty\AI\RunwareAI;
use App\Helpers\FirstParty\AspectRatio;
use App\Helpers\FirstParty\Meme\MemeGenerator;
use App\Models\Category;
-use App\Models\Meme;
use App\Models\MemeMedia;
-use Pgvector\Laravel\Distance;
use Str;
class TestController extends Controller
@@ -20,6 +17,23 @@ public function index()
//
}
+ public function getMemeKeywords()
+ {
+
+ $meme_media = MemeMedia::whereNull('action_keywords')->first();
+
+ $meme_keywords_response = OpenAI::getMemeKeywords($meme_media->name, $meme_media->description);
+
+ $meme_keywords_output = json_decode(OpenAI::getOpenAIOutput($meme_keywords_response));
+
+ $meme_media->description = $meme_keywords_output->description;
+ $meme_media->action_keywords = $meme_keywords_output->action_keywords;
+ $meme_media->emotion_keywords = $meme_keywords_output->emotion_keywords;
+ $meme_media->misc_keywords = $meme_keywords_output->misc_keywords;
+
+ $meme_media->save();
+ }
+
public function aspectRatio()
{
$aspect_ratio = AspectRatio::get(1024, 1024);
diff --git a/app/Models/BackgroundMedia.php b/app/Models/BackgroundMedia.php
index cc6b53d..264b103 100644
--- a/app/Models/BackgroundMedia.php
+++ b/app/Models/BackgroundMedia.php
@@ -67,7 +67,7 @@ class BackgroundMedia extends Model
protected function ids(): Attribute
{
return Attribute::make(
- get: fn($value, $attributes) => hashids_encode($attributes['id']),
+ get: fn ($value, $attributes) => hashids_encode($attributes['id']),
);
}
}
diff --git a/app/Models/Category.php b/app/Models/Category.php
index a57f1f1..1ea318b 100644
--- a/app/Models/Category.php
+++ b/app/Models/Category.php
@@ -30,12 +30,10 @@
* @property string $payload
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
- *
- * @package App\Models
*/
class Category extends Model
{
- use NodeTrait, HasTags, HasNeighbors;
+ use HasNeighbors, HasTags, NodeTrait;
protected $table = 'categories';
@@ -62,7 +60,7 @@ class Category extends Model
'meme_angles',
'sample_captions',
'keywords',
- 'payload'
+ 'payload',
];
/**
diff --git a/app/Models/Meme.php b/app/Models/Meme.php
index 8e9da91..3eb1f6d 100644
--- a/app/Models/Meme.php
+++ b/app/Models/Meme.php
@@ -29,12 +29,10 @@
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property string|null $deleted_at
- *
- * @package App\Models
*/
class Meme extends Model
{
- use SoftDeletes, HasTags;
+ use HasTags, SoftDeletes;
protected $table = 'memes';
@@ -49,6 +47,10 @@ class Meme extends Model
'meme_keywords' => 'array',
'keywords' => 'array',
+
+ 'action_keywords' => 'array',
+ 'emotion_keywords' => 'array',
+ 'misc_keywords' => 'array',
];
protected $fillable = [
@@ -63,7 +65,10 @@ class Meme extends Model
'caption',
'meme_keywords',
'background',
- 'keywords'
+ 'keywords',
+ 'action_keywords',
+ 'emotion_keywords',
+ 'misc_keywords',
];
public function meme_media()
diff --git a/app/Models/MemeMedia.php b/app/Models/MemeMedia.php
index 78f8432..d6b280f 100644
--- a/app/Models/MemeMedia.php
+++ b/app/Models/MemeMedia.php
@@ -34,7 +34,7 @@
*/
class MemeMedia extends Model
{
- use HasNeighbors, SoftDeletes, HasTags;
+ use HasNeighbors, HasTags, SoftDeletes;
protected $table = 'meme_medias';
@@ -43,6 +43,9 @@ class MemeMedia extends Model
'duration' => 'double',
'keywords' => 'array',
'is_enabled' => 'boolean',
+ 'action_keywords' => 'array',
+ 'emotion_keywords' => 'array',
+ 'misc_keywords' => 'array',
];
protected $fillable = [
@@ -61,6 +64,9 @@ class MemeMedia extends Model
'gif_url',
'webp_url',
'embedding',
+ 'action_keywords',
+ 'emotion_keywords',
+ 'misc_keywords',
];
protected $hidden = [
@@ -88,7 +94,7 @@ class MemeMedia extends Model
protected function ids(): Attribute
{
return Attribute::make(
- get: fn($value, $attributes) => hashids_encode($attributes['id']),
+ get: fn ($value, $attributes) => hashids_encode($attributes['id']),
);
}
}
diff --git a/config/services.php b/config/services.php
index cf691f1..c464da0 100644
--- a/config/services.php
+++ b/config/services.php
@@ -41,6 +41,10 @@
'runware' => [
'api_key' => env('RUNWARE_API_KEY'),
- ]
+ ],
+
+ 'openai' => [
+ 'api_key' => env('OPENAI_API_KEY'),
+ ],
];
diff --git a/config/tags.php b/config/tags.php
index 628dc1a..cd61490 100644
--- a/config/tags.php
+++ b/config/tags.php
@@ -24,5 +24,5 @@
* The fully qualified class name of the pivot model.
*/
'class_name' => Illuminate\Database\Eloquent\Relations\MorphPivot::class,
- ]
+ ],
];
diff --git a/database/migrations/2025_06_19_002729_create_background_medias_table.php b/database/migrations/2025_06_19_002729_create_background_medias_table.php
index cae84d5..f575bcc 100644
--- a/database/migrations/2025_06_19_002729_create_background_medias_table.php
+++ b/database/migrations/2025_06_19_002729_create_background_medias_table.php
@@ -26,7 +26,6 @@ public function up(): void
});
}
-
/**
* Reverse the migrations.
*/
diff --git a/database/migrations/2025_06_20_030742_add_keyword_fields_to_meme_medias_table.php b/database/migrations/2025_06_20_030742_add_keyword_fields_to_meme_medias_table.php
new file mode 100644
index 0000000..a156d63
--- /dev/null
+++ b/database/migrations/2025_06_20_030742_add_keyword_fields_to_meme_medias_table.php
@@ -0,0 +1,32 @@
+json('action_keywords')->nullable();
+ $table->json('emotion_keywords')->nullable();
+ $table->json('misc_keywords')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('meme_medias', function (Blueprint $table) {
+ $table->dropColumn('action_keywords');
+ $table->dropColumn('emotion_keywords');
+ $table->dropColumn('misc_keywords');
+ });
+ }
+};
diff --git a/database/migrations/2025_06_20_041413_add_keyword_fields_to_memes_table.php b/database/migrations/2025_06_20_041413_add_keyword_fields_to_memes_table.php
new file mode 100644
index 0000000..5e2bd19
--- /dev/null
+++ b/database/migrations/2025_06_20_041413_add_keyword_fields_to_memes_table.php
@@ -0,0 +1,34 @@
+json('action_keywords')->nullable();
+ $table->json('emotion_keywords')->nullable();
+ $table->json('misc_keywords')->nullable();
+ $table->string('primary_keyword_type')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('memes', function (Blueprint $table) {
+ $table->dropColumn('action_keywords');
+ $table->dropColumn('emotion_keywords');
+ $table->dropColumn('misc_keywords');
+ $table->dropColumn('primary_keyword_type');
+ });
+ }
+};
diff --git a/database/seeders/CategorySeeder.php b/database/seeders/CategorySeeder.php
index f5fe6b4..60bbd58 100644
--- a/database/seeders/CategorySeeder.php
+++ b/database/seeders/CategorySeeder.php
@@ -3,9 +3,8 @@
namespace Database\Seeders;
use App\Helpers\FirstParty\AI\CloudflareAI;
-use Illuminate\Database\Console\Seeds\WithoutModelEvents;
-use Illuminate\Database\Seeder;
use App\Models\Category;
+use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
@@ -20,19 +19,20 @@ public function run(): void
$jsonPath = database_path('seeders/data/json/category');
// Check if directory exists
- if (!File::exists($jsonPath)) {
+ if (! File::exists($jsonPath)) {
$this->command->error("JSON directory not found: {$jsonPath}");
+
return;
}
// Get all JSON files except the schema file
- $jsonFiles = File::glob($jsonPath . '/*.json');
+ $jsonFiles = File::glob($jsonPath.'/*.json');
$jsonFiles = array_filter($jsonFiles, function ($file) {
- return !str_contains(basename($file), 'schema');
+ return ! str_contains(basename($file), 'schema');
});
$this->command->info('Starting to seed categories from JSON files...');
- $this->command->info('Found ' . count($jsonFiles) . ' JSON files to process.');
+ $this->command->info('Found '.count($jsonFiles).' JSON files to process.');
foreach ($jsonFiles as $jsonFile) {
$this->processJsonFile($jsonFile);
@@ -55,36 +55,40 @@ private function processJsonFile(string $filePath): void
$data = json_decode($jsonContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
- $this->command->error("Invalid JSON in file: {$fileName} - " . json_last_error_msg());
+ $this->command->error("Invalid JSON in file: {$fileName} - ".json_last_error_msg());
+
return;
}
// Validate JSON structure
- if (!isset($data['category'])) {
+ if (! isset($data['category'])) {
$this->command->error("Missing 'category' key in file: {$fileName}");
+
return;
}
$categoryData = $data['category'];
// Validate required fields
- if (!isset($categoryData['name']) || !isset($categoryData['description'])) {
+ if (! isset($categoryData['name']) || ! isset($categoryData['description'])) {
$this->command->error("Missing required fields (name/description) in file: {$fileName}");
+
return;
}
// Create main category
$mainCategory = $this->createMainCategory($categoryData, $data);
- if (!$mainCategory) {
+ if (! $mainCategory) {
$this->command->error("Failed to create main category for file: {$fileName}");
+
return;
}
// Create subcategories
if (isset($categoryData['subcategories']) && is_array($categoryData['subcategories'])) {
foreach ($categoryData['subcategories'] as $index => $subcategoryData) {
- if (!$this->createSubcategory($subcategoryData, $mainCategory, $data, $index)) {
+ if (! $this->createSubcategory($subcategoryData, $mainCategory, $data, $index)) {
$this->command->warn("Failed to create subcategory at index {$index} for file: {$fileName}");
}
}
@@ -92,10 +96,10 @@ private function processJsonFile(string $filePath): void
$this->command->info("✓ Successfully processed: {$fileName}");
} catch (\Exception $e) {
- $this->command->error("Error processing {$fileName}: " . $e->getMessage());
+ $this->command->error("Error processing {$fileName}: ".$e->getMessage());
Log::error("CategorySeeder error for {$fileName}", [
'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString()
+ 'trace' => $e->getTraceAsString(),
]);
}
}
@@ -113,6 +117,7 @@ private function createMainCategory(array $categoryData, array $originalData): ?
if ($existingCategory) {
$this->command->warn("Main category '{$categoryData['name']}' already exists. Skipping...");
+
return $existingCategory;
}
@@ -126,7 +131,7 @@ private function createMainCategory(array $categoryData, array $originalData): ?
'meme_angles' => null, // Main categories don't have meme_angles
'sample_captions' => null, // Main categories don't have sample_captions
'payload' => $originalData,
- 'embedding' => CloudflareAI::getVectorEmbeddingBgeSmall($categoryData['name'] . " " . $categoryData['description']),
+ 'embedding' => CloudflareAI::getVectorEmbeddingBgeSmall($categoryData['name'].' '.$categoryData['description']),
]);
// Add keywords as tags
@@ -138,11 +143,12 @@ private function createMainCategory(array $categoryData, array $originalData): ?
return $category;
} catch (\Exception $e) {
- $this->command->error("Error creating main category: " . $e->getMessage());
- Log::error("Error creating main category", [
+ $this->command->error('Error creating main category: '.$e->getMessage());
+ Log::error('Error creating main category', [
'category_data' => $categoryData,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
]);
+
return null;
}
}
@@ -154,8 +160,9 @@ private function createSubcategory(array $subcategoryData, Category $parentCateg
{
try {
// Validate required subcategory fields
- if (!isset($subcategoryData['name']) || !isset($subcategoryData['description'])) {
+ if (! isset($subcategoryData['name']) || ! isset($subcategoryData['description'])) {
$this->command->warn("Subcategory at index {$index} missing required fields (name/description). Skipping...");
+
return null;
}
@@ -166,6 +173,7 @@ private function createSubcategory(array $subcategoryData, Category $parentCateg
if ($existingSubcategory) {
$this->command->warn(" Subcategory '{$subcategoryData['name']}' already exists. Skipping...");
+
return $existingSubcategory;
}
@@ -174,8 +182,8 @@ private function createSubcategory(array $subcategoryData, Category $parentCateg
'subcategory' => $subcategoryData,
'parent_category' => [
'name' => $parentCategory->name,
- 'description' => $parentCategory->description
- ]
+ 'description' => $parentCategory->description,
+ ],
];
// Create the subcategory using the correct nested set method
@@ -189,7 +197,7 @@ private function createSubcategory(array $subcategoryData, Category $parentCateg
'subcategories' => null, // Subcategories don't have subcategories
'payload' => $subcategoryPayload,
'parent_id' => $parentCategory->id, // Set parent_id directly
- 'embedding' => CloudflareAI::getVectorEmbeddingBgeSmall($subcategoryData['name'] . " " . $subcategoryData['description']),
+ 'embedding' => CloudflareAI::getVectorEmbeddingBgeSmall($subcategoryData['name'].' '.$subcategoryData['description']),
]);
// Add keywords as tags
@@ -201,12 +209,13 @@ private function createSubcategory(array $subcategoryData, Category $parentCateg
return $subcategory;
} catch (\Exception $e) {
- $this->command->error("Error creating subcategory at index {$index}: " . $e->getMessage());
- Log::error("Error creating subcategory", [
+ $this->command->error("Error creating subcategory at index {$index}: ".$e->getMessage());
+ Log::error('Error creating subcategory', [
'subcategory_data' => $subcategoryData,
'parent_id' => $parentCategory->id,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
]);
+
return null;
}
}
@@ -219,11 +228,11 @@ private function attachKeywordsAsTags(Category $category, array $keywords): void
try {
$category->attachTags($keywords, 'category');
} catch (\Exception $e) {
- $this->command->warn("Failed to attach tags to category '{$category->name}': " . $e->getMessage());
- Log::warning("Failed to attach tags", [
+ $this->command->warn("Failed to attach tags to category '{$category->name}': ".$e->getMessage());
+ Log::warning('Failed to attach tags', [
'category_id' => $category->id,
'keywords' => $keywords,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
]);
}
}
diff --git a/database/seeders/MemeMediaSeeder.php b/database/seeders/MemeMediaSeeder.php
index 3945414..5fda720 100644
--- a/database/seeders/MemeMediaSeeder.php
+++ b/database/seeders/MemeMediaSeeder.php
@@ -52,7 +52,7 @@ public function run(): void
$csv_path = database_path('seeders/data/webm_metadata.csv');
$meme_data = $this->parseCsvFile($csv_path);
- $this->command->info('📊 Found ' . count($meme_data) . ' memes to import');
+ $this->command->info('📊 Found '.count($meme_data).' memes to import');
// Process records individually for PostgreSQL compatibility
$total_processed = 0;
@@ -60,12 +60,10 @@ public function run(): void
$total_failed = 0;
foreach ($meme_data as $index => $meme_record) {
- $this->command->info('Processing ' . ($index + 1) . '/' . count($meme_data) . ': ' . $meme_record['filename']);
-
+ $this->command->info('Processing '.($index + 1).'/'.count($meme_data).': '.$meme_record['filename']);
$meme_record['keywords'] = $this->stringToCleanArray($meme_record['keywords']);
-
try {
// Check for duplicates OUTSIDE of transaction
$base_filename = pathinfo($meme_record['filename'], PATHINFO_FILENAME);
@@ -158,6 +156,7 @@ private function stringToCleanArray($string)
return array_filter(array_map(function ($item) {
$item = trim($item); // Remove whitespace
$item = preg_replace('/[^\w\s]/', '', $item); // Remove punctuation
+
return trim(preg_replace('/\s+/', ' ', $item)); // Clean extra spaces
}, explode(',', $string)), function ($value) {
return $value !== '';
@@ -192,13 +191,13 @@ private function importSingleMeme(array $meme_record): bool
'save_url', // Mode: just save URL reference
null, // Auto-generate filename
'r2', // Disk (not used for URL mode)
- trim($meme_record['name']) . " ({$format})", // Name with format
+ trim($meme_record['name'])." ({$format})", // Name with format
null, // No specific user
$config['mime'] // MIME type
);
- $media_uuids[$format . '_uuid'] = $media->uuid;
- $media_urls[$format . '_url'] = $url;
+ $media_uuids[$format.'_uuid'] = $media->uuid;
+ $media_urls[$format.'_url'] = $url;
} catch (\Exception $e) {
$this->command->error("Failed to create {$format} media for {$meme_record['filename']}: {$e->getMessage()}");
throw $e;
@@ -208,7 +207,7 @@ private function importSingleMeme(array $meme_record): bool
// Generate embedding
try {
$embedding = CloudflareAI::getVectorEmbeddingBgeSmall(
- $meme_record['name'] . ' ' . $meme_record['description'] . ' ' . implode(' ', $meme_record['keywords'])
+ $meme_record['name'].' '.$meme_record['description'].' '.implode(' ', $meme_record['keywords'])
);
} catch (\Exception $e) {
$this->command->warn("Failed to generate embedding for {$meme_record['filename']}: {$e->getMessage()}");
@@ -256,7 +255,7 @@ private function importSingleMeme(array $meme_record): bool
// Add keywords as tags
$this->attachKeywordsAsTags($meme_media, $meme_record['keywords']);
- $this->command->info('✅ Imported: ' . trim($meme_record['name']));
+ $this->command->info('✅ Imported: '.trim($meme_record['name']));
return true;
} catch (\Exception $e) {
@@ -270,11 +269,11 @@ private function attachKeywordsAsTags(MemeMedia $meme_media, array $keywords): v
try {
$meme_media->attachTags($keywords, 'meme_media');
} catch (\Exception $e) {
- $this->command->warn("Failed to attach tags to meme media '{$meme_media->name}': " . $e->getMessage());
- Log::warning("Failed to attach tags", [
+ $this->command->warn("Failed to attach tags to meme media '{$meme_media->name}': ".$e->getMessage());
+ Log::warning('Failed to attach tags', [
'category_id' => $meme_media->id,
'keywords' => $keywords,
- 'error' => $e->getMessage()
+ 'error' => $e->getMessage(),
]);
}
}
@@ -284,7 +283,7 @@ private function attachKeywordsAsTags(MemeMedia $meme_media, array $keywords): v
*/
private function generateCdnUrl(string $base_filename, string $extension): string
{
- return self::CDN_BASE_URL . "/{$extension}/{$base_filename}.{$extension}";
+ return self::CDN_BASE_URL."/{$extension}/{$base_filename}.{$extension}";
}
/**
diff --git a/resources/js/modules/editor/partials/editor-controls.jsx b/resources/js/modules/editor/partials/editor-controls.jsx
index e26d854..17fa479 100644
--- a/resources/js/modules/editor/partials/editor-controls.jsx
+++ b/resources/js/modules/editor/partials/editor-controls.jsx
@@ -4,7 +4,7 @@ import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { useMitt } from '@/plugins/MittContext';
import useVideoEditorStore from '@/stores/VideoEditorStore';
-import { Download, Edit3, Play, Square, Type } from 'lucide-react';
+import { Download, Edit3, Play, Square } from 'lucide-react';
const EditorControls = ({ className = '', onEditClick = () => {}, isEditActive = false }) => {
const { videoIsPlaying } = useVideoEditorStore();
@@ -40,9 +40,9 @@ const EditorControls = ({ className = '', onEditClick = () => {}, isEditActive =
9:16
*/}
- */}
!open && onClose()}>
-
+
@@ -201,11 +201,12 @@ export default function TextSidebar({ isOpen, onClose }) {
value={textValue}
onChange={handleTextChange}
placeholder="Enter your text..."
- className="mt-2 text-center text-nowrap dark:bg-neutral-800"
+ className="mx-auto mt-2 text-center text-wrap dark:bg-neutral-800"
rows={4}
style={{
+ maxWidth: 300,
fontFamily: fontFamily,
- fontSize: `${fontSize * 0.45}px`, // Cap preview size for readability
+ fontSize: `${fontSize * 0.5}px`, // Cap preview size for readability
fontWeight: isBold ? 'bold' : 'normal',
fontStyle: isItalic ? 'italic' : 'normal',
color: fillColor,
diff --git a/routes/test.php b/routes/test.php
index 547f898..c8bc84e 100644
--- a/routes/test.php
+++ b/routes/test.php
@@ -13,3 +13,5 @@
Route::get('/generateMeme', [TestController::class, 'generateMeme']);
Route::get('/aspectRatio', [TestController::class, 'aspectRatio']);
+
+Route::get('/getMemeKeywords', [TestController::class, 'getMemeKeywords']);