This commit is contained in:
ct
2025-06-20 13:03:52 +08:00
parent eef45fdc9d
commit b502120091
22 changed files with 426 additions and 164 deletions

View File

@@ -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 {
/**
*

View File

@@ -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 35 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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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()
{

View File

@@ -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(

View File

@@ -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,
],
],

View File

@@ -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);

View File

@@ -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']),
);
}
}

View File

@@ -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',
];
/**

View File

@@ -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()

View File

@@ -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']),
);
}
}

View File

@@ -41,6 +41,10 @@
'runware' => [
'api_key' => env('RUNWARE_API_KEY'),
]
],
'openai' => [
'api_key' => env('OPENAI_API_KEY'),
],
];

View File

@@ -24,5 +24,5 @@
* The fully qualified class name of the pivot model.
*/
'class_name' => Illuminate\Database\Eloquent\Relations\MorphPivot::class,
]
],
];

View File

@@ -26,7 +26,6 @@ public function up(): void
});
}
/**
* Reverse the migrations.
*/

View File

@@ -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');
});
}
};

View File

@@ -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');
});
}
};

View File

@@ -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(),
]);
}
}

View File

@@ -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}";
}
/**

View File

@@ -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"

View File

@@ -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,

View File

@@ -13,3 +13,5 @@
Route::get('/generateMeme', [TestController::class, 'generateMeme']);
Route::get('/aspectRatio', [TestController::class, 'aspectRatio']);
Route::get('/getMemeKeywords', [TestController::class, 'getMemeKeywords']);