From 61923f4e1fdbbaee8bde1c1357ae6a9624e4d697 Mon Sep 17 00:00:00 2001 From: ct Date: Fri, 13 Jun 2025 17:11:59 +0800 Subject: [PATCH] Update --- app/Helpers/Global/user_access_helpers.php | 4 +- .../AdminBackgroundGenerationController.php | 191 ++++++++++++ .../Auth/RegisteredUserController.php | 2 - app/Http/Controllers/FrontMediaController.php | 3 +- app/Models/BackgroundMedia.php | 1 + config/platform/media.php | 12 + config/services.php | 4 + ..._053140_create_background_medias_table.php | 1 + resources/js/components/app-logo.tsx | 3 +- resources/js/components/app-sidebar.tsx | 13 +- resources/js/components/ui/sidebar.tsx | 2 +- resources/js/pages/admin/background-gen.tsx | 271 ++++++++++++++++++ resources/js/pages/auth/register.tsx | 30 +- resources/js/types/index.d.ts | 1 - resources/js/ziggy.js | 2 +- routes/web.php | 14 + tsconfig.json | 2 +- 17 files changed, 515 insertions(+), 41 deletions(-) create mode 100644 app/Http/Controllers/AdminBackgroundGenerationController.php create mode 100644 resources/js/pages/admin/background-gen.tsx diff --git a/app/Helpers/Global/user_access_helpers.php b/app/Helpers/Global/user_access_helpers.php index 6a663bc..aeef54b 100644 --- a/app/Helpers/Global/user_access_helpers.php +++ b/app/Helpers/Global/user_access_helpers.php @@ -11,8 +11,8 @@ function user_is_master_admin(?User $user) return false; } - $emails = ['autopilotshorts@gmail.com', 'team@autopilotshorts.com', 'charles@exastellar.com']; - $user_id = 1; + $emails = ['autopilotshorts@gmail.com', 'team@autopilotshorts.com', 'charles@exastellar.com', 'charlesteh90@gmail.com']; + $user_id = 0; if ($user->id == $user_id) { return true; diff --git a/app/Http/Controllers/AdminBackgroundGenerationController.php b/app/Http/Controllers/AdminBackgroundGenerationController.php new file mode 100644 index 0000000..37093e4 --- /dev/null +++ b/app/Http/Controllers/AdminBackgroundGenerationController.php @@ -0,0 +1,191 @@ +whereIn('list_type', ['residential', 'commercial']) + ->inRandomOrder() + ->first(); + + return Inertia::render('admin/background-gen', [ + 'pendingMedia' => $pendingMedia, + ]); + } + + public function generate(Request $request) + { + $request->validate([ + 'prompt' => 'required|string|max:1000', + 'media_id' => 'required|exists:background_medias,id', + ]); + + $prompt = $request->input('prompt'); + + // Call Replicate API + $replicateResponse = $this->callReplicateAPI($prompt); + + if (! $replicateResponse || ! isset($replicateResponse['urls']['get'])) { + return response()->json(['error' => 'Failed to start generation'], 500); + } + + // Poll for result + $result = $this->pollReplicateResult($replicateResponse['urls']['get']); + + if (! $result || ! isset($result['output'][0])) { + return response()->json(['error' => 'Generation failed or timed out'], 500); + } + + return response()->json([ + 'success' => true, + 'image_url' => $result['output'][0], + ]); + } + + public function delete($id) + { + try { + $media = BackgroundMedia::where('id', $id) + ->where('status', 'pending_media') + ->first(); + + if (! $media) { + return response()->json([ + 'success' => false, + 'error' => 'Pending media not found or already processed', + ], 404); + } + + $media->delete(); + + return response()->json([ + 'success' => true, + 'message' => 'Media deleted successfully', + ]); + } catch (\Exception $e) { + \Log::error('Error deleting pending media: '.$e->getMessage()); + + return response()->json([ + 'success' => false, + 'error' => 'An error occurred while deleting the media', + ], 500); + } + } + + public function save(Request $request) + { + $request->validate([ + 'media_id' => 'required|exists:background_medias,id', + 'image_url' => 'required|url', + 'prompt' => 'required|string', + ]); + + $backgroundMedia = BackgroundMedia::findOrFail($request->input('media_id')); + + // try { + // Use MediaEngine to download and store the image + $media = \App\Helpers\FirstParty\MediaEngine\MediaEngine::addMedia( + 'system-i', + 'image', + 'system_uploaded', + 'replicate', + null, + $request->input('image_url'), + 'download' + ); + + // Update the background media record + $backgroundMedia->update([ + 'status' => 'completed', + 'media_uuid' => $media->uuid, + 'media_url' => \App\Helpers\FirstParty\MediaEngine\MediaEngine::getMediaCloudUrl($media), + 'prompt' => $request->input('prompt'), + ]); + + return response()->json(['success' => true]); + // } catch (\Exception $e) { + // return response()->json(['error' => 'Failed to save media: ' . $e->getMessage()], 500); + // } + } + + private function callReplicateAPI($prompt) + { + $apiToken = config('services.replicate.api_token'); + + $data = [ + 'input' => [ + 'prompt' => $prompt, + 'go_fast' => true, + 'megapixels' => '1', + 'num_outputs' => 1, + 'aspect_ratio' => '1:1', + 'output_format' => 'webp', + 'output_quality' => 80, + 'num_inference_steps' => 4, + ], + ]; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, 'https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer '.$apiToken, + 'Content-Type: application/json', + 'Prefer: wait', + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 201) { + return false; + } + + return json_decode($response, true); + } + + private function pollReplicateResult($getUrl) + { + $maxAttempts = 30; // 5 minutes max (30 * 10 seconds) + $attempt = 0; + + while ($attempt < $maxAttempts) { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $getUrl); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer '.config('services.replicate.api_token'), + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + $result = json_decode($response, true); + + if ($result['status'] === 'succeeded' && isset($result['output'][0])) { + return $result; + } elseif ($result['status'] === 'failed') { + return false; + } + } + + $attempt++; + sleep(3); // Wait 10 seconds before next poll + } + + return false; // Timeout + } +} diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php index db903e8..ea87cde 100644 --- a/app/Http/Controllers/Auth/RegisteredUserController.php +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -31,13 +31,11 @@ public function create(): Response public function store(Request $request): RedirectResponse { $request->validate([ - 'name' => 'required|string|max:255', 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class, 'password' => ['required', 'confirmed', Rules\Password::defaults()], ]); $user = User::create([ - 'name' => $request->name, 'email' => $request->email, 'password' => Hash::make($request->password), ]); diff --git a/app/Http/Controllers/FrontMediaController.php b/app/Http/Controllers/FrontMediaController.php index 50b6a6c..c36437e 100644 --- a/app/Http/Controllers/FrontMediaController.php +++ b/app/Http/Controllers/FrontMediaController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Models\BackgroundMedia; use App\Models\MemeMedia; use Illuminate\Http\Request; @@ -22,7 +23,7 @@ public function memes(Request $request) public function background(Request $request) { - $backgrounds = MemeMedia::where('type', 'image')->where('sub_type', 'background')->take('30')->inRandomOrder()->get(); + $backgrounds = BackgroundMedia::where('status', 'completed')->take('30')->inRandomOrder()->get(); return response()->json([ 'success' => [ diff --git a/app/Models/BackgroundMedia.php b/app/Models/BackgroundMedia.php index 08778e5..75fe36d 100644 --- a/app/Models/BackgroundMedia.php +++ b/app/Models/BackgroundMedia.php @@ -44,5 +44,6 @@ class BackgroundMedia extends Model 'media_uuid', 'media_url', 'embedding', + 'prompt', ]; } diff --git a/config/platform/media.php b/config/platform/media.php index f36837a..b3a5c65 100644 --- a/config/platform/media.php +++ b/config/platform/media.php @@ -49,4 +49,16 @@ 'description' => 'Audios uploaded by user.', 'is_system' => false, ], + 'system-i' => [ + 'owner_type' => 'system', + 'location' => '/system-i/', + 'prefix' => 'si_', + 'postfix' => '', + 'extension' => 'png', + 'mime' => 'image/png', + 'media_type' => 'image', + 'name' => 'System Images', + 'description' => 'Images uploaded by system.', + 'is_system' => true, + ], ]; diff --git a/config/services.php b/config/services.php index 27a3617..31c2972 100644 --- a/config/services.php +++ b/config/services.php @@ -35,4 +35,8 @@ ], ], + 'replicate' => [ + 'api_token' => env('REPLICATE_API_TOKEN'), + ], + ]; diff --git a/database/migrations/2025_06_13_053140_create_background_medias_table.php b/database/migrations/2025_06_13_053140_create_background_medias_table.php index f587644..6a93b34 100644 --- a/database/migrations/2025_06_13_053140_create_background_medias_table.php +++ b/database/migrations/2025_06_13_053140_create_background_medias_table.php @@ -16,6 +16,7 @@ public function up(): void $table->string('list_type'); $table->enum('area', ['interior', 'exterior']); $table->string('location_name'); + $table->text('prompt')->nullable(); $table->enum('status', ['pending_media', 'completed'])->default('pending_media'); $table->uuid('media_uuid')->nullable(); $table->string('media_url')->nullable(); diff --git a/resources/js/components/app-logo.tsx b/resources/js/components/app-logo.tsx index 7c5919d..4144e2e 100644 --- a/resources/js/components/app-logo.tsx +++ b/resources/js/components/app-logo.tsx @@ -1,4 +1,3 @@ - export default function AppLogo() { return ( <> @@ -6,7 +5,7 @@ export default function AppLogo() { */}
- Video² AI + Meme AI Gen
); diff --git a/resources/js/components/app-sidebar.tsx b/resources/js/components/app-sidebar.tsx index 48c73bb..96ece55 100644 --- a/resources/js/components/app-sidebar.tsx +++ b/resources/js/components/app-sidebar.tsx @@ -2,11 +2,10 @@ import { NavFooter } from '@/components/nav-footer'; import { NavMain } from '@/components/nav-main'; import { NavUser } from '@/components/nav-user'; import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, SidebarMenuItem } from '@/components/ui/sidebar'; -import { type NavItem } from '@/types'; +import { SharedData, type NavItem } from '@/types'; import { Link, usePage } from '@inertiajs/react'; -import { LayoutGrid, UserCog } from 'lucide-react'; +import { LayoutGrid, UserCog } from 'lucide-react'; import AppLogo from './app-logo'; -import { SharedData } from '@/types'; const mainNavItems: NavItem[] = [ { @@ -22,6 +21,11 @@ const adminMainNavItems: NavItem[] = [ href: route('admin.dashboard'), icon: UserCog, }, + { + title: 'Background Generation', + href: route('admin.background-generation'), + icon: UserCog, + }, ]; const footerNavItems: NavItem[] = [ @@ -38,7 +42,6 @@ const footerNavItems: NavItem[] = [ ]; export function AppSidebar() { - const { auth } = usePage().props; return ( @@ -60,8 +63,6 @@ export function AppSidebar() { {auth?.user_is_admin && } - - diff --git a/resources/js/components/ui/sidebar.tsx b/resources/js/components/ui/sidebar.tsx index 1ee5a45..30638ac 100644 --- a/resources/js/components/ui/sidebar.tsx +++ b/resources/js/components/ui/sidebar.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" -import { cva, VariantProps } from "class-variance-authority" +import { cva, type VariantProps } from "class-variance-authority" import { PanelLeftIcon } from "lucide-react" import { useIsMobile } from "@/hooks/use-mobile" diff --git a/resources/js/pages/admin/background-gen.tsx b/resources/js/pages/admin/background-gen.tsx new file mode 100644 index 0000000..7d6bb8a --- /dev/null +++ b/resources/js/pages/admin/background-gen.tsx @@ -0,0 +1,271 @@ +import { Spinner } from '@/components/ui/spinner'; +import AppLayout from '@/layouts/app-layout'; +import { useAxios } from '@/plugins/AxiosContext'; +import { type BreadcrumbItem } from '@/types'; +import { Head } from '@inertiajs/react'; +import { useEffect, useState } from 'react'; + +const breadcrumbs: BreadcrumbItem[] = [ + { + title: 'Admin', + href: route('admin.dashboard'), + }, + { + title: 'Background Generation', + href: route('admin.background-generation'), + }, +]; + +export default function BackgroundGeneration({ pendingMedia }) { + const [prompt, setPrompt] = useState(pendingMedia?.prompt || ''); + const [isGenerating, setIsGenerating] = useState(false); + const [generatedImageUrl, setGeneratedImageUrl] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const axios = useAxios(); + + // Auto-assemble prompt when pendingMedia is available + useEffect(() => { + if (pendingMedia && pendingMedia.location_name && pendingMedia.list_type && pendingMedia.area) { + const assembledPrompt = + `${pendingMedia.location_name}, ${pendingMedia.list_type} ${pendingMedia.area}, wide angle shot, low angle view, sharp focus, cinematic composition`.toLowerCase(); + setPrompt(assembledPrompt); + } + }, [pendingMedia]); + + // If no pending media, show completed state + if (!pendingMedia) { + return ( + + +
+
+
+ + + +
+

All Done!

+

+ There are no pending media records to process. All background generation tasks have been completed. +

+ +
+
+
+ ); + } + + const handleGenerate = async () => { + if (!prompt.trim()) { + alert('Please enter a prompt'); + return; + } + + setIsGenerating(true); + setGeneratedImageUrl(null); + + try { + const response = await axios.post(route('admin.background-generation.generate'), { + prompt: prompt, + media_id: pendingMedia.id, + }); + + if (response.data.success) { + setGeneratedImageUrl(response.data.image_url); + } else { + alert(response.data.error || 'Generation failed'); + } + } catch (error) { + console.error('Generation error:', error); + alert('Failed to generate image'); + } finally { + setIsGenerating(false); + } + }; + + const handleSave = async () => { + if (!generatedImageUrl) { + alert('No generated image to save'); + return; + } + + setIsSaving(true); + + try { + const response = await axios.post(route('admin.background-generation.save'), { + media_id: pendingMedia.id, + image_url: generatedImageUrl, + prompt: prompt, + }); + + if (response.data.success) { + // Reload the page to start the cycle again + window.location.reload(); + } else { + alert(response.data.error || 'Failed to save'); + } + } catch (error) { + console.error('Save error:', error); + alert('Failed to save image'); + } finally { + setIsSaving(false); + } + }; + + const handleDelete = async () => { + if (!confirm('Are you sure you want to delete this pending media? This action cannot be undone.')) { + return; + } + + setIsDeleting(true); + + try { + const response = await axios.post(route('admin.background-generation.delete', pendingMedia.id)); + + if (response.data.success) { + // Reload the page to start the cycle again or show completed state + window.location.reload(); + } else { + alert(response.data.error || 'Failed to delete'); + } + } catch (error) { + console.error('Delete error:', error); + alert('Failed to delete media'); + } finally { + setIsDeleting(false); + } + }; + + return ( + + +
+ {/* Left Column - Controls */} +
+ {/* Pending Media Info */} +
+

Pending Media Information

+
+
+ ID: + {pendingMedia.id} +
+
+ List Type: + {pendingMedia.list_type} +
+
+ Area: + {pendingMedia.area} +
+
+ Location: + {pendingMedia.location_name} +
+
+ Status: + + {pendingMedia.status} + +
+
+ Created: + {new Date(pendingMedia.created_at).toLocaleDateString()} +
+
+
+ +
+
+ + {/* Generate Section */} +
+

Generate Background Image

+
+
+ +