Update
This commit is contained in:
@@ -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,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user