diff --git a/Video2AI/All Renders.bru b/Video2AI/All Renders.bru deleted file mode 100644 index 7ab22a6..0000000 --- a/Video2AI/All Renders.bru +++ /dev/null @@ -1,15 +0,0 @@ -meta { - name: All Renders - type: http - seq: 8 -} - -get { - url: https://video2ai.test/api/render/all - body: json - auth: bearer -} - -auth:bearer { - token: {{AUTH_TOKEN}} -} diff --git a/Video2AI/Check Render Status.bru b/Video2AI/Check Render Status.bru deleted file mode 100644 index 0755539..0000000 --- a/Video2AI/Check Render Status.bru +++ /dev/null @@ -1,15 +0,0 @@ -meta { - name: Check Render Status - type: http - seq: 3 -} - -get { - url: https://video2ai.test/api/render/0e6b6e60-b488-4ed2-aa14-c6b2a5b67f42 - body: none - auth: bearer -} - -auth:bearer { - token: {{AUTH_TOKEN}} -} diff --git a/Video2AI/Login.bru b/Video2AI/Login.bru deleted file mode 100644 index 0229f52..0000000 --- a/Video2AI/Login.bru +++ /dev/null @@ -1,20 +0,0 @@ -meta { - name: Login - type: http - seq: 5 -} - -post { - url: https://video2ai.test/api/user/login - body: json - auth: none -} - -headers { - Content-Type: application/json - Accept: application/json -} - -body:json { - { "email": "test@example.com", "password": "password123" } -} diff --git a/Video2AI/Logout.bru b/Video2AI/Logout.bru deleted file mode 100644 index 3f1c061..0000000 --- a/Video2AI/Logout.bru +++ /dev/null @@ -1,20 +0,0 @@ -meta { - name: Logout - type: http - seq: 6 -} - -post { - url: https://video2ai.test/api/user/logout - body: none - auth: bearer -} - -headers { - Content-Type: application/json - Accept: application/json -} - -auth:bearer { - token: 2|DSl9SRQI5za0QjqLkZBKhejSsfBEr3yECNcWk5mz1e10adc7 -} diff --git a/Video2AI/Register.bru b/Video2AI/Register.bru deleted file mode 100644 index 547104a..0000000 --- a/Video2AI/Register.bru +++ /dev/null @@ -1,20 +0,0 @@ -meta { - name: Register - type: http - seq: 4 -} - -post { - url: https://video2ai.test/api/user/register - body: json - auth: none -} - -headers { - Content-Type: application/json - Accept: application/json -} - -body:json { - { "email": "test@example.com", "password": "password123", "password_confirmation": "password123" } -} diff --git a/Video2AI/Start Render.bru b/Video2AI/Start Render.bru deleted file mode 100644 index d103a20..0000000 --- a/Video2AI/Start Render.bru +++ /dev/null @@ -1,233 +0,0 @@ -meta { - name: Start Render - type: http - seq: 2 -} - -post { - url: https://video2ai.test/api/render - body: json - auth: bearer -} - -auth:bearer { - token: {{AUTH_TOKEN}} -} - -body:json { - { - "external_id": "12345-12334293523", - "content_type":"moving_images", - "width": 720, - "height": 1280, - "aspect_ratio": "9:16", - "fps": 30, - "video_bitrate":"3M", - "audio_bitrate": "128K", - "captions": [ - { - "time":0, - "duration": 5, - "text": "Hello welcome chicken rice", - "parameters": { - "font": "Lilita One", - "font_size": 15, - "animate_rotate_words": true, - "scale_rotate_words":true, - "v_position_percentage": 20, - "h_position_percentage": 50 - }, - "words": [ - { - "start": 0, - "end": 1.0, - "text":"Hello" - }, - { - "start": 1.0, - "end": 2.0, - "text":"welcome" - }, - { - "start": 2.0, - "end": 3.0, - "text":"chicken" - }, - { - "start": 3.0, - "end": 5.0, - "text":"rice" - } - ] - }, - { - "time":5, - "duration": 7, - "text": "Follow us now", - "v_position_percentage": 20, - "h_position_percentage": 50, - "parameters": { - "font": "Lilita One", - "font_size": 15, - "animate_rotate_words": true, - "scale_rotate_words":true - }, - "words": [ - { - "start": 0, - "end": 1.0, - "text":"Follow" - }, - { - "start": 1.0, - "end": 1.4, - "text":"us" - }, - { - "start": 1.4, - "end": 2.0, - "text":"now" - } - ] - } - ], - "elements": [ - { - "url":"https://cdn.autopilotshorts.com/system-ov/sov_1741963443481-aps-watermark-25fps.mov", - "type":"video", - "external_reference":"watermark_overlay", - "time":0.0, - "track":7, - "duration": 5, - "parameters": { - "loop": true, - "background_size":"fit|maintain_aspect_ratio" - } - }, - { - "url":"https://cdn.autopilotshorts.com/generated-i/gi_1745421422078-cover-14763-1745421422045.png", - "type":"image", - "external_reference":"cover_photo", - "time":0.0, - "track":6, - "duration": 0.04, - "parameters": { - "loop": true, - "background_size":"fit|maintain_aspect_ratio" - } - }, - { - "url":"https://cdn.autopilotshorts.com/generated-i/gi_1745421417131-bg-55951.png", - "type":"image", - "external_reference":"slideshow", - "time":1.0, - "track":5, - "duration": 1.0, - "parameters": { - "animate":"random", - "particle_overlay": true, - "background_size":"fit|maintain_aspect_ratio" - } - - }, - { - "url":"https://cdn.autopilotshorts.com/generated-i/gi_1745421417296-bg-55952.png", - "type":"image", - "external_reference":"slideshow", - "time":2.0, - "track":5, - "duration": 1.0, - "parameters": { - "animate":"random", - "background_size":"fit|maintain_aspect_ratio" - } - }, - { - "url":"https://cdn.autopilotshorts.com/generated-i/gi_1745421419185-bg-55953.png", - "type":"image", - "external_reference":"slideshow", - "time":3.0, - "track":5, - "duration": 1.0, - "parameters": { - "animate":"random", - "particle_overlay": true, - "background_size":"fit|maintain_aspect_ratio" - } - }, - { - "url":"https://cdn.autopilotshorts.com/generated-i/gi_1745421418777-bg-55954.png", - "type":"image", - "external_reference":"slideshow", - "time":4.0, - "track":5, - "duration": 1.0, - "parameters": { - "animate":"random", - "particle_overlay": true, - "background_size":"fit|maintain_aspect_ratio" - } - }, - { - "url":"https://cdn.autopilotshorts.com/generated-i/gi_1745421417131-bg-55951.png", - "type":"image", - "external_reference":"slideshow", - "time":5.0, - "track":5, - "duration": 2, - "parameters": { - "animate":"random", - "background_size":"fit|maintain_aspect_ratio" - } - }, - { - "url":"https://cdn.autopilotshorts.com/generated-i/gi_1745421417131-bg-55951.png", - "type":"image", - "external_reference":"slideshow", - "time":5.0, - "track":6, - "duration": 2, - "parameters": { - "animate":"random", - "v_position_percentage": 70, - "h_position_percentage": 50, - "scale": 0.4 - } - }, - { - "url":"https://cdn.autopilotshorts.com/generated-tts/gtts_1745421385079-script-14763-1745421378856.mp3", - "type": "audio", - "external_reference":"script_audio", - "time":0, - "track": 4, - "duration": 5.0, - "parameters": { - "volume": 0.9 - } - }, - { - "url":"https://cdn.autopilotshorts.com/generated-tts/gtts_1745422892152-post-script-14763-1745422889747.mp3", - "type": "audio", - "external_reference":"post_script_audio", - "time":5, - "track": 4, - "duration": 2.0, - "parameters": { - "volume": 0.9 - } - - }, - { - "url":"https://cdn.autopilotshorts.com/system-bgm/sbgm_1723719114058-bgm-advertime.mp3", - "type": "audio", - "external_reference":"background_music", - "time":0, - "track": 3, - "duration": 7.0, - "parameters": { - "volume": 0.4 - } - } - ] - } -} diff --git a/Video2AI/User.bru b/Video2AI/User.bru deleted file mode 100644 index 56d7a32..0000000 --- a/Video2AI/User.bru +++ /dev/null @@ -1,24 +0,0 @@ -meta { - name: User - type: http - seq: 7 -} - -get { - url: https://video2ai.test/api/user - body: json - auth: bearer -} - -headers { - Content-Type: application/json - Accept: application/json -} - -auth:bearer { - token: {{AUTH_TOKEN}} -} - -body:json { - { "email": "test@example.com", "password": "password123" } -} diff --git a/Video2AI/bruno.json b/Video2AI/bruno.json deleted file mode 100644 index 3b1d002..0000000 --- a/Video2AI/bruno.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "version": "1", - "name": "Video2AI", - "type": "collection", - "ignore": [ - "node_modules", - ".git" - ] -} \ No newline at end of file diff --git a/Video2AI/environments/DEV.bru b/Video2AI/environments/DEV.bru deleted file mode 100644 index b55c69a..0000000 --- a/Video2AI/environments/DEV.bru +++ /dev/null @@ -1,3 +0,0 @@ -vars { - AUTH_TOKEN: 2|4r7GpplTJeIFllLsr4fPD0oW4IX11gvbPTQg7E7Jd47df523 -} diff --git a/app/Helpers/FirstParty/Jobs/JobTrigger.php b/app/Helpers/FirstParty/Jobs/JobTrigger.php deleted file mode 100644 index f2bdab4..0000000 --- a/app/Helpers/FirstParty/Jobs/JobTrigger.php +++ /dev/null @@ -1,19 +0,0 @@ -first(); - - if ($video) { - $job = new RunVideoRenderPipelineJob($video->id); - $job->handle(); - } else { - echo 'NO VIDEO'; - } - } -} diff --git a/app/Helpers/FirstParty/Render/FfmpegVideoRenderer.php b/app/Helpers/FirstParty/Render/FfmpegVideoRenderer.php deleted file mode 100644 index 7efccfa..0000000 --- a/app/Helpers/FirstParty/Render/FfmpegVideoRenderer.php +++ /dev/null @@ -1,163 +0,0 @@ -where('external_reference', 'slideshow')->sortBy('time')->values(); - $overlay_images = $elements->where('type', 'image') - ->where('external_reference', '!=', 'slideshow') - ->sortBy('time') - ->values(); - $video_element = $elements->firstWhere('type', 'video'); - $audio_tracks = $elements->where('type', 'audio')->sortBy('time')->values(); - - if ($slide_images->isEmpty() || ! $video_element) { - throw new \InvalidArgumentException('At least one slideshow and one video element are required.'); - } - - // Build ffmpeg input arguments - $input_args = []; - foreach ($slide_images as $slide) { - $duration = number_format($slide->duration, 2); - $input_args[] = "-loop 1 -t {$duration} -i \"{$slide->asset_url}\""; - } - foreach ($overlay_images as $overlay) { - $duration = number_format($overlay->duration, 2); - $input_args[] = "-loop 1 -t {$duration} -i \"{$overlay->asset_url}\""; - } - $input_args[] = "-i \"{$video_element->asset_url}\""; - foreach ($audio_tracks as $audio) { - $input_args[] = "-i \"{$audio->asset_url}\""; - } - - // Build filter_complex chains - $filters = []; - $slide_count = $slide_images->count(); - $concat_input = ''; - for ($i = 0; $i < $slide_count; $i++) { - $concat_input .= "[{$i}:v]"; - } - $filters[] = "{$concat_input}concat=n={$slide_count}:v=1:a=0[slideshow]"; - - $current_label = 'slideshow'; - foreach ($overlay_images as $idx => $overlay) { - $stream_idx = $slide_count + $idx; - $start_time = number_format($overlay->time, 2); - $end_time = number_format($overlay->time + $overlay->duration, 2); - $next_label = "overlay{$idx}"; - $filters[] = "[{$current_label}][{$stream_idx}:v]overlay=enable=between(t\,{$start_time}\,{$end_time})[{$next_label}]"; - $current_label = $next_label; - } - - $video_idx = $slide_count + $overlay_images->count(); - $v_start = number_format($video_element->time, 2); - $v_end = number_format($video_element->time + $video_element->duration, 2); - $filters[] = "[{$current_label}][{$video_idx}:v]overlay=enable=between(t\,{$v_start}\,{$v_end})[v_out]"; - - $audio_labels = []; - $first_audio = $video_idx + 1; - foreach ($audio_tracks as $idx => $audio) { - $stream_idx = $first_audio + $idx; - $dur = number_format($audio->duration, 2); - $start_time = number_format($audio->time, 2); - - $chain = "[{$stream_idx}:a]atrim=duration={$dur},asetpts=PTS-STARTPTS"; - if ($audio->time > 0) { - $delay_ms = (int) round($audio->time * 1000); - $chain .= ",adelay={$delay_ms}|{$delay_ms}"; - } - - $label = "a{$idx}"; - $filters[] = "{$chain}[{$label}]"; - $audio_labels[] = $label; - } - - $mix_input = ''; - foreach ($audio_labels as $lbl) { - $mix_input .= "[{$lbl}]"; - } - $audio_count = count($audio_labels); - $filters[] = "{$mix_input}amix=inputs={$audio_count}:duration=longest[a_out]"; - - $filter_complex = implode(';', $filters); - - // Prepare output - $filename = Str::random(12).'.mp4'; - $output_path = sys_get_temp_dir().DIRECTORY_SEPARATOR.$filename; - - // FFmpeg command, including render settings - $video_bitrate = $settings['video_bitrate']; - $audio_bitrate = $settings['audio_bitrate']; - $fps = $settings['fps']; - - $command = 'ffmpeg '.implode(' ', $input_args) - ." -r {$fps}" - ." -filter_complex \"{$filter_complex}\"" - .' -map "[v_out]" -map "[a_out]"' - ." -c:v libx264 -b:v {$video_bitrate}" - ." -c:a aac -b:a {$audio_bitrate}" - ." -shortest \"{$output_path}\" -y"; - - $process = Process::fromShellCommandline($command, null, null, null, 300); - $process->run(); - - if (! $process->isSuccessful()) { - return (object) [ - 'success' => false, - 'exception' => new \RuntimeException('FFmpeg failed: '.$process->getErrorOutput()), - ]; - throw new \RuntimeException('FFmpeg failed: '.$process->getErrorOutput()); - } - - return (object) [ - 'success' => true, - 'name' => $filename, - 'path' => $output_path, - ]; - } - - /** - * Fetch video elements for rendering. - */ - private static function get_video_elements(Video $video): Collection - { - return $video->video_elements; - } - - /** - * Extract render settings or defaults. - */ - private static function get_render_settings(Video $video): array - { - return [ - 'video_bitrate' => $video->render_settings?->video_bitrate ?? '3M', - 'audio_bitrate' => $video->render_settings?->audio_bitrate ?? '128k', - 'fps' => $video->render_settings?->fps ?? '30', - ]; - } -} diff --git a/app/Helpers/FirstParty/Render/RenderConstants.php b/app/Helpers/FirstParty/Render/RenderConstants.php deleted file mode 100644 index 6a561eb..0000000 --- a/app/Helpers/FirstParty/Render/RenderConstants.php +++ /dev/null @@ -1,27 +0,0 @@ -where('sub_type', 'overlay')->take('30')->inRandomOrder()->get(); + + return response()->json([ + 'success' => [ + 'data' => [ + 'memes' => $memes + ] + ] + ]); + } + + public function background(Request $request) + { + $backgrounds = MemeMedia::where('type', 'image')->where('sub_type', 'background')->take('30')->inRandomOrder()->get(); + + return response()->json([ + 'success' => [ + 'data' => [ + 'backgrounds' => $backgrounds + ] + ] + ]); + } +} diff --git a/app/Http/Controllers/RenderController.php b/app/Http/Controllers/RenderController.php deleted file mode 100644 index cfef8f7..0000000 --- a/app/Http/Controllers/RenderController.php +++ /dev/null @@ -1,377 +0,0 @@ -json([ - 'error' => [ - 'message' => 'Invalid UUID format.', - ], - ], 400); - } - - $video = Video::with('video_elements')->where('uuid', $uuid)->first(); - - if (! $video) { - return response()->json([ - 'error' => [ - 'message' => 'Video not found.', - ], - ]); - } - - return response()->json((object) [ - 'success' => [ - 'data' => [ - 'video_elements' => $video->video_elements, - ], - ], - ]); - - if (! $video_render) { - return response()->json([ - 'error' => [ - 'message' => 'Video render not found.', - ], - ]); - } - - $video = Video::where('id', $video_render->video_id)->first(); - - if (! $video) { - return response()->json([ - 'error' => [ - 'message' => 'Video not found.', - ], - ]); - } - } - - public function startRender(Request $request) - { - - $video_render_request = array_to_object_2025($request->all()); - - $video_render_action = $this->saveUserVideoRenderRequest(Auth::user(), $video_render_request); - - if (! $video_render_action->success) { - $error_message = $video_render_action?->message ? $video_render_action->message : 'Unable to render, possibly because the video is already being rendered. Check external ID.'; - - return response()->json([ - 'error' => [ - 'message' => $error_message, - - ], - ]); - } - - // Create a video - return response()->json((object) [ - 'success' => [ - 'data' => [ - 'uuid' => $video_render_action->model->uuid, - 'status' => $video_render_action->model->status, - ], - ], - ]); - } - - public function renderStatus(Request $request, string $uuid) - { - if (! Str::isUuid($uuid)) { - return response()->json([ - 'error' => [ - 'message' => 'Invalid UUID.', - ], - ]); - } - $video_render = VideoRender::where('uuid', $uuid)->first(); - - if (! $video_render) { - return response()->json([ - 'error' => [ - 'message' => 'Video render not found.', - ], - ]); - } - - return response()->json((object) [ - 'success' => [ - 'data' => [ - 'uuid' => $video_render->uuid, - 'status' => $video_render->status, - ], - ], - ]); - } - - public function allRenders(Request $request) - { - $user = Auth::user(); - - $video_renders = VideoRender::where('user_id', $user->id) - ->orderBy('id', 'desc')->get(); - - return response()->json((object) [ - 'success' => [ - 'data' => [ - 'video_renders' => $video_renders, - ], - ], - ]); - } - - private function saveUserVideoRenderRequest(User $user, stdClass $video_render_request) - { - // check if there is an existing video render request with the same external id - - $video_render_is_busy = VideoRender::where('user_id', $user->id) - ->where('external_id', $video_render_request->external_id) - ->whereIn('status', [ - RenderConstants::STATUS_PLANNED, - RenderConstants::STATUS_WAITING, - RenderConstants::STATUS_TRANSCRIBING, - RenderConstants::STATUS_RENDERING, - ])->first(); - - if ($video_render_is_busy) { - return (object) [ - 'success' => false, - 'message' => 'Video is already in queue or rendering. Status: '.$video_render_is_busy->status, - ]; - } - // dd($video_render_request); - - $video = $this->getUserVideoByExternalId($user, $video_render_request->external_id); - - $video = $this->updateVideoWithRenderRequest($video, $video_render_request); - - $video = $this->saveUserVideo($video); - - $this->saveVideoCaptions($video); - - try { - $this->saveVideoElements($video); - } catch (\Exception $e) { - return (object) [ - 'success' => false, - 'message' => $e->getMessage(), - ]; - } - - $new_video_render = VideoRender::create([ - 'user_id' => $user->id, - 'video_id' => $video->id, - 'external_id' => $video_render_request->external_id, - 'payload' => $video_render_request, - 'status' => RenderConstants::STATUS_PLANNED, - ]); - - return (object) [ - 'success' => true, - 'model' => $new_video_render, - ]; - } - - private function saveVideoCaptions(Video $video) - { - VideoCaption::where('video_id', $video->id)->delete(); - - if (isset($video->payload->captions)) { - foreach ($video->payload->captions as $caption) { - $video_caption = new VideoCaption; - $video_caption->video_id = $video->id; - $video_caption->time = $caption->time; - $video_caption->duration = $caption->duration; - $video_caption->text = $caption->text; - - if (isset($caption->parameters)) { - $video_caption->parameters = $caption->parameters; - } - - $video_caption->words = $caption->words; - $video_caption->save(); - } - } - } - - private function saveVideoElements(Video $video) - { - if (isset($video->payload->elements)) { - $existing_video_elements = VideoElement::where('video_id', $video->id)->get(); - - // Create a lookup array of existing elements by asset hash, but keep ALL matching elements - $existing_elements_by_hash = []; - foreach ($existing_video_elements as $existing_element) { - if (! isset($existing_elements_by_hash[$existing_element->asset_hash])) { - $existing_elements_by_hash[$existing_element->asset_hash] = []; - } - $existing_elements_by_hash[$existing_element->asset_hash][] = $existing_element; - } - - // Track which elements we're keeping - $kept_element_ids = []; - // Track which hashes we've already processed to handle duplicates - $processed_hashes = []; - - // Validate element URL if exist - foreach ($video->payload->elements as $element) { - if (isset($element->url)) { - if (! $this->validateElementUrl($element->url)) { - throw new \Exception('Invalid URL: '.$element->url); - } - } - } - - // Save - foreach ($video->payload->elements as $element) { - $asset_hash = $this->getAssetHash($video, $element->url); - - if (isset($existing_elements_by_hash[$asset_hash]) && count($existing_elements_by_hash[$asset_hash]) > 0) { - // Get the next unused element with this hash - $unused_elements = array_filter($existing_elements_by_hash[$asset_hash], function ($elem) use ($kept_element_ids) { - return ! in_array($elem->id, $kept_element_ids); - }); - - if (count($unused_elements) > 0) { - // Use the first unused element - $video_element = reset($unused_elements); - $kept_element_ids[] = $video_element->id; - } else { - // All elements with this hash are already used, create a new one - $video_element = new VideoElement; - $video_element->video_id = $video->id; - $video_element->asset_hash = $asset_hash; - $video_element->original_asset_url = $element->url; - } - } else { - // No elements with this hash, create a new one - $video_element = new VideoElement; - $video_element->video_id = $video->id; - $video_element->asset_hash = $asset_hash; - $video_element->original_asset_url = $element->url; - } - - if (isset($element->external_reference) && ! is_empty($element->external_reference)) { - $video_element->external_reference = $element->external_reference; - } - $video_element->type = $element->type; - $video_element->time = $element->time; - $video_element->track = $element->track; - $video_element->duration = $element->duration; - - if (isset($element->parameters)) { - $video_element->parameters = $element->parameters; - } - - $video_element->save(); - - // Add newly created ID to kept list if needed - if (! in_array($video_element->id, $kept_element_ids)) { - $kept_element_ids[] = $video_element->id; - } - } - - // Delete elements that weren't in the payload - if (count($existing_video_elements) > 0) { - VideoElement::where('video_id', $video->id) - ->whereNotIn('id', $kept_element_ids) - ->delete(); - } - } - } - - private function getAssetHash(Video $video, $url) - { - return hash('sha256', $video->id.'-'.$url); - } - - private function validateElementUrl(string $url) - { - // First check if it's a valid URL format - $validator = Validator::make(['url' => $url], [ - 'url' => 'required|url', - ]); - - if ($validator->fails()) { - return false; - } - - // validate url by making a http head request, return true | false boolean - try { - // Using Laravel's HTTP client to make a HEAD request - $response = Http::withOptions([ - 'timeout' => 10, - 'connect_timeout' => 5, - 'verify' => true, - 'http_errors' => false, // Don't throw exceptions for 4xx/5xx responses - ])->head($url); - - // Check if the response is successful (2xx status code) or a redirect (3xx) - return $response->successful(); - } catch (\Exception $e) { - // Catch any exceptions (connection issues, invalid URLs, etc.) - return false; - } - } - - private function saveUserVideo(Video $video) - { - if ($video->isDirty()) { - $video->save(); - } - - return $video; - } - - private function updateVideoWithRenderRequest(Video $video, stdClass $video_render_request) - { - // dd($video_render_request); - - $video->content_type = $video_render_request->content_type; - $video->width = $video_render_request->width; - $video->height = $video_render_request->height; - $video->aspect_ratio = $video_render_request->aspect_ratio; - $video->payload = $video_render_request; - $video->render_settings = (object) [ - 'video_bitrate' => $video_render_request->video_bitrate, - 'audio_bitrate' => $video_render_request->audio_bitrate, - 'fps' => $video_render_request->fps, - ]; - - return $video; - } - - private function getUserVideoByExternalId(User $user, string $external_id) - { - $video = Video::where('user_id', $user->id) - ->where('external_id', $external_id) - ->first(); - - if (! $video) { - - $video = new Video; - $video->user_id = $user->id; - $video->external_id = $external_id; - } - - return $video; - } -} diff --git a/app/Http/Controllers/TestController.php b/app/Http/Controllers/TestController.php index d3ef1ae..5b683dc 100644 --- a/app/Http/Controllers/TestController.php +++ b/app/Http/Controllers/TestController.php @@ -2,26 +2,10 @@ namespace App\Http\Controllers; -use App\Jobs\RunVideoRenderPipelineJob; -use App\Models\Video; - class TestController extends Controller { - public function RunVideoRenderPipelineJob() + public function index() { - $dispatch_job = true; - $video = Video::latest()->first(); - - if ($video) { - - if ($dispatch_job) { - RunVideoRenderPipelineJob::dispatch($video->id)->onQueue('render'); - } else { - $job = new RunVideoRenderPipelineJob($video->id); - $job->handle(); - } - } else { - echo 'NO VIDEO'; - } + // } } diff --git a/app/Jobs/RunVideoRenderPipelineJob.php b/app/Jobs/RunVideoRenderPipelineJob.php deleted file mode 100644 index 3002d78..0000000 --- a/app/Jobs/RunVideoRenderPipelineJob.php +++ /dev/null @@ -1,79 +0,0 @@ -onQueue('general_video'); - $this->video_id = $video_id; - } - - /** - * Execute the job. - */ - public function handle(): void - { - if ($this->batch()?->cancelled()) { - return; - } - - $video = Video::with('video_elements', 'latest_render')->find($this->video_id); - - if (! $video) { - return; - } - - $video_render = $video->latest_render; - $video_render->status = RenderConstants::STATUS_RENDERING; - $video_render->processing_started_at = now(); - $video_render->save(); - - $output = FfmpegVideoRenderer::render($video); - - if ($output->success) { - $video_render->status = RenderConstants::STATUS_SUCCEEDED; - - $saved_media = MediaEngine::addMedia( - MediaEngine::getCollectionKeyByOwnerMediaType('user', 'video'), - 'video', - MediaEngine::USER_RENDERED, - MediaEngine::USER, - $output->name, - file_get_contents($output->path), - ); - - $video_render->completed_video_uuid = $saved_media->uuid; - $video_render->completed_video_full_url = MediaEngine::getMediaCloudUrl($saved_media); - $video_render->processing_finished_at = now(); - $video_render->save(); - } else { - $video_render->processing_finished_at = now(); - $video_render->status = RenderConstants::STATUS_FAILED; - $video_render->save(); - - throw $output->exception; - } - } -} diff --git a/app/Jobs/SaveVideoElementsBatchJob.php b/app/Jobs/SaveVideoElementsBatchJob.php deleted file mode 100644 index 2476a95..0000000 --- a/app/Jobs/SaveVideoElementsBatchJob.php +++ /dev/null @@ -1,78 +0,0 @@ -onQueue('general_video'); - $this->video_id = $video_id; - } - - /** - * Execute the job. - */ - public function handle(): void - { - if ($this->batch()?->cancelled()) { - return; - } - - $video = Video::with('video_elements')->find($this->video_id); - - if (! $video) { - return; - } - - foreach ($video->video_elements as $video_element) { - - if (! is_empty($video_element->asset_uuid)) { - continue; - } - - // dump($video_element); - - // Media Details: Filename, extension and mimetype - $media_details = MediaEngine::getFileDetailsbyUrl($video_element->original_asset_url); - - // Media Content: Blob - $media_content = file_get_contents($video_element->original_asset_url); - - // Media Filename: generate a new filename - $media_filename = $video_element->type.'_'.epoch_now_timestamp().'.'.$media_details->extension; - - $saved_media = MediaEngine::addMedia( - MediaEngine::getCollectionKeyByOwnerMediaType('user', $video_element->type), - $video_element->type, - MediaEngine::USER_UPLOADED, - MediaEngine::USER, - $media_filename, - $media_content, - ); - - $video_element->asset_uuid = $saved_media->uuid; - $video_element->asset_url = MediaEngine::getMediaCloudUrl($saved_media); - $video_element->save(); - } - } -} diff --git a/app/Models/MemeMedia.php b/app/Models/MemeMedia.php new file mode 100644 index 0000000..1f63651 --- /dev/null +++ b/app/Models/MemeMedia.php @@ -0,0 +1,59 @@ + 'uuid', + 'media_2_uuid' => 'uuid', + 'embedding' => Vector::class, + ]; + + protected $fillable = [ + 'type', + 'sub_type', + 'name', + 'description', + 'keywords', + 'media_1_uuid', + 'media_2_uuid', + 'media_1_mime_type', + 'media_2_mime_type', + 'embedding' + ]; +} diff --git a/app/Models/Video.php b/app/Models/Video.php deleted file mode 100644 index 67d7771..0000000 --- a/app/Models/Video.php +++ /dev/null @@ -1,80 +0,0 @@ - 'int', - 'height' => 'int', - 'payload' => 'object', - 'render_settings' => 'object', - ]; - - protected $fillable = [ - 'external_id', - 'content_type', - 'width', - 'height', - 'aspect_ratio', - 'payload', - 'render_settings', - ]; - - protected $hidden = [ - 'id', - ]; - - public function video_renders() - { - return $this->hasMany(VideoRender::class)->orderBy('id', 'DESC'); - } - - public function video_captions() - { - return $this->hasMany(VideoCaption::class)->orderBy('time', 'ASC'); - } - - public function video_elements() - { - return $this->hasMany(VideoElement::class)->orderBy('time', 'ASC'); - } - - public function latest_render() - { - return $this->hasOne(VideoRender::class)->latest(); - } - - /** - * The "booted" method of the model. - */ - protected static function booted(): void - { - static::creating(function ($model) { - $model->uuid = $model->uuid ?? (string) Str::uuid(); - }); - } -} diff --git a/app/Models/VideoCaption.php b/app/Models/VideoCaption.php deleted file mode 100644 index 32e2c0d..0000000 --- a/app/Models/VideoCaption.php +++ /dev/null @@ -1,56 +0,0 @@ - 'int', - 'time' => 'float', - 'duration' => 'float', - 'parameters' => 'object', - 'words' => 'object', - ]; - - protected $fillable = [ - 'video_id', - 'time', - 'duration', - - 'text', - 'parameters', - 'words', - ]; - - public function video() - { - return $this->belongsTo(Video::class); - } -} diff --git a/app/Models/VideoElement.php b/app/Models/VideoElement.php deleted file mode 100644 index 3b4a278..0000000 --- a/app/Models/VideoElement.php +++ /dev/null @@ -1,54 +0,0 @@ - 'float', - 'track' => 'int', - 'duration' => 'float', - 'parameters' => 'object', - ]; - - protected $fillable = [ - 'parent_element_id', - 'external_reference', - 'asset_hash', - 'original_asset_url', - 'asset_uuid', - 'asset_url', - 'type', - 'time', - 'track', - 'duration', - 'parameters', - ]; -} diff --git a/app/Models/VideoRender.php b/app/Models/VideoRender.php deleted file mode 100644 index 9fab37b..0000000 --- a/app/Models/VideoRender.php +++ /dev/null @@ -1,83 +0,0 @@ - 'int', - 'user_id' => 'int', - 'processing_started_at' => 'datetime', - 'processing_finished_at' => 'datetime', - 'payload' => 'object', - ]; - - protected $fillable = [ - 'uuid', - 'external_id', - 'video_id', - 'user_id', - 'payload', - 'status', - 'processing_started_at', - 'processing_finished_at', - 'completed_video_uuid', - 'completed_video_full_url', - ]; - - protected $hidden = [ - 'id', - ]; - - public function video() - { - return $this->belongsTo(Video::class); - } - - public function user() - { - return $this->belongsTo(User::class); - } - - /** - * The "booted" method of the model. - */ - protected static function booted(): void - { - static::creating(function ($model) { - $model->uuid = $model->uuid ?? (string) Str::uuid(); - }); - } -} diff --git a/bash/convert_webm_to_prores4444.sh b/bash/convert_webm_to_prores4444.sh new file mode 100755 index 0000000..45fd759 --- /dev/null +++ b/bash/convert_webm_to_prores4444.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Script to convert all WebM files with alpha to ProRes 4444 MOV files +# Overwrites existing MOV files by default + +# Check if directory parameter is provided +if [ $# -eq 0 ]; then + echo "Usage: sh convert_webm_to_mov.sh " + echo "Example: sh convert_webm_to_mov.sh /Users/charlesteh/Desktop/mag-group-1/" + exit 1 +fi + +# Get the target directory from parameter +target_dir="$1" + +# Check if directory exists +if [ ! -d "$target_dir" ]; then + echo "Error: Directory '$target_dir' does not exist" + exit 1 +fi + +echo "Converting all WebM files in '$target_dir' to ProRes 4444 MOV..." + +# Check if ffmpeg is installed +if ! command -v ffmpeg &> /dev/null; then + echo "Error: ffmpeg is not installed or not in PATH" + exit 1 +fi + +# Change to target directory +cd "$target_dir" || { + echo "Error: Cannot access directory '$target_dir'" + exit 1 +} + +# Count total WebM files +webm_count=$(ls *.webm 2>/dev/null | wc -l) + +if [ $webm_count -eq 0 ]; then + echo "No WebM files found in current directory" + exit 0 +fi + +echo "Found $webm_count WebM file(s) to convert" + +# Counter for progress +count=0 + +# Loop through all WebM files in current directory +for webm_file in *.webm; do + # Check if file actually exists (in case glob doesn't match) + if [ ! -f "$webm_file" ]; then + continue + fi + + # Increment counter + count=$((count + 1)) + + # Get filename without extension + base_name=$(basename "$webm_file" .webm) + + # Output MOV filename + mov_file="${base_name}.mov" + + echo "[$count/$webm_count] Converting: $webm_file -> $mov_file" + + # Convert with ffmpeg + # -y: overwrite output files without asking + # -vcodec libvpx-vp9: Force libvpx decoder (native VP9 decoder doesn't handle alpha) + # -i: input file + # -c:v prores_ks: ProRes encoder (much better compression than qtrle) + # -profile:v 4444: ProRes 4444 profile (supports alpha) + # -pix_fmt yuva444p10le: Pixel format with alpha channel support + # -c:a aac: convert audio to AAC (MOV compatible, since Opus isn't supported in MOV) + ffmpeg -y -vcodec libvpx-vp9 -i "$webm_file" -c:v prores_ks -profile:v 4444 -pix_fmt yuva444p10le -c:a aac "$mov_file" + + # Check if conversion was successful + if [ $? -eq 0 ]; then + echo "✓ Successfully converted: $mov_file" + else + echo "✗ Failed to convert: $webm_file" + fi + + echo "" +done + +echo "Conversion complete! Processed $count file(s)." diff --git a/bash/test.sh b/bash/test.sh new file mode 100755 index 0000000..d9c0f03 --- /dev/null +++ b/bash/test.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# Debug script to test HEVC alpha conversion step by step +# Usage: ./test_hevc.sh input.webm + +if [ $# -eq 0 ]; then + echo "Usage: $0 input.webm" + exit 1 +fi + +INPUT="$1" +BASE=$(basename "$INPUT" .webm) + +echo "=== Testing HEVC Alpha Conversion ===" +echo "Input: $INPUT" +echo "" + +# Test 1: Basic HEVC (your original command format) +echo "Test 1: Basic HEVC with alpha..." +ffmpeg -y -c:v libvpx-vp9 -i "$INPUT" -c:v hevc_videotoolbox -q:v 60 -allow_sw 1 -alpha_quality 0.7 -vtag hvc1 -movflags +faststart "${BASE}_test1.mp4" +if [ $? -eq 0 ]; then + echo "✓ Test 1 successful" + ls -lh "${BASE}_test1.mp4" +else + echo "✗ Test 1 failed" +fi +echo "" + +# Test 2: Simplified approach (no audio) +echo "Test 2: HEVC without audio..." +ffmpeg -y -c:v libvpx-vp9 -i "$INPUT" -c:v hevc_videotoolbox -q:v 60 -allow_sw 1 -alpha_quality 0.7 -vtag hvc1 -an "${BASE}_test2.mp4" +if [ $? -eq 0 ]; then + echo "✓ Test 2 successful" + ls -lh "${BASE}_test2.mp4" +else + echo "✗ Test 2 failed" +fi +echo "" + +# Test 3: MOV container instead of MP4 +echo "Test 3: HEVC with MOV container..." +ffmpeg -y -c:v libvpx-vp9 -i "$INPUT" -c:v hevc_videotoolbox -q:v 60 -allow_sw 1 -alpha_quality 0.7 -vtag hvc1 "${BASE}_test3.mov" +if [ $? -eq 0 ]; then + echo "✓ Test 3 successful" + ls -lh "${BASE}_test3.mov" +else + echo "✗ Test 3 failed" +fi +echo "" + +# Test 4: Check if any files actually have alpha +echo "=== Checking results with ffprobe ===" +for file in "${BASE}_test"*.{mp4,mov}; do + if [ -f "$file" ]; then + echo "--- $file ---" + ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name,pix_fmt -of csv=p=0 "$file" + fi +done + +echo "" +echo "=== Manual Test ===" +echo "Try opening the test files in Safari/QuickTime to check for transparency" diff --git a/bash/test_prores_to_hevc.sh b/bash/test_prores_to_hevc.sh new file mode 100755 index 0000000..c9dedf9 --- /dev/null +++ b/bash/test_prores_to_hevc.sh @@ -0,0 +1,165 @@ +#!/bin/bash + +# Fixed script to convert ProRes 4444 alpha to HEVC alpha +# Usage: ./test_prores_to_hevc.sh input.mov + +if [ $# -eq 0 ]; then + echo "Usage: $0 input_prores4444.mov" + echo "Example: ./test_prores_to_hevc.sh animation.mov" + exit 1 +fi + +prores_file="$1" + +# Check if input file exists +if [ ! -f "$prores_file" ]; then + echo "Error: File '$prores_file' not found." + exit 1 +fi + +# Generate output filename +base_name=$(basename "$prores_file" .mov) +hevc_output="${base_name}_hevc.mov" + +echo "=== Testing ProRes 4444 to HEVC Alpha Conversion ===" +echo "Input ProRes: $prores_file" +echo "Output HEVC: $hevc_output" +echo "" + +# Check input file properties +echo "=== Input File Analysis ===" +echo "ProRes file properties:" +ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name,pix_fmt,width,height,bit_rate -of csv=p=0 "$prores_file" +echo "" + +# Check if input has alpha channel (HEVC embeds alpha natively) +has_alpha=$(ffprobe -v quiet -select_streams v:0 -show_entries stream=pix_fmt -of csv=p=0 "$prores_file" | grep -E "(yuva|rgba|argb|bgra|abgr)") +if [ -z "$has_alpha" ]; then + echo "Note: Input shows standard pixel format (HEVC will embed alpha natively if present)" +fi + +# Show input file size +input_size=$(stat -f%z "$prores_file" 2>/dev/null || stat -c%s "$prores_file" 2>/dev/null) +echo "Input file size: $(awk "BEGIN {printf \"%.1f\", $input_size/1024/1024}")MB" +echo "" + +# Test if VideoToolbox supports HEVC on this system +echo "=== Testing VideoToolbox HEVC Support ===" +vt_test=$(ffmpeg -f lavfi -i "color=red:size=100x100:duration=1" -c:v hevc_videotoolbox -f null - 2>&1) +if echo "$vt_test" | grep -q "not supported\|failed\|error"; then + echo "VideoToolbox doesn't support HEVC on this system, falling back to software encoder" + use_software=true +else + echo "VideoToolbox HEVC support detected" + use_software=false +fi +echo "" + +# Convert ProRes 4444 to HEVC Alpha with improved settings +echo "=== Converting ProRes 4444 to HEVC Alpha ===" + +if [ "$use_software" = true ]; then + # Software encoder fallback matching Apple's settings + echo "Using software HEVC encoder (libx265)..." + cmd="ffmpeg -y -i \"$prores_file\" -map 0:a -map 0:v -c:a aac -b:a 129k -c:v libx265 -b:v 4885k -preset medium -colorspace bt709 -color_primaries bt709 -color_trc bt709 -movflags +faststart \"$hevc_output\"" + echo "Command: $cmd" + echo "" + + ffmpeg -y -i "$prores_file" \ + -map 0:a -map 0:v \ + -c:a aac -b:a 129k \ + -c:v libx265 \ + -b:v 4885k \ + -preset medium \ + -colorspace bt709 \ + -color_primaries bt709 \ + -color_trc bt709 \ + -movflags +faststart \ + "$hevc_output" +else + # VideoToolbox matching Apple's exact Encode Media output + echo "Using VideoToolbox HEVC encoder (matching Apple's exact Encode Media)..." + cmd="ffmpeg -y -i \"$prores_file\" -map 0:a -map 0:v -c:a aac -b:a 129k -c:v hevc_videotoolbox -b:v 4885k -colorspace bt709 -color_primaries bt709 -color_trc bt709 -vtag hvc1 -movflags +faststart \"$hevc_output\"" + echo "Command: $cmd" + echo "" + + ffmpeg -y -i "$prores_file" \ + -c:v hevc_videotoolbox \ + -pix_fmt bgra \ + -alpha_quality 1 \ + -tag:v hvc1 \ + "$hevc_output" +fi + +if [ $? -eq 0 ] && [ -f "$hevc_output" ]; then + echo "" + echo "✓ SUCCESS: HEVC conversion complete!" + ls -lh "$hevc_output" + + # Verify the output file isn't corrupted + echo "" + echo "=== Verifying Output File Integrity ===" + verify_result=$(ffprobe -v error -select_streams v:0 -count_frames -show_entries stream=nb_frames -of csv=p=0 "$hevc_output" 2>&1) + if echo "$verify_result" | grep -q "error\|corrupt\|invalid"; then + echo "✗ WARNING: Output file may be corrupted" + echo "Verification result: $verify_result" + else + echo "✓ Output file integrity verified" + echo "Frame count: $verify_result" + fi + + # Show file size comparison + output_size=$(stat -f%z "$hevc_output" 2>/dev/null || stat -c%s "$hevc_output" 2>/dev/null) + if [ ! -z "$input_size" ] && [ ! -z "$output_size" ]; then + ratio=$(awk "BEGIN {printf \"%.1f\", $output_size/$input_size}") + compression=$(awk "BEGIN {printf \"%.1f\", $input_size/$output_size}") + echo "" + echo "=== File Size Comparison ===" + echo "ProRes 4444: $(awk "BEGIN {printf \"%.1f\", $input_size/1024/1024}")MB" + echo "HEVC Alpha: $(awk "BEGIN {printf \"%.1f\", $output_size/1024/1024}")MB" + echo "Size ratio: ${ratio}x (${compression}x compression)" + fi + + echo "" + echo "=== Output File Analysis ===" + echo "HEVC file properties:" + ffprobe -v quiet -select_streams v:0 -show_entries stream=codec_name,pix_fmt,width,height,bit_rate -of csv=p=0 "$hevc_output" + + # HEVC embeds alpha natively - visual verification required + echo "✓ HEVC file created (alpha embedded natively - verify visually)" + + echo "" + echo "✓ TEST COMPLETE: $hevc_output created successfully!" + echo "✓ Open the file in Safari or QuickTime to verify alpha transparency works!" + +else + echo "" + echo "✗ FAILED: HEVC conversion failed" + + # Try alternative approach if VideoToolbox failed + if [ "$use_software" = false ]; then + echo "" + echo "Trying software encoder as fallback..." + + ffmpeg -y -i "$prores_file" \ + -map 0:a -map 0:v \ + -c:a aac -b:a 129k \ + -c:v libx265 \ + -b:v 4885k \ + -preset medium \ + -colorspace bt709 \ + -color_primaries bt709 \ + -color_trc bt709 \ + -movflags +faststart \ + "${base_name}_hevc_sw.mov" + + if [ $? -eq 0 ]; then + echo "✓ SUCCESS with software encoder: ${base_name}_hevc_sw.mov" + else + echo "✗ Both hardware and software encoders failed" + exit 1 + fi + else + exit 1 + fi +fi diff --git a/bash/webm_metadata.sh b/bash/webm_metadata.sh new file mode 100755 index 0000000..b3ace14 --- /dev/null +++ b/bash/webm_metadata.sh @@ -0,0 +1,280 @@ +#!/bin/bash + +# Script to process .webm files and generate metadata using OpenAI API +# Requires: dotenv-cli, jq +# Usage: ./webm_metadata.sh [directory_path] + +set -e # Exit on any error + +# Get the directory where the script is located +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Get target directory (default to current directory) +TARGET_DIR="${1:-.}" +TARGET_DIR="$(cd "$TARGET_DIR" && pwd)" # Convert to absolute path + +# Check dependencies +command -v dotenv >/dev/null 2>&1 || { echo "Error: dotenv-cli is required. Install with: npm install -g dotenv-cli" >&2; exit 1; } +command -v jq >/dev/null 2>&1 || { echo "Error: jq is required. Install with: apt-get install jq or brew install jq" >&2; exit 1; } + +# Check if .env exists in script directory +ENV_FILE="$SCRIPT_DIR/.env" +if [ ! -f "$ENV_FILE" ]; then + echo "Error: .env file not found at $ENV_FILE" + echo "Please create one with OPENAI_API_KEY=your_key" + exit 1 +fi + +echo "Script directory: $SCRIPT_DIR" +echo "Target directory: $TARGET_DIR" +echo "Looking for .env at: $ENV_FILE" +echo "" + +# Configuration +CSV_FILE="$TARGET_DIR/webm_metadata.csv" +DELAY_SECONDS=1 + +# Function to check if filename already processed +is_processed() { + local filename="$1" + grep -q "^\"$filename\"," "$CSV_FILE" 2>/dev/null +} + +# Function to escape CSV field +escape_csv() { + local field="$1" + # Escape double quotes and wrap in quotes + echo "\"$(echo "$field" | sed 's/"/\"\"/g')\"" +} + +# Function to make API call and extract metadata +process_file() { + local filename="$1" + echo "Processing: $filename" + + # Create the system prompt text + local system_prompt="You are an AI assistant that receives a meme filename. Based on the filename, your task is to generate detailed metadata describing the meme in a structured format. + +The metadata should include these fields: +- **type**: either \`video\` or \`image\` depending on the file extension. +- **sub_type**: a classification such as \`background\` or \`overlay\` (choose \`overlay\` if it seems like an edit or reaction video/image). +- **name**: a concise, title-cased name derived from the filename (remove file extension and normalize, without the Meme word if exist). +- **description**: a short paragraph describing the meme content and context inferred from the name, and reaction +- **keywords**: a comma-separated list of relevant keywords extracted from the filename or meme context. +- **media_1_mime_type**: the MIME type derived from the file extension (e.g., \`video/webm\`, \`image/png\`). + +Return the output as a JSON object containing these fields. Use null for any unknown or missing values. + +--- + +#### Example input: +\`7th element oiiaa cat.webm\` + +#### Example output: +\`\`\`json +{ + \"type\": \"video\", + \"sub_type\": \"overlay\", + \"name\": \"7th Element Oiiaa Cat\", + \"description\": \"a cat edited to mimic or react to Vitas' bizarre vocals from the viral \\\"7th Element\\\" performance.\", + \"keywords\": \"cat, oiiaa, 7th element, vitas, webm, funny\", + \"media_1_mime_type\": \"video/webm\" +} +\`\`\`" + + local assistant_example="\`\`\`json +{ + \"type\": \"video\", + \"sub_type\": \"overlay\", + \"name\": \"Angry Cat\", + \"description\": \"A video meme featuring an angry-looking cat, typically used to express frustration, annoyance, or irritation in a humorous way.\", + \"keywords\": \"angry, cat, reaction, meme, webm, annoyed, frustration\", + \"media_1_mime_type\": \"video/webm\" +} +\`\`\`" + + # Use jq to properly construct the JSON payload + local json_payload=$(jq -n \ + --arg system_prompt "$system_prompt" \ + --arg filename "$filename" \ + --arg assistant_example "$assistant_example" \ + '{ + "model": "gpt-4.1-nano", + "input": [ + { + "role": "system", + "content": [ + { + "type": "input_text", + "text": $system_prompt + } + ] + }, + { + "role": "user", + "content": [ + { + "type": "input_text", + "text": $filename + } + ] + }, + { + "role": "assistant", + "content": [ + { + "type": "output_text", + "text": $assistant_example + } + ] + } + ], + "text": { + "format": { + "type": "json_object" + } + }, + "reasoning": {}, + "tools": [], + "temperature": 1, + "max_output_tokens": 2048, + "top_p": 1, + "store": true + }') + + # Make the API call + local response=$(curl -s "https://api.openai.com/v1/responses" \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -d "$json_payload") + + # Check for API errors + if echo "$response" | jq -e '.error' >/dev/null 2>&1; then + echo "API Error for $filename:" + echo "$response" | jq '.error' + return 1 + fi + + # Extract the response text from the correct path + local response_text=$(echo "$response" | jq -r '.output[0].content[0].text // empty') + + if [ -z "$response_text" ]; then + echo "Error: Empty response for $filename" + return 1 + fi + + # With JSON mode enabled, response should always be direct JSON + local json_content="$response_text" + + # Validate that it's valid JSON before parsing + if ! echo "$json_content" | jq . >/dev/null 2>&1; then + echo "Error: Invalid JSON response for $filename" + echo "Content: $json_content" + return 1 + fi + + # Parse individual fields + local type=$(echo "$json_content" | jq -r '.type // "unknown"') + local sub_type=$(echo "$json_content" | jq -r '.sub_type // "unknown"') + local name=$(echo "$json_content" | jq -r '.name // "unknown"') + local description=$(echo "$json_content" | jq -r '.description // "unknown"') + local keywords=$(echo "$json_content" | jq -r '.keywords // "unknown"') + local media_1_mime_type=$(echo "$json_content" | jq -r '.media_1_mime_type // "unknown"') + + # Write to CSV + local csv_line="$(escape_csv "$filename"),$(escape_csv "$type"),$(escape_csv "$sub_type"),$(escape_csv "$name"),$(escape_csv "$description"),$(escape_csv "$keywords"),$(escape_csv "$media_1_mime_type")" + echo "$csv_line" >> "$CSV_FILE" + + echo "✓ Successfully processed: $filename" +} + +# Export functions so they're available in the dotenv subshell +export -f is_processed +export -f escape_csv +export -f process_file +export CSV_FILE +export DELAY_SECONDS +export TARGET_DIR + +# Change to script directory so dotenv can find the .env file +cd "$SCRIPT_DIR" + +# Use dotenv to run the main processing logic +dotenv -- bash -c ' +# Check if OPENAI_API_KEY is loaded +if [ -z "$OPENAI_API_KEY" ]; then + echo "Error: OPENAI_API_KEY not found in environment variables" + exit 1 +fi + +echo "✓ OpenAI API key loaded successfully" +echo "CSV file will be: $CSV_FILE" +echo "Delay between requests: ${DELAY_SECONDS}s" +echo "" + +# Create CSV with headers if it doesn'\''t exist +if [ ! -f "$CSV_FILE" ]; then + echo "filename,type,sub_type,name,description,keywords,media_1_mime_type" > "$CSV_FILE" + echo "Created $CSV_FILE with headers" +fi + +echo "Starting processing of .webm files..." + +# Count total files and processed files +total_files=$(ls -1 "$TARGET_DIR"/*.webm 2>/dev/null | wc -l) +processed_count=0 +skipped_count=0 +error_count=0 + +if [ "$total_files" -eq 0 ]; then + echo "No .webm files found in $TARGET_DIR" + exit 0 +fi + +echo "Found $total_files .webm files" + +# Process each .webm file +for webm_file in "$TARGET_DIR"/*.webm; do + if [ ! -f "$webm_file" ]; then + continue + fi + + # Get just the filename (not full path) for processing + filename=$(basename "$webm_file") + + # Check if already processed + if is_processed "$filename"; then + echo "⏭️ Skipping (already processed): $filename" + ((skipped_count++)) + continue + fi + + # Process the file + if process_file "$filename"; then + ((processed_count++)) + else + echo "❌ Failed to process: $filename" + ((error_count++)) + fi + + # Progress update + total_done=$((processed_count + skipped_count + error_count)) + echo "Progress: $total_done/$total_files (Processed: $processed_count, Skipped: $skipped_count, Errors: $error_count)" + echo "" + + # Rate limiting delay (skip on last file) + if [ "$total_done" -lt "$total_files" ]; then + echo "Waiting ${DELAY_SECONDS}s before next request..." + sleep "$DELAY_SECONDS" + fi +done + +echo "====================" +echo "Processing complete!" +echo "Total files: $total_files" +echo "Newly processed: $processed_count" +echo "Already processed (skipped): $skipped_count" +echo "Errors: $error_count" +echo "CSV file: $CSV_FILE" +echo "====================" +' diff --git a/composer.json b/composer.json index f3bd5ef..ff77277 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "laravel/sanctum": "^4.0", "laravel/tinker": "^2.10.1", "league/flysystem-aws-s3-v3": "^3.0", + "pgvector/pgvector": "^0.2.2", "tightenco/ziggy": "^2.4" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 759bbc9..4866021 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ae6be189232a65183967f7be5f8dfdfb", + "content-hash": "df8a990ceab229698a516f5c9ce18d1a", "packages": [ { "name": "artesaos/seotools", @@ -3063,6 +3063,63 @@ ], "time": "2024-11-21T10:39:51+00:00" }, + { + "name": "pgvector/pgvector", + "version": "v0.2.2", + "source": { + "type": "git", + "url": "https://github.com/pgvector/pgvector-php.git", + "reference": "79e2c080df13f38e696ebd66039898dc87bdbcc7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pgvector/pgvector-php/zipball/79e2c080df13f38e696ebd66039898dc87bdbcc7", + "reference": "79e2c080df13f38e696ebd66039898dc87bdbcc7", + "shasum": "" + }, + "require": { + "php": ">= 8.1" + }, + "require-dev": { + "doctrine/dbal": "^4", + "doctrine/orm": "^3", + "illuminate/database": ">= 10", + "laravel/serializable-closure": "^1.3", + "phpunit/phpunit": "^10", + "symfony/cache": "^6" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Pgvector\\Laravel\\PgvectorServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Pgvector\\": "src/", + "Pgvector\\Laravel\\": "src/laravel/", + "Pgvector\\Doctrine\\": "src/doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andrew Kane", + "email": "andrew@ankane.org" + } + ], + "description": "pgvector support for PHP", + "support": { + "issues": "https://github.com/pgvector/pgvector-php/issues", + "source": "https://github.com/pgvector/pgvector-php" + }, + "time": "2025-02-15T23:27:08+00:00" + }, { "name": "phpoption/phpoption", "version": "1.9.3", diff --git a/config/seotools.php b/config/seotools.php index 73ea778..aa8493c 100644 --- a/config/seotools.php +++ b/config/seotools.php @@ -3,7 +3,6 @@ /** * @see https://github.com/artesaos/seotools */ - $title = 'Meme AI Gen'; $description = 'Generate memes using AI'; diff --git a/database/migrations/2022_08_03_000000_create_vector_extension.php b/database/migrations/2022_08_03_000000_create_vector_extension.php new file mode 100644 index 0000000..fccfe19 --- /dev/null +++ b/database/migrations/2022_08_03_000000_create_vector_extension.php @@ -0,0 +1,27 @@ +id(); - $table->softDeletes(); - $table->uuid('uuid'); - $table->foreignId('user_id')->nullable(); - $table->string('external_id')->nullable(); - - $table->string('content_type')->default('blank')->index(); - $table->integer('width'); - $table->integer('height'); - $table->enum('aspect_ratio', ['16:9', '9:16', '1:1', '4:3', '3:4'])->default('9:16'); - $table->json('payload'); - $table->json('render_settings'); - - $table->timestamps(); - - $table->foreign('user_id')->references('id')->on('users'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('videos'); - } -}; diff --git a/database/migrations/2025_04_23_103056_create_video_renders_table.php b/database/migrations/2025_04_23_103056_create_video_renders_table.php deleted file mode 100644 index 894bc6a..0000000 --- a/database/migrations/2025_04_23_103056_create_video_renders_table.php +++ /dev/null @@ -1,46 +0,0 @@ -id(); - $table->softDeletes(); - - $table->uuid('uuid'); - $table->string('external_id')->nullable(); - $table->foreignId('video_id')->nullable(); - $table->foreignId('user_id')->nullable(); - $table->json('payload'); - $table->enum('status', RenderConstants::STATUSES)->default(RenderConstants::STATUS_PLANNED); - - $table->datetime('processing_started_at')->nullable(); - $table->datetime('processing_finished_at')->nullable(); - - $table->uuid('completed_video_uuid')->nullable(); - $table->string('completed_video_full_url')->nullable(); - - $table->timestamps(); - - $table->foreign('video_id')->references('id')->on('videos'); - $table->foreign('user_id')->references('id')->on('users'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('video_renders'); - } -}; diff --git a/database/migrations/2025_04_23_145304_create_video_captions_table.php b/database/migrations/2025_04_23_145304_create_video_captions_table.php deleted file mode 100644 index 0b2e0d1..0000000 --- a/database/migrations/2025_04_23_145304_create_video_captions_table.php +++ /dev/null @@ -1,38 +0,0 @@ -id(); - $table->softDeletes(); - - $table->foreignId('video_id'); - $table->double('time'); - $table->double('duration'); - $table->text('text'); - $table->json('parameters'); - $table->json('words'); - - $table->timestamps(); - - $table->foreign('video_id')->references('id')->on('videos'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('video_captions'); - } -}; diff --git a/database/migrations/2025_04_24_081316_create_video_elements_table.php b/database/migrations/2025_04_24_081316_create_video_elements_table.php deleted file mode 100644 index 9bc6975..0000000 --- a/database/migrations/2025_04_24_081316_create_video_elements_table.php +++ /dev/null @@ -1,46 +0,0 @@ -id(); - $table->softDeletes(); - - $table->foreignId('video_id'); - - $table->string('external_reference')->nullable(); - - $table->string('asset_hash', 64); - - $table->string('original_asset_url')->nullable(); - $table->uuid('asset_uuid')->nullable(); - $table->string('asset_url')->nullable(); - - $table->enum('type', ['video', 'image', 'audio']); - $table->double('time'); - $table->integer('track'); - $table->double('duration'); - $table->json('parameters'); - $table->timestamps(); - - $table->foreign('video_id')->references('id')->on('videos'); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('video_elements'); - } -}; diff --git a/database/migrations/2025_04_27_062340_add_parent_element_id_to_video_elements_table.php b/database/migrations/2025_04_27_062340_add_parent_element_id_to_video_elements_table.php deleted file mode 100644 index 0066c07..0000000 --- a/database/migrations/2025_04_27_062340_add_parent_element_id_to_video_elements_table.php +++ /dev/null @@ -1,30 +0,0 @@ -foreignId('parent_element_id')->nullable(); - - $table->foreign('parent_element_id')->references('id')->on('video_elements')->nullable(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::table('video_elements', function (Blueprint $table) { - $table->dropForeign('video_elements_parent_element_id_foreign'); - }); - } -}; diff --git a/database/migrations/2025_06_07_131622_create_meme_medias_table.php b/database/migrations/2025_06_07_131622_create_meme_medias_table.php new file mode 100644 index 0000000..44c5061 --- /dev/null +++ b/database/migrations/2025_06_07_131622_create_meme_medias_table.php @@ -0,0 +1,52 @@ +id(); + $table->enum('type', ['video', 'image']); + $table->enum('sub_type', ['background', 'overlay']); + $table->string('name'); + $table->text('description'); + $table->string('keywords'); + $table->uuid('media_1_uuid'); + $table->uuid('media_2_uuid')->nullable(); + $table->enum('media_1_mime_type', [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'video/mp4', + 'video/webm' + ]); + $table->enum('media_2_mime_type', [ + 'image/jpeg', + 'image/png', + 'image/gif', + 'image/webp', + 'video/mp4', + 'video/webm' + ])->nullable(); + $table->vector('embedding', 384)->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('meme_medias'); + } +}; diff --git a/resources/js/ziggy.js b/resources/js/ziggy.js index cf79c2d..d69ee26 100644 --- a/resources/js/ziggy.js +++ b/resources/js/ziggy.js @@ -1,4 +1,4 @@ -const Ziggy = {"url":"https:\/\/memeaigen.test","port":null,"defaults":{},"routes":{"horizon.stats.index":{"uri":"horizon\/api\/stats","methods":["GET","HEAD"]},"horizon.workload.index":{"uri":"horizon\/api\/workload","methods":["GET","HEAD"]},"horizon.masters.index":{"uri":"horizon\/api\/masters","methods":["GET","HEAD"]},"horizon.monitoring.index":{"uri":"horizon\/api\/monitoring","methods":["GET","HEAD"]},"horizon.monitoring.store":{"uri":"horizon\/api\/monitoring","methods":["POST"]},"horizon.monitoring-tag.paginate":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["GET","HEAD"],"parameters":["tag"]},"horizon.monitoring-tag.destroy":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["DELETE"],"wheres":{"tag":".*"},"parameters":["tag"]},"horizon.jobs-metrics.index":{"uri":"horizon\/api\/metrics\/jobs","methods":["GET","HEAD"]},"horizon.jobs-metrics.show":{"uri":"horizon\/api\/metrics\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.queues-metrics.index":{"uri":"horizon\/api\/metrics\/queues","methods":["GET","HEAD"]},"horizon.queues-metrics.show":{"uri":"horizon\/api\/metrics\/queues\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.index":{"uri":"horizon\/api\/batches","methods":["GET","HEAD"]},"horizon.jobs-batches.show":{"uri":"horizon\/api\/batches\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.retry":{"uri":"horizon\/api\/batches\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.pending-jobs.index":{"uri":"horizon\/api\/jobs\/pending","methods":["GET","HEAD"]},"horizon.completed-jobs.index":{"uri":"horizon\/api\/jobs\/completed","methods":["GET","HEAD"]},"horizon.silenced-jobs.index":{"uri":"horizon\/api\/jobs\/silenced","methods":["GET","HEAD"]},"horizon.failed-jobs.index":{"uri":"horizon\/api\/jobs\/failed","methods":["GET","HEAD"]},"horizon.failed-jobs.show":{"uri":"horizon\/api\/jobs\/failed\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.retry-jobs.show":{"uri":"horizon\/api\/jobs\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.jobs.show":{"uri":"horizon\/api\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.index":{"uri":"horizon\/{view?}","methods":["GET","HEAD"],"wheres":{"view":"(.*)"},"parameters":["view"]},"sanctum.csrf-cookie":{"uri":"sanctum\/csrf-cookie","methods":["GET","HEAD"]},"render.start":{"uri":"api\/render","methods":["POST"]},"render.all":{"uri":"api\/render\/all","methods":["GET","HEAD"]},"render.status":{"uri":"api\/render\/{uuid}","methods":["GET","HEAD"],"parameters":["uuid"]},"render.elements.get":{"uri":"api\/render\/{uuid}\/elements","methods":["GET","HEAD"],"parameters":["uuid"]},"dashboard":{"uri":"dashboard","methods":["GET","HEAD"]},"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"]},"profile.edit":{"uri":"settings\/profile","methods":["GET","HEAD"]},"profile.update":{"uri":"settings\/profile","methods":["PATCH"]},"profile.destroy":{"uri":"settings\/profile","methods":["DELETE"]},"password.edit":{"uri":"settings\/password","methods":["GET","HEAD"]},"password.update":{"uri":"settings\/password","methods":["PUT"]},"appearance":{"uri":"settings\/appearance","methods":["GET","HEAD"]},"register":{"uri":"register","methods":["GET","HEAD"]},"login":{"uri":"login","methods":["GET","HEAD"]},"password.request":{"uri":"forgot-password","methods":["GET","HEAD"]},"password.email":{"uri":"forgot-password","methods":["POST"]},"password.reset":{"uri":"reset-password\/{token}","methods":["GET","HEAD"],"parameters":["token"]},"password.store":{"uri":"reset-password","methods":["POST"]},"verification.notice":{"uri":"verify-email","methods":["GET","HEAD"]},"verification.verify":{"uri":"verify-email\/{id}\/{hash}","methods":["GET","HEAD"],"parameters":["id","hash"]},"verification.send":{"uri":"email\/verification-notification","methods":["POST"]},"password.confirm":{"uri":"confirm-password","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"home":{"uri":"\/","methods":["GET","HEAD"]},"storage.local":{"uri":"storage\/{path}","methods":["GET","HEAD"],"wheres":{"path":".*"},"parameters":["path"]}}}; +const Ziggy = {"url":"https:\/\/memeaigen.test","port":null,"defaults":{},"routes":{"horizon.stats.index":{"uri":"horizon\/api\/stats","methods":["GET","HEAD"]},"horizon.workload.index":{"uri":"horizon\/api\/workload","methods":["GET","HEAD"]},"horizon.masters.index":{"uri":"horizon\/api\/masters","methods":["GET","HEAD"]},"horizon.monitoring.index":{"uri":"horizon\/api\/monitoring","methods":["GET","HEAD"]},"horizon.monitoring.store":{"uri":"horizon\/api\/monitoring","methods":["POST"]},"horizon.monitoring-tag.paginate":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["GET","HEAD"],"parameters":["tag"]},"horizon.monitoring-tag.destroy":{"uri":"horizon\/api\/monitoring\/{tag}","methods":["DELETE"],"wheres":{"tag":".*"},"parameters":["tag"]},"horizon.jobs-metrics.index":{"uri":"horizon\/api\/metrics\/jobs","methods":["GET","HEAD"]},"horizon.jobs-metrics.show":{"uri":"horizon\/api\/metrics\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.queues-metrics.index":{"uri":"horizon\/api\/metrics\/queues","methods":["GET","HEAD"]},"horizon.queues-metrics.show":{"uri":"horizon\/api\/metrics\/queues\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.index":{"uri":"horizon\/api\/batches","methods":["GET","HEAD"]},"horizon.jobs-batches.show":{"uri":"horizon\/api\/batches\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.jobs-batches.retry":{"uri":"horizon\/api\/batches\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.pending-jobs.index":{"uri":"horizon\/api\/jobs\/pending","methods":["GET","HEAD"]},"horizon.completed-jobs.index":{"uri":"horizon\/api\/jobs\/completed","methods":["GET","HEAD"]},"horizon.silenced-jobs.index":{"uri":"horizon\/api\/jobs\/silenced","methods":["GET","HEAD"]},"horizon.failed-jobs.index":{"uri":"horizon\/api\/jobs\/failed","methods":["GET","HEAD"]},"horizon.failed-jobs.show":{"uri":"horizon\/api\/jobs\/failed\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.retry-jobs.show":{"uri":"horizon\/api\/jobs\/retry\/{id}","methods":["POST"],"parameters":["id"]},"horizon.jobs.show":{"uri":"horizon\/api\/jobs\/{id}","methods":["GET","HEAD"],"parameters":["id"]},"horizon.index":{"uri":"horizon\/{view?}","methods":["GET","HEAD"],"wheres":{"view":"(.*)"},"parameters":["view"]},"sanctum.csrf-cookie":{"uri":"sanctum\/csrf-cookie","methods":["GET","HEAD"]},"dashboard":{"uri":"dashboard","methods":["GET","HEAD"]},"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"]},"profile.edit":{"uri":"settings\/profile","methods":["GET","HEAD"]},"profile.update":{"uri":"settings\/profile","methods":["PATCH"]},"profile.destroy":{"uri":"settings\/profile","methods":["DELETE"]},"password.edit":{"uri":"settings\/password","methods":["GET","HEAD"]},"password.update":{"uri":"settings\/password","methods":["PUT"]},"appearance":{"uri":"settings\/appearance","methods":["GET","HEAD"]},"register":{"uri":"register","methods":["GET","HEAD"]},"login":{"uri":"login","methods":["GET","HEAD"]},"password.request":{"uri":"forgot-password","methods":["GET","HEAD"]},"password.email":{"uri":"forgot-password","methods":["POST"]},"password.reset":{"uri":"reset-password\/{token}","methods":["GET","HEAD"],"parameters":["token"]},"password.store":{"uri":"reset-password","methods":["POST"]},"verification.notice":{"uri":"verify-email","methods":["GET","HEAD"]},"verification.verify":{"uri":"verify-email\/{id}\/{hash}","methods":["GET","HEAD"],"parameters":["id","hash"]},"verification.send":{"uri":"email\/verification-notification","methods":["POST"]},"password.confirm":{"uri":"confirm-password","methods":["GET","HEAD"]},"logout":{"uri":"logout","methods":["POST"]},"home":{"uri":"\/","methods":["GET","HEAD"]},"test":{"uri":"tests","methods":["GET","HEAD"]},"storage.local":{"uri":"storage\/{path}","methods":["GET","HEAD"],"wheres":{"path":".*"},"parameters":["path"]}}}; if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') { Object.assign(Ziggy.routes, window.Ziggy.routes); } diff --git a/routes/api.php b/routes/api.php index 67e6de4..a050dc2 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,7 +1,7 @@ 'app'], function () { + + Route::post('memes', [FrontMediaController::class, 'memes'])->name('api.app.memes'); + + Route::post('background', [FrontMediaController::class, 'background'])->name('api.app.background'); +}); diff --git a/routes/test.php b/routes/test.php index 96c2edc..f470310 100644 --- a/routes/test.php +++ b/routes/test.php @@ -2,4 +2,4 @@ use App\Http\Controllers\TestController; -Route::get('RunVideoRenderPipelineJob', [TestController::class, 'RunVideoRenderPipelineJob']); +Route::get('/', [TestController::class, 'index'])->name('test');