Update
This commit is contained in:
@@ -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}}
|
|
||||||
}
|
|
||||||
@@ -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}}
|
|
||||||
}
|
|
||||||
@@ -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" }
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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" }
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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" }
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "1",
|
|
||||||
"name": "Video2AI",
|
|
||||||
"type": "collection",
|
|
||||||
"ignore": [
|
|
||||||
"node_modules",
|
|
||||||
".git"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
vars {
|
|
||||||
AUTH_TOKEN: 2|4r7GpplTJeIFllLsr4fPD0oW4IX11gvbPTQg7E7Jd47df523
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Jobs\RunVideoRenderPipelineJob;
|
|
||||||
use App\Models\Video;
|
|
||||||
|
|
||||||
class JobTrigger
|
|
||||||
{
|
|
||||||
public static function RunVideoRenderPipelineJob()
|
|
||||||
{
|
|
||||||
$video = Video::latest()->first();
|
|
||||||
|
|
||||||
if ($video) {
|
|
||||||
$job = new RunVideoRenderPipelineJob($video->id);
|
|
||||||
$job->handle();
|
|
||||||
} else {
|
|
||||||
echo 'NO VIDEO';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Helpers\FirstParty\Render;
|
|
||||||
|
|
||||||
use App\Models\Video;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
use Symfony\Component\Process\Process;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class FfmpegVideoRenderer
|
|
||||||
*
|
|
||||||
* Builds and optionally executes an ffmpeg command using VideoElement models and render settings.
|
|
||||||
*/
|
|
||||||
class FfmpegVideoRenderer
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Render the given Video to a file via ffmpeg.
|
|
||||||
*
|
|
||||||
* @return object { string $name, string $path }
|
|
||||||
*
|
|
||||||
* @throws \InvalidArgumentException if required elements missing
|
|
||||||
* @throws \RuntimeException on ffmpeg failure
|
|
||||||
*/
|
|
||||||
public static function render(Video $video): object
|
|
||||||
{
|
|
||||||
$elements = self::get_video_elements($video);
|
|
||||||
$settings = self::get_render_settings($video);
|
|
||||||
|
|
||||||
// Gather elements by type and sort by time
|
|
||||||
$slide_images = $elements->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',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Helpers\FirstParty\Render;
|
|
||||||
|
|
||||||
class RenderConstants
|
|
||||||
{
|
|
||||||
const STATUS_PLANNED = 'STATUS_PLANNED'; // the render is queued for rendering
|
|
||||||
|
|
||||||
const STATUS_WAITING = 'STATUS_WAITING'; // the render is waiting for a third-party service (e.g., OpenAI or ElevenLabs) to finish
|
|
||||||
|
|
||||||
const STATUS_TRANSCRIBING = 'STATUS_TRANSCRIBING'; // an input file is being transcribed
|
|
||||||
|
|
||||||
const STATUS_RENDERING = 'STATUS_RENDERING'; // the render is being processed
|
|
||||||
|
|
||||||
const STATUS_SUCCEEDED = 'STATUS_SUCCEEDED'; // the render has been completed successfully
|
|
||||||
|
|
||||||
const STATUS_FAILED = 'STATUS_FAILED'; // the render failed due to the reason specified in the error_message field
|
|
||||||
|
|
||||||
const STATUSES = [
|
|
||||||
self::STATUS_PLANNED,
|
|
||||||
self::STATUS_WAITING,
|
|
||||||
self::STATUS_TRANSCRIBING,
|
|
||||||
self::STATUS_RENDERING,
|
|
||||||
self::STATUS_SUCCEEDED,
|
|
||||||
self::STATUS_FAILED,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App;
|
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
class FrontHomeController extends Controller
|
class FrontHomeController extends Controller
|
||||||
@@ -10,7 +9,6 @@ class FrontHomeController extends Controller
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
return Inertia::render('home/home');
|
return Inertia::render('home/home');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
app/Http/Controllers/FrontMediaController.php
Normal file
35
app/Http/Controllers/FrontMediaController.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\MemeMedia;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class FrontMediaController extends Controller
|
||||||
|
{
|
||||||
|
public function memes(Request $request)
|
||||||
|
{
|
||||||
|
$memes = MemeMedia::where('type', 'video')->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
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,377 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\Helpers\FirstParty\Render\RenderConstants;
|
|
||||||
use App\Models\User;
|
|
||||||
use App\Models\Video;
|
|
||||||
use App\Models\VideoCaption;
|
|
||||||
use App\Models\VideoElement;
|
|
||||||
use App\Models\VideoRender;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Facades\Validator;
|
|
||||||
use stdClass;
|
|
||||||
use Str;
|
|
||||||
|
|
||||||
class RenderController extends Controller
|
|
||||||
{
|
|
||||||
public function getVideoElements(Request $request, string $uuid)
|
|
||||||
{
|
|
||||||
if (! Str::isUuid($uuid)) {
|
|
||||||
return response()->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,26 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use App\Jobs\RunVideoRenderPipelineJob;
|
|
||||||
use App\Models\Video;
|
|
||||||
|
|
||||||
class TestController extends Controller
|
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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Jobs;
|
|
||||||
|
|
||||||
use App\Helpers\FirstParty\MediaEngine\MediaEngine;
|
|
||||||
use App\Helpers\FirstParty\Render\FfmpegVideoRenderer;
|
|
||||||
use App\Helpers\FirstParty\Render\RenderConstants;
|
|
||||||
use App\Models\Video;
|
|
||||||
use Illuminate\Bus\Batchable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class RunVideoRenderPipelineJob implements ShouldQueue
|
|
||||||
{
|
|
||||||
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
protected $video_id;
|
|
||||||
|
|
||||||
public $timeout = 300;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new job instance.
|
|
||||||
*/
|
|
||||||
public function __construct(int $video_id)
|
|
||||||
{
|
|
||||||
$this->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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Jobs;
|
|
||||||
|
|
||||||
use App\Helpers\FirstParty\MediaEngine\MediaEngine;
|
|
||||||
use App\Models\Media;
|
|
||||||
use App\Models\Video;
|
|
||||||
use Illuminate\Bus\Batchable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Foundation\Queue\Queueable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
|
|
||||||
class SaveVideoElementsBatchJob implements ShouldQueue
|
|
||||||
{
|
|
||||||
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
protected $video_id;
|
|
||||||
|
|
||||||
public $timeout = 300;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new job instance.
|
|
||||||
*/
|
|
||||||
public function __construct(int $video_id)
|
|
||||||
{
|
|
||||||
$this->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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
59
app/Models/MemeMedia.php
Normal file
59
app/Models/MemeMedia.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Reliese Model.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Pgvector\Laravel\HasNeighbors;
|
||||||
|
use Pgvector\Laravel\Vector;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class MemeMedia
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property string $type
|
||||||
|
* @property string $sub_type
|
||||||
|
* @property string $name
|
||||||
|
* @property string $description
|
||||||
|
* @property string $keywords
|
||||||
|
* @property uuid $media_1_uuid
|
||||||
|
* @property uuid|null $media_2_uuid
|
||||||
|
* @property string $media_1_mime_type
|
||||||
|
* @property string|null $media_2_mime_type
|
||||||
|
* @property USER-DEFINED|null $embedding
|
||||||
|
* @property Carbon|null $created_at
|
||||||
|
* @property Carbon|null $updated_at
|
||||||
|
* @property string|null $deleted_at
|
||||||
|
*
|
||||||
|
* @package App\Models
|
||||||
|
*/
|
||||||
|
class MemeMedia extends Model
|
||||||
|
{
|
||||||
|
use SoftDeletes, HasNeighbors;
|
||||||
|
protected $table = 'meme_medias';
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'media_1_uuid' => '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'
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Reliese Model.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
||||||
use Str;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Video
|
|
||||||
*
|
|
||||||
* @property int $id
|
|
||||||
* @property string|null $external_id
|
|
||||||
* @property Carbon|null $created_at
|
|
||||||
* @property Carbon|null $updated_at
|
|
||||||
* @property Collection|VideoRender[] $video_renders
|
|
||||||
*/
|
|
||||||
class Video extends Model
|
|
||||||
{
|
|
||||||
use SoftDeletes;
|
|
||||||
|
|
||||||
protected $table = 'videos';
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'width' => '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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Reliese Model.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class VideoCaption
|
|
||||||
*
|
|
||||||
* @property int $id
|
|
||||||
* @property int $video_id
|
|
||||||
* @property float $time
|
|
||||||
* @property float $duration
|
|
||||||
* @property string $text
|
|
||||||
* @property string $parameters
|
|
||||||
* @property string $payload
|
|
||||||
* @property Carbon|null $created_at
|
|
||||||
* @property Carbon|null $updated_at
|
|
||||||
* @property string|null $deleted_at
|
|
||||||
* @property Video $video
|
|
||||||
*/
|
|
||||||
class VideoCaption extends Model
|
|
||||||
{
|
|
||||||
use SoftDeletes;
|
|
||||||
|
|
||||||
protected $table = 'video_captions';
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'video_id' => '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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Reliese Model.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class VideoElement
|
|
||||||
*
|
|
||||||
* @property int $id
|
|
||||||
* @property string|null $original_asset_url
|
|
||||||
* @property uuid|null $asset_uuid
|
|
||||||
* @property string|null $asset_url
|
|
||||||
* @property string $type
|
|
||||||
* @property float $time
|
|
||||||
* @property int $track
|
|
||||||
* @property float $duration
|
|
||||||
* @property string $parameters
|
|
||||||
* @property Carbon|null $created_at
|
|
||||||
* @property Carbon|null $updated_at
|
|
||||||
*/
|
|
||||||
class VideoElement extends Model
|
|
||||||
{
|
|
||||||
use SoftDeletes;
|
|
||||||
|
|
||||||
protected $table = 'video_elements';
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'time' => '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',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by Reliese Model.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace App\Models;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
|
||||||
use Str;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class VideoRender
|
|
||||||
*
|
|
||||||
* @property int $id
|
|
||||||
* @property uuid $uuid
|
|
||||||
* @property string|null $external_id
|
|
||||||
* @property int|null $video_id
|
|
||||||
* @property int|null $user_id
|
|
||||||
* @property string $payload
|
|
||||||
* @property string $status
|
|
||||||
* @property Carbon|null $processing_started_at
|
|
||||||
* @property Carbon|null $processing_finished_at
|
|
||||||
* @property uuid|null $completed_video_uuid
|
|
||||||
* @property string|null $completed_video_full_url
|
|
||||||
* @property Carbon|null $created_at
|
|
||||||
* @property Carbon|null $updated_at
|
|
||||||
* @property Video|null $video
|
|
||||||
* @property User|null $user
|
|
||||||
*/
|
|
||||||
class VideoRender extends Model
|
|
||||||
{
|
|
||||||
use SoftDeletes;
|
|
||||||
|
|
||||||
protected $table = 'video_renders';
|
|
||||||
|
|
||||||
protected $casts = [
|
|
||||||
'video_id' => '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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
87
bash/convert_webm_to_prores4444.sh
Executable file
87
bash/convert_webm_to_prores4444.sh
Executable file
@@ -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 <absolute_path_to_directory>"
|
||||||
|
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)."
|
||||||
62
bash/test.sh
Executable file
62
bash/test.sh
Executable file
@@ -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"
|
||||||
165
bash/test_prores_to_hevc.sh
Executable file
165
bash/test_prores_to_hevc.sh
Executable file
@@ -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
|
||||||
280
bash/webm_metadata.sh
Executable file
280
bash/webm_metadata.sh
Executable file
@@ -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 "===================="
|
||||||
|
'
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
"laravel/sanctum": "^4.0",
|
"laravel/sanctum": "^4.0",
|
||||||
"laravel/tinker": "^2.10.1",
|
"laravel/tinker": "^2.10.1",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
|
"pgvector/pgvector": "^0.2.2",
|
||||||
"tightenco/ziggy": "^2.4"
|
"tightenco/ziggy": "^2.4"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
|
|||||||
59
composer.lock
generated
59
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "ae6be189232a65183967f7be5f8dfdfb",
|
"content-hash": "df8a990ceab229698a516f5c9ce18d1a",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "artesaos/seotools",
|
"name": "artesaos/seotools",
|
||||||
@@ -3063,6 +3063,63 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-11-21T10:39:51+00:00"
|
"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",
|
"name": "phpoption/phpoption",
|
||||||
"version": "1.9.3",
|
"version": "1.9.3",
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
/**
|
/**
|
||||||
* @see https://github.com/artesaos/seotools
|
* @see https://github.com/artesaos/seotools
|
||||||
*/
|
*/
|
||||||
|
|
||||||
$title = 'Meme AI Gen';
|
$title = 'Meme AI Gen';
|
||||||
$description = 'Generate memes using AI';
|
$description = 'Generate memes using AI';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
DB::statement('CREATE EXTENSION IF NOT EXISTS vector');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
DB::statement('DROP EXTENSION vector');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
<?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::create('videos', function (Blueprint $table) {
|
|
||||||
$table->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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use App\Helpers\FirstParty\Render\RenderConstants;
|
|
||||||
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::create('video_renders', function (Blueprint $table) {
|
|
||||||
$table->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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<?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::create('video_captions', function (Blueprint $table) {
|
|
||||||
$table->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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<?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::create('video_elements', function (Blueprint $table) {
|
|
||||||
$table->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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<?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('video_elements', function (Blueprint $table) {
|
|
||||||
$table->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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?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::create('meme_medias', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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') {
|
if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') {
|
||||||
Object.assign(Ziggy.routes, window.Ziggy.routes);
|
Object.assign(Ziggy.routes, window.Ziggy.routes);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Auth\SanctumAuthController;
|
use App\Http\Controllers\Auth\SanctumAuthController;
|
||||||
use App\Http\Controllers\RenderController;
|
use App\Http\Controllers\FrontMediaController;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@@ -20,3 +20,10 @@
|
|||||||
Route::post('/logout', [SanctumAuthController::class, 'logout']);
|
Route::post('/logout', [SanctumAuthController::class, 'logout']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Route::group(['prefix' => 'app'], function () {
|
||||||
|
|
||||||
|
Route::post('memes', [FrontMediaController::class, 'memes'])->name('api.app.memes');
|
||||||
|
|
||||||
|
Route::post('background', [FrontMediaController::class, 'background'])->name('api.app.background');
|
||||||
|
});
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
|
|
||||||
use App\Http\Controllers\TestController;
|
use App\Http\Controllers\TestController;
|
||||||
|
|
||||||
Route::get('RunVideoRenderPipelineJob', [TestController::class, 'RunVideoRenderPipelineJob']);
|
Route::get('/', [TestController::class, 'index'])->name('test');
|
||||||
|
|||||||
Reference in New Issue
Block a user