Update
This commit is contained in:
148
_ide_helper.php
148
_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 {
|
||||
/**
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
class AspectRatio
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the aspect ratio for given width and height
|
||||
* Returns common aspect ratios first, then computed ratios
|
||||
@@ -17,7 +16,7 @@ public static function get($width, $height)
|
||||
{
|
||||
// Handle edge cases
|
||||
if ($width <= 0 || $height <= 0) {
|
||||
return "Invalid dimensions";
|
||||
return 'Invalid dimensions';
|
||||
}
|
||||
|
||||
// Calculate the actual ratio
|
||||
@@ -70,7 +69,7 @@ private static function computeSimplifiedRatio($width, $height)
|
||||
$simplifiedWidth = $intWidth / $gcd;
|
||||
$simplifiedHeight = $intHeight / $gcd;
|
||||
|
||||
return $simplifiedWidth . ':' . $simplifiedHeight;
|
||||
return $simplifiedWidth.':'.$simplifiedHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,6 +87,7 @@ private static function gcd($a, $b)
|
||||
$b = $a % $b;
|
||||
$a = $temp;
|
||||
}
|
||||
|
||||
return $a;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
|
||||
'runware' => [
|
||||
'api_key' => env('RUNWARE_API_KEY'),
|
||||
]
|
||||
],
|
||||
|
||||
'openai' => [
|
||||
'api_key' => env('OPENAI_API_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
|
||||
@@ -24,5 +24,5 @@
|
||||
* The fully qualified class name of the pivot model.
|
||||
*/
|
||||
'class_name' => Illuminate\Database\Eloquent\Relations\MorphPivot::class,
|
||||
]
|
||||
],
|
||||
];
|
||||
|
||||
@@ -26,7 +26,6 @@ public function up(): void
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('meme_medias', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('memes', function (Blueprint $table) {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 =
|
||||
<span className="text-sm font-medium ">9:16</span>
|
||||
</Button> */}
|
||||
|
||||
<Button variant="outline" size="icon" className="h-12 w-12 rounded-full border shadow-sm">
|
||||
{/* <Button variant="outline" size="icon" className="h-12 w-12 rounded-full border shadow-sm">
|
||||
<Type className="h-8 w-8" />
|
||||
</Button>
|
||||
</Button> */}
|
||||
|
||||
<Button
|
||||
id="edit"
|
||||
|
||||
@@ -183,7 +183,7 @@ export default function TextSidebar({ isOpen, onClose }) {
|
||||
|
||||
return (
|
||||
<Sheet open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<SheetContent side="right" className="max-[140px] w-full overflow-y-auto dark:bg-neutral-900">
|
||||
<SheetContent side="right" className="max-w-[300px] overflow-y-auto dark:bg-neutral-900">
|
||||
<SheetHeader>
|
||||
<SheetTitle className="flex items-center gap-3">
|
||||
<Type className="h-6 w-6" />
|
||||
@@ -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,
|
||||
|
||||
@@ -13,3 +13,5 @@
|
||||
Route::get('/generateMeme', [TestController::class, 'generateMeme']);
|
||||
|
||||
Route::get('/aspectRatio', [TestController::class, 'aspectRatio']);
|
||||
|
||||
Route::get('/getMemeKeywords', [TestController::class, 'getMemeKeywords']);
|
||||
|
||||
Reference in New Issue
Block a user