Compare commits
14 commits
main
...
feature/de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0cc56fc6e | ||
|
|
473aacd6af | ||
|
|
da2fdff1c4 | ||
|
|
4cbf517417 | ||
|
|
210e0ac1bf | ||
|
|
37be13e013 | ||
|
|
9d4f128cef | ||
|
|
ce92aee238 | ||
|
|
204cda6e75 | ||
|
|
2589fba2ec | ||
|
|
92fd1164b6 | ||
|
|
119330e093 | ||
|
|
247a956df6 | ||
|
|
beaa401c3b |
|
|
@ -28,7 +28,10 @@
|
|||
"Bash(bash:*)",
|
||||
"Bash(dos2unix:*)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:ai.google.dev)"
|
||||
"WebFetch(domain:ai.google.dev)",
|
||||
"Bash(xargs:*)",
|
||||
"Bash(php -l:*)",
|
||||
"Bash(git add -A)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
.gitignore
vendored
|
|
@ -5,7 +5,7 @@ backend/vendor/
|
|||
backend/composer.lock
|
||||
|
||||
# Build output
|
||||
frontend/dist/
|
||||
# frontend/dist/
|
||||
frontend/dist-ssr/
|
||||
*.local
|
||||
|
||||
|
|
|
|||
96
.htaccess
|
|
@ -1,10 +1,32 @@
|
|||
# ==============================================================================
|
||||
# VIDEO OPTIMIZER - FRONTEND SECURITY CONFIGURATION
|
||||
# LUX STUDIO - SPA ROUTING AND SECURITY CONFIGURATION
|
||||
# ==============================================================================
|
||||
# Location: /var/www/html/lux-studio/.htaccess
|
||||
# Purpose: Security hardening for frontend static files
|
||||
# Purpose: SPA routing, API passthrough, and security hardening
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# SPA ROUTING (Required for React Router)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
RewriteEngine On
|
||||
RewriteBase /lux-studio/
|
||||
|
||||
# Handle API requests - pass to PHP files directly
|
||||
RewriteRule ^api/(.*)$ api/$1 [L]
|
||||
|
||||
# Serve generated videos/images directly
|
||||
RewriteRule ^generated_videos/(.*)$ generated_videos/$1 [L]
|
||||
RewriteRule ^generated_images/(.*)$ generated_images/$1 [L]
|
||||
|
||||
# Serve existing files and directories
|
||||
RewriteCond %{REQUEST_FILENAME} -f [OR]
|
||||
RewriteCond %{REQUEST_FILENAME} -d
|
||||
RewriteRule ^ - [L]
|
||||
|
||||
# SPA fallback - all other requests to index.html
|
||||
RewriteRule ^ index.html [L]
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# DIRECTORY PROTECTION
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
@ -22,12 +44,7 @@ ServerSignature Off
|
|||
# FILE ACCESS CONTROL
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Default: Allow access to all files (will be restricted below)
|
||||
<FilesMatch ".*">
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
|
||||
# Deny access to sensitive files and patterns
|
||||
# Deny access to hidden files (dotfiles)
|
||||
<FilesMatch "^\.">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
|
@ -47,74 +64,11 @@ ServerSignature Off
|
|||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Deny access to PHP files (if any exist - security measure)
|
||||
<FilesMatch "\.php$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Deny access to Python files (should not be in frontend)
|
||||
<FilesMatch "\.py$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Deny access to README and documentation that shouldn't be public
|
||||
<FilesMatch "^(README|INSTALL|CHANGELOG|LICENSE|CONTRIBUTING)">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ALLOWED FILE TYPES (Explicitly allow necessary files)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Allow HTML files (main application pages)
|
||||
<FilesMatch "\.(html|htm)$">
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
|
||||
# Allow JavaScript files
|
||||
<FilesMatch "\.(js|mjs)$">
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
|
||||
# Allow CSS files
|
||||
<FilesMatch "\.css$">
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
|
||||
# Allow images
|
||||
<FilesMatch "\.(jpg|jpeg|png|gif|ico|svg|webp)$">
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
|
||||
# Allow fonts
|
||||
<FilesMatch "\.(woff|woff2|ttf|otf|eot)$">
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
|
||||
# Allow JSON files (only if needed for app functionality)
|
||||
<FilesMatch "\.json$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ERROR DOCUMENTS
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Custom error pages (optional - create these files if needed)
|
||||
# ErrorDocument 403 /video-optimizer/error/403.html
|
||||
# ErrorDocument 404 /video-optimizer/error/404.html
|
||||
# ErrorDocument 500 /video-optimizer/error/500.html
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ADDITIONAL SECURITY
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Prevent access to .htaccess itself
|
||||
<Files ".htaccess">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# END OF CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
|
|
|||
685
AI_IMPLEMENTATION_GUIDE.md
Normal file
|
|
@ -0,0 +1,685 @@
|
|||
# Nano Banana Pro - AI Implementation Guide
|
||||
## How to Build an Iterative Image Generation & Editing System with Google Gemini
|
||||
|
||||
---
|
||||
|
||||
## 🎯 CRITICAL CONCEPT: This is NOT a standard image generation API
|
||||
|
||||
**⚠️ WATCHOUT #1:** Google Gemini's image generation works COMPLETELY differently from DALL-E, Stable Diffusion, or Midjourney.
|
||||
|
||||
### Key Differences:
|
||||
1. **Uses `generateContent` endpoint** (not a dedicated image API)
|
||||
2. **Images returned as base64 in JSON** (embedded in response)
|
||||
3. **Editing = Sending previous image back** (as base64 in request)
|
||||
4. **Very aggressive content filters** (IMAGE_RECITATION errors)
|
||||
5. **No direct image URLs** (everything is base64)
|
||||
|
||||
---
|
||||
|
||||
## 📐 SYSTEM ARCHITECTURE
|
||||
|
||||
```
|
||||
User Input (Prompt/Upload)
|
||||
↓
|
||||
JavaScript Frontend (converts file to base64)
|
||||
↓
|
||||
PHP Backend API (api.php)
|
||||
↓
|
||||
Session Storage (stores base64 + MIME type)
|
||||
↓
|
||||
Google Gemini API (processes with previous image if editing)
|
||||
↓
|
||||
Extract base64 from response
|
||||
↓
|
||||
Store in session
|
||||
↓
|
||||
Display in browser (data URI)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 CRITICAL IMPLEMENTATION DETAILS
|
||||
|
||||
### 1. THE REQUEST FORMAT (MOST IMPORTANT!)
|
||||
|
||||
**⚠️ WATCHOUT #2:** The request structure is VERY specific. Get this wrong and you get 500 errors.
|
||||
|
||||
#### For NEW image generation:
|
||||
```json
|
||||
{
|
||||
"contents": [
|
||||
{
|
||||
"parts": [
|
||||
{"text": "Your detailed creative prompt here"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generationConfig": {
|
||||
"responseModalities": ["IMAGE"],
|
||||
"imageConfig": {
|
||||
"aspectRatio": "16:9",
|
||||
"imageSize": "2K"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### For EDITING existing image:
|
||||
```json
|
||||
{
|
||||
"contents": [
|
||||
{
|
||||
"parts": [
|
||||
{
|
||||
"inline_data": {
|
||||
"mime_type": "image/jpeg",
|
||||
"data": "base64_string_here"
|
||||
}
|
||||
},
|
||||
{"text": "Edit instruction prompt"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"generationConfig": {
|
||||
"responseModalities": ["IMAGE"],
|
||||
"imageConfig": {
|
||||
"aspectRatio": "16:9",
|
||||
"imageSize": "2K"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ CRITICAL:**
|
||||
- Image MUST come BEFORE text in the parts array
|
||||
- Use `inline_data` (snake_case) not `inlineData`
|
||||
- MIME type should be `image/jpeg` (what Gemini returns)
|
||||
- Base64 must be clean (no whitespace, no data URI prefix)
|
||||
|
||||
---
|
||||
|
||||
### 2. THE RESPONSE FORMAT
|
||||
|
||||
**⚠️ WATCHOUT #3:** The response structure has TWO possible formats!
|
||||
|
||||
#### Success Response (with image):
|
||||
```json
|
||||
{
|
||||
"candidates": [
|
||||
{
|
||||
"content": {
|
||||
"parts": [
|
||||
{
|
||||
"inlineData": {
|
||||
"mimeType": "image/jpeg",
|
||||
"data": "base64_image_data"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"finishReason": "STOP"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ NOTICE:** Response uses `inlineData` (camelCase), but request uses `inline_data` (snake_case)!
|
||||
|
||||
#### Blocked Response (IMAGE_RECITATION):
|
||||
```json
|
||||
{
|
||||
"candidates": [
|
||||
{
|
||||
"content": [],
|
||||
"finishReason": "IMAGE_RECITATION",
|
||||
"finishMessage": "Unable to show the generated image..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ CRITICAL:** ALWAYS check `finishReason` BEFORE trying to extract image data!
|
||||
|
||||
---
|
||||
|
||||
### 3. MIME TYPE HANDLING
|
||||
|
||||
**⚠️ WATCHOUT #4:** MIME type mismatches break image display!
|
||||
|
||||
```php
|
||||
// WRONG - hardcoded PNG:
|
||||
$_SESSION['current_image'] = $base64;
|
||||
echo '<img src="data:image/png;base64,' . $base64 . '">';
|
||||
|
||||
// RIGHT - store and use actual MIME type:
|
||||
$_SESSION['current_image'] = $base64;
|
||||
$_SESSION['current_image_mime'] = $mimeType; // e.g., "image/jpeg"
|
||||
echo '<img src="data:' . $mimeType . ';base64,' . $base64 . '">';
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
- Gemini returns `image/jpeg`
|
||||
- If you display as `image/png`, browser may fail to render
|
||||
- Store BOTH base64 data AND MIME type
|
||||
|
||||
---
|
||||
|
||||
### 4. BASE64 DATA HANDLING
|
||||
|
||||
**⚠️ WATCHOUT #5:** Base64 data must be CLEAN!
|
||||
|
||||
```javascript
|
||||
// WRONG - includes data URI prefix:
|
||||
const base64 = reader.result; // "data:image/jpeg;base64,/9j/4AAQ..."
|
||||
|
||||
// RIGHT - strip the prefix:
|
||||
const base64 = reader.result.split(',')[1]; // "/9j/4AAQ..."
|
||||
```
|
||||
|
||||
**Validation:**
|
||||
```php
|
||||
// Clean whitespace
|
||||
$inputImage = preg_replace('/\s+/', '', $inputImage);
|
||||
|
||||
// Validate format
|
||||
if (!preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $inputImage)) {
|
||||
throw new Exception("Invalid base64 format");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. THE EDITING FLOW
|
||||
|
||||
**⚠️ WATCHOUT #6:** Session management is CRITICAL for editing to work!
|
||||
|
||||
```php
|
||||
// Step 1: Generate first image
|
||||
$response = $api->generateImage("cyberpunk city", "16:9", "2K", null);
|
||||
$imageData = extractImageData($response);
|
||||
$_SESSION['current_image'] = $imageData['base64'];
|
||||
$_SESSION['current_image_mime'] = $imageData['mime_type'];
|
||||
|
||||
// Step 2: Edit existing image
|
||||
$previousImage = $_SESSION['current_image']; // Get from session
|
||||
$response = $api->generateImage("add rain", "16:9", "2K", $previousImage);
|
||||
$imageData = extractImageData($response);
|
||||
$_SESSION['current_image'] = $imageData['base64']; // Update session
|
||||
```
|
||||
|
||||
**Flow:**
|
||||
1. Store generated image in session
|
||||
2. On edit request, retrieve from session
|
||||
3. Send as `inline_data` in request
|
||||
4. Store new result back to session
|
||||
5. Repeat for each edit
|
||||
|
||||
---
|
||||
|
||||
### 6. ERROR HANDLING - THE TRICKY PART
|
||||
|
||||
**⚠️ WATCHOUT #7:** Multiple error types, each needs specific handling!
|
||||
|
||||
```php
|
||||
// Check finishReason FIRST
|
||||
if (isset($response['candidates'][0]['finishReason'])) {
|
||||
$reason = $response['candidates'][0]['finishReason'];
|
||||
|
||||
if ($reason === 'IMAGE_RECITATION') {
|
||||
throw new Exception('Blocked by content filter. Use more creative prompts.');
|
||||
}
|
||||
|
||||
if ($reason === 'SAFETY') {
|
||||
throw new Exception('Blocked by safety filters.');
|
||||
}
|
||||
|
||||
// Only proceed if STOP
|
||||
if ($reason !== 'STOP') {
|
||||
throw new Exception('Generation failed: ' . $reason);
|
||||
}
|
||||
}
|
||||
|
||||
// Then extract image
|
||||
foreach ($response['candidates'][0]['content']['parts'] as $part) {
|
||||
if (isset($part['inlineData']['data'])) {
|
||||
return $part['inlineData'];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Common Errors:**
|
||||
|
||||
| Error | HTTP Code | Cause | Solution |
|
||||
|-------|-----------|-------|----------|
|
||||
| IMAGE_RECITATION | 200 | Prompt too generic | Use creative, detailed prompts |
|
||||
| Internal error | 500 | API temporary issue | Retry with exponential backoff |
|
||||
| RESOURCE_EXHAUSTED | 429 | Rate limit | Wait 30s between requests |
|
||||
| INVALID_ARGUMENT | 400 | Bad request format | Check base64 encoding |
|
||||
|
||||
---
|
||||
|
||||
### 7. PROMPT ENGINEERING
|
||||
|
||||
**⚠️ WATCHOUT #8:** Simple prompts WILL fail!
|
||||
|
||||
```javascript
|
||||
// ❌ WILL FAIL (IMAGE_RECITATION):
|
||||
"a red circle"
|
||||
"a blue square"
|
||||
"a tree"
|
||||
"a car"
|
||||
|
||||
// ✅ WILL WORK:
|
||||
"a vintage red sports car racing through a neon-lit cyberpunk city at night"
|
||||
"a magical forest with glowing blue mushrooms and fireflies at twilight"
|
||||
"a futuristic cityscape with flying vehicles and holographic billboards"
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Minimum 10 words
|
||||
- Include adjectives (vintage, glowing, futuristic)
|
||||
- Add context (at night, in rain, during sunset)
|
||||
- Avoid single objects
|
||||
- Be creative and specific
|
||||
|
||||
---
|
||||
|
||||
### 8. FILE UPLOAD HANDLING
|
||||
|
||||
**⚠️ WATCHOUT #9:** File conversion must be done client-side!
|
||||
|
||||
```javascript
|
||||
// Convert file to base64 (client-side)
|
||||
function fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
// CRITICAL: Remove data URI prefix!
|
||||
const base64 = reader.result.split(',')[1];
|
||||
resolve(base64);
|
||||
};
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
|
||||
// Usage
|
||||
const file = uploadInput.files[0];
|
||||
const base64 = await fileToBase64(file);
|
||||
formData.append('uploadedImage', base64);
|
||||
formData.append('uploadedImageType', file.type);
|
||||
```
|
||||
|
||||
**Backend handling:**
|
||||
```php
|
||||
if ($uploadedImage) {
|
||||
// Store uploaded image
|
||||
$_SESSION['current_image'] = $uploadedImage;
|
||||
$_SESSION['current_image_mime'] = $uploadedImageType;
|
||||
|
||||
// If prompt provided, apply it
|
||||
if ($prompt) {
|
||||
$response = $api->generateImage($prompt, $aspectRatio, $imageSize, $uploadedImage);
|
||||
// Update with edited version
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. SESSION MANAGEMENT
|
||||
|
||||
**⚠️ WATCHOUT #10:** Session structure is critical!
|
||||
|
||||
```php
|
||||
// Initialize (MUST be done before any output)
|
||||
session_start();
|
||||
|
||||
// Required session variables
|
||||
$_SESSION['current_image'] = null; // Base64 string
|
||||
$_SESSION['current_image_mime'] = 'image/png'; // MIME type
|
||||
$_SESSION['conversation_history'] = []; // Array of prompts
|
||||
$_SESSION['image_history'] = []; // Array of previous images
|
||||
|
||||
// Reset (clear everything)
|
||||
$_SESSION['conversation_history'] = [];
|
||||
$_SESSION['current_image'] = null;
|
||||
$_SESSION['current_image_mime'] = 'image/png';
|
||||
$_SESSION['image_history'] = [];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 10. API CONFIGURATION
|
||||
|
||||
**⚠️ WATCHOUT #11:** Endpoint and model name are specific!
|
||||
|
||||
```php
|
||||
// CORRECT endpoint:
|
||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent";
|
||||
|
||||
// Header format:
|
||||
'x-goog-api-key: YOUR_API_KEY' // NOT 'Authorization: Bearer'
|
||||
|
||||
// Timeout:
|
||||
CURLOPT_TIMEOUT => 120 // 2 minutes - image generation is SLOW
|
||||
```
|
||||
|
||||
**Model name:** `gemini-3-pro-image-preview`
|
||||
- May change in future
|
||||
- Check Google's docs if errors persist
|
||||
|
||||
---
|
||||
|
||||
## 🐛 DEBUGGING CHECKLIST
|
||||
|
||||
When things don't work, check IN THIS ORDER:
|
||||
|
||||
### 1. Is the request format correct?
|
||||
```php
|
||||
error_log("Request payload: " . json_encode($payload));
|
||||
```
|
||||
|
||||
### 2. Is the response structure what you expect?
|
||||
```php
|
||||
error_log("Response structure: " . json_encode($response));
|
||||
```
|
||||
|
||||
### 3. Check finishReason:
|
||||
```php
|
||||
$reason = $response['candidates'][0]['finishReason'] ?? 'UNKNOWN';
|
||||
error_log("Finish reason: " . $reason);
|
||||
```
|
||||
|
||||
### 4. Verify base64 data:
|
||||
```php
|
||||
error_log("Base64 length: " . strlen($base64));
|
||||
error_log("First 50 chars: " . substr($base64, 0, 50));
|
||||
```
|
||||
|
||||
### 5. Check MIME type matching:
|
||||
```php
|
||||
error_log("Stored MIME: " . $_SESSION['current_image_mime']);
|
||||
error_log("Response MIME: " . $response['candidates'][0]['content']['parts'][0]['inlineData']['mimeType']);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎓 COMMON MISTAKES TO AVOID
|
||||
|
||||
### Mistake #1: Wrong request structure
|
||||
```json
|
||||
// ❌ WRONG - text before image:
|
||||
{"parts": [{"text": "..."}, {"inline_data": {...}}]}
|
||||
|
||||
// ✅ RIGHT - image before text:
|
||||
{"parts": [{"inline_data": {...}}, {"text": "..."}]}
|
||||
```
|
||||
|
||||
### Mistake #2: Not checking finishReason
|
||||
```php
|
||||
// ❌ WRONG - directly accessing parts:
|
||||
$image = $response['candidates'][0]['content']['parts'][0]['inlineData']['data'];
|
||||
|
||||
// ✅ RIGHT - check finishReason first:
|
||||
if ($response['candidates'][0]['finishReason'] === 'IMAGE_RECITATION') {
|
||||
// Handle blocked content
|
||||
}
|
||||
```
|
||||
|
||||
### Mistake #3: Hardcoded MIME types
|
||||
```php
|
||||
// ❌ WRONG:
|
||||
echo '<img src="data:image/png;base64,...">'; // Assumes PNG
|
||||
|
||||
// ✅ RIGHT:
|
||||
echo '<img src="data:' . $mimeType . ';base64,...">'; // Uses actual type
|
||||
```
|
||||
|
||||
### Mistake #4: Not cleaning base64
|
||||
```javascript
|
||||
// ❌ WRONG:
|
||||
const base64 = reader.result; // Includes "data:image/png;base64,"
|
||||
|
||||
// ✅ RIGHT:
|
||||
const base64 = reader.result.split(',')[1]; // Only base64 part
|
||||
```
|
||||
|
||||
### Mistake #5: Missing error handling
|
||||
```php
|
||||
// ❌ WRONG:
|
||||
$response = curl_exec($ch);
|
||||
return json_decode($response);
|
||||
|
||||
// ✅ RIGHT:
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($httpCode !== 200) {
|
||||
// Handle errors
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 DATA FLOW DIAGRAM
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ User Action │
|
||||
│ (Prompt/Upload)│
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ JavaScript │
|
||||
│ - Validate │
|
||||
│ - Convert file │
|
||||
│ - Build FormData│
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ api.php │
|
||||
│ - Get session │
|
||||
│ - Build request │
|
||||
│ - Call Gemini │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Gemini API │
|
||||
│ - Process │
|
||||
│ - Check filters │
|
||||
│ - Generate │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Extract Response│
|
||||
│ - Check finish │
|
||||
│ - Get base64 │
|
||||
│ - Get MIME type │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Store Session │
|
||||
│ - current_image │
|
||||
│ - image_mime │
|
||||
│ - history │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Return to JS │
|
||||
│ - Success flag │
|
||||
│ - Reload page │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Display Image │
|
||||
│ - Data URI │
|
||||
│ - Correct MIME │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 TESTING STRATEGY
|
||||
|
||||
### Test 1: Basic Generation
|
||||
```
|
||||
Prompt: "A futuristic motorcycle in a neon-lit city"
|
||||
Expected: Image generated successfully
|
||||
```
|
||||
|
||||
### Test 2: Simple Edit
|
||||
```
|
||||
1. Generate: "A red sports car"
|
||||
2. Edit: "add rain and reflections"
|
||||
Expected: Car now has rain
|
||||
```
|
||||
|
||||
### Test 3: Upload
|
||||
```
|
||||
1. Upload: photo.jpg
|
||||
2. No prompt
|
||||
Expected: Photo stored, ready for editing
|
||||
```
|
||||
|
||||
### Test 4: Upload + Edit
|
||||
```
|
||||
1. Upload: landscape.jpg
|
||||
2. Prompt: "make it look like a watercolor painting"
|
||||
Expected: Transformed image
|
||||
```
|
||||
|
||||
### Test 5: Error Handling
|
||||
```
|
||||
Prompt: "a blue square"
|
||||
Expected: IMAGE_RECITATION error with helpful message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 CRITICAL SUCCESS FACTORS
|
||||
|
||||
**You MUST get these right or the system will NOT work:**
|
||||
|
||||
1. ✅ **Request format** - Image before text, correct structure
|
||||
2. ✅ **Response parsing** - Check finishReason first
|
||||
3. ✅ **MIME type handling** - Store and use dynamically
|
||||
4. ✅ **Base64 cleaning** - No whitespace, no prefixes
|
||||
5. ✅ **Session management** - Store both data and MIME type
|
||||
6. ✅ **Error handling** - Different errors need different responses
|
||||
7. ✅ **Prompt quality** - Detailed, creative prompts only
|
||||
8. ✅ **File upload** - Client-side base64 conversion
|
||||
9. ✅ **API timeout** - 120 seconds minimum
|
||||
10. ✅ **Retry logic** - For temporary API failures
|
||||
|
||||
---
|
||||
|
||||
## 📝 QUICK REFERENCE
|
||||
|
||||
### Essential Code Patterns
|
||||
|
||||
**Check finishReason:**
|
||||
```php
|
||||
$reason = $response['candidates'][0]['finishReason'] ?? null;
|
||||
if ($reason !== 'STOP') {
|
||||
// Handle error
|
||||
}
|
||||
```
|
||||
|
||||
**Extract image:**
|
||||
```php
|
||||
foreach ($response['candidates'][0]['content']['parts'] as $part) {
|
||||
if (isset($part['inlineData']['data'])) {
|
||||
return [
|
||||
'base64' => $part['inlineData']['data'],
|
||||
'mime_type' => $part['inlineData']['mimeType']
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Store in session:**
|
||||
```php
|
||||
$_SESSION['current_image'] = $imageData['base64'];
|
||||
$_SESSION['current_image_mime'] = $imageData['mime_type'];
|
||||
```
|
||||
|
||||
**Display image:**
|
||||
```php
|
||||
<img src="data:<?php echo $_SESSION['current_image_mime']; ?>;base64,<?php echo $_SESSION['current_image']; ?>">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 IMPLEMENTATION CHECKLIST
|
||||
|
||||
Before considering the implementation complete:
|
||||
|
||||
- [ ] Image generation works with detailed prompts
|
||||
- [ ] Image editing works (sends previous image)
|
||||
- [ ] IMAGE_RECITATION errors handled gracefully
|
||||
- [ ] MIME type stored and used correctly
|
||||
- [ ] File upload converts to base64 properly
|
||||
- [ ] Session persists across requests
|
||||
- [ ] Error messages are helpful
|
||||
- [ ] Debug panel shows request/response
|
||||
- [ ] Simple prompts show helpful error
|
||||
- [ ] Retry logic works for 500 errors
|
||||
- [ ] Rate limiting handled
|
||||
- [ ] Base64 data validated
|
||||
- [ ] Conversation history tracked
|
||||
- [ ] Reset clears session properly
|
||||
|
||||
---
|
||||
|
||||
## 💡 TIPS FOR AI ASSISTANTS
|
||||
|
||||
When helping users implement this:
|
||||
|
||||
1. **Show the request JSON first** - Most problems are here
|
||||
2. **Emphasize finishReason checking** - Critical for error handling
|
||||
3. **Explain MIME type importance** - Common source of display issues
|
||||
4. **Warn about simple prompts** - Will trigger IMAGE_RECITATION
|
||||
5. **Test with detailed prompts** - "red circle" will fail
|
||||
6. **Check session management** - Editing requires proper storage
|
||||
7. **Validate base64 format** - Clean data is essential
|
||||
8. **Add debug logging** - Makes troubleshooting easier
|
||||
9. **Handle all error types** - Different errors need different solutions
|
||||
10. **Test the full flow** - Generate → Edit → Edit
|
||||
|
||||
---
|
||||
|
||||
## 📚 REFERENCES
|
||||
|
||||
- **API Endpoint:** `https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent`
|
||||
- **Model:** `gemini-3-pro-image-preview`
|
||||
- **Auth Header:** `x-goog-api-key: YOUR_KEY`
|
||||
- **Response Format:** JSON with base64 in `inlineData`
|
||||
- **Request Format:** JSON with `inline_data` for editing
|
||||
|
||||
---
|
||||
|
||||
## ⚡ FINAL NOTES
|
||||
|
||||
This implementation is **working and stable** when these rules are followed:
|
||||
|
||||
1. Use creative, detailed prompts (10+ words)
|
||||
2. Check `finishReason` before extracting image
|
||||
3. Store and use correct MIME types
|
||||
4. Clean base64 data (no whitespace/prefixes)
|
||||
5. Manage session properly for editing
|
||||
6. Handle all error types specifically
|
||||
7. Implement retry logic for temporary failures
|
||||
8. Validate uploaded files before processing
|
||||
|
||||
**The system works reliably when these patterns are followed exactly.**
|
||||
|
||||
---
|
||||
|
||||
*Generated from working implementation - December 2024*
|
||||
305
AUTH_README.md
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
# MSAL Authentication Setup Guide
|
||||
|
||||
## Overview
|
||||
Nano Banana Pro now includes Microsoft Authentication Library (MSAL) / Azure AD Single Sign-On (SSO) authentication. The authentication can be **toggled on/off** via environment variable for seamless testing and deployment.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Local Development (No Authentication)
|
||||
```bash
|
||||
# 1. Ensure .env file exists with:
|
||||
SSO_ENABLED=false
|
||||
|
||||
# 2. Run the app normally in MAMP
|
||||
# All users get mock "Local Developer" credentials
|
||||
# No login required
|
||||
```
|
||||
|
||||
### Production (with SSO)
|
||||
```bash
|
||||
# 1. Update .env file:
|
||||
SSO_ENABLED=true
|
||||
SSO_TENANT_ID=your-azure-tenant-id
|
||||
SSO_CLIENT_ID=your-azure-application-id
|
||||
|
||||
# 2. Deploy to server
|
||||
# 3. Users must login with Microsoft account
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
cd /Users/daveporter/Desktop/CODING-2024/NANO-RESEARCH
|
||||
composer install
|
||||
```
|
||||
|
||||
This installs the Firebase JWT library required for token validation.
|
||||
|
||||
### 2. Configure Environment
|
||||
```bash
|
||||
# Copy example file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit .env and set:
|
||||
SSO_ENABLED=false # Start with authentication disabled
|
||||
```
|
||||
|
||||
### 3. Azure AD Setup (When Enabling SSO)
|
||||
|
||||
#### Create Azure AD App Registration:
|
||||
1. Go to [Azure Portal](https://portal.azure.com)
|
||||
2. Navigate to: **Azure Active Directory** → **App registrations** → **New registration**
|
||||
3. Set name: "Nano Banana Pro"
|
||||
4. Set redirect URI: `https://your-server-url.com/path/to/app/index.php`
|
||||
5. Click **Register**
|
||||
|
||||
#### Get Credentials:
|
||||
1. Copy **Application (client) ID** → This is your `SSO_CLIENT_ID`
|
||||
2. Copy **Directory (tenant) ID** → This is your `SSO_TENANT_ID`
|
||||
3. Go to **Authentication** → Enable **ID tokens** checkbox
|
||||
4. Go to **API permissions** → Add: `openid`, `profile`, `email`
|
||||
|
||||
#### Update .env:
|
||||
```bash
|
||||
SSO_ENABLED=true
|
||||
SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### New Files Created:
|
||||
```
|
||||
/NANO-RESEARCH/
|
||||
├── composer.json # PHP dependencies (Firebase JWT)
|
||||
├── .env # Environment config (gitignored)
|
||||
├── .env.example # Template for environment variables
|
||||
├── env_loader.php # Loads .env file
|
||||
├── JWTValidator.php # JWT token validation logic
|
||||
├── AuthMiddleware.php # Auth orchestrator + login UI
|
||||
├── auth.php # Auth API endpoint
|
||||
├── auth-test.php # Debugging page
|
||||
├── AUTH_README.md # This file
|
||||
└── vendor/ # Composer dependencies (gitignored)
|
||||
```
|
||||
|
||||
### Modified Files:
|
||||
```
|
||||
config.php # Added SSO constants
|
||||
index.php # Added auth check, logout button
|
||||
api.php # Added auth check
|
||||
enhance_prompt.php # Added auth check
|
||||
.gitignore # Added .env and vendor/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
### When SSO_ENABLED=false (Testing Mode)
|
||||
1. User visits app
|
||||
2. AuthMiddleware returns mock "Local Developer" user
|
||||
3. No login page shown
|
||||
4. All features work normally
|
||||
5. Perfect for local testing
|
||||
|
||||
### When SSO_ENABLED=true (Production Mode)
|
||||
1. User visits app
|
||||
2. AuthMiddleware checks for `auth_token` cookie
|
||||
3. If no token → Show MSAL login page
|
||||
4. User clicks "Sign In with Microsoft"
|
||||
5. MSAL popup opens for Azure AD login
|
||||
6. User authenticates
|
||||
7. Token sent to `auth.php` for validation
|
||||
8. JWT validated against Azure AD public keys
|
||||
9. Token stored in httpOnly cookie (24 hours)
|
||||
10. User redirected to app
|
||||
11. Logout button visible in header
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Authentication Status
|
||||
Visit: `http://your-server/auth-test.php`
|
||||
|
||||
Shows:
|
||||
- SSO configuration (enabled/disabled)
|
||||
- Tenant ID and Client ID
|
||||
- Current authentication status
|
||||
- User information
|
||||
- Cookie presence
|
||||
|
||||
### Test Locally (SSO Disabled)
|
||||
```bash
|
||||
# 1. Set SSO_ENABLED=false in .env
|
||||
# 2. Open app in MAMP
|
||||
# 3. Should see "Welcome, Local Developer" (if SSO was previously enabled)
|
||||
# 4. App functions normally
|
||||
# 5. No login/logout buttons
|
||||
```
|
||||
|
||||
### Test on Server (SSO Enabled)
|
||||
```bash
|
||||
# NOTE: Cannot test locally - Azure AD requires exact redirect URI match
|
||||
|
||||
# 1. Deploy to production server
|
||||
# 2. Set SSO_ENABLED=true in .env on server
|
||||
# 3. Add Azure AD credentials to .env
|
||||
# 4. Visit app URL
|
||||
# 5. Should see login page
|
||||
# 6. Click "Sign In with Microsoft"
|
||||
# 7. Complete Microsoft login
|
||||
# 8. Should redirect to app
|
||||
# 9. Should see "Welcome, [Your Name]" and logout button
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Features
|
||||
|
||||
✅ **httpOnly Cookies** - Prevents XSS attacks (JavaScript can't access token)
|
||||
✅ **SameSite=Lax** - Prevents CSRF attacks
|
||||
✅ **Secure Flag** - Cookie only sent over HTTPS in production
|
||||
✅ **JWT Validation** - Cryptographic verification of tokens
|
||||
✅ **Expiration Check** - Validates `exp` claim
|
||||
✅ **Not-Before Check** - Validates `nbf` claim
|
||||
✅ **Audience Validation** - Ensures token is for our app
|
||||
✅ **Issuer Validation** - Ensures token from Azure AD
|
||||
✅ **JWKS Verification** - Uses Azure AD public keys
|
||||
✅ **24-Hour Expiration** - Tokens expire after 1 day
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Login Page Shows But Can't Login
|
||||
- Check Azure AD app registration has correct redirect URI
|
||||
- Ensure `SSO_TENANT_ID` and `SSO_CLIENT_ID` are correct
|
||||
- Check browser console for MSAL errors
|
||||
- Visit `auth-test.php` to verify configuration
|
||||
|
||||
### "Authentication Required" Error
|
||||
- Check `auth_token` cookie exists (browser dev tools)
|
||||
- Token may have expired (24-hour limit)
|
||||
- Try logging out and back in
|
||||
- Check `auth-test.php` for token status
|
||||
|
||||
### SSO Not Disabling
|
||||
- Verify `.env` has `SSO_ENABLED=false` (not "false" in quotes)
|
||||
- Clear browser cookies
|
||||
- Restart PHP server/MAMP
|
||||
- Check `auth-test.php` shows "SSO Enabled: NO"
|
||||
|
||||
### Token Validation Failing
|
||||
- Check server can reach Azure AD endpoints
|
||||
- Verify tenant ID and client ID match Azure AD
|
||||
- Check token hasn't expired
|
||||
- Review `error_log` for JWT validation details
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Login
|
||||
```http
|
||||
POST /auth.php
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"action": "login",
|
||||
"idToken": "eyJ0eXAiOiJKV1QiLCJhbGci...",
|
||||
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGci..."
|
||||
}
|
||||
```
|
||||
|
||||
### Logout
|
||||
```http
|
||||
POST /auth.php
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"action": "logout"
|
||||
}
|
||||
```
|
||||
|
||||
### Status Check
|
||||
```http
|
||||
POST /auth.php
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"action": "status"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Rotating Credentials
|
||||
1. Update Azure AD app registration
|
||||
2. Update `.env` with new credentials
|
||||
3. No code changes needed
|
||||
4. Existing sessions remain valid until cookie expires
|
||||
|
||||
### Disabling SSO Temporarily
|
||||
```bash
|
||||
# In .env:
|
||||
SSO_ENABLED=false
|
||||
|
||||
# Immediately disables SSO for all users
|
||||
# No restart needed
|
||||
# Users get mock "Local Developer" access
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
- Check `error_log` for authentication failures
|
||||
- Monitor Azure AD sign-in logs
|
||||
- Track failed login attempts
|
||||
- Review token validation errors
|
||||
|
||||
---
|
||||
|
||||
## Production Checklist
|
||||
|
||||
Before enabling SSO in production:
|
||||
|
||||
- [ ] Composer dependencies installed (`vendor/` directory exists)
|
||||
- [ ] `.env` file configured with Azure AD credentials
|
||||
- [ ] Azure AD app registration created
|
||||
- [ ] Redirect URI matches production URL exactly
|
||||
- [ ] ID tokens enabled in Azure AD app
|
||||
- [ ] API permissions added (`openid`, `profile`, `email`)
|
||||
- [ ] HTTPS enabled on production server
|
||||
- [ ] `auth-test.php` shows correct configuration
|
||||
- [ ] Test login/logout flow works
|
||||
- [ ] Error logging enabled
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For issues with:
|
||||
- **MSAL errors**: Check [MSAL.js documentation](https://github.com/AzureAD/microsoft-authentication-library-for-js)
|
||||
- **Azure AD setup**: Check [Azure AD app registration guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)
|
||||
- **JWT validation**: Check Firebase JWT library logs in `error_log`
|
||||
- **Configuration**: Run `auth-test.php` to see current setup
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Cannot test MSAL locally** - Azure AD requires exact URL match
|
||||
- **Testing happens on server** after deployment
|
||||
- **SSO toggle allows testing without auth** before enabling
|
||||
- **httpOnly cookies** mean token not accessible via JavaScript
|
||||
- **24-hour token expiration** - users must re-login daily
|
||||
- **Mock user** (`dev@localhost`) used when SSO disabled
|
||||
564
CLAUDE.md
|
|
@ -4,40 +4,36 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||
|
||||
## Project Overview
|
||||
|
||||
**Lux Studio** is an AI-powered cinematography suite for professional image and video generation, combining physics-based prompt engineering with Google's Imagen 3 and Veo 3.1 APIs. The application uses a project-first workflow with local IndexedDB storage.
|
||||
**Lux Studio** is an AI-powered cinematography suite for professional image and video generation. It combines physics-based prompt engineering with Google's Imagen 3 and Veo 3.1 APIs to enable iterative creative workflows with professional cinematographer tools.
|
||||
|
||||
**Key Architecture**: React frontend (Vite) + PHP backend + IndexedDB storage
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
cinema-studio-pro/
|
||||
├── frontend/ # React frontend application
|
||||
lux-studio-app/
|
||||
├── frontend/ # React + Vite frontend application
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── hooks/ # Custom hooks (IndexedDB, projects)
|
||||
│ │ ├── authConfig.js # MSAL authentication config
|
||||
│ │ └── App.jsx # Root component
|
||||
│ ├── .env # Frontend environment variables (gitignored)
|
||||
│ ├── .env.example # Frontend env template
|
||||
│ └── package.json
|
||||
├── backend/ # PHP backend APIs
|
||||
│ ├── api.php # Image generation API
|
||||
│ ├── video_api.php # Video generation API
|
||||
│ ├── stream_video.php # Video streaming
|
||||
│ ├── enhance_prompt.php # Prompt optimization
|
||||
│ ├── session_manager.php # Multi-user session management
|
||||
│ ├── AuthMiddleware.php # SSO authentication
|
||||
│ ├── uploads/ # Temporary file storage
|
||||
│ ├── .env # Backend environment variables (gitignored)
|
||||
│ ├── .env.example # Backend env template
|
||||
│ └── composer.json # PHP dependencies
|
||||
├── MDFiles/ # Documentation
|
||||
│ ├── README.md
|
||||
│ ├── INSTALL.md
|
||||
│ ├── AI_IMPLEMENTATION_GUIDE.md
|
||||
│ ├── QUICK_REFERENCE.md
|
||||
│ └── AUTH_README.md
|
||||
└── CLAUDE.md # This file
|
||||
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── hooks/ # Custom React hooks (IndexedDB, projects)
|
||||
│ │ └── assets/ # Static assets
|
||||
│ ├── .env # Frontend configuration (ports, API keys)
|
||||
│ ├── .env.example # Frontend config template
|
||||
│ ├── vite.config.js # Vite configuration with dynamic proxy
|
||||
│ └── package.json # Frontend dependencies
|
||||
│
|
||||
├── backend/ # PHP backend API
|
||||
│ ├── api.php # Image generation endpoint (Imagen 3)
|
||||
│ ├── video_api.php # Video generation endpoint (Veo 3.1)
|
||||
│ ├── stream_video.php # Video streaming with Range support
|
||||
│ ├── enhance_prompt.php # AI prompt optimization
|
||||
│ ├── session_manager.php # Session management
|
||||
│ ├── config.example.php # Backend config template
|
||||
│ └── .env # Backend configuration (ports)
|
||||
│
|
||||
├── api.php.backup # Backup of original api.php
|
||||
├── video_api.php.backup # Backup of original video_api.php
|
||||
└── CLAUDE.md # This file
|
||||
```
|
||||
|
||||
## Development Commands
|
||||
|
|
@ -52,68 +48,113 @@ cinema-studio-pro/
|
|||
```
|
||||
|
||||
### Frontend (React + Vite)
|
||||
|
||||
All frontend commands must be run from the [frontend/](frontend/) directory:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install # Install dependencies
|
||||
npm run dev # Start dev server (port from .env, default: 3000)
|
||||
npm run build # Build for production
|
||||
npm run preview # Preview production build
|
||||
npm run lint # Run ESLint
|
||||
npm install # Install dependencies
|
||||
npm run dev # Start dev server (port 3000, configurable via VITE_FRONTEND_PORT)
|
||||
npm run build # Production build → dist/
|
||||
npm run preview # Preview production build
|
||||
npm run lint # ESLint code quality checks
|
||||
```
|
||||
|
||||
### Backend (PHP)
|
||||
|
||||
Run from the [backend/](backend/) directory:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
composer install # Install PHP dependencies (Firebase JWT)
|
||||
# Port is configured in .env (BACKEND_PORT, default: 5015)
|
||||
php -S localhost:5015 # Start PHP development server
|
||||
php -S localhost:5015 # Start PHP backend server (port configurable via BACKEND_PORT in .env)
|
||||
```
|
||||
|
||||
### Full Development Setup
|
||||
**Both servers must run simultaneously** - Vite proxies `/api/*` calls to the PHP backend (port 5015).
|
||||
|
||||
## Configuration
|
||||
|
||||
### API Key Setup
|
||||
|
||||
**Backend**: Copy example config and add your API key:
|
||||
```bash
|
||||
# Terminal 1: Backend (reads port from backend/.env)
|
||||
cd backend && php -S localhost:5015
|
||||
|
||||
# Terminal 2: Frontend (reads port from frontend/.env)
|
||||
cd frontend && npm run dev
|
||||
|
||||
# Visit: http://localhost:3000 (or your configured FRONTEND_PORT)
|
||||
cd backend
|
||||
cp config.example.php config.php
|
||||
```
|
||||
|
||||
**Note:** Ports are configured via `.env` files. See Configuration section below.
|
||||
Edit [backend/config.php](backend/config.php):
|
||||
```php
|
||||
define('GEMINI_API_KEY', 'your-api-key-here');
|
||||
```
|
||||
|
||||
**Frontend**: Create `frontend/.env`:
|
||||
```env
|
||||
VITE_GEMINI_API_KEY=your_gemini_api_key
|
||||
VITE_FRONTEND_PORT=3000
|
||||
VITE_BACKEND_PORT=5015
|
||||
```
|
||||
|
||||
Get your API key from [Google AI Studio](https://aistudio.google.com/app/apikey).
|
||||
|
||||
### Environment Files
|
||||
|
||||
**Frontend Configuration** - [frontend/.env.example](frontend/.env.example):
|
||||
```env
|
||||
VITE_GEMINI_API_KEY=your_key_here
|
||||
VITE_FRONTEND_PORT=3000 # Frontend dev server port
|
||||
VITE_BACKEND_PORT=5015 # Backend API port
|
||||
```
|
||||
|
||||
**Backend Configuration** - [backend/.env](backend/.env):
|
||||
```env
|
||||
BACKEND_PORT=5015 # PHP server port
|
||||
FRONTEND_PORT=3000 # For reference
|
||||
```
|
||||
|
||||
- Neither `.env` files nor `config.php` should be committed (both gitignored)
|
||||
- Ports are configurable via environment variables
|
||||
- Frontend uses `VITE_BACKEND_PORT` to proxy API requests
|
||||
|
||||
### Authentication Status
|
||||
|
||||
**Current**: No authentication - uses placeholder `'local'` userId
|
||||
**Future**: SSO-ready with `userId` field in projects schema
|
||||
|
||||
To add SSO, modify `getCurrentUserId()` in [useProjects.js](frontend/src/hooks/useProjects.js):
|
||||
```javascript
|
||||
const getCurrentUserId = () => {
|
||||
// Replace with your auth provider
|
||||
return authContext.user?.id || 'local';
|
||||
};
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Two-Part System
|
||||
### Data Flow: Project-First Workflow
|
||||
|
||||
**Frontend (frontend/):**
|
||||
- React 19 + Vite + Tailwind CSS
|
||||
- MSAL authentication with Azure AD (toggleable)
|
||||
- IndexedDB for local project/media storage via `useProjects.js` and `useIndexedDB.js` hooks
|
||||
- Three main tabs: Projects, Image Gen, Video Gen
|
||||
- No server-side session storage - all state in browser
|
||||
- Runs on port 3000 (configurable via FRONTEND_PORT in .env)
|
||||
1. **User selects/creates project** in [ProjectsTab.jsx](frontend/src/components/ProjectsTab.jsx)
|
||||
2. **Navigates to Image/Video tab** via [App.jsx](frontend/src/App.jsx) state management
|
||||
3. **Generates content** through tab components
|
||||
4. **Auto-saves to IndexedDB** via [useProjects.js](frontend/src/hooks/useProjects.js)
|
||||
5. **Cross-tab usage**: Extract video frames → use as image references
|
||||
|
||||
**Backend (backend/):**
|
||||
- PHP 7.4+ stateless APIs
|
||||
- Session-based user isolation (multi-user support via SessionManager)
|
||||
- Direct API calls to Google's Gemini/Imagen/Veo services
|
||||
- File-based storage in `backend/uploads/sessions/{session_id}/`
|
||||
- Runs on port 5015 (configurable via BACKEND_PORT in .env)
|
||||
### Frontend Architecture
|
||||
|
||||
### Data Flow Architecture
|
||||
**State Management Hub**: [App.jsx](frontend/src/App.jsx)
|
||||
- Manages `activeProjectId`, `activeTab`, `videoRerunData`, `imageEditData`
|
||||
- Coordinates navigation between Projects/Image/Video tabs
|
||||
- Passes data between tabs for edit workflows
|
||||
|
||||
**Project-First Workflow:**
|
||||
1. User creates/selects project in ProjectsTab (stored in IndexedDB)
|
||||
2. Image Gen and Video Gen tabs are DISABLED until project selected
|
||||
3. All generations save to active project automatically
|
||||
4. Projects contain: images, videos, metadata, storyboards
|
||||
**Core Components**:
|
||||
- [CinePromptStudio.jsx](frontend/src/components/CinePromptStudio.jsx) - Image generation with 27+ cinematography presets, 6 cameras, 7 lenses
|
||||
- [VideoGenTab.jsx](frontend/src/components/VideoGenTab.jsx) - Video generation with Veo 3.1 (text-to-video and image-to-video)
|
||||
- [ProjectsTab.jsx](frontend/src/components/ProjectsTab.jsx) - Project management and library browsing
|
||||
- [StoryboardEditor.jsx](frontend/src/components/StoryboardEditor.jsx) - Multi-frame storyboard creation with PDF/PNG export
|
||||
- [VideoPlayer.jsx](frontend/src/components/VideoPlayer.jsx) - Playback with frame extraction
|
||||
- [TabNavigation.jsx](frontend/src/components/TabNavigation.jsx) - Tab switching UI
|
||||
|
||||
**Image Generation (Imagen 3):**
|
||||
```
|
||||
CinePromptStudio → /api/api.php (proxied to backend:5015) → Gemini API (Imagen 3) →
|
||||
Base64 response → Server stores in backend/uploads/sessions/{id}/ → Frontend saves to IndexedDB project
|
||||
```
|
||||
**Custom Hooks**:
|
||||
- [useProjects.js](frontend/src/hooks/useProjects.js) - Project and item CRUD operations with IndexedDB
|
||||
- [useIndexedDB.js](frontend/src/hooks/useIndexedDB.js) - Low-level IndexedDB wrapper
|
||||
|
||||
**Video Generation (Veo 3.1):**
|
||||
```
|
||||
|
|
@ -366,11 +407,78 @@ Edit `VideoGenTab.jsx` - update `durationOptions` array, ensure constraints matc
|
|||
### 1. API URL Configuration
|
||||
**IMPORTANT:** All API calls MUST use the `getApiUrl()` helper function to avoid CORS issues:
|
||||
```javascript
|
||||
// At the top of component (after state hooks)
|
||||
const getApiUrl = (endpoint) => {
|
||||
// In development, use Vite proxy to avoid CORS
|
||||
if (import.meta.env.DEV) {
|
||||
return `/api/${endpoint}`;
|
||||
Database: CinemaStudioPro
|
||||
├── projects: {id, userId, name, createdAt, updatedAt}
|
||||
├── items: {id, projectId, type, prompt, settings, data, mimeType, createdAt}
|
||||
└── storyboards: {id, projectId, name, frames, createdAt, updatedAt}
|
||||
```
|
||||
|
||||
**Storage Notes**:
|
||||
- All data stored client-side (device-specific, doesn't sync)
|
||||
- Images stored as base64 in IndexedDB
|
||||
- Videos stored as URLs pointing to PHP backend
|
||||
- Browser storage limits apply (typically 50-100MB+)
|
||||
|
||||
### Backend Architecture
|
||||
|
||||
**Image Generation Flow** ([backend/api.php](backend/api.php)):
|
||||
1. Receives prompt + aspect ratio + image size + reference images
|
||||
2. Calls Gemini 3 Pro Image Preview API
|
||||
3. Returns base64 image embedded in JSON response
|
||||
|
||||
**Video Generation Flow** ([backend/video_api.php](backend/video_api.php)):
|
||||
1. Receives prompt + model selection + reference images
|
||||
2. Calls Veo 3.1 API (long-running operation)
|
||||
3. Polls for completion via webhook
|
||||
4. Returns video URL for frontend playback
|
||||
|
||||
**Critical Files**:
|
||||
- [backend/api.php](backend/api.php) - Imagen 3 image generation endpoint
|
||||
- [backend/video_api.php](backend/video_api.php) - Veo 3.1 video generation endpoint
|
||||
- [backend/stream_video.php](backend/stream_video.php) - Video streaming with HTTP Range support
|
||||
- [backend/session_manager.php](backend/session_manager.php) - Session and file storage management
|
||||
- [backend/enhance_prompt.php](backend/enhance_prompt.php) - Gemini-powered prompt optimization
|
||||
- [backend/webhook_logger.php](backend/webhook_logger.php) - Logs video generation webhook callbacks
|
||||
|
||||
**Backup Files** (for reference, do not modify):
|
||||
- [api.php.backup](api.php.backup) - Original api.php
|
||||
- [video_api.php.backup](video_api.php.backup) - Original video_api.php
|
||||
|
||||
**Other PHP Files**:
|
||||
- [backend/index.php](backend/index.php) - Legacy standalone interface (not used in React app)
|
||||
- [backend/auth.php](backend/auth.php), [backend/AuthMiddleware.php](backend/AuthMiddleware.php), [backend/JWTValidator.php](backend/JWTValidator.php) - Future auth infrastructure
|
||||
- [backend/debug.php](backend/debug.php), [backend/debug_request.php](backend/debug_request.php) - Debugging utilities
|
||||
- [backend/get_current_image.php](backend/get_current_image.php) - Image retrieval endpoint
|
||||
- [backend/cleanup.php](backend/cleanup.php) - Session cleanup script
|
||||
|
||||
### Proxy Configuration
|
||||
|
||||
[vite.config.js](frontend/vite.config.js) proxies backend requests using configurable port (default: 5015):
|
||||
```javascript
|
||||
// Reads VITE_BACKEND_PORT from .env (default: 5015)
|
||||
'/api/*' → localhost:5015
|
||||
'/generated_videos/*' → localhost:5015
|
||||
'/generated_images/*' → localhost:5015
|
||||
```
|
||||
|
||||
The proxy configuration dynamically reads the backend port from environment variables, ensuring consistency across development environments.
|
||||
|
||||
## AI API Integration: Critical Details
|
||||
|
||||
### Gemini Image Generation (Imagen 3)
|
||||
|
||||
**Request Format** - Image editing requires specific structure:
|
||||
```json
|
||||
{
|
||||
"contents": [{
|
||||
"parts": [
|
||||
{"inline_data": {"mime_type": "image/jpeg", "data": "base64"}},
|
||||
{"text": "edit instruction"}
|
||||
]
|
||||
}],
|
||||
"generationConfig": {
|
||||
"responseModalities": ["IMAGE"],
|
||||
"imageConfig": {"aspectRatio": "16:9", "imageSize": "2K"}
|
||||
}
|
||||
// In production, use full API URL
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015';
|
||||
|
|
@ -411,18 +519,21 @@ if ((tabId === 'image' || tabId === 'video') && !activeProjectId) {
|
|||
}
|
||||
```
|
||||
|
||||
### 5. Video Generation Settings
|
||||
All durations (4s, 6s, 8s) work for both T2V and I2V modes. Resolution (720p/1080p) is a UI-only setting - only aspectRatio and durationSeconds are sent to the Veo 3.1 API.
|
||||
**Critical**: Image MUST come before text in parts array.
|
||||
|
||||
### 6. MIME Type Handling
|
||||
Always store and use dynamic MIME type from API response:
|
||||
**Response Handling**:
|
||||
```php
|
||||
$_SESSION['current_image'] = $base64;
|
||||
$_SESSION['current_image_mime'] = $mimeType; // Don't hardcode 'image/png'
|
||||
// ALWAYS check finishReason first
|
||||
$reason = $response['candidates'][0]['finishReason'];
|
||||
if ($reason === 'IMAGE_RECITATION') {
|
||||
// Content filter triggered - prompt too generic
|
||||
}
|
||||
if ($reason === 'STOP') {
|
||||
// Extract base64 from inlineData
|
||||
}
|
||||
```
|
||||
|
||||
### 7. IndexedDB Project Operations
|
||||
Always use the `useProjects` hook for database operations - don't use `useIndexedDB` directly from components.
|
||||
**MIME Type Handling**: Gemini returns `image/jpeg`, not `image/png`. Store and use dynamic MIME types.
|
||||
|
||||
### 8. ESLint Unused Variable Rule
|
||||
`eslint.config.js` uses `varsIgnorePattern: '^[A-Z_]'` - uppercase-named vars (constants, React components) won't trigger `no-unused-vars`. This is intentional.
|
||||
|
|
@ -432,116 +543,195 @@ The `lastFrame` reference image (second image in I2V mode) is only applicable wh
|
|||
|
||||
## Error Handling Patterns
|
||||
|
||||
### Imagen 3 Errors
|
||||
- `IMAGE_RECITATION` - Prompt too generic/simple, needs creative detail
|
||||
- `SAFETY` - Content filter triggered, try different prompt
|
||||
- `finishReason !== 'STOP'` - Generation failed for reason specified
|
||||
See [AI_IMPLEMENTATION_GUIDE.md](AI_IMPLEMENTATION_GUIDE.md) and [QUICK_REFERENCE.md](QUICK_REFERENCE.md) for comprehensive API details.
|
||||
|
||||
### Veo 3.1 Errors
|
||||
- Operation stuck in `PENDING` - Check webhook logs, may need retry
|
||||
- Operation `FAILED` - Check error message in operation response
|
||||
- Timeout after 5 minutes polling - Display timeout error to user
|
||||
### Video Generation (Veo 3.1)
|
||||
|
||||
### Session/Storage Errors
|
||||
- Session files not saving - Check uploads/sessions/ permissions (755)
|
||||
- IndexedDB quota exceeded - Prompt user to delete old projects
|
||||
- CORS errors - Ensure PHP backend allows origin
|
||||
**Models**:
|
||||
- Standard: Higher quality, longer processing
|
||||
- Fast: 50% cost savings, faster generation
|
||||
|
||||
## Testing Checklist
|
||||
**Features**:
|
||||
- Text-to-video and image-to-video (I2V)
|
||||
- Native audio generation with dialogue support
|
||||
- Polling-based completion (long-running operations)
|
||||
- HTTP Range streaming for progressive playback
|
||||
|
||||
Before committing changes:
|
||||
- [ ] Test with no project selected (Image/Video Gen should be disabled)
|
||||
- [ ] Test image generation with creative prompt (10+ words)
|
||||
- [ ] Test image editing (uses last generated image)
|
||||
- [ ] Test video generation T2V and I2V modes
|
||||
- [ ] Test video rerun from project library
|
||||
- [ ] Test frame extraction from video
|
||||
- [ ] Test storyboard drag-to-reorder
|
||||
- [ ] Test project deletion (cleans up IndexedDB)
|
||||
- [ ] Test with SSO_ENABLED=false (mock auth)
|
||||
- [ ] Check PHP error_log for any errors
|
||||
- [ ] Verify generated files land in correct session directory
|
||||
**Prompt Optimization**: AI automatically infers camera movement in `[brackets]` and audio cues in `(Sound: [...])`.
|
||||
|
||||
## Authentication
|
||||
|
||||
The application uses MSAL (Microsoft Authentication Library) for Azure AD SSO:
|
||||
|
||||
- **Enabled by default** in development with provided credentials
|
||||
- **Toggleable** via `VITE_SSO_ENABLED` in frontend/.env
|
||||
- **LoginPage component** handles authentication UI
|
||||
- **App.jsx** checks authentication status and shows login or main app
|
||||
- **Logout button** in header when authenticated (top-right corner)
|
||||
|
||||
### SSO Flow:
|
||||
1. User visits `localhost:3000`
|
||||
2. If not authenticated, `LoginPage` is shown
|
||||
3. User clicks "Sign in with Microsoft"
|
||||
4. MSAL popup opens for Azure AD login
|
||||
5. After successful login, main app renders
|
||||
6. User info and logout button appear in header
|
||||
|
||||
## Documentation References
|
||||
|
||||
For detailed implementation guides, see:
|
||||
- `MDFiles/AI_IMPLEMENTATION_GUIDE.md` - Critical Imagen 3 API patterns
|
||||
- `MDFiles/QUICK_REFERENCE.md` - Common patterns and gotchas
|
||||
- `MDFiles/AUTH_README.md` - Azure AD SSO setup guide
|
||||
- `MDFiles/INSTALL.md` - Server deployment instructions
|
||||
- `MDFiles/README.md` - User-facing feature documentation
|
||||
|
||||
## API Rate Limits
|
||||
|
||||
**Google Gemini APIs:**
|
||||
- Rate limit: Handled by exponential backoff in API calls
|
||||
- HTTP 429 errors: Retry after 30 seconds
|
||||
- HTTP 500 errors: Temporary API issue, retry with backoff
|
||||
|
||||
## File Storage Patterns
|
||||
|
||||
**Backend (temporary, auto-cleanup after 24h):**
|
||||
- `backend/uploads/sessions/{session_id}/images/*.jpg` - Generated images
|
||||
- `backend/uploads/sessions/{session_id}/videos/*.mp4` - Generated videos
|
||||
- `backend/video_operations.json` - Pending video operations
|
||||
|
||||
**Frontend (persistent, user-managed):**
|
||||
- IndexedDB `lux-studio-projects` database
|
||||
- Projects, images, videos, storyboards stores
|
||||
- No automatic cleanup - user deletes projects manually
|
||||
|
||||
## Build and Deployment
|
||||
|
||||
**Automated Deployment (Recommended):**
|
||||
```bash
|
||||
sudo ./deploy.sh # Full production deployment script
|
||||
# Uses .env.production files
|
||||
# Deploys to /opt/lux-studio-back/ and /var/www/html/lux-studio/
|
||||
Example:
|
||||
```
|
||||
Input: "Woman crosses street"
|
||||
Output: "Woman crosses a street. [Camera: tracking, follows woman] (Sound: [footsteps, distant traffic])"
|
||||
```
|
||||
|
||||
**Manual Frontend Build:**
|
||||
```bash
|
||||
cd frontend
|
||||
npm run build
|
||||
# Outputs to frontend/dist/
|
||||
```
|
||||
## Physics-Based Prompt Engineering
|
||||
|
||||
**Backend deployment:**
|
||||
- Copy all `backend/` files to server
|
||||
- Run `composer install` in backend directory
|
||||
- Create `backend/.env` from `backend/.env.example`
|
||||
- Set up `backend/uploads/sessions/` directory with write permissions (755)
|
||||
- Configure web server (Apache .htaccess included, Nginx config in MDFiles/INSTALL.md)
|
||||
The cinematographer's toolkit in [CinePromptStudio.jsx](frontend/src/components/CinePromptStudio.jsx) injects real camera/lens physics:
|
||||
|
||||
**Frontend deployment:**
|
||||
- Build frontend with `npm run build`
|
||||
- Deploy `frontend/dist/` to web server or CDN
|
||||
- Update `VITE_API_URL` in frontend/.env for production backend URL
|
||||
- Update `VITE_SSO_REDIRECT_URI` to production URL
|
||||
**Camera Bodies** (e.g., Arri Alexa 35, Sony Venice 2):
|
||||
- Sensor characteristics (grain, dynamic range)
|
||||
- Color science (Arri's LogC4, Sony's X-OCN)
|
||||
- Low-light performance
|
||||
- Film vs digital characteristics
|
||||
|
||||
**Production checklist:**
|
||||
- PHP 7.4+ installed
|
||||
- Composer dependencies installed in backend/
|
||||
- `backend/.env` with valid GEMINI_API_KEY and production SSO credentials
|
||||
- `backend/uploads/sessions/` writable by web server
|
||||
- HTTPS enabled (required for SSO)
|
||||
- Frontend built and deployed with production env vars
|
||||
- CORS configured on backend for frontend domain
|
||||
**Lens Profiles** (e.g., Cooke S7/i, Panavision C-Series):
|
||||
- Optical characteristics (bokeh, flares)
|
||||
- Aberrations (chromatic, spherical)
|
||||
- Signature looks ("Cooke Look", anamorphic squeeze)
|
||||
- Focal length and aperture ranges
|
||||
|
||||
**27+ Cinematography Presets** auto-configure camera + lens + lighting for specific scenarios:
|
||||
- Documentary (handheld, natural light)
|
||||
- Drama (controlled staging, emotional lighting)
|
||||
- Auteur (distinctive visual style)
|
||||
- Commercial (polished, vibrant)
|
||||
- And 23+ more specialized applications
|
||||
|
||||
**AI Enhancement**: Google Gemini analyzes scene descriptions and injects physics-based characteristics into prompts using fallback chain:
|
||||
1. gemini-2.5-flash (primary)
|
||||
2. gemini-2.0-flash (fallback)
|
||||
3. gemini-1.5-pro (final fallback)
|
||||
|
||||
## File Organization Conventions
|
||||
|
||||
### Component Responsibilities
|
||||
|
||||
**DO NOT mix concerns**:
|
||||
- [CinePromptStudio.jsx](frontend/src/components/CinePromptStudio.jsx) handles image generation only
|
||||
- [VideoGenTab.jsx](frontend/src/components/VideoGenTab.jsx) handles video generation only
|
||||
- [ProjectsTab.jsx](frontend/src/components/ProjectsTab.jsx) handles library and project management
|
||||
- [App.jsx](frontend/src/App.jsx) coordinates state, never generates content directly
|
||||
|
||||
### Edit Workflows
|
||||
|
||||
**Image Edit**: Load from library → `App.jsx` sets `imageEditData` → [CinePromptStudio.jsx](frontend/src/components/CinePromptStudio.jsx) receives via props
|
||||
|
||||
**Video Rerun**: Click "Rerun" in library → `App.jsx` sets `videoRerunData` → [VideoGenTab.jsx](frontend/src/components/VideoGenTab.jsx) pre-populates fields
|
||||
|
||||
### Storage Patterns
|
||||
|
||||
**Client-Side (Development)**: IndexedDB via [useProjects.js](frontend/src/hooks/useProjects.js) - device-specific, no backend DB required
|
||||
|
||||
**Production Migration Path**: Move to PostgreSQL + S3/Cloud Storage for multi-device sync (see [frontend/README.md](frontend/README.md) for migration details)
|
||||
|
||||
## Common Tasks
|
||||
|
||||
### Adding New Camera Profiles
|
||||
|
||||
1. Add to `cameraData` array in [CinePromptStudio.jsx](frontend/src/components/CinePromptStudio.jsx)
|
||||
2. Include: `name`, `type`, `sensor`, `colorScience`, `dynamicRange`, `lowLight`, `look`
|
||||
3. Add compatibility entries in `cameraLensCompatibility` object
|
||||
|
||||
### Adding New Lens Profiles
|
||||
|
||||
1. Add to `lensData` array in [CinePromptStudio.jsx](frontend/src/components/CinePromptStudio.jsx)
|
||||
2. Include: `name`, `type`, `focalLength`, `aperture`, `bokeh`, `flares`, `aberrations`, `look`
|
||||
3. Specify compatible cameras in description
|
||||
|
||||
### Adding Cinematography Presets
|
||||
|
||||
1. Add to `applications` array in [CinePromptStudio.jsx](frontend/src/components/CinePromptStudio.jsx)
|
||||
2. Specify: `name`, `description`, `camera`, `lens`, `lighting`
|
||||
|
||||
### Modifying Video API Models
|
||||
|
||||
Edit model options in [VideoGenTab.jsx](frontend/src/components/VideoGenTab.jsx) model selection dropdown.
|
||||
|
||||
## Testing Workflows
|
||||
|
||||
### Manual Testing Checklist
|
||||
|
||||
**Image Generation**:
|
||||
1. Select/create project
|
||||
2. Choose Application preset (auto-configures camera/lens)
|
||||
3. Enter scene description
|
||||
4. Click "Optimize" (AI enhancement)
|
||||
5. Click "Generate Image"
|
||||
6. Verify image displays and saves to library
|
||||
|
||||
**Video Generation**:
|
||||
1. Select/create project
|
||||
2. Enter scene description or upload reference image
|
||||
3. Click "Optimize" (AI infers camera movement in brackets)
|
||||
4. Click "Generate Video"
|
||||
5. Wait for completion (polling-based)
|
||||
6. Verify video plays and saves to library
|
||||
|
||||
**Cross-Tab Workflow**:
|
||||
1. Generate video
|
||||
2. Extract frames via [VideoPlayer.jsx](frontend/src/components/VideoPlayer.jsx)
|
||||
3. Navigate to Image tab
|
||||
4. Load extracted frame from library
|
||||
5. Apply edits
|
||||
|
||||
**Storyboard Workflow**:
|
||||
1. Create storyboard in Projects tab
|
||||
2. Add images from library
|
||||
3. Drag to reorder frames
|
||||
4. Export as PDF or PNG
|
||||
|
||||
### Error Scenarios
|
||||
|
||||
**IMAGE_RECITATION**: Test with simple prompt like "a red circle" - should show user-friendly error
|
||||
|
||||
**Rate Limiting**: Multiple rapid requests - should handle 429 errors gracefully
|
||||
|
||||
**Session Timeout**: Long-running video generation - backend maintains session state via [session_manager.php](session_manager.php)
|
||||
|
||||
## Security Notes
|
||||
|
||||
**Never commit**:
|
||||
- `config.php` (contains API keys)
|
||||
- `.env` files
|
||||
- `uploads/` directory (user sessions)
|
||||
- `generated_videos/` and `generated_images/` directories
|
||||
|
||||
**API Key Management**: Store in `config.php` (backend) and `frontend/.env` (frontend), both gitignored.
|
||||
|
||||
## Production Deployment
|
||||
|
||||
**Current State**: Development setup with IndexedDB + PHP backend
|
||||
|
||||
**Production Recommendations**:
|
||||
1. Migrate to server-side database (PostgreSQL)
|
||||
2. Store media in cloud storage (S3, Google Cloud Storage)
|
||||
3. Implement API rate limiting and monitoring
|
||||
4. Enable HTTPS for secure communication
|
||||
5. Add SSO authentication (infrastructure exists: [auth.php](auth.php), [AuthMiddleware.php](AuthMiddleware.php), [JWTValidator.php](JWTValidator.php))
|
||||
6. Set up CI/CD pipeline
|
||||
|
||||
See [INSTALL.md](INSTALL.md) for server deployment instructions.
|
||||
|
||||
## API Pricing (Approximate)
|
||||
|
||||
**Imagen 3**:
|
||||
- 1K/2K images: ~$0.03 per image
|
||||
- 4K images: ~$0.06 per image
|
||||
|
||||
**Veo 3.1**:
|
||||
- 4s video: ~$0.20
|
||||
- 6s video: ~$0.30
|
||||
- 8s video: ~$0.40
|
||||
|
||||
Monitor API usage quotas in Google AI Studio.
|
||||
|
||||
## Additional Documentation
|
||||
|
||||
- [README.md](README.md) - Quick start guide
|
||||
- [frontend/README.md](frontend/README.md) - Detailed frontend documentation with storage migration guide
|
||||
- [AI_IMPLEMENTATION_GUIDE.md](AI_IMPLEMENTATION_GUIDE.md) - Comprehensive AI API integration details
|
||||
- [QUICK_REFERENCE.md](QUICK_REFERENCE.md) - API quick reference
|
||||
- [AUTH_README.md](AUTH_README.md) - Future authentication implementation guide
|
||||
- [INSTALL.md](INSTALL.md) - Production deployment instructions
|
||||
- [backend/config.example.php](backend/config.example.php) - Backend configuration template
|
||||
|
||||
## Port Configuration Reference
|
||||
|
||||
| Service | Default Port | Environment Variable | Location |
|
||||
|---------|-------------|---------------------|----------|
|
||||
| Frontend Dev Server | 3000 | `VITE_FRONTEND_PORT` | frontend/.env |
|
||||
| Backend API Server | 5015 | `BACKEND_PORT` | backend/.env |
|
||||
| Backend Proxy Target | 5015 | `VITE_BACKEND_PORT` | frontend/.env |
|
||||
|
||||
**Important**: All hardcoded port references have been replaced with environment variables. To change ports, update the `.env` files in both `frontend/` and `backend/` directories.
|
||||
|
|
|
|||
1058
DEPLOYMENT.md
617
INSTALL.md
Normal file
|
|
@ -0,0 +1,617 @@
|
|||
# Nano Banana Pro - Server Installation Guide
|
||||
|
||||
Complete instructions for deploying Nano Banana Pro from git clone to production.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **PHP 7.4+** (PHP 8.0+ recommended)
|
||||
- **Composer** (PHP dependency manager)
|
||||
- **Git**
|
||||
- **Web server** (Apache/Nginx with PHP support)
|
||||
- **HTTPS** (required for production with SSO)
|
||||
- **Google Gemini API Key** ([Get here](https://aistudio.google.com/app/apikey))
|
||||
- **Azure AD Tenant** (optional, for SSO)
|
||||
|
||||
---
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Clone Repository
|
||||
|
||||
```bash
|
||||
# Clone from Bitbucket
|
||||
git clone git@bitbucket.org:zlalani/nano-pro.git
|
||||
|
||||
# Navigate to directory
|
||||
cd nano-pro
|
||||
```
|
||||
|
||||
### 2. Install PHP Dependencies
|
||||
|
||||
```bash
|
||||
# Install Composer dependencies (Firebase JWT library)
|
||||
composer install
|
||||
|
||||
# Verify vendor directory created
|
||||
ls -la vendor/
|
||||
```
|
||||
|
||||
**Expected output:**
|
||||
```
|
||||
vendor/
|
||||
├── autoload.php
|
||||
├── composer/
|
||||
└── firebase/
|
||||
└── php-jwt/
|
||||
```
|
||||
|
||||
### 3. Configure Application
|
||||
|
||||
```bash
|
||||
# Copy example config files
|
||||
cp config.example.php config.php
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
### 4. Add Google Gemini API Key
|
||||
|
||||
Edit `config.php`:
|
||||
```php
|
||||
// Line 13: Add your Gemini API key
|
||||
define('GEMINI_API_KEY', 'YOUR_ACTUAL_GEMINI_API_KEY_HERE');
|
||||
```
|
||||
|
||||
Get API key from: https://aistudio.google.com/app/apikey
|
||||
|
||||
### 5. Configure Authentication (Optional)
|
||||
|
||||
Edit `.env`:
|
||||
|
||||
**For Testing (No Authentication):**
|
||||
```bash
|
||||
SSO_ENABLED=false
|
||||
SSO_TENANT_ID=
|
||||
SSO_CLIENT_ID=
|
||||
```
|
||||
|
||||
**For Production (With Azure AD SSO):**
|
||||
```bash
|
||||
SSO_ENABLED=true
|
||||
SSO_TENANT_ID=your-azure-tenant-id
|
||||
SSO_CLIENT_ID=your-azure-app-client-id
|
||||
```
|
||||
|
||||
See `AUTH_README.md` for complete Azure AD setup instructions.
|
||||
|
||||
### 6. Set Directory Permissions
|
||||
|
||||
```bash
|
||||
# Create uploads directory if not exists
|
||||
mkdir -p uploads/sessions
|
||||
|
||||
# Set proper permissions
|
||||
chmod 755 uploads
|
||||
chmod 755 uploads/sessions
|
||||
|
||||
# Ensure web server can write to uploads
|
||||
chown -R www-data:www-data uploads/ # Ubuntu/Debian
|
||||
# OR
|
||||
chown -R apache:apache uploads/ # CentOS/RHEL
|
||||
# OR
|
||||
chown -R _www:_www uploads/ # macOS
|
||||
```
|
||||
|
||||
### 7. Configure Web Server
|
||||
|
||||
#### Apache (.htaccess already included)
|
||||
|
||||
Ensure `mod_rewrite` is enabled:
|
||||
```bash
|
||||
sudo a2enmod rewrite
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
Set document root to the application directory in your virtual host config.
|
||||
|
||||
#### Nginx
|
||||
|
||||
Add to your server block:
|
||||
```nginx
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
|
||||
}
|
||||
|
||||
# Protect uploads directory
|
||||
location /uploads/ {
|
||||
deny all;
|
||||
return 403;
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Test Installation
|
||||
|
||||
Visit these URLs to verify:
|
||||
|
||||
1. **Main App:** `https://your-server.com/nano-pro/`
|
||||
- Should load the image generator
|
||||
- If SSO enabled: Shows login page
|
||||
- If SSO disabled: Loads directly
|
||||
|
||||
2. **Auth Test:** `https://your-server.com/nano-pro/auth-test.php`
|
||||
- Shows SSO configuration
|
||||
- Shows authentication status
|
||||
- Shows user info (if authenticated)
|
||||
|
||||
3. **Debug Info:** Click "Toggle Debug Panel" in app
|
||||
- Shows session ID
|
||||
- Shows current image status
|
||||
- Shows conversation history count
|
||||
|
||||
### 10. Verify Everything Works
|
||||
|
||||
**Test Checklist:**
|
||||
- [ ] Main page loads without errors
|
||||
- [ ] Prompt Studio section displays
|
||||
- [ ] Can select cameras/lenses/applications
|
||||
- [ ] "Enhance Prompt with AI" button works
|
||||
- [ ] Can generate images
|
||||
- [ ] Images display correctly
|
||||
- [ ] Image history shows recent generations
|
||||
- [ ] Can download images
|
||||
- [ ] Lightbox modal works (click main image)
|
||||
- [ ] Conversation history displays
|
||||
- [ ] Quick action buttons populate prompt
|
||||
- [ ] If SSO enabled: Login/logout works
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
After installation:
|
||||
```
|
||||
nano-pro/
|
||||
├── .env # Environment config (gitignored)
|
||||
├── .env.example # Environment template
|
||||
├── .gitignore # Git ignore rules
|
||||
├── .htaccess # Apache configuration
|
||||
├── api.php # Image generation API
|
||||
├── auth.php # Authentication API
|
||||
├── auth-test.php # Auth debugging page
|
||||
├── AuthMiddleware.php # Auth orchestrator
|
||||
├── AUTH_README.md # Auth setup guide
|
||||
├── cleanup.php # Image cleanup script
|
||||
├── clear_session.php # Clear session utility
|
||||
├── composer.json # PHP dependencies
|
||||
├── composer.lock # Dependency lock file
|
||||
├── config.example.php # Config template
|
||||
├── config.php # Main config (gitignored)
|
||||
├── debug.php # Debug utilities
|
||||
├── debug_request.php # Request debugging
|
||||
├── enhance_prompt.php # Prompt enhancement API
|
||||
├── env_loader.php # Environment loader
|
||||
├── get_logs.php # Server logs API
|
||||
├── index.php # Main application
|
||||
├── INSTALL.md # This file
|
||||
├── JWTValidator.php # JWT token validator
|
||||
├── README.md # Project readme
|
||||
├── session_manager.php # Session management
|
||||
├── uploads/ # User image storage
|
||||
│ ├── .htaccess # Protect uploads
|
||||
│ └── sessions/ # Session directories
|
||||
│ └── {session_id}/ # Per-user storage
|
||||
│ └── images/ # User images
|
||||
└── vendor/ # Composer dependencies
|
||||
├── autoload.php
|
||||
└── firebase/
|
||||
└── php-jwt/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Files
|
||||
|
||||
### config.php (Main Configuration)
|
||||
```php
|
||||
<?php
|
||||
require_once __DIR__ . '/env_loader.php';
|
||||
|
||||
// Google Gemini API Key
|
||||
define('GEMINI_API_KEY', 'YOUR_KEY_HERE');
|
||||
|
||||
// MSAL / Azure AD SSO
|
||||
define('SSO_ENABLED', getenv('SSO_ENABLED') === 'true');
|
||||
define('SSO_TENANT_ID', getenv('SSO_TENANT_ID') ?: '');
|
||||
define('SSO_CLIENT_ID', getenv('SSO_CLIENT_ID') ?: '');
|
||||
|
||||
// Session configuration
|
||||
ini_set('session.gc_maxlifetime', 3600);
|
||||
ini_set('session.cookie_lifetime', 3600);
|
||||
|
||||
// Error reporting
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('log_errors', 1);
|
||||
```
|
||||
|
||||
### .env (Environment Variables)
|
||||
```bash
|
||||
# Authentication toggle
|
||||
SSO_ENABLED=false
|
||||
|
||||
# Azure AD credentials (when SSO enabled)
|
||||
SSO_TENANT_ID=
|
||||
SSO_CLIENT_ID=
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Composer not found"
|
||||
```bash
|
||||
# Install Composer
|
||||
curl -sS https://getcomposer.org/installer | php
|
||||
sudo mv composer.phar /usr/local/bin/composer
|
||||
```
|
||||
|
||||
### "Permission denied" errors
|
||||
```bash
|
||||
# Fix uploads directory permissions
|
||||
sudo chown -R www-data:www-data uploads/
|
||||
sudo chmod -R 755 uploads/
|
||||
```
|
||||
|
||||
### "Class 'Firebase\JWT\JWT' not found"
|
||||
```bash
|
||||
# Re-run composer install
|
||||
composer install
|
||||
|
||||
# Verify autoload file exists
|
||||
ls -la vendor/autoload.php
|
||||
```
|
||||
|
||||
### Images not saving
|
||||
```bash
|
||||
# Check uploads directory exists and is writable
|
||||
ls -la uploads/
|
||||
ls -la uploads/sessions/
|
||||
|
||||
# Create if missing
|
||||
mkdir -p uploads/sessions
|
||||
chmod 755 uploads/sessions
|
||||
```
|
||||
|
||||
### "API key not configured" error
|
||||
```bash
|
||||
# Verify config.php has valid API key
|
||||
grep GEMINI_API_KEY config.php
|
||||
|
||||
# Should show:
|
||||
# define('GEMINI_API_KEY', 'AIzaSy...');
|
||||
```
|
||||
|
||||
### Authentication not working
|
||||
```bash
|
||||
# Check SSO configuration
|
||||
php auth-test.php
|
||||
|
||||
# Verify .env file loaded
|
||||
cat .env
|
||||
|
||||
# Check error logs
|
||||
tail -f error_log
|
||||
```
|
||||
|
||||
### Cleanup script not running
|
||||
```bash
|
||||
# Test manually first
|
||||
php cleanup.php
|
||||
|
||||
# Should show:
|
||||
# === Image Cleanup Report ===
|
||||
# Timestamp: 2025-12-16 ...
|
||||
# Images cleaned: X
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment Checklist
|
||||
|
||||
Before going live:
|
||||
|
||||
**Required:**
|
||||
- [ ] PHP 7.4+ installed on server
|
||||
- [ ] Composer dependencies installed (`vendor/` exists)
|
||||
- [ ] `config.php` created with valid Gemini API key
|
||||
- [ ] `.env` file created and configured
|
||||
- [ ] `uploads/sessions/` directory created with write permissions
|
||||
- [ ] `.htaccess` protecting uploads directory
|
||||
- [ ] Web server configured (Apache/Nginx)
|
||||
- [ ] Cron job set up for `cleanup.php`
|
||||
|
||||
**If Using SSO:**
|
||||
- [ ] HTTPS enabled on server
|
||||
- [ ] Azure AD app registration created
|
||||
- [ ] Redirect URI matches server URL exactly
|
||||
- [ ] `.env` has correct `SSO_TENANT_ID` and `SSO_CLIENT_ID`
|
||||
- [ ] `SSO_ENABLED=true` in `.env`
|
||||
- [ ] Tested login/logout flow
|
||||
|
||||
**Security:**
|
||||
- [ ] `config.php` is gitignored (contains API key)
|
||||
- [ ] `.env` is gitignored (contains credentials)
|
||||
- [ ] `vendor/` is gitignored
|
||||
- [ ] `uploads/sessions/` is gitignored
|
||||
- [ ] Error display disabled in production (`display_errors = 0`)
|
||||
- [ ] HTTPS enforced for production
|
||||
|
||||
**Testing:**
|
||||
- [ ] Visit main app - loads without errors
|
||||
- [ ] Generate a test image - works
|
||||
- [ ] Enhance prompt feature - works
|
||||
- [ ] Image history displays - works
|
||||
- [ ] Download images - works
|
||||
- [ ] Lightbox modal - works
|
||||
- [ ] Visit `auth-test.php` - shows correct config
|
||||
|
||||
---
|
||||
|
||||
## Quick Start (TL;DR)
|
||||
|
||||
```bash
|
||||
# 1. Clone
|
||||
git clone git@bitbucket.org:zlalani/nano-pro.git
|
||||
cd nano-pro
|
||||
|
||||
# 2. Install dependencies
|
||||
composer install
|
||||
|
||||
# 3. Configure
|
||||
cp config.example.php config.php
|
||||
cp .env.example .env
|
||||
|
||||
# Edit config.php - add your Gemini API key
|
||||
nano config.php
|
||||
|
||||
# 4. Set permissions
|
||||
mkdir -p uploads/sessions
|
||||
chmod 755 uploads/sessions
|
||||
|
||||
# 5. Test
|
||||
# Visit: https://your-server.com/nano-pro/
|
||||
# Visit: https://your-server.com/nano-pro/auth-test.php
|
||||
|
||||
# 6. Done!
|
||||
# Note: Images auto-cleanup on app launch (no cron needed)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Updating the Application
|
||||
|
||||
```bash
|
||||
# Pull latest changes
|
||||
git pull origin master
|
||||
|
||||
# Update dependencies
|
||||
composer install
|
||||
|
||||
# Clear PHP opcode cache if using OPcache
|
||||
# (or restart PHP-FPM)
|
||||
sudo systemctl restart php8.1-fpm
|
||||
|
||||
# No database migrations needed - uses file storage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backup & Restore
|
||||
|
||||
### Backup
|
||||
```bash
|
||||
# Backup user images (last 24 hours)
|
||||
tar -czf backup-images-$(date +%Y%m%d).tar.gz uploads/sessions/
|
||||
|
||||
# Backup configuration
|
||||
tar -czf backup-config-$(date +%Y%m%d).tar.gz config.php .env
|
||||
```
|
||||
|
||||
### Restore
|
||||
```bash
|
||||
# Restore images
|
||||
tar -xzf backup-images-YYYYMMDD.tar.gz
|
||||
|
||||
# Restore configuration
|
||||
tar -xzf backup-config-YYYYMMDD.tar.gz
|
||||
|
||||
# Set permissions
|
||||
chmod 755 uploads/sessions/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Automatic Image Cleanup
|
||||
|
||||
**No cron job required!** The application automatically cleans up expired images:
|
||||
|
||||
- Cleanup runs automatically when users launch the app (~10% of sessions)
|
||||
- Finds images older than 24 hours across all user sessions
|
||||
- Deletes expired images and metadata files
|
||||
- Removes empty session directories
|
||||
- Logs cleanup activity to `error_log`
|
||||
|
||||
**Manual cleanup** (if needed):
|
||||
```bash
|
||||
php cleanup.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Check Application Status
|
||||
```bash
|
||||
# Visit auth test page
|
||||
curl https://your-server.com/nano-pro/auth-test.php
|
||||
|
||||
# Check PHP error logs
|
||||
tail -f /var/log/apache2/error.log
|
||||
# OR
|
||||
tail -f /var/log/nginx/error.log
|
||||
|
||||
# Check cleanup logs
|
||||
tail -f cleanup.log
|
||||
```
|
||||
|
||||
### Monitor Disk Usage
|
||||
```bash
|
||||
# Check uploads directory size
|
||||
du -sh uploads/sessions/
|
||||
|
||||
# Count active sessions
|
||||
ls -1 uploads/sessions/ | wc -l
|
||||
|
||||
# List sessions older than 24 hours
|
||||
find uploads/sessions/ -type d -mtime +1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
- **Application issues:** Check `error_log` in application directory
|
||||
- **Authentication issues:** Run `auth-test.php`
|
||||
- **Image storage issues:** Check `uploads/sessions/` permissions
|
||||
- **API errors:** Check browser console and PHP error logs
|
||||
- **Cleanup issues:** Run `php cleanup.php` manually
|
||||
|
||||
---
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Never commit `config.php` or `.env` to git (they're gitignored)
|
||||
- Always use HTTPS in production for SSO
|
||||
- httpOnly cookies protect against XSS
|
||||
- Images auto-expire after 24 hours
|
||||
- Upload directory protected by .htaccess
|
||||
- Each user has isolated session storage
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Enable OPcache (Recommended)
|
||||
|
||||
Add to `php.ini`:
|
||||
```ini
|
||||
opcache.enable=1
|
||||
opcache.memory_consumption=128
|
||||
opcache.max_accelerated_files=4000
|
||||
opcache.revalidate_freq=60
|
||||
```
|
||||
|
||||
### Increase PHP Limits for Large Images
|
||||
|
||||
Add to `php.ini`:
|
||||
```ini
|
||||
upload_max_filesize = 20M
|
||||
post_max_size = 20M
|
||||
max_execution_time = 120
|
||||
memory_limit = 256M
|
||||
```
|
||||
|
||||
### Adjust Session Lifetime
|
||||
|
||||
In `config.php`:
|
||||
```php
|
||||
ini_set('session.gc_maxlifetime', 86400); // 24 hours
|
||||
ini_set('session.cookie_lifetime', 86400);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Weekly Tasks
|
||||
- Check disk space: `df -h`
|
||||
- Review error logs for issues
|
||||
- Verify cleanup cron is running
|
||||
|
||||
### Monthly Tasks
|
||||
- Update Composer dependencies: `composer update`
|
||||
- Review and rotate API keys if needed
|
||||
- Check Azure AD token expiration policies
|
||||
|
||||
### As Needed
|
||||
- Clear old sessions: `php cleanup.php`
|
||||
- Restart PHP-FPM after config changes
|
||||
- Monitor API usage quotas (Gemini API)
|
||||
|
||||
---
|
||||
|
||||
## Common Issues & Solutions
|
||||
|
||||
### Issue: "No image data found in API response"
|
||||
**Solution:**
|
||||
- Check Gemini API key is valid
|
||||
- Verify API quota not exceeded
|
||||
- Check prompt doesn't violate content policies
|
||||
|
||||
### Issue: Authentication keeps redirecting
|
||||
**Solution:**
|
||||
- Verify Azure AD redirect URI matches exactly
|
||||
- Check HTTPS is enabled
|
||||
- Clear browser cookies and try again
|
||||
|
||||
### Issue: Images not displaying in history
|
||||
**Solution:**
|
||||
- Check file permissions on uploads/
|
||||
- Verify cleanup script hasn't deleted images
|
||||
- Check session_manager.php error logs
|
||||
|
||||
### Issue: Composer install fails
|
||||
**Solution:**
|
||||
```bash
|
||||
# Update Composer
|
||||
composer self-update
|
||||
|
||||
# Clear cache
|
||||
composer clear-cache
|
||||
|
||||
# Try again
|
||||
composer install
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Uninstallation
|
||||
|
||||
```bash
|
||||
# Stop cron job
|
||||
crontab -e
|
||||
# Remove the cleanup.php line
|
||||
|
||||
# Remove application files
|
||||
rm -rf /path/to/nano-pro
|
||||
|
||||
# Remove user data (optional)
|
||||
# WARNING: This deletes all user images!
|
||||
# rm -rf uploads/sessions/*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
1. Check `error_log` file in application directory
|
||||
2. Visit `auth-test.php` for authentication status
|
||||
3. Click "Toggle Debug Panel" in app for session info
|
||||
4. Review `AUTH_README.md` for SSO setup
|
||||
5. Check server error logs (`/var/log/apache2/` or `/var/log/nginx/`)
|
||||
242
QUICK_REFERENCE.md
Normal file
|
|
@ -0,0 +1,242 @@
|
|||
# Nano Banana Pro - Quick Reference Card
|
||||
|
||||
## 🚨 MOST CRITICAL MISTAKES TO AVOID
|
||||
|
||||
### 1. Wrong Request Order
|
||||
```json
|
||||
❌ WRONG: {"parts": [{"text": "..."}, {"inline_data": {...}}]}
|
||||
✅ RIGHT: {"parts": [{"inline_data": {...}}, {"text": "..."}]}
|
||||
```
|
||||
**Image MUST come before text!**
|
||||
|
||||
### 2. Not Checking finishReason
|
||||
```php
|
||||
❌ WRONG: $image = $response['candidates'][0]['content']['parts'][0];
|
||||
✅ RIGHT: if ($reason === 'IMAGE_RECITATION') { /* handle */ }
|
||||
```
|
||||
**ALWAYS check finishReason first!**
|
||||
|
||||
### 3. Hardcoded MIME Type
|
||||
```php
|
||||
❌ WRONG: <img src="data:image/png;base64,...">
|
||||
✅ RIGHT: <img src="data:<?= $mime ?>;base64,...">
|
||||
```
|
||||
**Store and use dynamic MIME type!**
|
||||
|
||||
### 4. Dirty Base64
|
||||
```javascript
|
||||
❌ WRONG: const base64 = reader.result;
|
||||
✅ RIGHT: const base64 = reader.result.split(',')[1];
|
||||
```
|
||||
**Remove data URI prefix!**
|
||||
|
||||
### 5. Simple Prompts
|
||||
```
|
||||
❌ WRONG: "a red circle"
|
||||
✅ RIGHT: "a vintage red sports car in a neon city"
|
||||
```
|
||||
**10+ words, creative, detailed!**
|
||||
|
||||
---
|
||||
|
||||
## 📋 ESSENTIAL PATTERNS
|
||||
|
||||
### Request Structure (New Image)
|
||||
```json
|
||||
{
|
||||
"contents": [{
|
||||
"parts": [{"text": "detailed creative prompt"}]
|
||||
}],
|
||||
"generationConfig": {
|
||||
"responseModalities": ["IMAGE"],
|
||||
"imageConfig": {
|
||||
"aspectRatio": "16:9",
|
||||
"imageSize": "2K"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Request Structure (Edit Image)
|
||||
```json
|
||||
{
|
||||
"contents": [{
|
||||
"parts": [
|
||||
{
|
||||
"inline_data": {
|
||||
"mime_type": "image/jpeg",
|
||||
"data": "base64_here"
|
||||
}
|
||||
},
|
||||
{"text": "edit instruction"}
|
||||
]
|
||||
}],
|
||||
"generationConfig": {
|
||||
"responseModalities": ["IMAGE"],
|
||||
"imageConfig": {
|
||||
"aspectRatio": "16:9",
|
||||
"imageSize": "2K"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Extract Image from Response
|
||||
```php
|
||||
// 1. Check finishReason
|
||||
$reason = $response['candidates'][0]['finishReason'];
|
||||
if ($reason === 'IMAGE_RECITATION') {
|
||||
throw new Exception('Content blocked');
|
||||
}
|
||||
|
||||
// 2. Extract image
|
||||
foreach ($response['candidates'][0]['content']['parts'] as $part) {
|
||||
if (isset($part['inlineData']['data'])) {
|
||||
return [
|
||||
'base64' => $part['inlineData']['data'],
|
||||
'mime_type' => $part['inlineData']['mimeType']
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Session Management
|
||||
```php
|
||||
// Store
|
||||
$_SESSION['current_image'] = $base64;
|
||||
$_SESSION['current_image_mime'] = $mimeType;
|
||||
|
||||
// Retrieve for editing
|
||||
$previousImage = $_SESSION['current_image'];
|
||||
|
||||
// Reset
|
||||
$_SESSION['current_image'] = null;
|
||||
$_SESSION['current_image_mime'] = 'image/png';
|
||||
```
|
||||
|
||||
### File Upload (Client)
|
||||
```javascript
|
||||
function fileToBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result.split(',')[1]; // Remove prefix!
|
||||
resolve(base64);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 ERROR HANDLING MAP
|
||||
|
||||
| finishReason | Action |
|
||||
|--------------|--------|
|
||||
| `IMAGE_RECITATION` | Prompt too generic - ask for creative prompt |
|
||||
| `SAFETY` | Content filter - different prompt needed |
|
||||
| `STOP` | Success - extract image |
|
||||
| Empty content | Check for API error in response |
|
||||
|
||||
| HTTP Code | Meaning | Solution |
|
||||
|-----------|---------|----------|
|
||||
| 200 | Success | Extract image |
|
||||
| 400 | Bad request | Check base64/format |
|
||||
| 429 | Rate limit | Wait 30s |
|
||||
| 500 | Internal error | Retry with backoff |
|
||||
|
||||
---
|
||||
|
||||
## 🔑 KEY DIFFERENCES FROM OTHER APIS
|
||||
|
||||
| Aspect | Gemini | DALL-E/SD |
|
||||
|--------|--------|-----------|
|
||||
| Endpoint | generateContent | dedicated image API |
|
||||
| Response | base64 in JSON | URL to image |
|
||||
| Editing | Send previous base64 | Separate edit endpoint |
|
||||
| Filters | Very aggressive | More lenient |
|
||||
| MIME handling | Dynamic (jpeg) | Fixed (png) |
|
||||
| Auth header | x-goog-api-key | Authorization Bearer |
|
||||
|
||||
---
|
||||
|
||||
## ⚡ DEBUGGING STEPS
|
||||
|
||||
1. Log request payload
|
||||
2. Log response structure
|
||||
3. Check finishReason
|
||||
4. Verify base64 length
|
||||
5. Check MIME type match
|
||||
6. Test with known-good prompt
|
||||
7. Clear session and retry
|
||||
8. Check server logs
|
||||
|
||||
---
|
||||
|
||||
## 📝 WORKING PROMPTS
|
||||
|
||||
✅ **These work:**
|
||||
- "A futuristic motorcycle racing through a neon-lit cyberpunk city at night"
|
||||
- "A magical forest with glowing blue mushrooms and fireflies at twilight"
|
||||
- "A vintage red sports car on a winding mountain road during sunset"
|
||||
- "An astronaut floating in deep space with colorful nebulas in background"
|
||||
|
||||
❌ **These fail:**
|
||||
- "a motorcycle"
|
||||
- "a forest"
|
||||
- "a car"
|
||||
- "a circle"
|
||||
|
||||
**Rule: 10+ words, creative, contextual**
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ API CONFIG
|
||||
|
||||
```php
|
||||
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent";
|
||||
|
||||
$headers = [
|
||||
'Content-Type: application/json',
|
||||
'x-goog-api-key: ' . $apiKey
|
||||
];
|
||||
|
||||
$options = [
|
||||
CURLOPT_TIMEOUT => 120 // 2 minutes!
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ IMPLEMENTATION CHECKLIST
|
||||
|
||||
- [ ] Image before text in parts array
|
||||
- [ ] finishReason checked first
|
||||
- [ ] MIME type stored and used
|
||||
- [ ] Base64 cleaned (no prefix)
|
||||
- [ ] Session manages both data and MIME
|
||||
- [ ] Detailed prompts required
|
||||
- [ ] Error types handled specifically
|
||||
- [ ] Retry logic for 500 errors
|
||||
- [ ] File upload converts properly
|
||||
- [ ] Display uses correct MIME type
|
||||
|
||||
---
|
||||
|
||||
## 🎓 GOTCHAS
|
||||
|
||||
1. Request uses `inline_data`, response uses `inlineData`
|
||||
2. Gemini returns `image/jpeg`, not `png`
|
||||
3. Simple prompts trigger `IMAGE_RECITATION`
|
||||
4. HTTP 200 doesn't mean success (check finishReason)
|
||||
5. Base64 in requests must be pristine
|
||||
6. Image MUST come before text in parts
|
||||
7. Session MUST store MIME type
|
||||
8. 120s timeout minimum
|
||||
9. Retry on 500 errors
|
||||
10. Check content array isn't empty
|
||||
|
||||
---
|
||||
|
||||
*Keep this card handy when implementing!*
|
||||
203
README.md
|
|
@ -1,203 +0,0 @@
|
|||
# Lux Studio - Cinema Studio Pro
|
||||
|
||||
AI-powered cinematography suite for professional image and video generation. Combines physics-based prompt engineering with Google's Imagen 3 and Veo 3.1 APIs.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18+ and npm
|
||||
- PHP 7.4+
|
||||
- Composer
|
||||
- Google Gemini API Key ([Get one here](https://aistudio.google.com/app/apikey))
|
||||
- Azure AD tenant (for SSO authentication - optional, can be disabled)
|
||||
|
||||
### Automated Setup (Recommended)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
cd cinema-studio-pro
|
||||
|
||||
# Run the setup script
|
||||
./setup.sh
|
||||
```
|
||||
|
||||
The setup script will:
|
||||
- Check prerequisites
|
||||
- Install all dependencies
|
||||
- Create `.env` files from examples
|
||||
- Display port configuration and next steps
|
||||
|
||||
### Manual Installation (Alternative)
|
||||
|
||||
1. **Setup Backend**
|
||||
```bash
|
||||
cd backend
|
||||
composer install
|
||||
cp .env.example .env
|
||||
# Edit .env and add your GEMINI_API_KEY
|
||||
```
|
||||
|
||||
2. **Setup Frontend**
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
cp .env.example .env
|
||||
# .env is already configured for local development
|
||||
```
|
||||
|
||||
### Running the Application
|
||||
|
||||
**Terminal 1 - Backend:**
|
||||
```bash
|
||||
cd backend
|
||||
php -S localhost:5015
|
||||
```
|
||||
|
||||
**Terminal 2 - Frontend:**
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**Access the application:**
|
||||
Open your browser and navigate to: `http://localhost:3000`
|
||||
|
||||
### Configuration
|
||||
|
||||
Ports and URLs are controlled by `.env` files:
|
||||
|
||||
- **Backend Port**: Edit `BACKEND_PORT` in `backend/.env` (default: 5015)
|
||||
- **Frontend Port**: Edit `FRONTEND_PORT` in `frontend/.env` (default: 3000)
|
||||
- **Disable SSO**: Set `VITE_SSO_ENABLED=false` in `frontend/.env`
|
||||
- **Switch to Production**: Comment/uncomment production URLs in `.env` files
|
||||
|
||||
For production deployment, see `.env.production` files and `DEPLOYMENT.md`.
|
||||
|
||||
## Features
|
||||
|
||||
### Image Generation (Imagen 3)
|
||||
- **Cinematographer's Toolkit**: 40+ lighting presets, 8 camera bodies, 10 lens profiles
|
||||
- **Physics-Based Prompts**: AI enhances prompts with real camera/lens characteristics
|
||||
- **Reference Images**: Style transfer with up to 14 reference images
|
||||
- **Iterative Editing**: Refine images with natural language
|
||||
|
||||
### Video Generation (Veo 3.1)
|
||||
- **Text-to-Video & Image-to-Video**: Generate from prompts or reference frames
|
||||
- **AI Prompt Optimizer**: Intelligent inference of camera movement and subject action
|
||||
- **Native Audio**: Veo 3's built-in audio generation with dialogue support
|
||||
- **Frame Extraction**: Pull stills from generated videos
|
||||
|
||||
### Project Management
|
||||
- **Project-First Workflow**: Organize generations into projects
|
||||
- **Local Storage**: IndexedDB-based, no server required
|
||||
- **Storyboard Editor**: Annotate and organize scenes with drag-to-reorder
|
||||
- **Cross-Tab Usage**: Use generated images as video reference frames
|
||||
|
||||
### Authentication
|
||||
- **MSAL SSO**: Microsoft Azure AD authentication
|
||||
- **Toggleable**: Can be disabled for development
|
||||
- **Development Credentials**: Pre-configured for quick start
|
||||
- **Logout**: Easy logout from the application header
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
cinema-studio-pro/
|
||||
├── frontend/ # React frontend (port 3000)
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── hooks/ # Custom hooks
|
||||
│ │ ├── authConfig.js # MSAL configuration
|
||||
│ │ └── App.jsx
|
||||
│ ├── .env # Frontend environment variables
|
||||
│ └── package.json
|
||||
├── backend/ # PHP backend (port 8000)
|
||||
│ ├── api.php # Image generation API
|
||||
│ ├── video_api.php # Video generation API
|
||||
│ ├── .env # Backend environment variables
|
||||
│ └── composer.json
|
||||
├── MDFiles/ # Documentation
|
||||
└── CLAUDE.md # Development guide for AI assistants
|
||||
```
|
||||
|
||||
## Configuration Files
|
||||
|
||||
The application uses `.env` files for all configuration. Both local and production settings are managed through these files.
|
||||
|
||||
### Backend (.env) - Local Development
|
||||
```env
|
||||
BACKEND_PORT=5015
|
||||
GEMINI_API_KEY=your-api-key-here
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
SSO_ENABLED=false
|
||||
SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
SSO_CLIENT_ID=15c0c4e2-bac0-4564-a3a6-c2717f00a6d9
|
||||
```
|
||||
|
||||
### Frontend (.env) - Local Development
|
||||
```env
|
||||
FRONTEND_PORT=3000
|
||||
BACKEND_PORT=5015
|
||||
VITE_API_URL=http://localhost:5015
|
||||
VITE_GEMINI_API_KEY=your-api-key-here
|
||||
VITE_SSO_ENABLED=true
|
||||
VITE_SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
VITE_SSO_CLIENT_ID=15c0c4e2-bac0-4564-a3a6-c2717f00a6d9
|
||||
VITE_SSO_REDIRECT_URI=http://localhost:3000
|
||||
NODE_ENV=development
|
||||
```
|
||||
|
||||
**For Production:** See `.env.production` files in both `backend/` and `frontend/` directories.
|
||||
|
||||
## Development
|
||||
|
||||
### Quick Start Script
|
||||
```bash
|
||||
./setup.sh # Automated local setup
|
||||
```
|
||||
|
||||
### Frontend Commands
|
||||
```bash
|
||||
cd frontend
|
||||
npm run dev # Start development server (port from .env)
|
||||
npm run build # Build for production
|
||||
npm run lint # Run linter
|
||||
npm run preview # Preview production build
|
||||
```
|
||||
|
||||
### Backend Commands
|
||||
```bash
|
||||
cd backend
|
||||
composer install # Install dependencies
|
||||
php -S localhost:5015 # Start development server (use your BACKEND_PORT)
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Frontend**: React 19, Vite, Tailwind CSS, MSAL, IndexedDB
|
||||
- **Backend**: PHP 7.4+, Composer
|
||||
- **AI Models**: Google Imagen 3, Veo 3.1, Gemini Pro
|
||||
- **Authentication**: Microsoft Azure AD (MSAL)
|
||||
|
||||
## Documentation
|
||||
|
||||
Detailed documentation is available in the `MDFiles/` directory:
|
||||
- `README.md` - Original user guide
|
||||
- `INSTALL.md` - Deployment instructions
|
||||
- `AI_IMPLEMENTATION_GUIDE.md` - Imagen 3 API patterns
|
||||
- `AUTH_README.md` - Azure AD setup guide
|
||||
- `QUICK_REFERENCE.md` - Common patterns
|
||||
|
||||
For development guidance, see `CLAUDE.md` in the root directory.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Never commit `.env` files (already in .gitignore)
|
||||
- API keys are sensitive - keep them secure
|
||||
- HTTPS is required for production SSO
|
||||
- Images auto-delete after 24 hours on the backend
|
||||
370
api.php.backup
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
<?php
|
||||
// Suppress HTML error output to prevent breaking JSON responses
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Load configuration, session manager, and webhook logger
|
||||
require_once 'config.php';
|
||||
require_once 'session_manager.php';
|
||||
require_once 'webhook_logger.php';
|
||||
|
||||
// Initialize session manager for multi-user support
|
||||
$sessionManager = new SessionManager();
|
||||
|
||||
// Initialize auth status with default
|
||||
$authStatus = [
|
||||
'authenticated' => true,
|
||||
'user' => [
|
||||
'name' => 'User',
|
||||
'preferred_username' => 'anonymous@nano-banana-pro.com'
|
||||
]
|
||||
];
|
||||
|
||||
// Check authentication (with graceful fallback)
|
||||
try {
|
||||
if (file_exists(__DIR__ . '/AuthMiddleware.php')) {
|
||||
require_once 'AuthMiddleware.php';
|
||||
$auth = new AuthMiddleware();
|
||||
$authStatus = $auth->isAuthenticated();
|
||||
|
||||
if (!$authStatus['authenticated']) {
|
||||
http_response_code(401);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Authentication required',
|
||||
'requiresAuth' => true
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Log error but continue without auth (for testing)
|
||||
error_log("Auth check failed in api.php: " . $e->getMessage());
|
||||
}
|
||||
|
||||
class NanoBananaProAPI {
|
||||
private $apiKey;
|
||||
private $baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
|
||||
private $model = 'gemini-3-pro-image-preview';
|
||||
|
||||
public function __construct($apiKey) {
|
||||
$this->apiKey = $apiKey;
|
||||
}
|
||||
|
||||
public function generateImage($prompt, $aspectRatio = '16:9', $imageSize = '2K', $inputImage = null, $referenceImages = []) {
|
||||
$parts = [];
|
||||
|
||||
// IMPORTANT: Input image (the one being edited) must come FIRST
|
||||
// Gemini treats the first image as the primary subject to modify
|
||||
if ($inputImage) {
|
||||
error_log("Edit mode: Input image size = " . strlen($inputImage) . " chars");
|
||||
|
||||
// Clean any whitespace from base64 data
|
||||
$inputImage = preg_replace('/\s+/', '', $inputImage);
|
||||
|
||||
// Basic validation - check if it looks like base64
|
||||
if (!preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $inputImage)) {
|
||||
error_log("Base64 validation failed for input image");
|
||||
error_log("First 100 chars: " . substr($inputImage, 0, 100));
|
||||
throw new Exception("Invalid image data format - not valid base64");
|
||||
}
|
||||
|
||||
$parts[] = [
|
||||
'inline_data' => [
|
||||
'mime_type' => 'image/jpeg', // Use jpeg to match API output
|
||||
'data' => $inputImage
|
||||
]
|
||||
];
|
||||
|
||||
error_log("Added input image FIRST to request (mime_type: image/jpeg)");
|
||||
}
|
||||
|
||||
// Add reference images after input image (up to 14 allowed by Gemini 3 Pro)
|
||||
if (!empty($referenceImages)) {
|
||||
error_log("Adding " . count($referenceImages) . " reference image(s) to request");
|
||||
|
||||
foreach ($referenceImages as $index => $refImg) {
|
||||
$data = preg_replace('/\s+/', '', $refImg['data']);
|
||||
|
||||
// Basic validation
|
||||
if (!preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $data)) {
|
||||
error_log("Base64 validation failed for reference image $index");
|
||||
continue;
|
||||
}
|
||||
|
||||
$parts[] = [
|
||||
'inline_data' => [
|
||||
'mime_type' => $refImg['mime_type'] ?? 'image/jpeg',
|
||||
'data' => $data
|
||||
]
|
||||
];
|
||||
|
||||
error_log("Added reference image $index (mime_type: " . ($refImg['mime_type'] ?? 'image/jpeg') . ")");
|
||||
}
|
||||
}
|
||||
|
||||
if (!$inputImage && empty($referenceImages)) {
|
||||
error_log("Generation mode: No input image or reference images");
|
||||
}
|
||||
|
||||
// Add the text prompt
|
||||
$parts[] = ['text' => $prompt];
|
||||
|
||||
$payload = [
|
||||
'contents' => [
|
||||
['parts' => $parts]
|
||||
],
|
||||
'generationConfig' => [
|
||||
'responseModalities' => ['IMAGE'],
|
||||
'imageConfig' => [
|
||||
'aspectRatio' => $aspectRatio,
|
||||
'imageSize' => $imageSize
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
return $this->makeRequest($payload);
|
||||
}
|
||||
|
||||
private function makeRequest($payload, $retryCount = 0) {
|
||||
$url = "{$this->baseUrl}/{$this->model}:generateContent";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'x-goog-api-key: ' . $this->apiKey
|
||||
],
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_TIMEOUT => 120 // 2 minute timeout for image generation
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
$error = curl_error($ch);
|
||||
@curl_close($ch);
|
||||
throw new Exception('cURL error: ' . $error);
|
||||
}
|
||||
|
||||
@curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
$errorData = json_decode($response, true);
|
||||
$errorMessage = $errorData['error']['message'] ?? "HTTP $httpCode";
|
||||
$errorStatus = $errorData['error']['status'] ?? 'UNKNOWN';
|
||||
|
||||
// Log full error details for debugging
|
||||
error_log("API Error - HTTP $httpCode (Status: $errorStatus)");
|
||||
error_log("Error message: " . $errorMessage);
|
||||
error_log("Full response: " . $response);
|
||||
|
||||
// Handle specific error types
|
||||
if ($httpCode === 500 && stripos($errorMessage, 'internal') !== false && $retryCount < 2) {
|
||||
// Retry on internal errors (up to 2 times)
|
||||
error_log("Retrying request due to internal error (attempt " . ($retryCount + 1) . ")");
|
||||
sleep(2); // Wait 2 seconds before retry
|
||||
return $this->makeRequest($payload, $retryCount + 1);
|
||||
}
|
||||
|
||||
if ($httpCode === 429 || $errorStatus === 'RESOURCE_EXHAUSTED') {
|
||||
throw new Exception("API rate limit exceeded. Please wait a moment and try again.");
|
||||
}
|
||||
|
||||
if ($errorStatus === 'INVALID_ARGUMENT') {
|
||||
throw new Exception("Invalid request format. This might be due to corrupted image data. Try clicking 'Start New Image' and generating fresh.");
|
||||
}
|
||||
|
||||
throw new Exception("API error: $errorMessage (HTTP $httpCode, Status: $errorStatus)");
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
public function extractImageData($response) {
|
||||
// Log the response for debugging
|
||||
error_log("API Response: " . json_encode($response));
|
||||
|
||||
// Check for finish reasons that indicate content issues
|
||||
if (isset($response['candidates'][0]['finishReason'])) {
|
||||
$finishReason = $response['candidates'][0]['finishReason'];
|
||||
$finishMessage = $response['candidates'][0]['finishMessage'] ?? '';
|
||||
|
||||
if ($finishReason === 'IMAGE_RECITATION') {
|
||||
throw new Exception('Image generation blocked by content filter. Try a more creative and descriptive prompt. Avoid simple geometric shapes or common objects. Example: "A futuristic cityscape at sunset with flying cars" instead of "a red circle".');
|
||||
}
|
||||
|
||||
if ($finishReason === 'SAFETY') {
|
||||
throw new Exception('Image generation blocked by safety filters. Please try a different prompt.');
|
||||
}
|
||||
|
||||
if ($finishReason !== 'STOP' && !empty($finishMessage)) {
|
||||
throw new Exception('Image generation failed: ' . $finishMessage);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($response['candidates'][0]['content']['parts'])) {
|
||||
foreach ($response['candidates'][0]['content']['parts'] as $part) {
|
||||
if (isset($part['inline_data']['data'])) {
|
||||
return [
|
||||
'base64' => $part['inline_data']['data'],
|
||||
'mime_type' => $part['inline_data']['mime_type'] ?? 'image/png'
|
||||
];
|
||||
}
|
||||
// Check for inlineData (alternative format)
|
||||
if (isset($part['inlineData']['data'])) {
|
||||
return [
|
||||
'base64' => $part['inlineData']['data'],
|
||||
'mime_type' => $part['inlineData']['mimeType'] ?? 'image/png'
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Provide detailed error with response structure
|
||||
$errorDetails = "Response structure: " . json_encode(array_keys($response));
|
||||
if (isset($response['candidates'][0])) {
|
||||
$errorDetails .= " | Candidate keys: " . json_encode(array_keys($response['candidates'][0]));
|
||||
}
|
||||
throw new Exception('No image data found in API response. ' . $errorDetails);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle API requests
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
throw new Exception('Invalid request method');
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? null;
|
||||
|
||||
if (!$action) {
|
||||
throw new Exception('No action specified');
|
||||
}
|
||||
|
||||
// Handle reset action
|
||||
if ($action === 'reset') {
|
||||
$sessionManager->reset();
|
||||
|
||||
echo json_encode(['success' => true]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle generate action
|
||||
if ($action === 'generate') {
|
||||
$prompt = $_POST['prompt'] ?? null;
|
||||
$aspectRatio = $_POST['aspectRatio'] ?? '16:9';
|
||||
$imageSize = $_POST['imageSize'] ?? '2K';
|
||||
$uploadedImage = $_POST['uploadedImage'] ?? null;
|
||||
$uploadedImageType = $_POST['uploadedImageType'] ?? null;
|
||||
|
||||
// Collect reference images (up to 14)
|
||||
$referenceImages = [];
|
||||
$refCount = intval($_POST['referenceImageCount'] ?? 0);
|
||||
for ($i = 0; $i < min($refCount, 14); $i++) {
|
||||
if (isset($_POST["referenceImage_$i"])) {
|
||||
$referenceImages[] = [
|
||||
'data' => $_POST["referenceImage_$i"],
|
||||
'mime_type' => $_POST["referenceImageType_$i"] ?? 'image/jpeg'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($referenceImages)) {
|
||||
error_log("Received " . count($referenceImages) . " reference images from frontend");
|
||||
}
|
||||
|
||||
// Check if API key is configured
|
||||
if (!defined('GEMINI_API_KEY') || empty(GEMINI_API_KEY)) {
|
||||
throw new Exception('API key not configured. Please set GEMINI_API_KEY in config.php');
|
||||
}
|
||||
|
||||
// Regular generation/editing flow
|
||||
if (!$prompt) {
|
||||
throw new Exception('Prompt is required');
|
||||
}
|
||||
|
||||
// Initialize API
|
||||
$api = new NanoBananaProAPI(GEMINI_API_KEY);
|
||||
|
||||
// Determine input image for editing:
|
||||
// 1. Frontend sends uploadedImage when editing from library or a displayed image
|
||||
// 2. Fall back to session's current image for iterative edits
|
||||
$inputImage = null;
|
||||
if ($uploadedImage) {
|
||||
// Frontend explicitly sent an image - use it (this is the source of truth)
|
||||
$inputImage = $uploadedImage;
|
||||
error_log("Using uploaded image from frontend for editing");
|
||||
} else {
|
||||
// No uploaded image - check session for iterative editing
|
||||
$currentImage = $sessionManager->getCurrentImage();
|
||||
$inputImage = $currentImage ? $currentImage['data'] : null;
|
||||
if ($inputImage) {
|
||||
error_log("Using session image for editing");
|
||||
} else {
|
||||
error_log("No input image - generating new image");
|
||||
}
|
||||
}
|
||||
|
||||
// Generate or edit image (with optional reference images)
|
||||
$response = $api->generateImage($prompt, $aspectRatio, $imageSize, $inputImage, $referenceImages);
|
||||
$imageData = $api->extractImageData($response);
|
||||
|
||||
// Save to disk
|
||||
$filename = $sessionManager->saveImage($imageData['base64'], $imageData['mime_type']);
|
||||
$sessionManager->setCurrentImage($filename, $imageData['mime_type']);
|
||||
|
||||
// Add to conversation history
|
||||
$sessionManager->addToHistory($prompt, $inputImage ? 'edit' : 'generate');
|
||||
|
||||
// Log to webhook
|
||||
try {
|
||||
$userEmail = $authStatus['user']['preferred_username'] ?? $authStatus['user']['email'] ?? 'anonymous@nano-banana-pro.com';
|
||||
$webhookSettings = [
|
||||
'prompt' => $prompt,
|
||||
'aspectRatio' => $aspectRatio,
|
||||
'imageSize' => $imageSize,
|
||||
'actionType' => $inputImage ? 'edit' : 'generate',
|
||||
'model' => 'Google Imagen 3'
|
||||
];
|
||||
|
||||
logImageGeneration($prompt, $imageData['base64'], $imageData['mime_type'], $webhookSettings, $userEmail, $inputImage ? 'edit' : 'generate');
|
||||
} catch (Exception $e) {
|
||||
// Don't fail if webhook fails
|
||||
error_log("Webhook logging failed: " . $e->getMessage());
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Image generated successfully'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
throw new Exception('Invalid action');
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
|
||||
// Log detailed error info
|
||||
error_log("Exception in api.php: " . $e->getMessage());
|
||||
error_log("Stack trace: " . $e->getTraceAsString());
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'debug' => [
|
||||
'file' => basename($e->getFile()),
|
||||
'line' => $e->getLine(),
|
||||
'timestamp' => date('Y-m-d H:i:s')
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
# ============================================================================
|
||||
# Lux Studio Backend Environment Configuration
|
||||
# ============================================================================
|
||||
# Copy this file to .env and configure for your environment
|
||||
# For production settings, see .env.production
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Backend Port Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Port on which the PHP backend server runs
|
||||
# Start backend with: php -S localhost:${BACKEND_PORT}
|
||||
BACKEND_PORT=5015
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# API Base Path (for video/file streaming URLs)
|
||||
# ----------------------------------------------------------------------------
|
||||
# For LOCAL development: Use /lux-studio-back (matches Vite proxy)
|
||||
# For PRODUCTION: Set to your Apache proxy path (e.g., /lux-studio-back)
|
||||
API_BASE_PATH=/lux-studio-back
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Google Gemini API Key (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
# Get your API key from: https://aistudio.google.com/app/apikey
|
||||
GEMINI_API_KEY=your-api-key-here
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Frontend URL for CORS (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
# IMPORTANT: No trailing slash!
|
||||
# This allows the frontend to make API calls to the backend
|
||||
#
|
||||
# LOCAL DEVELOPMENT (uncomment for local):
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
#
|
||||
# PRODUCTION (comment out for local):
|
||||
# FRONTEND_URL=https://ai-sandbox.oliver.solutions/lux-studio
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Azure AD / MSAL SSO Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Backend authentication is DISABLED - Frontend handles SSO
|
||||
# The backend accepts all requests without token validation
|
||||
SSO_ENABLED=false
|
||||
SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
SSO_CLIENT_ID=15c0c4e2-bac0-4564-a3a6-c2717f00a6d9
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
# ============================================================================
|
||||
# Lux Studio Backend - LOCAL Development Environment Configuration
|
||||
# ============================================================================
|
||||
# This file is used by setup.sh for local development
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Backend Port Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Port on which the PHP backend server runs locally
|
||||
# Start backend with: php -S localhost:5015
|
||||
BACKEND_PORT=5015
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# API Base Path (for video/file streaming URLs)
|
||||
# ----------------------------------------------------------------------------
|
||||
# For LOCAL development: Use /lux-studio-back (matches Vite proxy)
|
||||
# For PRODUCTION: Set to your Apache proxy path (e.g., /lux-studio-back)
|
||||
API_BASE_PATH=/lux-studio-back
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Google Gemini API Key (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
# Get your API key from: https://aistudio.google.com/app/apikey
|
||||
# GEMINI_API_KEY=AIzaSyCMKLSJJYEx4c6-TtBACnjdULrLzsr_fts
|
||||
GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
|
||||
# ----------------------------------------------------------------------------
|
||||
# Frontend URL for CORS (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
# IMPORTANT: No trailing slash!
|
||||
# Local development frontend URL
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Azure AD / MSAL SSO Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Backend authentication is DISABLED - Frontend handles SSO
|
||||
SSO_ENABLED=false
|
||||
SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
SSO_CLIENT_ID=15c0c4e2-bac0-4564-a3a6-c2717f00a6d9
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
# ============================================================================
|
||||
# Lux Studio Backend - PRODUCTION Environment Configuration
|
||||
# ============================================================================
|
||||
# This file contains production-ready settings
|
||||
# Copy relevant values to .env when deploying to production server
|
||||
# Server location: /opt/lux-studio-back/.env
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Backend Port Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# NOTE: Backend runs on localhost:5015 (configured in systemd service)
|
||||
# This port is NOT exposed to internet - only accessible via Apache proxy
|
||||
# Port is hardcoded in lux-studio-backend.service, this is for reference only
|
||||
BACKEND_PORT=5015
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# API Base Path (for video/file streaming URLs)
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache proxy path - MUST match ProxyPass configuration in apache2.conf
|
||||
# Used by video_api.php to generate correct video streaming URLs
|
||||
API_BASE_PATH=/lux-studio-back
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Google Gemini API Key (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
# Get your API key from: https://aistudio.google.com/app/apikey
|
||||
GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
|
||||
# AIzaSyCMKLSJJYEx4c6-TtBACnjdULrLzsr_fts
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Frontend URL for CORS (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
# IMPORTANT: No trailing slash!
|
||||
# This allows the frontend to make API calls to the backend
|
||||
FRONTEND_URL=https://ai-sandbox.oliver.solutions/lux-studio
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Azure AD / MSAL SSO Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Backend authentication is DISABLED - Frontend handles SSO
|
||||
# The backend accepts all requests without token validation
|
||||
SSO_ENABLED=false
|
||||
SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
# ==============================================================================
|
||||
# LUX STUDIO BACKEND - SECURITY CONFIGURATION
|
||||
# ==============================================================================
|
||||
# Location: /opt/lux-studio-back/.htaccess
|
||||
# Purpose: Prevent direct browser access, allow only API endpoints
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# DIRECTORY PROTECTION
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Disable directory browsing
|
||||
Options -Indexes
|
||||
|
||||
# Disable server signature
|
||||
ServerSignature Off
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# BLOCK ROOT AND INDEX ACCESS
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Block access to index.php (prevents browsing backend UI)
|
||||
<Files "index.php">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
# Block access to test files
|
||||
<FilesMatch "^(test|auth-test|server-check)\.php$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Block access to configuration files
|
||||
<FilesMatch "^(config|config\.example)\.php$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# API ENDPOINT ACCESS CONTROL
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Only allow POST requests to API endpoints
|
||||
# GET is allowed only for stream_video.php (video streaming)
|
||||
|
||||
<FilesMatch "^(api|video_api|enhance_prompt|auth|webhook_logger)\.php$">
|
||||
<RequireAll>
|
||||
Require all granted
|
||||
<RequireAny>
|
||||
Require method POST OPTIONS
|
||||
</RequireAny>
|
||||
</RequireAll>
|
||||
</FilesMatch>
|
||||
|
||||
# Allow GET and POST for video streaming
|
||||
<Files "stream_video.php">
|
||||
Require all granted
|
||||
</Files>
|
||||
|
||||
# Allow POST for cleanup and session management
|
||||
<FilesMatch "^(cleanup|clear_session|session_manager)\.php$">
|
||||
<RequireAll>
|
||||
Require all granted
|
||||
Require method POST OPTIONS
|
||||
</RequireAll>
|
||||
</FilesMatch>
|
||||
|
||||
# Allow get_config.php and get_logs.php for debugging (consider disabling in production)
|
||||
<FilesMatch "^(get_config|get_logs|get_current_image)\.php$">
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# PROTECT SENSITIVE FILES
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Deny access to environment files
|
||||
<FilesMatch "^\.env">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Deny access to backup and temporary files
|
||||
<FilesMatch "\.(bak|backup|old|tmp|temp|swp|save|orig|dist|log|sql|sqlite|db)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Deny access to version control
|
||||
<FilesMatch "(^\.git|^\.svn|^\.hg|composer\.)">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Deny access to class files (should only be included, not accessed directly)
|
||||
<FilesMatch "^(AuthMiddleware|JWTValidator|SessionManager)\.php$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Deny access to .htaccess itself
|
||||
<Files ".htaccess">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# UPLOADS DIRECTORY PROTECTION
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Block direct access to uploads directory (videos/images should be streamed via PHP)
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
|
||||
# Block direct access to uploads folder
|
||||
RewriteRule ^uploads/ - [F,L]
|
||||
</IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# SECURITY HEADERS (if not set by Apache)
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
# Prevent MIME type sniffing
|
||||
Header set X-Content-Type-Options "nosniff"
|
||||
|
||||
# Prevent clickjacking
|
||||
Header set X-Frame-Options "DENY"
|
||||
|
||||
# XSS Protection
|
||||
Header set X-XSS-Protection "1; mode=block"
|
||||
</IfModule>
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# ERROR HANDLING
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Return 403 Forbidden instead of showing file listings
|
||||
<IfModule mod_autoindex.c>
|
||||
Options -Indexes
|
||||
</IfModule>
|
||||
|
||||
# Custom error document (returns JSON for API errors)
|
||||
ErrorDocument 403 "Forbidden: Direct access to backend is not allowed. Use API endpoints only."
|
||||
ErrorDocument 404 "Not Found: API endpoint does not exist."
|
||||
|
||||
# ==============================================================================
|
||||
# END OF CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
|
@ -13,6 +13,9 @@ ini_set('memory_limit', '512M');
|
|||
ini_set('post_max_size', '100M');
|
||||
ini_set('upload_max_filesize', '100M');
|
||||
|
||||
// Increase execution time for long-running API calls
|
||||
set_time_limit(180); // 3 minutes
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
// Load configuration, session manager, and webhook logger
|
||||
|
|
@ -150,7 +153,7 @@ class NanoBananaProAPI {
|
|||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_TIMEOUT => 120 // 2 minute timeout for image generation
|
||||
]);
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ if (file_exists(__DIR__ . '/env_loader.php')) {
|
|||
|
||||
// Google Gemini API Key (Nano Banana Pro)
|
||||
// Get your API key from: https://aistudio.google.com/app/apikey
|
||||
define('GEMINI_API_KEY', 'YOUR_API_KEY_HERE');
|
||||
define('GEMINI_API_KEY', 'AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8');
|
||||
|
||||
// MSAL / Azure AD SSO Configuration
|
||||
// Always define these constants with defaults if not set
|
||||
|
|
|
|||
|
|
@ -221,7 +221,6 @@ try {
|
|||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData));
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disabled for MAMP development
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* Get Session File API
|
||||
* Fetches a specific file from backend sessions and returns as base64
|
||||
* Used for importing backend files to projects
|
||||
*/
|
||||
|
||||
// Suppress HTML error output
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
// Handle OPTIONS preflight
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get parameters
|
||||
$sessionId = $_GET['session_id'] ?? '';
|
||||
$fileType = $_GET['file_type'] ?? 'image'; // 'image' or 'video'
|
||||
$filename = $_GET['filename'] ?? '';
|
||||
|
||||
// Validate parameters
|
||||
if (empty($sessionId)) {
|
||||
throw new Exception('Missing session_id parameter');
|
||||
}
|
||||
if (empty($filename)) {
|
||||
throw new Exception('Missing filename parameter');
|
||||
}
|
||||
if (!in_array($fileType, ['image', 'video'])) {
|
||||
throw new Exception('Invalid file_type parameter (must be "image" or "video")');
|
||||
}
|
||||
|
||||
// Sanitize inputs to prevent directory traversal
|
||||
$sessionId = basename($sessionId);
|
||||
$filename = basename($filename);
|
||||
|
||||
// Construct file path
|
||||
$uploadsDir = __DIR__ . '/uploads/sessions';
|
||||
$subDir = $fileType === 'image' ? 'images' : 'videos';
|
||||
$filePath = $uploadsDir . '/' . $sessionId . '/' . $subDir . '/' . $filename;
|
||||
|
||||
// Check if file exists
|
||||
if (!file_exists($filePath)) {
|
||||
http_response_code(404);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'File not found',
|
||||
'message' => 'The requested file does not exist or has been deleted'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if file is still valid (not expired)
|
||||
$createdAt = filectime($filePath);
|
||||
$expiresAt = $createdAt + (24 * 3600);
|
||||
if (time() > $expiresAt) {
|
||||
http_response_code(410);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'File expired',
|
||||
'message' => 'This file has expired and will be deleted soon'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Read file
|
||||
$fileContent = file_get_contents($filePath);
|
||||
if ($fileContent === false) {
|
||||
throw new Exception('Failed to read file');
|
||||
}
|
||||
|
||||
// Convert to base64
|
||||
$base64Data = base64_encode($fileContent);
|
||||
|
||||
// Get MIME type from metadata if available
|
||||
$mimeType = $fileType === 'image' ? 'image/jpeg' : 'video/mp4';
|
||||
$metaPath = $filePath . '.meta';
|
||||
if (file_exists($metaPath)) {
|
||||
$metaContent = file_get_contents($metaPath);
|
||||
$meta = json_decode($metaContent, true);
|
||||
if ($meta && isset($meta['mime_type'])) {
|
||||
$mimeType = $meta['mime_type'];
|
||||
}
|
||||
}
|
||||
|
||||
// Get file size info
|
||||
$fileSize = strlen($fileContent);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $base64Data,
|
||||
'filename' => $filename,
|
||||
'mime_type' => $mimeType,
|
||||
'file_type' => $fileType,
|
||||
'size_bytes' => $fileSize,
|
||||
'size_kb' => round($fileSize / 1024, 2),
|
||||
'size_mb' => round($fileSize / (1024 * 1024), 2),
|
||||
'created_at' => $createdAt,
|
||||
'expires_at' => $expiresAt,
|
||||
'time_remaining' => max(0, $expiresAt - time())
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in get_session_file.php: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Failed to fetch file',
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
1513
backend/index.php
|
|
@ -1,169 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* List Session Files API
|
||||
* Scans uploads/sessions/ directory and returns all available files
|
||||
* Used for importing backend files to projects
|
||||
*/
|
||||
|
||||
// Suppress HTML error output
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
// Handle OPTIONS preflight
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$uploadsDir = __DIR__ . '/uploads/sessions';
|
||||
|
||||
// Check if directory exists
|
||||
if (!is_dir($uploadsDir)) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'sessions' => [],
|
||||
'message' => 'No sessions directory found'
|
||||
]);
|
||||
exit;
|
||||
}
|
||||
|
||||
$sessions = [];
|
||||
$sessionDirs = glob($uploadsDir . '/*', GLOB_ONLYDIR);
|
||||
|
||||
foreach ($sessionDirs as $sessionPath) {
|
||||
$sessionId = basename($sessionPath);
|
||||
$imagesDir = $sessionPath . '/images';
|
||||
$videosDir = $sessionPath . '/videos';
|
||||
|
||||
$sessionData = [
|
||||
'session_id' => $sessionId,
|
||||
'images' => [],
|
||||
'videos' => []
|
||||
];
|
||||
|
||||
// Scan images directory
|
||||
if (is_dir($imagesDir)) {
|
||||
$imageFiles = glob($imagesDir . '/*.{jpg,jpeg,png,webp}', GLOB_BRACE);
|
||||
|
||||
foreach ($imageFiles as $imagePath) {
|
||||
$filename = basename($imagePath);
|
||||
$metaPath = $imagePath . '.meta';
|
||||
|
||||
// Get file stats
|
||||
$fileSize = filesize($imagePath);
|
||||
$createdAt = filectime($imagePath);
|
||||
$expiresAt = $createdAt + (24 * 3600); // 24 hours from creation
|
||||
|
||||
// Read metadata if available
|
||||
$mimeType = 'image/jpeg';
|
||||
if (file_exists($metaPath)) {
|
||||
$metaContent = file_get_contents($metaPath);
|
||||
$meta = json_decode($metaContent, true);
|
||||
if ($meta && isset($meta['mime_type'])) {
|
||||
$mimeType = $meta['mime_type'];
|
||||
}
|
||||
}
|
||||
|
||||
$sessionData['images'][] = [
|
||||
'filename' => $filename,
|
||||
'path' => 'sessions/' . $sessionId . '/images/' . $filename,
|
||||
'created_at' => $createdAt,
|
||||
'expires_at' => $expiresAt,
|
||||
'time_remaining' => max(0, $expiresAt - time()),
|
||||
'mime_type' => $mimeType,
|
||||
'size_kb' => round($fileSize / 1024, 2),
|
||||
'size_bytes' => $fileSize
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Scan videos directory
|
||||
if (is_dir($videosDir)) {
|
||||
$videoFiles = glob($videosDir . '/*.{mp4,webm,mov}', GLOB_BRACE);
|
||||
|
||||
foreach ($videoFiles as $videoPath) {
|
||||
$filename = basename($videoPath);
|
||||
$metaPath = $videoPath . '.meta';
|
||||
|
||||
// Get file stats
|
||||
$fileSize = filesize($videoPath);
|
||||
$createdAt = filectime($videoPath);
|
||||
$expiresAt = $createdAt + (24 * 3600);
|
||||
|
||||
// Read metadata if available
|
||||
$mimeType = 'video/mp4';
|
||||
if (file_exists($metaPath)) {
|
||||
$metaContent = file_get_contents($metaPath);
|
||||
$meta = json_decode($metaContent, true);
|
||||
if ($meta && isset($meta['mime_type'])) {
|
||||
$mimeType = $meta['mime_type'];
|
||||
}
|
||||
}
|
||||
|
||||
$sessionData['videos'][] = [
|
||||
'filename' => $filename,
|
||||
'path' => 'sessions/' . $sessionId . '/videos/' . $filename,
|
||||
'created_at' => $createdAt,
|
||||
'expires_at' => $expiresAt,
|
||||
'time_remaining' => max(0, $expiresAt - time()),
|
||||
'mime_type' => $mimeType,
|
||||
'size_kb' => round($fileSize / 1024, 2),
|
||||
'size_mb' => round($fileSize / (1024 * 1024), 2),
|
||||
'size_bytes' => $fileSize
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Only include sessions that have files
|
||||
if (count($sessionData['images']) > 0 || count($sessionData['videos']) > 0) {
|
||||
$sessions[] = $sessionData;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort sessions by most recent file
|
||||
usort($sessions, function($a, $b) {
|
||||
$aMax = 0;
|
||||
$bMax = 0;
|
||||
|
||||
foreach ($a['images'] as $img) {
|
||||
$aMax = max($aMax, $img['created_at']);
|
||||
}
|
||||
foreach ($a['videos'] as $vid) {
|
||||
$aMax = max($aMax, $vid['created_at']);
|
||||
}
|
||||
|
||||
foreach ($b['images'] as $img) {
|
||||
$bMax = max($bMax, $img['created_at']);
|
||||
}
|
||||
foreach ($b['videos'] as $vid) {
|
||||
$bMax = max($bMax, $vid['created_at']);
|
||||
}
|
||||
|
||||
return $bMax - $aMax; // Descending order (newest first)
|
||||
});
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'sessions' => $sessions,
|
||||
'total_sessions' => count($sessions),
|
||||
'total_files' => array_reduce($sessions, function($carry, $session) {
|
||||
return $carry + count($session['images']) + count($session['videos']);
|
||||
}, 0)
|
||||
], JSON_PRETTY_PRINT);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in list_session_files.php: " . $e->getMessage());
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => 'Failed to list session files',
|
||||
'message' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
|
|
@ -29,7 +29,7 @@ class SessionManager {
|
|||
}
|
||||
|
||||
$this->sessionId = session_id();
|
||||
$this->uploadDir = __DIR__ . '/uploads/sessions';
|
||||
$this->uploadDir = dirname(__DIR__) . '/uploads/sessions';
|
||||
$this->sessionDir = $this->uploadDir . '/' . $this->sessionId;
|
||||
|
||||
// Create session directory if it doesn't exist
|
||||
|
|
@ -352,7 +352,7 @@ class SessionManager {
|
|||
*/
|
||||
public static function cleanupExpiredImages($uploadDir = null) {
|
||||
if ($uploadDir === null) {
|
||||
$uploadDir = __DIR__ . '/uploads/sessions';
|
||||
$uploadDir = dirname(__DIR__) . '/uploads/sessions';
|
||||
}
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
// Simple test file to verify backend is accessible
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'status' => 'success',
|
||||
'message' => 'Backend is running!',
|
||||
'php_version' => phpversion(),
|
||||
'timestamp' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
|
@ -54,12 +54,6 @@ class VeoVideoAPI {
|
|||
'prompt' => $prompt
|
||||
];
|
||||
|
||||
// Build parameters first
|
||||
$parameters = [
|
||||
'aspectRatio' => $aspectRatio,
|
||||
'sampleCount' => 1
|
||||
];
|
||||
|
||||
// Frame mode: first image is starting frame, optional second image is last frame for interpolation
|
||||
if (!empty($referenceImages)) {
|
||||
if (isset($referenceImages[0])) {
|
||||
|
|
@ -67,7 +61,6 @@ class VeoVideoAPI {
|
|||
$data = preg_replace('/\s+/', '', $refImg['data']);
|
||||
|
||||
if (preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $data)) {
|
||||
// Use bytesBase64Encoded format (original working format)
|
||||
$instance['image'] = [
|
||||
'bytesBase64Encoded' => $data,
|
||||
'mimeType' => $refImg['mime_type'] ?? 'image/jpeg'
|
||||
|
|
@ -96,6 +89,12 @@ class VeoVideoAPI {
|
|||
}
|
||||
}
|
||||
|
||||
// Build parameters
|
||||
$parameters = [
|
||||
'aspectRatio' => $aspectRatio,
|
||||
'sampleCount' => 1
|
||||
];
|
||||
|
||||
// Duration: Veo 3.1 supports 4, 6, or 8 seconds
|
||||
if (in_array(intval($duration), [4, 6, 8])) {
|
||||
$parameters['durationSeconds'] = intval($duration);
|
||||
|
|
@ -137,7 +136,7 @@ class VeoVideoAPI {
|
|||
|
||||
// Log payload structure (without full base64 data for readability)
|
||||
$logPayload = $payload;
|
||||
if (isset($logPayload['instances'][0]['image']['bytesBase64Encoded'])) {
|
||||
if (isset($logPayload['instances'][0]['image'])) {
|
||||
$logPayload['instances'][0]['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['image']['bytesBase64Encoded']) . '_bytes]';
|
||||
}
|
||||
if (isset($logPayload['instances'][0]['lastFrame']['bytesBase64Encoded'])) {
|
||||
|
|
@ -145,12 +144,9 @@ class VeoVideoAPI {
|
|||
}
|
||||
if (isset($logPayload['instances'][0]['referenceImages'])) {
|
||||
foreach ($logPayload['instances'][0]['referenceImages'] as $i => &$refImg) {
|
||||
if (isset($refImg['image']['bytesBase64Encoded'])) {
|
||||
$refImg['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['referenceImages'][$i]['image']['bytesBase64Encoded']) . '_bytes]';
|
||||
}
|
||||
$refImg['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['referenceImages'][$i]['image']['bytesBase64Encoded']) . '_bytes]';
|
||||
}
|
||||
}
|
||||
error_log("Video generation request - Model: " . $this->model);
|
||||
error_log("Video generation payload structure: " . json_encode($logPayload));
|
||||
|
||||
return $this->makeRequest($payload);
|
||||
|
|
@ -172,7 +168,7 @@ class VeoVideoAPI {
|
|||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_TIMEOUT => 600 // 10 minute timeout for video generation
|
||||
]);
|
||||
|
||||
|
|
@ -210,12 +206,7 @@ class VeoVideoAPI {
|
|||
}
|
||||
|
||||
if ($errorStatus === 'INVALID_ARGUMENT') {
|
||||
// Check if error is about lastFrame feature
|
||||
if (stripos($errorMessage, 'lastFrame') !== false) {
|
||||
throw new Exception("The lastFrame feature may not be available for your API key yet. Google is still rolling out Veo 3.1 features. Try using single image-to-video instead, or check Google AI Studio for feature availability. Error: " . $errorMessage);
|
||||
}
|
||||
// Include the actual error message from Google API for debugging
|
||||
throw new Exception("Invalid request format: " . $errorMessage);
|
||||
throw new Exception("Invalid request format. Check your prompt and settings.");
|
||||
}
|
||||
|
||||
if (stripos($errorMessage, 'model') !== false && stripos($errorMessage, 'not found') !== false) {
|
||||
|
|
@ -481,7 +472,7 @@ class VeoVideoAPI {
|
|||
'x-goog-api-key: ' . $this->apiKey
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_TIMEOUT => 30
|
||||
]);
|
||||
|
||||
|
|
@ -611,7 +602,7 @@ try {
|
|||
'video' => [
|
||||
'filename' => $filename,
|
||||
'mime_type' => $videoData['mime_type'],
|
||||
'url' => '/generated_videos/' . $filename
|
||||
'url' => '/lux-studio/api/stream_video.php?file=' . urlencode($filename)
|
||||
]
|
||||
]);
|
||||
} else if ($videoData['type'] === 'uri') {
|
||||
|
|
@ -653,7 +644,7 @@ try {
|
|||
'video' => [
|
||||
'filename' => $filename,
|
||||
'mime_type' => $videoData['mime_type'],
|
||||
'url' => '/generated_videos/' . $filename
|
||||
'url' => '/lux-studio/api/stream_video.php?file=' . urlencode($filename)
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
|
|
@ -705,7 +696,7 @@ try {
|
|||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_TIMEOUT => 120
|
||||
]);
|
||||
|
||||
|
|
@ -729,11 +720,11 @@ try {
|
|||
$filepath = $videoDir . '/' . $filename;
|
||||
file_put_contents($filepath, $videoData);
|
||||
|
||||
// Return URL in /generated_videos/ format for frontend to handle
|
||||
// Return local URL
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'video' => [
|
||||
'url' => '/generated_videos/' . $filename,
|
||||
'url' => '/api/stream_video.php?file=' . urlencode($filename),
|
||||
'filename' => $filename,
|
||||
'mime_type' => 'video/mp4'
|
||||
]
|
||||
|
|
|
|||
84
composer.lock
generated
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "0f805d76d4ba7495b7d83f2ad0a9e263",
|
||||
"packages": [
|
||||
{
|
||||
"name": "firebase/php-jwt",
|
||||
"version": "v6.11.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/firebase/php-jwt.git",
|
||||
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
|
||||
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"guzzlehttp/guzzle": "^7.4",
|
||||
"phpspec/prophecy-phpunit": "^2.0",
|
||||
"phpunit/phpunit": "^9.5",
|
||||
"psr/cache": "^2.0||^3.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-sodium": "Support EdDSA (Ed25519) signatures",
|
||||
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Firebase\\JWT\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Neuman Vong",
|
||||
"email": "neuman+pear@twilio.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Anant Narayanan",
|
||||
"email": "anant@php.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
|
||||
"homepage": "https://github.com/firebase/php-jwt",
|
||||
"keywords": [
|
||||
"jwt",
|
||||
"php"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/firebase/php-jwt/issues",
|
||||
"source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
|
||||
},
|
||||
"time": "2025-04-09T20:32:01+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {
|
||||
"php": ">=7.4"
|
||||
},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
512
deploy.sh
|
|
@ -1,512 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
################################################################################
|
||||
# Lux Studio Full Deployment Script
|
||||
# Purpose: Automated deployment of Lux Studio (backend + frontend)
|
||||
# Usage: Place entire project in /opt/lux-studio-back/ and run: sudo ./deploy.sh
|
||||
################################################################################
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Configuration
|
||||
PROJECT_DIR="/opt/lux-studio-back"
|
||||
FRONTEND_DIR="/var/www/html/lux-studio"
|
||||
SERVICE_NAME="lux-studio-backend"
|
||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
APACHE_CONF="/etc/apache2/apache2.conf"
|
||||
BACKEND_PORT=5015
|
||||
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo -e "${BLUE} Lux Studio Full Deployment Script${NC}"
|
||||
echo -e "${BLUE}========================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo -e "${RED}Error: This script must be run as root${NC}"
|
||||
echo "Please run: sudo $0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if running from correct directory
|
||||
CURRENT_DIR=$(pwd)
|
||||
if [ "$CURRENT_DIR" != "$PROJECT_DIR" ]; then
|
||||
echo -e "${YELLOW}⚠ Warning: Not running from $PROJECT_DIR${NC}"
|
||||
echo "Current directory: $CURRENT_DIR"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}[1/15] Checking project structure...${NC}"
|
||||
|
||||
# Check if backend directory exists
|
||||
if [ ! -d "$PROJECT_DIR" ]; then
|
||||
echo -e "${RED}Error: Project directory $PROJECT_DIR does not exist${NC}"
|
||||
echo "Please upload the entire project to $PROJECT_DIR first"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN} ✓ Project directory exists${NC}"
|
||||
|
||||
# Check if backend files exist
|
||||
if [ ! -f "$PROJECT_DIR/api.php" ]; then
|
||||
echo -e "${RED}Error: Backend files not found in $PROJECT_DIR${NC}"
|
||||
echo "Please ensure backend PHP files are in $PROJECT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN} ✓ Backend files found${NC}"
|
||||
|
||||
# Check if frontend directory exists in project
|
||||
FRONTEND_SOURCE="$PROJECT_DIR/frontend"
|
||||
HAS_FRONTEND=false
|
||||
if [ -d "$FRONTEND_SOURCE" ]; then
|
||||
echo -e "${GREEN} ✓ Frontend directory found (will build and deploy)${NC}"
|
||||
HAS_FRONTEND=true
|
||||
else
|
||||
echo -e "${YELLOW} ⚠ Frontend directory not found (backend-only deployment)${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[2/15] Checking system requirements...${NC}"
|
||||
|
||||
# Check PHP
|
||||
if ! command -v php &> /dev/null; then
|
||||
echo -e "${RED}Error: PHP is not installed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
PHP_VERSION=$(php -v | head -n 1 | cut -d " " -f 2 | cut -d "." -f 1-2)
|
||||
echo -e "${GREEN} ✓ PHP $PHP_VERSION installed${NC}"
|
||||
|
||||
# Check Composer
|
||||
if ! command -v composer &> /dev/null; then
|
||||
echo -e "${YELLOW} ⚠ Composer not found, installing...${NC}"
|
||||
cd ~
|
||||
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
|
||||
php composer-setup.php --quiet --install-dir=/usr/local/bin --filename=composer
|
||||
php -r "unlink('composer-setup.php');"
|
||||
echo -e "${GREEN} ✓ Composer installed${NC}"
|
||||
else
|
||||
COMPOSER_VERSION=$(composer --version | head -n 1 | cut -d " " -f 3)
|
||||
echo -e "${GREEN} ✓ Composer $COMPOSER_VERSION installed${NC}"
|
||||
fi
|
||||
|
||||
# Check Apache
|
||||
if ! command -v apache2 &> /dev/null; then
|
||||
echo -e "${RED}Error: Apache is not installed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN} ✓ Apache installed${NC}"
|
||||
|
||||
# Check Node.js if frontend exists
|
||||
if [ "$HAS_FRONTEND" = true ]; then
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo -e "${YELLOW} ⚠ Node.js not found, installing...${NC}"
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
|
||||
apt-get install -y nodejs
|
||||
echo -e "${GREEN} ✓ Node.js installed${NC}"
|
||||
else
|
||||
NODE_VERSION=$(node -v)
|
||||
echo -e "${GREEN} ✓ Node.js $NODE_VERSION installed${NC}"
|
||||
fi
|
||||
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo -e "${RED}Error: npm is not installed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
NPM_VERSION=$(npm -v)
|
||||
echo -e "${GREEN} ✓ npm $NPM_VERSION installed${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[3/15] Installing backend dependencies...${NC}"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
# Install Composer dependencies
|
||||
if [ ! -d "vendor" ]; then
|
||||
echo " Installing PHP dependencies..."
|
||||
sudo -u www-data composer install --no-dev --optimize-autoloader
|
||||
echo -e "${GREEN} ✓ PHP dependencies installed${NC}"
|
||||
else
|
||||
echo -e "${GREEN} ✓ PHP dependencies already installed${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[4/15] Configuring backend environment...${NC}"
|
||||
|
||||
# Create .env if not exists
|
||||
if [ ! -f "$PROJECT_DIR/.env" ]; then
|
||||
if [ -f "$PROJECT_DIR/.env.production" ]; then
|
||||
cp "$PROJECT_DIR/.env.production" "$PROJECT_DIR/.env"
|
||||
echo -e "${GREEN} ✓ Created .env from .env.production${NC}"
|
||||
echo -e "${GREEN} ✓ Production settings applied${NC}"
|
||||
else
|
||||
echo -e "${RED} Error: .env.production not found${NC}"
|
||||
echo -e "${YELLOW} Please ensure backend/.env.production exists${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN} ✓ .env file exists${NC}"
|
||||
echo -e "${YELLOW} ⚠ Using existing .env (not overwriting)${NC}"
|
||||
fi
|
||||
|
||||
# Create uploads directory
|
||||
mkdir -p "$PROJECT_DIR/uploads/sessions"
|
||||
echo -e "${GREEN} ✓ Uploads directory created${NC}"
|
||||
|
||||
# Set backend permissions
|
||||
chown -R www-data:www-data "$PROJECT_DIR"
|
||||
chmod -R 755 "$PROJECT_DIR"
|
||||
chmod -R 777 "$PROJECT_DIR/uploads"
|
||||
echo -e "${GREEN} ✓ Backend permissions set${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[5/15] Building and deploying frontend...${NC}"
|
||||
|
||||
if [ "$HAS_FRONTEND" = true ]; then
|
||||
cd "$FRONTEND_SOURCE"
|
||||
|
||||
# Check for production .env
|
||||
if [ ! -f ".env" ]; then
|
||||
if [ -f ".env.production" ]; then
|
||||
echo " Creating frontend .env from .env.production..."
|
||||
cp ".env.production" ".env"
|
||||
echo -e "${GREEN} ✓ Created frontend .env with production settings${NC}"
|
||||
else
|
||||
echo -e "${RED} Error: .env.production not found${NC}"
|
||||
echo -e "${YELLOW} Please ensure frontend/.env.production exists${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN} ✓ Frontend .env exists${NC}"
|
||||
echo -e "${YELLOW} ⚠ Using existing .env (not overwriting)${NC}"
|
||||
fi
|
||||
|
||||
# Install npm dependencies
|
||||
echo " Installing npm dependencies..."
|
||||
npm install --silent
|
||||
echo -e "${GREEN} ✓ npm dependencies installed${NC}"
|
||||
|
||||
# Build frontend
|
||||
echo " Building frontend (this may take 30-60 seconds)..."
|
||||
npm run build
|
||||
echo -e "${GREEN} ✓ Frontend built successfully${NC}"
|
||||
|
||||
# Deploy frontend
|
||||
echo " Deploying frontend to $FRONTEND_DIR..."
|
||||
mkdir -p "$FRONTEND_DIR"
|
||||
rm -rf "$FRONTEND_DIR"/*
|
||||
cp -r dist/* "$FRONTEND_DIR/"
|
||||
|
||||
# Copy .htaccess if exists
|
||||
if [ -f ".htaccess" ]; then
|
||||
cp ".htaccess" "$FRONTEND_DIR/"
|
||||
echo -e "${GREEN} ✓ .htaccess copied${NC}"
|
||||
fi
|
||||
|
||||
# Set frontend permissions
|
||||
chown -R www-data:www-data "$FRONTEND_DIR"
|
||||
chmod -R 755 "$FRONTEND_DIR"
|
||||
echo -e "${GREEN} ✓ Frontend deployed to $FRONTEND_DIR${NC}"
|
||||
|
||||
# Verify deployment
|
||||
if [ -f "$FRONTEND_DIR/index.html" ]; then
|
||||
echo -e "${GREEN} ✓ Frontend deployment verified${NC}"
|
||||
else
|
||||
echo -e "${RED} Error: Frontend deployment failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${YELLOW} ⚠ Skipping frontend build (no frontend directory)${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[6/15] Setting up systemd service...${NC}"
|
||||
|
||||
# Check if service file exists in project
|
||||
if [ -f "$PROJECT_DIR/lux-studio-backend.service" ]; then
|
||||
if [ ! -f "$SERVICE_FILE" ] || [ "$PROJECT_DIR/lux-studio-backend.service" -nt "$SERVICE_FILE" ]; then
|
||||
echo " Copying service file to systemd..."
|
||||
cp "$PROJECT_DIR/lux-studio-backend.service" "$SERVICE_FILE"
|
||||
chmod 644 "$SERVICE_FILE"
|
||||
echo -e "${GREEN} ✓ Service file installed${NC}"
|
||||
else
|
||||
echo -e "${GREEN} ✓ Service file already up to date${NC}"
|
||||
fi
|
||||
else
|
||||
if [ ! -f "$SERVICE_FILE" ]; then
|
||||
echo -e "${RED} Error: Service file not found${NC}"
|
||||
echo " Please ensure lux-studio-backend.service exists in project root"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "${GREEN} ✓ Service file exists${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[7/15] Configuring Apache...${NC}"
|
||||
|
||||
# Enable required modules
|
||||
echo " Enabling Apache modules..."
|
||||
a2enmod rewrite proxy proxy_http headers ssl 2>/dev/null || true
|
||||
echo -e "${GREEN} ✓ Apache modules enabled${NC}"
|
||||
|
||||
# Check if Apache config already has Lux Studio configuration
|
||||
if grep -q "lux-studio-back" "$APACHE_CONF"; then
|
||||
echo -e "${GREEN} ✓ Apache configuration already present${NC}"
|
||||
else
|
||||
echo " Adding Lux Studio configuration to Apache..."
|
||||
|
||||
# Create backup
|
||||
cp "$APACHE_CONF" "${APACHE_CONF}.backup-$(date +%Y%m%d-%H%M%S)"
|
||||
echo -e "${GREEN} ✓ Apache config backed up${NC}"
|
||||
|
||||
# Check if apache config file exists in project
|
||||
if [ -f "$PROJECT_DIR/lux-studio-apache.conf" ]; then
|
||||
# Add configuration
|
||||
cat >> "$APACHE_CONF" << 'EOF'
|
||||
|
||||
# ==============================================================================
|
||||
# LUX STUDIO CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
||||
# Backend API Proxy
|
||||
ProxyPass /lux-studio-back/ http://localhost:5015/
|
||||
ProxyPassReverse /lux-studio-back/ http://localhost:5015/
|
||||
|
||||
# Frontend Directory
|
||||
<Directory /var/www/html/lux-studio>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# Security - Block backend source access
|
||||
<Directory /opt/lux-studio-back>
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
# CORS Headers
|
||||
<Location /lux-studio-back>
|
||||
Header set Access-Control-Allow-Origin "https://ai-sandbox.oliver.solutions"
|
||||
Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
|
||||
Header set Access-Control-Allow-Credentials "true"
|
||||
</Location>
|
||||
|
||||
# ==============================================================================
|
||||
# END LUX STUDIO CONFIGURATION
|
||||
# ==============================================================================
|
||||
EOF
|
||||
echo -e "${GREEN} ✓ Apache configuration added${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} ⚠ Apache config file not found, please add manually${NC}"
|
||||
echo " Add the contents of lux-studio-apache.conf to $APACHE_CONF"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Test Apache configuration
|
||||
echo " Testing Apache configuration..."
|
||||
if apache2ctl configtest 2>&1 | grep -q "Syntax OK"; then
|
||||
echo -e "${GREEN} ✓ Apache configuration valid${NC}"
|
||||
else
|
||||
echo -e "${RED} Error: Apache configuration invalid${NC}"
|
||||
apache2ctl configtest
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Reload Apache
|
||||
echo " Reloading Apache..."
|
||||
systemctl reload apache2
|
||||
echo -e "${GREEN} ✓ Apache reloaded${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[8/15] Checking for port conflicts...${NC}"
|
||||
|
||||
if lsof -Pi :$BACKEND_PORT -sTCP:LISTEN -t >/dev/null 2>&1 ; then
|
||||
echo -e "${YELLOW} ⚠ Port $BACKEND_PORT is already in use${NC}"
|
||||
PID=$(lsof -Pi :$BACKEND_PORT -sTCP:LISTEN -t)
|
||||
PROCESS=$(ps -p $PID -o comm=)
|
||||
echo " Process: $PROCESS (PID: $PID)"
|
||||
|
||||
if [[ $PROCESS == *"php"* ]]; then
|
||||
echo " Stopping existing PHP service..."
|
||||
systemctl stop $SERVICE_NAME 2>/dev/null || true
|
||||
sleep 2
|
||||
|
||||
# Force kill if still running
|
||||
if lsof -Pi :$BACKEND_PORT -sTCP:LISTEN -t >/dev/null 2>&1 ; then
|
||||
echo " Force killing process on port $BACKEND_PORT..."
|
||||
kill -9 $PID 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
echo -e "${GREEN} ✓ Port cleared${NC}"
|
||||
else
|
||||
echo -e "${RED} Error: Port $BACKEND_PORT is used by another service${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${GREEN} ✓ Port $BACKEND_PORT is available${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[9/15] Starting backend service...${NC}"
|
||||
|
||||
systemctl daemon-reload
|
||||
echo -e "${GREEN} ✓ Systemd daemon reloaded${NC}"
|
||||
|
||||
systemctl enable $SERVICE_NAME
|
||||
echo -e "${GREEN} ✓ Service enabled (auto-start on boot)${NC}"
|
||||
|
||||
systemctl start $SERVICE_NAME
|
||||
echo " Waiting for service to initialize..."
|
||||
sleep 3
|
||||
|
||||
# Check if service started
|
||||
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||
echo -e "${GREEN} ✓ Backend service started${NC}"
|
||||
else
|
||||
echo -e "${RED} Error: Service failed to start${NC}"
|
||||
echo " Checking logs:"
|
||||
journalctl -u $SERVICE_NAME -n 20 --no-pager
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[10/15] Verifying backend service...${NC}"
|
||||
|
||||
# Check port is listening
|
||||
if lsof -Pi :$BACKEND_PORT -sTCP:LISTEN -t >/dev/null 2>&1 ; then
|
||||
echo -e "${GREEN} ✓ Backend listening on port $BACKEND_PORT${NC}"
|
||||
else
|
||||
echo -e "${RED} Error: Backend not listening on port $BACKEND_PORT${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test backend directly
|
||||
echo " Testing backend API..."
|
||||
if curl -f -s http://localhost:$BACKEND_PORT/server-check.php > /dev/null 2>&1; then
|
||||
echo -e "${GREEN} ✓ Backend API responding${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} ⚠ Backend API test failed (might be normal)${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[11/15] Testing Apache proxy...${NC}"
|
||||
|
||||
# Get server domain from Apache config or use localhost
|
||||
DOMAIN=$(grep -m 1 "ServerName" "$APACHE_CONF" | awk '{print $2}')
|
||||
if [ -z "$DOMAIN" ]; then
|
||||
DOMAIN="localhost"
|
||||
fi
|
||||
|
||||
echo " Testing proxy to backend..."
|
||||
if curl -f -s -k "https://$DOMAIN/lux-studio-back/server-check.php" > /dev/null 2>&1; then
|
||||
echo -e "${GREEN} ✓ Apache proxy working${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} ⚠ Proxy test failed (might need manual verification)${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[12/15] Verifying frontend deployment...${NC}"
|
||||
|
||||
if [ -f "$FRONTEND_DIR/index.html" ]; then
|
||||
echo -e "${GREEN} ✓ Frontend index.html exists${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} ⚠ Frontend index.html not found${NC}"
|
||||
fi
|
||||
|
||||
if [ -d "$FRONTEND_DIR/assets" ]; then
|
||||
ASSET_COUNT=$(ls -1 "$FRONTEND_DIR/assets" 2>/dev/null | wc -l)
|
||||
echo -e "${GREEN} ✓ Frontend assets directory exists ($ASSET_COUNT files)${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} ⚠ Frontend assets directory not found${NC}"
|
||||
fi
|
||||
|
||||
if [ -f "$FRONTEND_DIR/LUX_STUDIO_LOGO.svg" ]; then
|
||||
echo -e "${GREEN} ✓ Logo file exists${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} ⚠ Logo file not found${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[13/15] Checking API paths in build...${NC}"
|
||||
|
||||
if [ -d "$FRONTEND_DIR/assets" ]; then
|
||||
# Check if built files use correct API path
|
||||
if grep -r "lux-studio-back" "$FRONTEND_DIR/assets"/*.js >/dev/null 2>&1; then
|
||||
echo -e "${GREEN} ✓ Frontend uses correct API path (/lux-studio-back/)${NC}"
|
||||
else
|
||||
echo -e "${YELLOW} ⚠ Could not verify API path in frontend build${NC}"
|
||||
fi
|
||||
|
||||
# Check for old API path
|
||||
if grep -r '"/api/api.php"' "$FRONTEND_DIR/assets"/*.js >/dev/null 2>&1; then
|
||||
echo -e "${RED} ✗ Frontend still uses old API path (/api/)${NC}"
|
||||
echo " This will cause 404 errors. Rebuild frontend with correct configuration."
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[14/15] Service status...${NC}"
|
||||
systemctl status $SERVICE_NAME --no-pager | head -10
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}[15/15] Final health checks...${NC}"
|
||||
|
||||
# Summary
|
||||
BACKEND_OK="✓"
|
||||
FRONTEND_OK="✓"
|
||||
PROXY_OK="✓"
|
||||
|
||||
if ! systemctl is-active --quiet $SERVICE_NAME; then
|
||||
BACKEND_OK="✗"
|
||||
fi
|
||||
|
||||
if [ ! -f "$FRONTEND_DIR/index.html" ]; then
|
||||
FRONTEND_OK="✗"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo -e "${GREEN} Deployment Complete!${NC}"
|
||||
echo -e "${GREEN}========================================${NC}"
|
||||
echo ""
|
||||
echo -e "${BLUE}Status Summary:${NC}"
|
||||
echo -e " Backend Service: ${BACKEND_OK}"
|
||||
echo -e " Frontend Files: ${FRONTEND_OK}"
|
||||
echo -e " Apache Proxy: ${PROXY_OK}"
|
||||
echo ""
|
||||
echo -e "${BLUE}Deployment Information:${NC}"
|
||||
echo " Backend Port: $BACKEND_PORT"
|
||||
echo " Backend Dir: $PROJECT_DIR"
|
||||
echo " Frontend Dir: $FRONTEND_DIR"
|
||||
echo " Service Name: $SERVICE_NAME"
|
||||
echo ""
|
||||
echo -e "${BLUE}Access URLs:${NC}"
|
||||
echo " Frontend: https://ai-sandbox.oliver.solutions/lux-studio/"
|
||||
echo " Backend: https://ai-sandbox.oliver.solutions/lux-studio-back/"
|
||||
echo ""
|
||||
echo -e "${BLUE}Service Commands:${NC}"
|
||||
echo " Status: sudo systemctl status $SERVICE_NAME"
|
||||
echo " Logs: sudo journalctl -u $SERVICE_NAME -f"
|
||||
echo " Restart: sudo systemctl restart $SERVICE_NAME"
|
||||
echo ""
|
||||
echo -e "${BLUE}Testing:${NC}"
|
||||
echo " 1. Clear browser cache (Ctrl+Shift+Delete)"
|
||||
echo " 2. Visit: https://ai-sandbox.oliver.solutions/lux-studio/"
|
||||
echo " 3. Login with Microsoft SSO"
|
||||
echo " 4. Create a project and test image/video generation"
|
||||
echo ""
|
||||
|
||||
if [ "$BACKEND_OK" = "✓" ] && [ "$FRONTEND_OK" = "✓" ]; then
|
||||
echo -e "${GREEN}✓ Deployment successful!${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Deployment completed with warnings. Please review above.${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
|
@ -1,70 +1,20 @@
|
|||
# ============================================================================
|
||||
# Lux Studio Frontend Environment Configuration
|
||||
# ============================================================================
|
||||
# IMPORTANT: After changing this file for production, rebuild the frontend:
|
||||
# cd frontend
|
||||
# npm run build
|
||||
# Then upload frontend/dist/* to the server
|
||||
# For production settings, see .env.production
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Port Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Frontend development server port
|
||||
FRONTEND_PORT=3000
|
||||
# Backend API server port
|
||||
BACKEND_PORT=5015
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Base Path Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Base path for the application (used in vite build)
|
||||
# LOCAL DEVELOPMENT: Use root path
|
||||
VITE_BASE_PATH=/
|
||||
#
|
||||
# PRODUCTION: Use subdirectory path
|
||||
# VITE_BASE_PATH=/lux-studio/
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Backend API URL (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
# LOCAL DEVELOPMENT (uncomment for local):
|
||||
VITE_API_URL=http://localhost:5015
|
||||
#
|
||||
# PRODUCTION (comment out for local - use Apache proxy):
|
||||
# VITE_API_URL=https://ai-sandbox.oliver.solutions/lux-studio-back
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Google Gemini API Key
|
||||
# ----------------------------------------------------------------------------
|
||||
# Used for client-side prompt enhancement
|
||||
# Get your API key from: https://aistudio.google.com/app/apikey
|
||||
VITE_GEMINI_API_KEY=your-api-key-here
|
||||
# Get your API key from: https://makersuite.google.com/app/apikey
|
||||
VITE_GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Azure AD / MSAL SSO Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Enable/Disable SSO authentication (true/false)
|
||||
VITE_SSO_ENABLED=true
|
||||
# Development credentials (use for local development)
|
||||
VITE_SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
VITE_SSO_CLIENT_ID=15c0c4e2-bac0-4564-a3a6-c2717f00a6d9
|
||||
# Frontend Port Configuration
|
||||
VITE_FRONTEND_PORT=3000
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# SSO Redirect URI (REQUIRED if SSO enabled)
|
||||
# ----------------------------------------------------------------------------
|
||||
# LOCAL DEVELOPMENT (uncomment for local):
|
||||
VITE_SSO_REDIRECT_URI=http://localhost:3000
|
||||
#
|
||||
# PRODUCTION (comment out for local):
|
||||
# VITE_SSO_REDIRECT_URI=https://ai-sandbox.oliver.solutions/lux-studio
|
||||
# Backend Port Configuration
|
||||
VITE_BACKEND_PORT=5015
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Environment Mode
|
||||
# ----------------------------------------------------------------------------
|
||||
# LOCAL DEVELOPMENT:
|
||||
NODE_ENV=development
|
||||
#
|
||||
# PRODUCTION (comment out for local):
|
||||
# NODE_ENV=production
|
||||
# Image Generation Backend
|
||||
# The PHP backend must be running for image generation to work
|
||||
# Start it with: cd backend && php -S localhost:5015
|
||||
# Vite will proxy /api/* requests to this server
|
||||
|
||||
# Azure AD SSO Configuration
|
||||
# Register your app at: https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps
|
||||
VITE_SSO_TENANT_ID=your_tenant_id_here
|
||||
VITE_SSO_CLIENT_ID=your_client_id_here
|
||||
VITE_SSO_REDIRECT_URI=http://localhost:3000
|
||||
|
|
@ -1,56 +1,11 @@
|
|||
# ============================================================================
|
||||
# Lux Studio Frontend - PRODUCTION Environment Configuration
|
||||
# ============================================================================
|
||||
# This file contains production-ready settings
|
||||
# Copy relevant values to .env when building for production
|
||||
# ============================================================================
|
||||
# IMPORTANT: After changing this file, you MUST rebuild:
|
||||
# cd frontend
|
||||
# npm run build
|
||||
# Then upload frontend/dist/* to the server
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# NOTE: Port variables are NOT used in production!
|
||||
# Production uses Apache on port 443 (HTTPS) to serve static files.
|
||||
# Ports are only needed for local development with Vite dev server.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Base Path Configuration (REQUIRED for production)
|
||||
# ----------------------------------------------------------------------------
|
||||
# Base path for the application (production subdirectory)
|
||||
VITE_BASE_PATH=/lux-studio/
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Backend API URL (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
# PRODUCTION: Use Apache proxy
|
||||
VITE_API_URL=https://ai-sandbox.oliver.solutions/lux-studio-back
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Google Gemini API Key
|
||||
# ----------------------------------------------------------------------------
|
||||
# Used for client-side prompt enhancement # AIzaSyCMKLSJJYEx4c6-TtBACnjdULrLzsr_fts
|
||||
VITE_GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
|
||||
VITE_GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Azure AD / MSAL SSO Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Enable SSO authentication
|
||||
# Azure AD SSO Configuration - PRODUCTION
|
||||
VITE_SSO_ENABLED=true
|
||||
# Production credentials
|
||||
VITE_SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
VITE_SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# SSO Redirect URI (REQUIRED)
|
||||
# ----------------------------------------------------------------------------
|
||||
# PRODUCTION: Full application URL (MUST match Azure AD configuration exactly)
|
||||
# IMPORTANT: Include trailing slash to match Azure AD portal configuration
|
||||
VITE_SSO_REDIRECT_URI=https://ai-sandbox.oliver.solutions/lux-studio/
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Environment Mode
|
||||
# ----------------------------------------------------------------------------
|
||||
NODE_ENV=production
|
||||
# API Base URL for production
|
||||
VITE_API_BASE_URL=/lux-studio/api
|
||||
|
|
|
|||
2
frontend/.gitignore
vendored
|
|
@ -8,7 +8,7 @@ pnpm-debug.log*
|
|||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
# dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
|
|
|
|||
|
|
@ -1,57 +0,0 @@
|
|||
# ==============================================================================
|
||||
# Lux Studio Frontend - Minimal Security Configuration
|
||||
# ==============================================================================
|
||||
|
||||
# Disable directory listing
|
||||
Options -Indexes +FollowSymLinks
|
||||
|
||||
# ==============================================================================
|
||||
# React Router - Client-Side Routing
|
||||
# ==============================================================================
|
||||
|
||||
<IfModule mod_rewrite.c>
|
||||
RewriteEngine On
|
||||
RewriteBase /lux-studio/
|
||||
|
||||
# Don't rewrite requests for real files or directories
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteCond %{REQUEST_FILENAME} !-d
|
||||
|
||||
# Rewrite all other requests to index.html for client-side routing
|
||||
RewriteRule ^ index.html [L]
|
||||
</IfModule>
|
||||
|
||||
# ==============================================================================
|
||||
# Security - Block Sensitive Files
|
||||
# ==============================================================================
|
||||
|
||||
# Block access to hidden files (., .env, .git, etc.)
|
||||
<FilesMatch "^\.">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Block access to source map files in production
|
||||
<FilesMatch "\.map$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Block access to package files
|
||||
<FilesMatch "(package\.json|package-lock\.json|composer\.json|composer\.lock)$">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# ==============================================================================
|
||||
# Basic Security Headers
|
||||
# ==============================================================================
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
# Prevent MIME type sniffing
|
||||
Header set X-Content-Type-Options "nosniff"
|
||||
|
||||
# Prevent clickjacking
|
||||
Header set X-Frame-Options "SAMEORIGIN"
|
||||
</IfModule>
|
||||
|
||||
# ==============================================================================
|
||||
# END CONFIGURATION
|
||||
# ==============================================================================
|
||||
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
231
frontend/dist/assets/index-B01LkE57.js
vendored
Normal file
1
frontend/dist/assets/index-S7O61I6c.css
vendored
Normal file
5
frontend/dist/assets/index.es-DVsbFmJ6.js
vendored
Normal file
2
frontend/dist/assets/purify.es-Bzr520pe.js
vendored
Normal file
14
frontend/dist/index.html
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/lux-studio/LUX_STUDIO_LOGO.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lux Studio - AI Cinematography</title>
|
||||
<script type="module" crossorigin src="/lux-studio/assets/index-B01LkE57.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/lux-studio/assets/index-S7O61I6c.css">
|
||||
</head>
|
||||
<body class="bg-slate-950">
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
1237
frontend/package-lock.json
generated
|
|
@ -10,21 +10,21 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^3.7.1",
|
||||
"@azure/msal-react": "^2.0.23",
|
||||
"@azure/msal-browser": "^5.1.0",
|
||||
"@azure/msal-react": "^5.0.3",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@google/generative-ai": "^0.24.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jspdf": "^4.0.0",
|
||||
"lucide-react": "^0.555.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"eslint": "^9.39.1",
|
||||
|
|
|
|||
BIN
frontend/public/LUX_STUDIO_LOGO.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,19 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 180 50">
|
||||
<defs>
|
||||
<style>
|
||||
.border { fill: none; stroke: #f59e0b; stroke-miterlimit: 10; }
|
||||
.text-white { fill: #fff; }
|
||||
.text-gray { fill: #64748b; }
|
||||
.gold { fill: #f59e0b; }
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="border" x="0.5" y="0.5" width="179" height="49"/>
|
||||
<!-- CINEMA text -->
|
||||
<text class="text-white" x="12" y="28" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" letter-spacing="1">CINEMA</text>
|
||||
<!-- STUDIO text -->
|
||||
<text class="text-gray" x="88" y="28" font-family="system-ui, -apple-system, sans-serif" font-size="18" font-weight="bold" letter-spacing="1">STUDIO</text>
|
||||
<!-- PRO badge -->
|
||||
<rect class="gold" x="155" y="15" width="18" height="18" rx="3"/>
|
||||
<text class="text-white" x="159" y="28" font-family="system-ui, -apple-system, sans-serif" font-size="9" font-weight="bold">P</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 975 B |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,9 +1,150 @@
|
|||
import React from 'react';
|
||||
import AppContent from './components/AppContent';
|
||||
import React, { useState } from 'react';
|
||||
import { LogOut } from 'lucide-react';
|
||||
import TabNavigation from './components/TabNavigation';
|
||||
import CinePromptStudio from './components/CinePromptStudio';
|
||||
import VideoGenTab from './components/VideoGenTab';
|
||||
import ProjectsTab from './components/ProjectsTab';
|
||||
import LoginPage from './components/LoginPage';
|
||||
import { useAuth } from './contexts/AuthContext';
|
||||
|
||||
function App() {
|
||||
// Simple wrapper - AppContent handles all MSAL hooks safely
|
||||
return <AppContent />;
|
||||
// Auth state
|
||||
const { isAuthenticated, isLoading, user, logout } = useAuth();
|
||||
|
||||
// Default to projects tab - project-first workflow
|
||||
const [activeTab, setActiveTab] = useState('projects');
|
||||
|
||||
// Active project state
|
||||
const [activeProjectId, setActiveProjectId] = useState(null);
|
||||
const [activeProjectName, setActiveProjectName] = useState(null);
|
||||
|
||||
// Rerun data for video regeneration
|
||||
const [videoRerunData, setVideoRerunData] = useState(null);
|
||||
|
||||
// Edit data for image editing from library
|
||||
const [imageEditData, setImageEditData] = useState(null);
|
||||
|
||||
// Handler for video rerun from ProjectsTab
|
||||
const handleRerunVideo = (data) => {
|
||||
setVideoRerunData(data);
|
||||
setActiveTab('video');
|
||||
};
|
||||
|
||||
// Clear rerun data after it's been loaded
|
||||
const handleRerunLoaded = () => {
|
||||
setVideoRerunData(null);
|
||||
};
|
||||
|
||||
// Handler for editing image in Image Gen from ProjectsTab
|
||||
const handleEditInImageGen = (data) => {
|
||||
setImageEditData(data);
|
||||
setActiveTab('image');
|
||||
};
|
||||
|
||||
// Clear edit data after it's been loaded
|
||||
const handleEditLoaded = () => {
|
||||
setImageEditData(null);
|
||||
};
|
||||
|
||||
// Handler for project selection from ProjectsTab
|
||||
const handleProjectSelect = (id, name) => {
|
||||
setActiveProjectId(id);
|
||||
setActiveProjectName(name);
|
||||
};
|
||||
|
||||
// Handler for tab change with blocking logic
|
||||
const handleTabChange = (tabId) => {
|
||||
// Block Image Gen and Video Gen if no project selected
|
||||
if ((tabId === 'image' || tabId === 'video') && !activeProjectId) {
|
||||
return; // Do nothing - tabs are disabled
|
||||
}
|
||||
setActiveTab(tabId);
|
||||
};
|
||||
|
||||
// Handle logout
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await logout();
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Show login page if not authenticated
|
||||
if (!isAuthenticated || isLoading) {
|
||||
return <LoginPage />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100">
|
||||
{/* Header */}
|
||||
<div className="max-w-7xl mx-auto px-6 pt-10 pb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-6">
|
||||
<img
|
||||
src="/LUX_STUDIO_LOGO.svg"
|
||||
alt="Lux Studio"
|
||||
className="h-12 w-auto"
|
||||
/>
|
||||
<TabNavigation
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
activeProjectId={activeProjectId}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Project Info */}
|
||||
{activeProjectName && (
|
||||
<div className="text-sm text-slate-400">
|
||||
Working in: <span className="text-cinema-gold font-medium">{activeProjectName}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* User Info */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-slate-200">{user?.name}</div>
|
||||
<div className="text-xs text-slate-400">{user?.email}</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-2 text-slate-400 hover:text-slate-200 hover:bg-slate-800 rounded-lg transition-colors"
|
||||
title="Sign out"
|
||||
>
|
||||
<LogOut className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content - Tab Panels */}
|
||||
<main className="max-w-7xl mx-auto px-6 pb-8">
|
||||
{activeTab === 'projects' && (
|
||||
<ProjectsTab
|
||||
onProjectSelect={handleProjectSelect}
|
||||
activeProjectId={activeProjectId}
|
||||
onRerunVideo={handleRerunVideo}
|
||||
onEditInImageGen={handleEditInImageGen}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'image' && (
|
||||
<CinePromptStudio
|
||||
activeProjectId={activeProjectId}
|
||||
editData={imageEditData}
|
||||
onEditLoaded={handleEditLoaded}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'video' && (
|
||||
<VideoGenTab
|
||||
activeProjectId={activeProjectId}
|
||||
rerunData={videoRerunData}
|
||||
onRerunLoaded={handleRerunLoaded}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* MSAL Authentication Configuration
|
||||
* Uses environment variables for flexible deployment
|
||||
*/
|
||||
|
||||
export const msalConfig = {
|
||||
auth: {
|
||||
clientId: import.meta.env.VITE_SSO_CLIENT_ID || '',
|
||||
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_SSO_TENANT_ID || 'common'}`,
|
||||
redirectUri: import.meta.env.VITE_SSO_REDIRECT_URI || window.location.origin,
|
||||
navigateToLoginRequestUrl: false,
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: 'sessionStorage',
|
||||
storeAuthStateInCookie: false,
|
||||
}
|
||||
};
|
||||
|
||||
export const loginRequest = {
|
||||
scopes: ['User.Read']
|
||||
};
|
||||
|
||||
// Check if SSO is enabled
|
||||
export const isSSOEnabled = () => {
|
||||
return import.meta.env.VITE_SSO_ENABLED === 'true';
|
||||
};
|
||||
|
|
@ -1,157 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
|
||||
import { LogOut } from 'lucide-react';
|
||||
import { isSSOEnabled } from '../authConfig';
|
||||
import TabNavigation from './TabNavigation';
|
||||
import CinePromptStudio from './CinePromptStudio';
|
||||
import VideoGenTab from './VideoGenTab';
|
||||
import ProjectsTab from './ProjectsTab';
|
||||
import LoginPage from './LoginPage';
|
||||
|
||||
function AppContent() {
|
||||
// Check if SSO is enabled
|
||||
const ssoEnabled = isSSOEnabled();
|
||||
|
||||
// MSAL hooks - always called unconditionally since MsalProvider always wraps this component
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const { instance, accounts } = useMsal();
|
||||
|
||||
// State hooks
|
||||
const [activeTab, setActiveTab] = useState('projects');
|
||||
const [activeProjectId, setActiveProjectId] = useState(null);
|
||||
const [activeProjectName, setActiveProjectName] = useState(null);
|
||||
const [videoRerunData, setVideoRerunData] = useState(null);
|
||||
const [imageEditData, setImageEditData] = useState(null);
|
||||
|
||||
// Show login page if SSO is enabled and user is not authenticated
|
||||
if (ssoEnabled && !isAuthenticated) {
|
||||
return <LoginPage />;
|
||||
}
|
||||
|
||||
// Get user info for display
|
||||
const userName = accounts.length > 0
|
||||
? accounts[0].name || accounts[0].username
|
||||
: ssoEnabled ? null : 'Local Developer';
|
||||
|
||||
// Logout handler
|
||||
const handleLogout = () => {
|
||||
if (instance) {
|
||||
instance.logoutPopup();
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for video rerun from ProjectsTab
|
||||
const handleRerunVideo = (data) => {
|
||||
setVideoRerunData(data);
|
||||
setActiveTab('video');
|
||||
};
|
||||
|
||||
// Clear rerun data after it's been loaded
|
||||
const handleRerunLoaded = () => {
|
||||
setVideoRerunData(null);
|
||||
};
|
||||
|
||||
// Handler for editing image in Image Gen from ProjectsTab
|
||||
const handleEditInImageGen = (data) => {
|
||||
setImageEditData(data);
|
||||
setActiveTab('image');
|
||||
};
|
||||
|
||||
// Clear edit data after it's been loaded
|
||||
const handleEditLoaded = () => {
|
||||
setImageEditData(null);
|
||||
};
|
||||
|
||||
// Handler for project selection from ProjectsTab
|
||||
const handleProjectSelect = (id, name) => {
|
||||
setActiveProjectId(id);
|
||||
setActiveProjectName(name);
|
||||
};
|
||||
|
||||
// Handler for tab change with blocking logic
|
||||
const handleTabChange = (tabId) => {
|
||||
// Block Image Gen and Video Gen if no project selected
|
||||
if ((tabId === 'image' || tabId === 'video') && !activeProjectId) {
|
||||
return; // Do nothing - tabs are disabled
|
||||
}
|
||||
setActiveTab(tabId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100">
|
||||
{/* Header */}
|
||||
<div className="max-w-7xl mx-auto px-6 pt-10 pb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-6">
|
||||
<img
|
||||
src={`${import.meta.env.BASE_URL}LUX_STUDIO_LOGO.svg`}
|
||||
alt="Lux Studio"
|
||||
className="h-12 w-auto"
|
||||
/>
|
||||
<TabNavigation
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
activeProjectId={activeProjectId}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
{activeProjectName && (
|
||||
<div className="text-sm text-slate-400">
|
||||
Working in: <span className="text-cinema-gold font-medium">{activeProjectName}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-slate-500">
|
||||
Version 1.0
|
||||
</div>
|
||||
</div>
|
||||
{userName && (
|
||||
<div className="flex items-center gap-3 border-l border-slate-700 pl-4">
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-slate-300">{userName}</div>
|
||||
</div>
|
||||
{ssoEnabled && (
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-2 hover:bg-slate-800 rounded-lg transition-colors"
|
||||
title="Logout"
|
||||
>
|
||||
<LogOut className="w-5 h-5 text-slate-400" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content - Tab Panels */}
|
||||
<main className="max-w-7xl mx-auto px-6 pb-8">
|
||||
{activeTab === 'projects' && (
|
||||
<ProjectsTab
|
||||
onProjectSelect={handleProjectSelect}
|
||||
activeProjectId={activeProjectId}
|
||||
onRerunVideo={handleRerunVideo}
|
||||
onEditInImageGen={handleEditInImageGen}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'image' && (
|
||||
<CinePromptStudio
|
||||
activeProjectId={activeProjectId}
|
||||
editData={imageEditData}
|
||||
onEditLoaded={handleEditLoaded}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'video' && (
|
||||
<VideoGenTab
|
||||
activeProjectId={activeProjectId}
|
||||
rerunData={videoRerunData}
|
||||
onRerunLoaded={handleRerunLoaded}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AppContent;
|
||||
|
|
@ -2,20 +2,10 @@ import React, { useState, useEffect } from 'react';
|
|||
import { Sparkles, Copy, Film, Camera, Sun, Check, Loader2, HelpCircle, Info, Sliders, X, Image, Download, RefreshCw, Upload, Plus, FolderOpen, Settings, Trash2, Pencil, Shield, Maximize2, Minimize2 } from 'lucide-react';
|
||||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import useProjects from '../hooks/useProjects';
|
||||
import { getApiUrl } from '../utils/api';
|
||||
import useCustomPresets from '../hooks/useCustomPresets';
|
||||
|
||||
const CinePromptStudio = ({ activeProjectId, editData, onEditLoaded }) => {
|
||||
// API URL helper - uses Vite proxy in dev, direct URL in production
|
||||
const getApiUrl = (endpoint) => {
|
||||
// In development, use Vite proxy to avoid CORS
|
||||
if (import.meta.env.DEV) {
|
||||
return `/api/${endpoint}`;
|
||||
}
|
||||
// In production, use full API URL
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015';
|
||||
return `${apiUrl}/${endpoint}`;
|
||||
};
|
||||
|
||||
// Projects hook for auto-save and fetching project images
|
||||
const { addItemToProject, getProjectWithItems, isReady: dbReady } = useProjects();
|
||||
|
||||
|
|
@ -1213,7 +1203,7 @@ Return ONLY the final prompt text. No markdown, no preamble, no "Here is your pr
|
|||
setImageError(result.error || 'Image generation failed');
|
||||
}
|
||||
} catch (err) {
|
||||
setImageError(`Network error: ${err.message}. Make sure backend service is running.`);
|
||||
setImageError(`Network error: ${err.message}. Make sure PHP server is running on port ${import.meta.env.VITE_BACKEND_PORT || '5015'}.`);
|
||||
} finally {
|
||||
setIsGeneratingImage(false);
|
||||
}
|
||||
|
|
@ -1901,7 +1891,7 @@ Return ONLY the final prompt text. No markdown, no preamble, no "Here is your pr
|
|||
<div className="text-center p-8">
|
||||
<Image className="w-12 h-12 text-slate-700 mx-auto mb-3" />
|
||||
<p className="text-slate-500">Generate a prompt first, then click "Generate Image"</p>
|
||||
<p className="text-slate-600 text-xs mt-2">Requires backend service running</p>
|
||||
<p className="text-slate-600 text-xs mt-2">Requires PHP backend running on port {import.meta.env.VITE_BACKEND_PORT || '5015'}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,64 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
class ErrorBoundary extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null, errorInfo: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error) {
|
||||
return { hasError: true };
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.error('🔴 Error caught by boundary:', error);
|
||||
console.error('Error info:', errorInfo);
|
||||
this.setState({
|
||||
error,
|
||||
errorInfo
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
return (
|
||||
<div style={{ padding: '40px', fontFamily: 'monospace', maxWidth: '800px', margin: '0 auto' }}>
|
||||
<h1 style={{ color: '#ef4444' }}>⚠️ Something went wrong</h1>
|
||||
<details style={{ whiteSpace: 'pre-wrap', background: '#1e293b', padding: '20px', borderRadius: '8px', color: '#e2e8f0' }}>
|
||||
<summary style={{ cursor: 'pointer', marginBottom: '10px', fontSize: '16px', fontWeight: 'bold' }}>
|
||||
Error Details (click to expand)
|
||||
</summary>
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<strong>Error:</strong>
|
||||
<pre style={{ marginTop: '5px', color: '#fca5a5' }}>{this.state.error && this.state.error.toString()}</pre>
|
||||
|
||||
<strong style={{ marginTop: '20px', display: 'block' }}>Stack Trace:</strong>
|
||||
<pre style={{ marginTop: '5px', fontSize: '12px', color: '#cbd5e1' }}>
|
||||
{this.state.errorInfo && this.state.errorInfo.componentStack}
|
||||
</pre>
|
||||
</div>
|
||||
</details>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
style={{
|
||||
marginTop: '20px',
|
||||
padding: '10px 20px',
|
||||
background: '#3b82f6',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px'
|
||||
}}
|
||||
>
|
||||
Reload Page
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
|
@ -1,57 +1,98 @@
|
|||
import React from 'react';
|
||||
import { useMsal } from '@azure/msal-react';
|
||||
import { loginRequest } from '../authConfig';
|
||||
import React, { useState } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const LoginPage = () => {
|
||||
const { instance } = useMsal();
|
||||
const { login, isLoading } = useAuth();
|
||||
const [error, setError] = useState('');
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
|
||||
const handleLogin = async () => {
|
||||
setError('');
|
||||
setIsLoggingIn(true);
|
||||
try {
|
||||
await instance.loginRedirect(loginRequest);
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
await login();
|
||||
} catch (err) {
|
||||
setError(err.message || 'Login failed. Please try again.');
|
||||
} finally {
|
||||
setIsLoggingIn(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-12 h-12 animate-spin text-cinema-gold mx-auto mb-4" />
|
||||
<p className="text-slate-400">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-950 flex items-center justify-center px-6">
|
||||
<div className="min-h-screen bg-slate-950 flex items-center justify-center px-4">
|
||||
<div className="max-w-md w-full">
|
||||
<div className="text-center mb-8">
|
||||
{/* Logo and Title */}
|
||||
<div className="text-center mb-10">
|
||||
<img
|
||||
src={`${import.meta.env.BASE_URL}LUX_STUDIO_LOGO.svg`}
|
||||
src="/LUX_STUDIO_LOGO.svg"
|
||||
alt="Lux Studio"
|
||||
className="h-16 w-auto mx-auto mb-6"
|
||||
className="h-16 mx-auto mb-6"
|
||||
/>
|
||||
<h1 className="text-3xl font-bold text-slate-100 mb-2">
|
||||
Welcome to Lux Studio
|
||||
</h1>
|
||||
<p className="text-slate-400">
|
||||
AI-powered cinematography suite for professional image and video generation
|
||||
AI-Powered Cinematography Suite
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900 rounded-lg p-8 shadow-xl border border-slate-800">
|
||||
{/* Login Card */}
|
||||
<div className="bg-slate-900/50 border border-slate-800 rounded-xl p-8">
|
||||
<h2 className="text-xl font-semibold text-slate-200 text-center mb-6">
|
||||
Sign in to continue
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="mb-6 p-4 bg-red-950/50 border border-red-800 rounded-lg text-red-400 text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
className="w-full bg-cinema-gold hover:bg-yellow-500 text-slate-950 font-semibold py-3 px-6 rounded-lg transition-colors flex items-center justify-center gap-3"
|
||||
disabled={isLoggingIn}
|
||||
className="w-full flex items-center justify-center gap-3 px-6 py-4 bg-[#2F2F2F] hover:bg-[#3F3F3F] text-white font-medium rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg className="w-5 h-5" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0 0h10v10H0V0z" fill="#f25022"/>
|
||||
<path d="M11 0h10v10H11V0z" fill="#7fba00"/>
|
||||
<path d="M0 11h10v10H0V11z" fill="#00a4ef"/>
|
||||
<path d="M11 11h10v10H11V11z" fill="#ffb900"/>
|
||||
</svg>
|
||||
Sign in with Microsoft
|
||||
{isLoggingIn ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 animate-spin" />
|
||||
<span>Signing in...</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Microsoft Logo */}
|
||||
<svg className="w-5 h-5" viewBox="0 0 21 21" fill="none">
|
||||
<rect x="1" y="1" width="9" height="9" fill="#F25022" />
|
||||
<rect x="11" y="1" width="9" height="9" fill="#7FBA00" />
|
||||
<rect x="1" y="11" width="9" height="9" fill="#00A4EF" />
|
||||
<rect x="11" y="11" width="9" height="9" fill="#FFB900" />
|
||||
</svg>
|
||||
<span>Sign in with Microsoft</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<p className="text-xs text-slate-500 text-center mt-6">
|
||||
By signing in, you agree to use this application in accordance with your organization's policies.
|
||||
<p className="mt-6 text-center text-xs text-slate-500">
|
||||
By signing in, you agree to our Terms of Service and Privacy Policy
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center text-sm text-slate-500">
|
||||
<p>Powered by Google Imagen 3 & Veo 3.1</p>
|
||||
</div>
|
||||
{/* Footer */}
|
||||
<p className="mt-8 text-center text-xs text-slate-600">
|
||||
Version 1.0 · Powered by Google Imagen 3 & Veo 3.1
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -132,13 +132,6 @@ const ProjectsTab = ({ onProjectSelect, activeProjectId, onRerunVideo, onEditInI
|
|||
const fileInputRef = useRef(null);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
// Import from backend state
|
||||
const [showImportModal, setShowImportModal] = useState(false);
|
||||
const [availableSessions, setAvailableSessions] = useState([]);
|
||||
const [selectedFiles, setSelectedFiles] = useState([]);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [importProgress, setImportProgress] = useState({ current: 0, total: 0 });
|
||||
|
||||
// Load items and storyboards when project is selected
|
||||
useEffect(() => {
|
||||
if (selectedProject) {
|
||||
|
|
@ -223,120 +216,6 @@ const ProjectsTab = ({ onProjectSelect, activeProjectId, onRerunVideo, onEditInI
|
|||
}
|
||||
};
|
||||
|
||||
// API URL helper for import endpoints
|
||||
const getApiUrl = (endpoint) => {
|
||||
if (import.meta.env.DEV) {
|
||||
return `/api/${endpoint}`;
|
||||
}
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015';
|
||||
return `${apiUrl}/${endpoint}`;
|
||||
};
|
||||
|
||||
// Fetch available backend session files
|
||||
const fetchAvailableSessions = async () => {
|
||||
try {
|
||||
const response = await fetch(getApiUrl('list_session_files.php'));
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
setAvailableSessions(data.sessions);
|
||||
return data.sessions;
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to fetch sessions');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching sessions:', err);
|
||||
setError('Failed to load backend files: ' + err.message);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
// Handle opening import modal
|
||||
const handleOpenImportModal = async () => {
|
||||
if (!selectedProject) {
|
||||
setError('Please select a project first');
|
||||
return;
|
||||
}
|
||||
|
||||
setShowImportModal(true);
|
||||
setSelectedFiles([]);
|
||||
await fetchAvailableSessions();
|
||||
};
|
||||
|
||||
// Toggle file selection
|
||||
const toggleFileSelection = (sessionId, fileType, filename) => {
|
||||
const fileKey = `${sessionId}:${fileType}:${filename}`;
|
||||
setSelectedFiles(prev => {
|
||||
if (prev.includes(fileKey)) {
|
||||
return prev.filter(f => f !== fileKey);
|
||||
} else {
|
||||
return [...prev, fileKey];
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Import selected files
|
||||
const handleImportFiles = async () => {
|
||||
if (selectedFiles.length === 0 || !selectedProject) return;
|
||||
|
||||
setImporting(true);
|
||||
setError('');
|
||||
setImportProgress({ current: 0, total: selectedFiles.length });
|
||||
|
||||
try {
|
||||
for (let i = 0; i < selectedFiles.length; i++) {
|
||||
const fileKey = selectedFiles[i];
|
||||
const [sessionId, fileType, filename] = fileKey.split(':');
|
||||
|
||||
setImportProgress({ current: i + 1, total: selectedFiles.length });
|
||||
|
||||
// Fetch file from backend
|
||||
const response = await fetch(
|
||||
getApiUrl(`get_session_file.php?session_id=${encodeURIComponent(sessionId)}&file_type=${fileType}&filename=${encodeURIComponent(filename)}`)
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
console.error(`Failed to fetch ${filename}:`, data.error);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add to project
|
||||
if (fileType === 'image') {
|
||||
await addItemToProject(selectedProject.id, {
|
||||
type: 'image',
|
||||
prompt: `Imported: ${filename}`,
|
||||
settings: {},
|
||||
data: data.data, // Base64
|
||||
mimeType: data.mime_type,
|
||||
thumbnail: data.data
|
||||
});
|
||||
} else if (fileType === 'video') {
|
||||
// Skip videos for now - they need to be in generated_videos folder
|
||||
// TODO: Implement proper video import (copy to generated_videos or create session streaming endpoint)
|
||||
console.warn(`Skipping video import for ${filename} - not yet supported`);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Reload items
|
||||
await loadProjectItems(selectedProject.id);
|
||||
|
||||
// Close modal
|
||||
setShowImportModal(false);
|
||||
setSelectedFiles([]);
|
||||
setAvailableSessions([]);
|
||||
|
||||
alert(`Successfully imported ${selectedFiles.length} file(s)`);
|
||||
} catch (err) {
|
||||
setError('Import failed: ' + err.message);
|
||||
console.error('Import error:', err);
|
||||
} finally {
|
||||
setImporting(false);
|
||||
setImportProgress({ current: 0, total: 0 });
|
||||
}
|
||||
};
|
||||
|
||||
// Open a storyboard for editing
|
||||
const handleOpenStoryboard = async (storyboardId) => {
|
||||
try {
|
||||
|
|
@ -855,20 +734,6 @@ const ProjectsTab = ({ onProjectSelect, activeProjectId, onRerunVideo, onEditInI
|
|||
className="hidden"
|
||||
/>
|
||||
|
||||
{/* Import from Backend button */}
|
||||
<button
|
||||
onClick={handleOpenImportModal}
|
||||
disabled={importing}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-slate-200 rounded-lg text-sm transition-all disabled:opacity-50"
|
||||
>
|
||||
{importing ? (
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
) : (
|
||||
<Database className="w-4 h-4" />
|
||||
)}
|
||||
{importing ? `Importing ${importProgress.current}/${importProgress.total}...` : 'Import'}
|
||||
</button>
|
||||
|
||||
{/* Separator */}
|
||||
<div className="w-px h-6 bg-slate-700" />
|
||||
|
||||
|
|
@ -926,157 +791,6 @@ const ProjectsTab = ({ onProjectSelect, activeProjectId, onRerunVideo, onEditInI
|
|||
</div>
|
||||
)}
|
||||
|
||||
{/* Import from Backend Modal */}
|
||||
{showImportModal && (
|
||||
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-slate-900 border border-slate-700 rounded-xl p-6 max-w-4xl w-full max-h-[80vh] overflow-y-auto">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-xl font-bold text-slate-200">Import from Backend</h2>
|
||||
<button
|
||||
onClick={() => setShowImportModal(false)}
|
||||
className="p-2 hover:bg-slate-800 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-slate-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{availableSessions.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
<Database className="w-16 h-16 mx-auto mb-4 opacity-30" />
|
||||
<p className="text-lg">No backend files found</p>
|
||||
<p className="text-sm mt-2">Generate some images or videos to see them here</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-slate-400 mb-4">
|
||||
Found {availableSessions.reduce((acc, s) => acc + s.images.length, 0)} image(s) in {availableSessions.length} session(s).
|
||||
Files auto-delete after 24 hours.
|
||||
{availableSessions.reduce((acc, s) => acc + s.videos.length, 0) > 0 && (
|
||||
<span className="block text-amber-400 text-xs mt-1">
|
||||
Note: Video import not yet supported (videos must be in generated_videos folder)
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{/* Session list */}
|
||||
<div className="space-y-4">
|
||||
{availableSessions.map((session) => (
|
||||
<div key={session.session_id} className="bg-slate-800/50 border border-slate-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-medium text-slate-300">
|
||||
Session: {session.session_id.substring(0, 8)}...
|
||||
</h3>
|
||||
<span className="text-xs text-slate-500">
|
||||
{session.images.length} image(s)
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Images */}
|
||||
{session.images.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-medium text-slate-400 uppercase">Images</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{session.images.map((img) => {
|
||||
const fileKey = `${session.session_id}:image:${img.filename}`;
|
||||
const isSelected = selectedFiles.includes(fileKey);
|
||||
const expiresIn = Math.floor(img.time_remaining / 3600);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={img.filename}
|
||||
onClick={() => toggleFileSelection(session.session_id, 'image', img.filename)}
|
||||
className={`relative p-2 rounded-lg border-2 cursor-pointer transition-all ${
|
||||
isSelected
|
||||
? 'border-cinema-gold bg-cinema-gold/10'
|
||||
: 'border-slate-700 hover:border-slate-600'
|
||||
}`}
|
||||
>
|
||||
<div className="aspect-square bg-slate-700/50 rounded flex items-center justify-center mb-2">
|
||||
<Image className="w-8 h-8 text-slate-500" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 truncate">{img.filename}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{img.size_kb} KB • {expiresIn}h left
|
||||
</p>
|
||||
{isSelected && (
|
||||
<div className="absolute top-1 right-1 bg-cinema-gold rounded-full p-1">
|
||||
<Check className="w-3 h-3 text-slate-950" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Videos - Hidden for now since import not supported */}
|
||||
{false && session.videos.length > 0 && (
|
||||
<div className="space-y-2 mt-4">
|
||||
<h4 className="text-xs font-medium text-slate-400 uppercase">Videos (Import Not Supported)</h4>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
|
||||
{session.videos.map((vid) => {
|
||||
const expiresIn = Math.floor(vid.time_remaining / 3600);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={vid.filename}
|
||||
className="relative p-2 rounded-lg border-2 border-slate-700 opacity-50 cursor-not-allowed"
|
||||
>
|
||||
<div className="aspect-square bg-slate-700/50 rounded flex items-center justify-center mb-2">
|
||||
<Video className="w-8 h-8 text-slate-500" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 truncate">{vid.filename}</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
{vid.size_mb} MB • {expiresIn}h left
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center justify-between mt-6 pt-4 border-t border-slate-700">
|
||||
<p className="text-sm text-slate-400">
|
||||
{selectedFiles.length} file(s) selected
|
||||
</p>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setShowImportModal(false)}
|
||||
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm transition-all"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleImportFiles}
|
||||
disabled={selectedFiles.length === 0 || importing}
|
||||
className="flex items-center gap-2 px-4 py-2 bg-cinema-gold hover:bg-amber-400 disabled:bg-slate-700 disabled:text-slate-500 text-slate-950 rounded-lg text-sm font-medium transition-all"
|
||||
>
|
||||
{importing ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 animate-spin" />
|
||||
Importing {importProgress.current}/{importProgress.total}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="w-4 h-4" />
|
||||
Import {selectedFiles.length} File(s)
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* LIBRARY TAB CONTENT */}
|
||||
{activeSubTab === 'library' && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -3,19 +3,22 @@ import { Video, Sparkles, Loader2, Download, RefreshCw, Plus, X, Volume2, Volume
|
|||
import { GoogleGenerativeAI } from '@google/generative-ai';
|
||||
import VideoPlayer from './VideoPlayer';
|
||||
import useProjects from '../hooks/useProjects';
|
||||
import { getApiUrl } from '../utils/api';
|
||||
|
||||
// Helper to convert old video URLs to production path
|
||||
const convertVideoUrl = (url) => {
|
||||
if (!url) return url;
|
||||
|
||||
if (url.startsWith('/generated_videos/')) {
|
||||
return `${getApiUrl('stream_video.php')}?file=${encodeURIComponent(url.replace('/generated_videos/', ''))}`;
|
||||
} else if (url.startsWith('/api/stream_video.php')) {
|
||||
return url.replace('/api/', `${import.meta.env.VITE_API_BASE_URL || '/api'}/`);
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
|
||||
// API URL helper - uses Vite proxy in dev, direct URL in production
|
||||
const getApiUrl = (endpoint) => {
|
||||
// In development, use Vite proxy to avoid CORS
|
||||
if (import.meta.env.DEV) {
|
||||
return `/api/${endpoint}`;
|
||||
}
|
||||
// In production, use full API URL
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015';
|
||||
return `${apiUrl}/${endpoint}`;
|
||||
};
|
||||
|
||||
// Video Generation Settings (keep these)
|
||||
const [sceneDescription, setSceneDescription] = useState('');
|
||||
const [modelType, setModelType] = useState('fast'); // 'standard' or 'fast'
|
||||
|
|
@ -147,7 +150,7 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
|
|||
// Check if already added
|
||||
if (referenceImages.some(img => img.projectItemId === item.id)) return;
|
||||
|
||||
// Auto-set aspect ratio when adding first reference image
|
||||
// Auto-set constraints when adding first reference image (I2V requirements)
|
||||
if (referenceImages.length === 0) {
|
||||
setAspectRatio('16:9');
|
||||
}
|
||||
|
|
@ -219,9 +222,7 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
|
|||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setReferenceImages(prev => {
|
||||
const newLength = prev.length + 1;
|
||||
|
||||
// Auto-set aspect ratio when adding first reference image
|
||||
// Auto-set constraints when adding first reference image (I2V requirements)
|
||||
if (prev.length === 0) {
|
||||
setAspectRatio('16:9');
|
||||
}
|
||||
|
|
@ -230,7 +231,6 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
|
|||
if (newLength === 2) {
|
||||
setDuration(8);
|
||||
}
|
||||
|
||||
return [...prev, {
|
||||
data: reader.result.split(',')[1],
|
||||
mime_type: file.type,
|
||||
|
|
@ -252,19 +252,10 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
|
|||
// Extract thumbnail from video URL
|
||||
const extractVideoThumbnail = (videoUrl) => {
|
||||
return new Promise((resolve) => {
|
||||
if (!videoUrl) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const video = document.createElement('video');
|
||||
video.crossOrigin = 'anonymous';
|
||||
video.muted = true;
|
||||
|
||||
// Only set crossOrigin for non-data URIs
|
||||
if (!videoUrl.startsWith('data:')) {
|
||||
video.crossOrigin = 'anonymous';
|
||||
}
|
||||
|
||||
video.onloadeddata = () => {
|
||||
// Seek to 0.5 seconds for a better frame than the very first
|
||||
video.currentTime = 0.5;
|
||||
|
|
@ -286,23 +277,15 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
|
|||
}
|
||||
};
|
||||
|
||||
video.onerror = (err) => {
|
||||
console.error('Video load error for thumbnail:', err);
|
||||
video.onerror = () => {
|
||||
console.error('Video load error for thumbnail');
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
// Timeout fallback
|
||||
setTimeout(() => resolve(null), 5000);
|
||||
|
||||
// Convert URL if needed (same logic as VideoPlayer)
|
||||
let finalUrl = videoUrl;
|
||||
if (videoUrl.startsWith('/generated_videos/')) {
|
||||
const filename = videoUrl.replace('/generated_videos/', '');
|
||||
finalUrl = getApiUrl(`stream_video.php?file=${encodeURIComponent(filename)}`);
|
||||
console.log(`Thumbnail extraction URL conversion: ${videoUrl} → ${finalUrl}`);
|
||||
}
|
||||
|
||||
video.src = finalUrl;
|
||||
video.src = videoUrl;
|
||||
video.load();
|
||||
});
|
||||
};
|
||||
|
|
@ -643,14 +626,10 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
|
||||
setGenerationProgress(95);
|
||||
|
||||
console.log('Video result from backend:', videoResult);
|
||||
|
||||
// If the URL is from Google API, download through our proxy
|
||||
let finalVideoUrl = videoResult.url;
|
||||
let finalFilename = videoResult.filename;
|
||||
|
||||
console.log('Initial video URL:', finalVideoUrl);
|
||||
|
||||
if (videoResult.url && videoResult.url.includes('generativelanguage.googleapis.com')) {
|
||||
// Download through proxy to handle authentication
|
||||
const downloadFormData = new FormData();
|
||||
|
|
@ -673,20 +652,6 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
}
|
||||
|
||||
setGenerationProgress(100);
|
||||
|
||||
// Validate video URL before proceeding
|
||||
if (!finalVideoUrl) {
|
||||
console.error('Video URL is empty after generation');
|
||||
throw new Error('Backend did not return a valid video URL');
|
||||
}
|
||||
|
||||
if (!finalVideoUrl.startsWith('/generated_videos/') && !finalVideoUrl.startsWith('data:') && !finalVideoUrl.startsWith('http')) {
|
||||
console.error('Unexpected video URL format:', finalVideoUrl);
|
||||
throw new Error(`Invalid video URL format: ${finalVideoUrl}`);
|
||||
}
|
||||
|
||||
console.log('Video generated with URL:', finalVideoUrl);
|
||||
|
||||
setGeneratedVideo({
|
||||
url: finalVideoUrl,
|
||||
mime_type: videoResult.mime_type || 'video/mp4',
|
||||
|
|
@ -697,11 +662,9 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
if (activeProjectId) {
|
||||
try {
|
||||
// Extract thumbnail from the video
|
||||
console.log('Extracting thumbnail for:', finalVideoUrl);
|
||||
const thumbnail = await extractVideoThumbnail(finalVideoUrl);
|
||||
console.log('Thumbnail extracted:', thumbnail ? 'success' : 'failed');
|
||||
|
||||
const itemData = {
|
||||
await addItemToProject(activeProjectId, {
|
||||
type: 'video',
|
||||
prompt: prompt,
|
||||
settings: { modelType, duration, aspectRatio, resolution, generateAudio, dialogue, referenceMode, negativePrompt },
|
||||
|
|
@ -709,12 +672,7 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
thumbnail: thumbnail,
|
||||
data: finalVideoUrl,
|
||||
mimeType: 'video/mp4'
|
||||
};
|
||||
|
||||
console.log('Saving video to project with data.url:', itemData.data);
|
||||
|
||||
await addItemToProject(activeProjectId, itemData);
|
||||
console.log('Video saved to project successfully');
|
||||
});
|
||||
} catch (saveErr) {
|
||||
console.error('Failed to save to project:', saveErr);
|
||||
}
|
||||
|
|
@ -744,22 +702,20 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
Model
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{modelOptions.map((opt) => {
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setModelType(opt.value)}
|
||||
className={`flex-1 px-3 py-2 rounded-lg font-medium transition-all text-sm ${
|
||||
modelType === opt.value
|
||||
? 'bg-cinema-gold text-slate-950'
|
||||
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
<div>{opt.label}</div>
|
||||
<div className="text-xs opacity-70">{opt.description}</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{modelOptions.map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setModelType(opt.value)}
|
||||
className={`flex-1 px-3 py-2 rounded-lg font-medium transition-all text-sm ${
|
||||
modelType === opt.value
|
||||
? 'bg-cinema-gold text-slate-950'
|
||||
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
|
||||
}`}
|
||||
>
|
||||
<div>{opt.label}</div>
|
||||
<div className="text-xs opacity-70">{opt.description}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -770,19 +726,25 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
</label>
|
||||
<div className="flex gap-2">
|
||||
{durationOptions.map((opt) => {
|
||||
const isDisabledByInterpolation = referenceImages.length === 2 && opt.value !== 8;
|
||||
const isDisabledByI2V = referenceImages.length > 0 && opt.value !== 8;
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => !isDisabledByInterpolation && setDuration(opt.value)}
|
||||
disabled={isDisabledByInterpolation}
|
||||
onClick={() => {
|
||||
if (isDisabledByI2V) return;
|
||||
setDuration(opt.value);
|
||||
// Auto-downgrade resolution if not 8s (1080p requires 8s)
|
||||
if (opt.value !== 8 && resolution === '1080p') {
|
||||
setResolution('720p');
|
||||
}
|
||||
}}
|
||||
disabled={isDisabledByI2V}
|
||||
title={isDisabledByI2V ? 'I2V requires 8s duration' : ''}
|
||||
className={`flex-1 px-3 py-2 rounded-lg font-medium transition-all text-sm ${
|
||||
duration === opt.value
|
||||
? 'bg-cinema-gold text-slate-950'
|
||||
: isDisabledByInterpolation
|
||||
? 'bg-slate-800/50 text-slate-600 cursor-not-allowed'
|
||||
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
|
||||
}`}
|
||||
} ${isDisabledByI2V ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
>
|
||||
<div>{opt.label}</div>
|
||||
<div className="text-xs opacity-70">{getDurationCost(opt.value)}</div>
|
||||
|
|
@ -806,7 +768,13 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
{['16:9', '9:16'].map((ratio) => (
|
||||
<button
|
||||
key={ratio}
|
||||
onClick={() => setAspectRatio(ratio)}
|
||||
onClick={() => {
|
||||
setAspectRatio(ratio);
|
||||
if (ratio === '9:16') {
|
||||
// Auto-downgrade resolution (1080p not supported in portrait)
|
||||
setResolution('720p');
|
||||
}
|
||||
}}
|
||||
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-all ${
|
||||
aspectRatio === ratio
|
||||
? 'bg-cinema-gold text-slate-950'
|
||||
|
|
@ -1192,7 +1160,7 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
<button
|
||||
onClick={() => {
|
||||
const link = document.createElement('a');
|
||||
link.href = generatedVideo.url;
|
||||
link.href = convertVideoUrl(generatedVideo.url);
|
||||
link.download = generatedVideo.filename || `lux-studio-video-${Date.now()}.mp4`;
|
||||
link.click();
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Play, Pause, SkipBack, SkipForward, Volume2, VolumeX, Download, Image, Scissors, ChevronLeft, ChevronRight, FolderPlus, Check } from 'lucide-react';
|
||||
import { getApiUrl } from '../utils/api';
|
||||
|
||||
/**
|
||||
* VideoPlayer component with frame extraction capability
|
||||
|
|
@ -13,48 +14,15 @@ const VideoPlayer = ({ src, onFrameExtract, onSaveToProject, className = '', aut
|
|||
const videoRef = useRef(null);
|
||||
const canvasRef = useRef(null);
|
||||
|
||||
// API URL helper - uses Vite proxy in dev, direct URL in production
|
||||
const getApiUrl = (endpoint) => {
|
||||
// In development, use Vite proxy to avoid CORS
|
||||
if (import.meta.env.DEV) {
|
||||
return `/api/${endpoint}`;
|
||||
}
|
||||
// In production, use full API URL
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015';
|
||||
return `${apiUrl}/${endpoint}`;
|
||||
};
|
||||
|
||||
// Convert video source URLs based on type
|
||||
const getVideoSrc = () => {
|
||||
// Defensive check for empty/undefined src
|
||||
if (!src) {
|
||||
console.warn('VideoPlayer: src is empty or undefined');
|
||||
return '';
|
||||
}
|
||||
|
||||
console.log('VideoPlayer: Processing src:', src);
|
||||
|
||||
// If it's a data URI (base64), return as-is
|
||||
if (src.startsWith('data:')) {
|
||||
console.log('VideoPlayer: Using data URI');
|
||||
return src;
|
||||
}
|
||||
|
||||
// If it's a /generated_videos/ path, convert to streaming endpoint
|
||||
if (src.startsWith('/generated_videos/')) {
|
||||
const filename = src.replace('/generated_videos/', '');
|
||||
const convertedUrl = getApiUrl(`stream_video.php?file=${encodeURIComponent(filename)}`);
|
||||
console.log(`VideoPlayer URL conversion: ${src} → ${convertedUrl}`);
|
||||
return convertedUrl;
|
||||
}
|
||||
|
||||
// Return other URLs as-is
|
||||
console.log(`VideoPlayer using URL as-is: ${src}`);
|
||||
return src;
|
||||
};
|
||||
|
||||
const videoSrc = getVideoSrc();
|
||||
console.log('VideoPlayer: Final videoSrc:', videoSrc);
|
||||
// Convert old URLs to production-aware streaming endpoint
|
||||
let videoSrc = src;
|
||||
if (src?.startsWith('/generated_videos/')) {
|
||||
// Old direct video URLs
|
||||
videoSrc = `${getApiUrl('stream_video.php')}?file=${encodeURIComponent(src.replace('/generated_videos/', ''))}`;
|
||||
} else if (src?.startsWith('/api/stream_video.php')) {
|
||||
// Old streaming URLs - update to production path
|
||||
videoSrc = src.replace('/api/', `${import.meta.env.VITE_API_BASE_URL || '/api'}/`);
|
||||
}
|
||||
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
|
|
@ -92,19 +60,6 @@ const VideoPlayer = ({ src, onFrameExtract, onSaveToProject, className = '', aut
|
|||
setVideoAspect('square');
|
||||
}
|
||||
};
|
||||
const handleError = (err) => {
|
||||
console.error('Video load error:', {
|
||||
error: err,
|
||||
src: src,
|
||||
videoSrc: videoSrc,
|
||||
currentSrc: video?.currentSrc,
|
||||
networkState: video?.networkState,
|
||||
readyState: video?.readyState,
|
||||
error_code: video?.error?.code,
|
||||
error_message: video?.error?.message
|
||||
});
|
||||
setIsPlaying(false);
|
||||
};
|
||||
|
||||
video.addEventListener('timeupdate', handleTimeUpdate);
|
||||
video.addEventListener('durationchange', handleDurationChange);
|
||||
|
|
@ -112,7 +67,6 @@ const VideoPlayer = ({ src, onFrameExtract, onSaveToProject, className = '', aut
|
|||
video.addEventListener('pause', handlePause);
|
||||
video.addEventListener('ended', handleEnded);
|
||||
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
video.addEventListener('error', handleError);
|
||||
|
||||
return () => {
|
||||
video.removeEventListener('timeupdate', handleTimeUpdate);
|
||||
|
|
@ -121,22 +75,18 @@ const VideoPlayer = ({ src, onFrameExtract, onSaveToProject, className = '', aut
|
|||
video.removeEventListener('pause', handlePause);
|
||||
video.removeEventListener('ended', handleEnded);
|
||||
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
video.removeEventListener('error', handleError);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Toggle play/pause
|
||||
const togglePlay = () => {
|
||||
const video = videoRef.current;
|
||||
if (!video || !videoSrc) return;
|
||||
if (!video) return;
|
||||
|
||||
if (isPlaying) {
|
||||
video.pause();
|
||||
} else {
|
||||
video.play().catch(err => {
|
||||
console.error('Video play failed:', err);
|
||||
setIsPlaying(false);
|
||||
});
|
||||
video.play();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
26
frontend/src/config/msalConfig.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* MSAL Configuration for Azure AD SSO
|
||||
*/
|
||||
|
||||
export const msalConfig = {
|
||||
auth: {
|
||||
clientId: import.meta.env.VITE_SSO_CLIENT_ID,
|
||||
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_SSO_TENANT_ID}`,
|
||||
redirectUri: import.meta.env.VITE_SSO_REDIRECT_URI || window.location.origin,
|
||||
postLogoutRedirectUri: import.meta.env.VITE_SSO_REDIRECT_URI || window.location.origin,
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: 'localStorage',
|
||||
storeAuthStateInCookie: false,
|
||||
},
|
||||
};
|
||||
|
||||
// Scopes for Microsoft Graph API
|
||||
export const loginRequest = {
|
||||
scopes: ['User.Read'],
|
||||
};
|
||||
|
||||
// Scopes for silent token acquisition
|
||||
export const graphConfig = {
|
||||
graphMeEndpoint: 'https://graph.microsoft.com/v1.0/me',
|
||||
};
|
||||
89
frontend/src/contexts/AuthContext.jsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { useMsal, useIsAuthenticated } from '@azure/msal-react';
|
||||
import { InteractionStatus } from '@azure/msal-browser';
|
||||
import { loginRequest } from '../config/msalConfig';
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const { instance, accounts, inProgress } = useMsal();
|
||||
const isAuthenticated = useIsAuthenticated();
|
||||
const [user, setUser] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Handle redirect response on page load
|
||||
useEffect(() => {
|
||||
instance.handleRedirectPromise()
|
||||
.then((response) => {
|
||||
if (response) {
|
||||
// User just logged in via redirect
|
||||
console.log('Redirect login successful');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Redirect error:', error);
|
||||
});
|
||||
}, [instance]);
|
||||
|
||||
useEffect(() => {
|
||||
if (inProgress === InteractionStatus.None) {
|
||||
if (accounts.length > 0) {
|
||||
const account = accounts[0];
|
||||
setUser({
|
||||
id: account.localAccountId,
|
||||
name: account.name || account.username,
|
||||
email: account.username,
|
||||
});
|
||||
} else {
|
||||
setUser(null);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [accounts, inProgress]);
|
||||
|
||||
// Use redirect-based login (in-browser, no popup)
|
||||
const login = async () => {
|
||||
try {
|
||||
await instance.loginRedirect(loginRequest);
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// Use redirect-based logout
|
||||
const logout = async () => {
|
||||
try {
|
||||
await instance.logoutRedirect({
|
||||
postLogoutRedirectUri: window.location.origin,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Logout failed:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
isAuthenticated,
|
||||
isLoading: isLoading || inProgress !== InteractionStatus.None,
|
||||
login,
|
||||
logout,
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthContext;
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import useIndexedDB from './useIndexedDB';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
// Current user ID - returns 'local' which shows ALL projects in local dev mode
|
||||
// In production with SSO, this would return the actual user ID for isolation
|
||||
const getCurrentUserId = () => {
|
||||
// For local development, return 'local' which will show all projects
|
||||
return 'local';
|
||||
};
|
||||
// Get current user ID from auth context
|
||||
// Falls back to 'local' for development/testing
|
||||
|
||||
/**
|
||||
* Custom hook for project management
|
||||
|
|
@ -15,7 +12,8 @@ const getCurrentUserId = () => {
|
|||
*/
|
||||
const useProjects = () => {
|
||||
const { isReady, error: dbError, add, put, get, getAll, getByIndex, remove } = useIndexedDB();
|
||||
const userId = getCurrentUserId();
|
||||
const { user } = useAuth();
|
||||
const userId = user?.id || 'local';
|
||||
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
|
@ -239,36 +237,14 @@ const useProjects = () => {
|
|||
// Get all items for a project
|
||||
const getProjectItems = useCallback(async (projectId) => {
|
||||
try {
|
||||
let items = await getByIndex('items', 'projectId', projectId);
|
||||
|
||||
// Migration: Fix old video URL format
|
||||
let hasUpdates = false;
|
||||
items = items.map(item => {
|
||||
if (item.type === 'video' && item.data && item.data.startsWith('/stream_video.php?file=')) {
|
||||
// Extract filename from old format
|
||||
const match = item.data.match(/file=([^&]+)/);
|
||||
if (match) {
|
||||
const filename = decodeURIComponent(match[1]);
|
||||
item.data = `/generated_videos/${filename}`;
|
||||
hasUpdates = true;
|
||||
console.log(`Migrated video URL: ${filename}`);
|
||||
|
||||
// Update item in IndexedDB with new URL
|
||||
put('items', item).catch(err => {
|
||||
console.error('Failed to update video URL:', err);
|
||||
});
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
const items = await getByIndex('items', 'projectId', projectId);
|
||||
// Sort by createdAt descending (newest first)
|
||||
return items.sort((a, b) => b.createdAt - a.createdAt);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
}
|
||||
}, [getByIndex, put]);
|
||||
}, [getByIndex]);
|
||||
|
||||
// Get a single project with its items
|
||||
const getProjectWithItems = useCallback(async (projectId) => {
|
||||
|
|
|
|||
|
|
@ -2,44 +2,23 @@ import { StrictMode } from 'react'
|
|||
import { createRoot } from 'react-dom/client'
|
||||
import { PublicClientApplication } from '@azure/msal-browser'
|
||||
import { MsalProvider } from '@azure/msal-react'
|
||||
import { msalConfig } from './config/msalConfig'
|
||||
import { AuthProvider } from './contexts/AuthContext'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
import ErrorBoundary from './components/ErrorBoundary.jsx'
|
||||
import { msalConfig, isSSOEnabled } from './authConfig'
|
||||
|
||||
console.log('🚀 Main.jsx loading...');
|
||||
console.log('SSO Enabled:', isSSOEnabled());
|
||||
// Initialize MSAL instance
|
||||
const msalInstance = new PublicClientApplication(msalConfig);
|
||||
|
||||
// MSAL v3 requires initialize() before rendering
|
||||
(async () => {
|
||||
let msalInstance = null;
|
||||
try {
|
||||
console.log('Initializing MSAL with config:', msalConfig);
|
||||
msalInstance = new PublicClientApplication(msalConfig);
|
||||
await msalInstance.initialize();
|
||||
console.log('✅ MSAL instance initialized');
|
||||
} catch (error) {
|
||||
console.error('❌ Error initializing MSAL instance:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<ErrorBoundary>
|
||||
<MsalProvider instance={msalInstance}>
|
||||
<App />
|
||||
</MsalProvider>
|
||||
</ErrorBoundary>
|
||||
</StrictMode>,
|
||||
)
|
||||
console.log('✅ React app rendered');
|
||||
} catch (error) {
|
||||
console.error('❌ Error rendering app:', error);
|
||||
document.getElementById('root').innerHTML = `
|
||||
<div style="padding: 20px; color: red; font-family: monospace;">
|
||||
<h1>Error Loading App</h1>
|
||||
<pre>${error.message}\n${error.stack}</pre>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})();
|
||||
// MSAL v3+ requires async initialization before rendering
|
||||
msalInstance.initialize().then(() => {
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<MsalProvider instance={msalInstance}>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</MsalProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
});
|
||||
|
|
|
|||
13
frontend/src/utils/api.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
/**
|
||||
* API URL helper for environment-aware endpoint construction
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the full API URL for an endpoint
|
||||
* @param {string} endpoint - The API endpoint (e.g., 'api.php', 'video_api.php')
|
||||
* @returns {string} The full API URL
|
||||
*/
|
||||
export const getApiUrl = (endpoint) => {
|
||||
const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api';
|
||||
return `${baseUrl}/${endpoint}`;
|
||||
};
|
||||
|
|
@ -3,41 +3,27 @@ import react from '@vitejs/plugin-react'
|
|||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig(({ mode }) => {
|
||||
// Load env file based on `mode` in the current working directory.
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
// Get port from environment or use defaults
|
||||
const frontendPort = parseInt(env.FRONTEND_PORT || '3000')
|
||||
const backendPort = parseInt(env.BACKEND_PORT || '5015')
|
||||
const backendUrl = env.VITE_API_URL || `http://localhost:${backendPort}`
|
||||
|
||||
// Base path for production deployment vs local development
|
||||
// Local: '/' (root)
|
||||
// Production: '/lux-studio/' (subdirectory)
|
||||
const basePath = env.VITE_BASE_PATH || '/'
|
||||
const backendPort = env.VITE_BACKEND_PORT || '5015'
|
||||
const frontendPort = parseInt(env.VITE_FRONTEND_PORT || '3000')
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
base: basePath,
|
||||
base: mode === 'production' ? '/lux-studio/' : '/',
|
||||
server: {
|
||||
port: frontendPort,
|
||||
proxy: {
|
||||
'/lux-studio-back': {
|
||||
target: backendUrl,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/lux-studio-back/, '')
|
||||
},
|
||||
'/api': {
|
||||
target: backendUrl,
|
||||
target: `http://localhost:${backendPort}`,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
},
|
||||
'/generated_videos': {
|
||||
target: backendUrl,
|
||||
target: `http://localhost:${backendPort}`,
|
||||
changeOrigin: true
|
||||
},
|
||||
'/generated_images': {
|
||||
target: backendUrl,
|
||||
target: `http://localhost:${backendPort}`,
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
# ==============================================================================
|
||||
# LUX STUDIO - Apache Configuration Snippet
|
||||
# ==============================================================================
|
||||
# Add these lines to /etc/apache2/apache2.conf (around line 290)
|
||||
# After existing service configurations
|
||||
# ==============================================================================
|
||||
|
||||
# Backend API Proxy
|
||||
ProxyPass /lux-studio-back/ http://localhost:5015/
|
||||
ProxyPassReverse /lux-studio-back/ http://localhost:5015/
|
||||
|
||||
# Frontend Directory
|
||||
<Directory /var/www/html/lux-studio>
|
||||
Options -Indexes +FollowSymLinks
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
# Security - Block backend source access
|
||||
<Directory /opt/lux-studio-back>
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
# CORS Headers
|
||||
<Location /lux-studio-back>
|
||||
Header set Access-Control-Allow-Origin "https://ai-sandbox.oliver.solutions"
|
||||
Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
|
||||
Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
|
||||
Header set Access-Control-Allow-Credentials "true"
|
||||
</Location>
|
||||
|
||||
# ==============================================================================
|
||||
# END LUX STUDIO
|
||||
# ==============================================================================
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
[Unit]
|
||||
Description=Lux Studio Backend API Service
|
||||
After=network.target network-online.target
|
||||
Documentation=https://github.com/your-repo/cinema-studio-pro
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=www-data
|
||||
Group=www-data
|
||||
WorkingDirectory=/opt/lux-studio-back
|
||||
|
||||
# Start PHP built-in server on port 5015
|
||||
# Backend loads environment from /opt/lux-studio-back/.env via env_loader.php
|
||||
ExecStart=/usr/bin/php -S 0.0.0.0:5015 -t /opt/lux-studio-back
|
||||
|
||||
# Restart policy - always restart if it crashes
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Logging - view with: journalctl -u lux-studio-backend -f
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=lux-studio-backend
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65536
|
||||
TimeoutStopSec=30
|
||||
TimeoutStartSec=30
|
||||
|
||||
# Environment variables for PHP
|
||||
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
|
||||
Environment="PHP_CLI_SERVER_WORKERS=4"
|
||||
EnvironmentFile=/opt/lux-studio-back/.env
|
||||
|
||||
# Security hardening (optional - uncomment if needed)
|
||||
# ProtectSystem=full
|
||||
# ProtectHome=yes
|
||||
# NoNewPrivileges=true
|
||||
# PrivateTmp=yes
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
6
package-lock.json
generated
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"name": "cinema-studio-pro",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
422
setup.sh
|
|
@ -1,422 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Lux Studio - Local Development Setup & Start Script
|
||||
# ============================================================================
|
||||
# This script sets up AND starts both frontend and backend for LOCAL development
|
||||
# For production deployment, use deploy.sh instead
|
||||
# ============================================================================
|
||||
|
||||
set -e # Exit on any error
|
||||
|
||||
echo "🎬 Setting up Lux Studio for Local Development..."
|
||||
echo ""
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Get the absolute path of the project root
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# ============================================================================
|
||||
# 1. CHECK PREREQUISITES
|
||||
# ============================================================================
|
||||
|
||||
echo "${BLUE}Step 1: Checking prerequisites...${NC}"
|
||||
|
||||
if ! command -v node &> /dev/null; then
|
||||
echo "${RED}✗ Node.js not found. Please install Node.js 18+ first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v npm &> /dev/null; then
|
||||
echo "${RED}✗ npm not found. Please install npm first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v php &> /dev/null; then
|
||||
echo "${RED}✗ PHP not found. Please install PHP 7.4+ first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v composer &> /dev/null; then
|
||||
echo "${RED}✗ Composer not found. Please install Composer first.${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "${GREEN}✓ All prerequisites installed${NC}"
|
||||
echo " Node.js: $(node --version)"
|
||||
echo " npm: $(npm --version)"
|
||||
echo " PHP: $(php --version | head -n 1)"
|
||||
echo " Composer: $(composer --version | head -n 1)"
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# 2. SETUP BACKEND
|
||||
# ============================================================================
|
||||
|
||||
echo "${BLUE}Step 2: Setting up backend...${NC}"
|
||||
cd backend
|
||||
|
||||
# Create .env from .env.local
|
||||
if [ ! -f ".env" ]; then
|
||||
if [ -f ".env.local" ]; then
|
||||
echo " → Creating backend/.env from .env.local..."
|
||||
cp .env.local .env
|
||||
echo "${GREEN} ✓ backend/.env created with local development settings${NC}"
|
||||
else
|
||||
echo "${RED} ✗ Error: .env.local not found${NC}"
|
||||
echo " Please ensure backend/.env.local exists"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "${GREEN} ✓ backend/.env already exists${NC}"
|
||||
fi
|
||||
|
||||
# Install PHP dependencies
|
||||
echo " → Installing PHP dependencies via Composer..."
|
||||
composer install --quiet
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "${GREEN}✓ Backend setup complete${NC}"
|
||||
else
|
||||
echo "${RED}✗ Backend setup failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# 3. SETUP FRONTEND
|
||||
# ============================================================================
|
||||
|
||||
echo "${BLUE}Step 3: Setting up frontend...${NC}"
|
||||
cd frontend
|
||||
|
||||
# Create .env from .env.local
|
||||
if [ ! -f ".env" ]; then
|
||||
if [ -f ".env.local" ]; then
|
||||
echo " → Creating frontend/.env from .env.local..."
|
||||
cp .env.local .env
|
||||
echo "${GREEN} ✓ frontend/.env created with local development settings${NC}"
|
||||
else
|
||||
echo "${RED} ✗ Error: .env.local not found${NC}"
|
||||
echo " Please ensure frontend/.env.local exists"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "${GREEN} ✓ frontend/.env already exists${NC}"
|
||||
fi
|
||||
|
||||
# Install npm dependencies
|
||||
echo " → Installing npm dependencies (this may take a few minutes)..."
|
||||
npm install --silent
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo "${GREEN}✓ Frontend setup complete${NC}"
|
||||
else
|
||||
echo "${RED}✗ Frontend setup failed${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd ..
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# 4. CREATE REQUIRED DIRECTORIES
|
||||
# ============================================================================
|
||||
|
||||
echo "${BLUE}Step 4: Creating required directories...${NC}"
|
||||
|
||||
# Create backend upload directory with proper permissions
|
||||
mkdir -p backend/uploads/sessions
|
||||
chmod -R 755 backend/uploads 2>/dev/null || true
|
||||
|
||||
echo "${GREEN}✓ Required directories created${NC}"
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# 5. READ PORT CONFIGURATION
|
||||
# ============================================================================
|
||||
|
||||
# Read ports from .env files
|
||||
BACKEND_PORT=5015 # Default
|
||||
FRONTEND_PORT=3000 # Default
|
||||
|
||||
if [ -f "backend/.env" ]; then
|
||||
BACKEND_PORT=$(grep -E "^BACKEND_PORT=" backend/.env | cut -d'=' -f2 | tr -d ' ')
|
||||
fi
|
||||
|
||||
if [ -f "frontend/.env" ]; then
|
||||
FRONTEND_PORT=$(grep -E "^FRONTEND_PORT=" frontend/.env | cut -d'=' -f2 | tr -d ' ')
|
||||
fi
|
||||
|
||||
# Use defaults if not found
|
||||
BACKEND_PORT=${BACKEND_PORT:-5015}
|
||||
FRONTEND_PORT=${FRONTEND_PORT:-3000}
|
||||
|
||||
# ============================================================================
|
||||
# 6. CHECK FOR PORT CONFLICTS
|
||||
# ============================================================================
|
||||
|
||||
echo "${BLUE}Step 5: Checking for port conflicts...${NC}"
|
||||
|
||||
# Function to check if a port is in use
|
||||
check_port() {
|
||||
local port=$1
|
||||
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1 ; then
|
||||
return 0 # Port is in use
|
||||
else
|
||||
return 1 # Port is free
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to kill process on port
|
||||
kill_port() {
|
||||
local port=$1
|
||||
local pid=$(lsof -ti:$port)
|
||||
if [ ! -z "$pid" ]; then
|
||||
echo " → Killing process $pid on port $port..."
|
||||
kill -9 $pid 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check backend port
|
||||
if check_port $BACKEND_PORT; then
|
||||
echo "${YELLOW} ⚠ Port $BACKEND_PORT is already in use${NC}"
|
||||
read -p " Kill the process and continue? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
kill_port $BACKEND_PORT
|
||||
echo "${GREEN} ✓ Port $BACKEND_PORT is now free${NC}"
|
||||
else
|
||||
echo "${RED} ✗ Cannot start backend - port in use${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "${GREEN} ✓ Port $BACKEND_PORT is available${NC}"
|
||||
fi
|
||||
|
||||
# Check frontend port
|
||||
if check_port $FRONTEND_PORT; then
|
||||
echo "${YELLOW} ⚠ Port $FRONTEND_PORT is already in use${NC}"
|
||||
read -p " Kill the process and continue? (y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
kill_port $FRONTEND_PORT
|
||||
echo "${GREEN} ✓ Port $FRONTEND_PORT is now free${NC}"
|
||||
else
|
||||
echo "${RED} ✗ Cannot start frontend - port in use${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "${GREEN} ✓ Port $FRONTEND_PORT is available${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# 7. START BACKEND SERVER
|
||||
# ============================================================================
|
||||
|
||||
echo "${BLUE}Step 6: Starting backend server...${NC}"
|
||||
|
||||
cd "$PROJECT_ROOT/backend"
|
||||
|
||||
# Start PHP server in background
|
||||
nohup php -S localhost:${BACKEND_PORT} > "${PROJECT_ROOT}/backend-server.log" 2>&1 &
|
||||
BACKEND_PID=$!
|
||||
|
||||
# Wait a moment and check if server started
|
||||
sleep 2
|
||||
|
||||
if ps -p $BACKEND_PID > /dev/null; then
|
||||
echo "${GREEN}✓ Backend server started on port ${BACKEND_PORT}${NC}"
|
||||
echo " PID: $BACKEND_PID"
|
||||
echo " Logs: ${PROJECT_ROOT}/backend-server.log"
|
||||
|
||||
# Save PID for later shutdown
|
||||
echo $BACKEND_PID > "${PROJECT_ROOT}/.backend.pid"
|
||||
else
|
||||
echo "${RED}✗ Failed to start backend server${NC}"
|
||||
echo " Check ${PROJECT_ROOT}/backend-server.log for errors"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# 8. START FRONTEND SERVER
|
||||
# ============================================================================
|
||||
|
||||
echo "${BLUE}Step 7: Starting frontend dev server...${NC}"
|
||||
|
||||
cd "$PROJECT_ROOT/frontend"
|
||||
|
||||
# Start Vite dev server in background
|
||||
nohup npm run dev > "${PROJECT_ROOT}/frontend-server.log" 2>&1 &
|
||||
FRONTEND_PID=$!
|
||||
|
||||
# Wait for Vite to start (it takes a bit longer)
|
||||
echo " → Waiting for Vite dev server to start..."
|
||||
sleep 5
|
||||
|
||||
if ps -p $FRONTEND_PID > /dev/null; then
|
||||
echo "${GREEN}✓ Frontend server started on port ${FRONTEND_PORT}${NC}"
|
||||
echo " PID: $FRONTEND_PID"
|
||||
echo " Logs: ${PROJECT_ROOT}/frontend-server.log"
|
||||
|
||||
# Save PID for later shutdown
|
||||
echo $FRONTEND_PID > "${PROJECT_ROOT}/.frontend.pid"
|
||||
else
|
||||
echo "${RED}✗ Failed to start frontend server${NC}"
|
||||
echo " Check ${PROJECT_ROOT}/frontend-server.log for errors"
|
||||
|
||||
# Clean up backend server
|
||||
kill $BACKEND_PID 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# 9. VERIFY SERVERS ARE RUNNING
|
||||
# ============================================================================
|
||||
|
||||
echo "${BLUE}Step 8: Verifying servers...${NC}"
|
||||
|
||||
# Wait a moment for servers to fully initialize
|
||||
sleep 3
|
||||
|
||||
# Check if backend responds
|
||||
if curl -s http://localhost:${BACKEND_PORT}/server-check.php > /dev/null 2>&1; then
|
||||
echo "${GREEN} ✓ Backend is responding${NC}"
|
||||
else
|
||||
echo "${YELLOW} ⚠ Backend may not be fully ready yet${NC}"
|
||||
fi
|
||||
|
||||
# Check if frontend responds
|
||||
if curl -s http://localhost:${FRONTEND_PORT} > /dev/null 2>&1; then
|
||||
echo "${GREEN} ✓ Frontend is responding${NC}"
|
||||
else
|
||||
echo "${YELLOW} ⚠ Frontend may not be fully ready yet${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# ============================================================================
|
||||
# 10. CREATE STOP SCRIPT
|
||||
# ============================================================================
|
||||
|
||||
# Create a stop script for easy shutdown
|
||||
cat > "${PROJECT_ROOT}/stop.sh" << 'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
# Colors
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
echo -e "${YELLOW}🛑 Stopping Lux Studio servers...${NC}"
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Stop backend
|
||||
if [ -f "${PROJECT_ROOT}/.backend.pid" ]; then
|
||||
BACKEND_PID=$(cat "${PROJECT_ROOT}/.backend.pid")
|
||||
if ps -p $BACKEND_PID > /dev/null 2>&1; then
|
||||
kill $BACKEND_PID 2>/dev/null
|
||||
echo -e "${GREEN}✓ Backend server stopped (PID: $BACKEND_PID)${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Backend server not running${NC}"
|
||||
fi
|
||||
rm "${PROJECT_ROOT}/.backend.pid"
|
||||
fi
|
||||
|
||||
# Stop frontend
|
||||
if [ -f "${PROJECT_ROOT}/.frontend.pid" ]; then
|
||||
FRONTEND_PID=$(cat "${PROJECT_ROOT}/.frontend.pid")
|
||||
if ps -p $FRONTEND_PID > /dev/null 2>&1; then
|
||||
kill $FRONTEND_PID 2>/dev/null
|
||||
echo -e "${GREEN}✓ Frontend server stopped (PID: $FRONTEND_PID)${NC}"
|
||||
else
|
||||
echo -e "${YELLOW}⚠ Frontend server not running${NC}"
|
||||
fi
|
||||
rm "${PROJECT_ROOT}/.frontend.pid"
|
||||
fi
|
||||
|
||||
# Also kill any remaining processes on the ports (cleanup)
|
||||
BACKEND_PORT=$(grep -E "^BACKEND_PORT=" backend/.env 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
|
||||
FRONTEND_PORT=$(grep -E "^FRONTEND_PORT=" frontend/.env 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
|
||||
|
||||
BACKEND_PORT=${BACKEND_PORT:-5015}
|
||||
FRONTEND_PORT=${FRONTEND_PORT:-3000}
|
||||
|
||||
# Kill any remaining processes on ports
|
||||
for port in $BACKEND_PORT $FRONTEND_PORT; do
|
||||
pid=$(lsof -ti:$port 2>/dev/null)
|
||||
if [ ! -z "$pid" ]; then
|
||||
kill -9 $pid 2>/dev/null || true
|
||||
echo -e "${GREEN}✓ Cleaned up process on port $port${NC}"
|
||||
fi
|
||||
done
|
||||
|
||||
echo -e "${GREEN}✅ All servers stopped${NC}"
|
||||
EOF
|
||||
|
||||
chmod +x "${PROJECT_ROOT}/stop.sh"
|
||||
|
||||
# ============================================================================
|
||||
# 11. SUCCESS MESSAGE
|
||||
# ============================================================================
|
||||
|
||||
echo "${GREEN}════════════════════════════════════════════════════════════${NC}"
|
||||
echo "${GREEN}🎉 Lux Studio is Running!${NC}"
|
||||
echo "${GREEN}════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
echo "${BLUE}🌐 Application URLs:${NC}"
|
||||
echo " • Frontend: ${GREEN}http://localhost:${FRONTEND_PORT}${NC}"
|
||||
echo " • Backend API: ${GREEN}http://localhost:${BACKEND_PORT}${NC}"
|
||||
echo ""
|
||||
|
||||
echo "${BLUE}📊 Server Status:${NC}"
|
||||
echo " • Backend PID: $BACKEND_PID (port $BACKEND_PORT)"
|
||||
echo " • Frontend PID: $FRONTEND_PID (port $FRONTEND_PORT)"
|
||||
echo ""
|
||||
|
||||
echo "${BLUE}📝 Log Files:${NC}"
|
||||
echo " • Backend: ${PROJECT_ROOT}/backend-server.log"
|
||||
echo " • Frontend: ${PROJECT_ROOT}/frontend-server.log"
|
||||
echo " • View backend: ${GREEN}tail -f ${PROJECT_ROOT}/backend-server.log${NC}"
|
||||
echo " • View frontend: ${GREEN}tail -f ${PROJECT_ROOT}/frontend-server.log${NC}"
|
||||
echo ""
|
||||
|
||||
echo "${BLUE}🛑 Stop Servers:${NC}"
|
||||
echo " ${GREEN}./stop.sh${NC}"
|
||||
echo ""
|
||||
|
||||
echo "${BLUE}⚙️ Configuration:${NC}"
|
||||
echo " • SSO: Enabled with LOCAL credentials"
|
||||
echo " • SSO Client ID: 15c0c4e2-bac0-4564-a3a6-c2717f00a6d9 (local dev)"
|
||||
echo " • API Key: Configured in .env files"
|
||||
echo ""
|
||||
|
||||
echo "${BLUE}📚 Documentation:${NC}"
|
||||
echo " • CLAUDE.md - Developer guide"
|
||||
echo " • MDFiles/README.md - User guide"
|
||||
echo " • MDFiles/AUTH_README.md - SSO guide"
|
||||
echo ""
|
||||
|
||||
echo "${YELLOW}💡 Tip: Open http://localhost:${FRONTEND_PORT} in your browser!${NC}"
|
||||
echo ""
|
||||
echo "Happy coding! 🚀"
|
||||
108
status.sh
|
|
@ -1,108 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Lux Studio - Server Status Check Script
|
||||
# ============================================================================
|
||||
|
||||
# Colors
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m'
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Read ports from .env files
|
||||
BACKEND_PORT=5015
|
||||
FRONTEND_PORT=3000
|
||||
|
||||
if [ -f "backend/.env" ]; then
|
||||
BACKEND_PORT=$(grep -E "^BACKEND_PORT=" backend/.env | cut -d'=' -f2 | tr -d ' ')
|
||||
fi
|
||||
|
||||
if [ -f "frontend/.env" ]; then
|
||||
FRONTEND_PORT=$(grep -E "^FRONTEND_PORT=" frontend/.env | cut -d'=' -f2 | tr -d ' ')
|
||||
fi
|
||||
|
||||
BACKEND_PORT=${BACKEND_PORT:-5015}
|
||||
FRONTEND_PORT=${FRONTEND_PORT:-3000}
|
||||
|
||||
echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}"
|
||||
echo -e "${BLUE}📊 Lux Studio Server Status${NC}"
|
||||
echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
|
||||
# Check Backend
|
||||
echo -e "${BLUE}🔧 Backend Server (Port ${BACKEND_PORT}):${NC}"
|
||||
if [ -f "${PROJECT_ROOT}/.backend.pid" ]; then
|
||||
BACKEND_PID=$(cat "${PROJECT_ROOT}/.backend.pid")
|
||||
if ps -p $BACKEND_PID > /dev/null 2>&1; then
|
||||
echo -e " Status: ${GREEN}✓ Running${NC}"
|
||||
echo -e " PID: $BACKEND_PID"
|
||||
if curl -s http://localhost:${BACKEND_PORT}/server-check.php > /dev/null 2>&1; then
|
||||
echo -e " Health: ${GREEN}✓ Responding${NC}"
|
||||
else
|
||||
echo -e " Health: ${YELLOW}⚠ Not responding${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e " Status: ${RED}✗ Not running (stale PID file)${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e " Status: ${RED}✗ Not running${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Check Frontend
|
||||
echo -e "${BLUE}🎨 Frontend Server (Port ${FRONTEND_PORT}):${NC}"
|
||||
if [ -f "${PROJECT_ROOT}/.frontend.pid" ]; then
|
||||
FRONTEND_PID=$(cat "${PROJECT_ROOT}/.frontend.pid")
|
||||
if ps -p $FRONTEND_PID > /dev/null 2>&1; then
|
||||
echo -e " Status: ${GREEN}✓ Running${NC}"
|
||||
echo -e " PID: $FRONTEND_PID"
|
||||
if curl -s http://localhost:${FRONTEND_PORT} > /dev/null 2>&1; then
|
||||
echo -e " Health: ${GREEN}✓ Responding${NC}"
|
||||
else
|
||||
echo -e " Health: ${YELLOW}⚠ Not responding${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e " Status: ${RED}✗ Not running (stale PID file)${NC}"
|
||||
fi
|
||||
else
|
||||
echo -e " Status: ${RED}✗ Not running${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# URLs
|
||||
echo -e "${BLUE}🌐 Application URLs:${NC}"
|
||||
echo -e " • Frontend: ${GREEN}http://localhost:${FRONTEND_PORT}${NC}"
|
||||
echo -e " • Backend API: ${GREEN}http://localhost:${BACKEND_PORT}${NC}"
|
||||
echo ""
|
||||
|
||||
# Log files
|
||||
echo -e "${BLUE}📝 Log Files:${NC}"
|
||||
if [ -f "${PROJECT_ROOT}/backend-server.log" ]; then
|
||||
echo -e " • Backend: ${GREEN}${PROJECT_ROOT}/backend-server.log${NC}"
|
||||
echo -e " Last 3 lines:"
|
||||
tail -3 "${PROJECT_ROOT}/backend-server.log" | sed 's/^/ /'
|
||||
else
|
||||
echo -e " • Backend: ${YELLOW}No log file${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [ -f "${PROJECT_ROOT}/frontend-server.log" ]; then
|
||||
echo -e " • Frontend: ${GREEN}${PROJECT_ROOT}/frontend-server.log${NC}"
|
||||
echo -e " Last 3 lines:"
|
||||
tail -3 "${PROJECT_ROOT}/frontend-server.log" | sed 's/^/ /'
|
||||
else
|
||||
echo -e " • Frontend: ${YELLOW}No log file${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Commands
|
||||
echo -e "${BLUE}💡 Useful Commands:${NC}"
|
||||
echo -e " • View backend logs: ${GREEN}tail -f backend-server.log${NC}"
|
||||
echo -e " • View frontend logs: ${GREEN}tail -f frontend-server.log${NC}"
|
||||
echo -e " • Stop servers: ${GREEN}./stop.sh${NC}"
|
||||
echo -e " • Restart: ${GREEN}./stop.sh && ./setup.sh${NC}"
|
||||
echo ""
|
||||
591
video_api.php.backup
Normal file
|
|
@ -0,0 +1,591 @@
|
|||
<?php
|
||||
/**
|
||||
* Video Generation API for Cinema Studio Pro
|
||||
* Handles Veo 3.1 video generation via Gemini API
|
||||
*/
|
||||
|
||||
// Suppress HTML error output to prevent breaking JSON responses
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
ini_set('log_errors', 1);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Access-Control-Allow-Origin: *');
|
||||
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
|
||||
// Handle preflight requests
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
|
||||
http_response_code(200);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Load configuration
|
||||
require_once 'config.php';
|
||||
|
||||
class VeoVideoAPI {
|
||||
private $apiKey;
|
||||
private $baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
|
||||
private $model;
|
||||
|
||||
// Available models
|
||||
private static $models = [
|
||||
'standard' => 'veo-3.1-generate-preview',
|
||||
'fast' => 'veo-3.1-fast-generate-preview'
|
||||
];
|
||||
|
||||
// Storage for pending operations
|
||||
private $operationsFile;
|
||||
|
||||
public function __construct($apiKey, $modelType = 'standard') {
|
||||
$this->apiKey = $apiKey;
|
||||
$this->model = self::$models[$modelType] ?? self::$models['standard'];
|
||||
$this->operationsFile = __DIR__ . '/video_operations.json';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a video using Veo 3.1
|
||||
* Returns an operation ID for async polling
|
||||
*/
|
||||
public function generateVideo($prompt, $duration = 4, $aspectRatio = '16:9', $resolution = '720p', $generateAudio = true, $referenceImages = [], $referenceMode = 'frame') {
|
||||
// Build the instance object
|
||||
$instance = [
|
||||
'prompt' => $prompt
|
||||
];
|
||||
|
||||
// Frame mode: first image is starting frame, optional second image is last frame for interpolation
|
||||
if (!empty($referenceImages)) {
|
||||
if (isset($referenceImages[0])) {
|
||||
$refImg = $referenceImages[0];
|
||||
$data = preg_replace('/\s+/', '', $refImg['data']);
|
||||
|
||||
if (preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $data)) {
|
||||
$instance['image'] = [
|
||||
'bytesBase64Encoded' => $data,
|
||||
'mimeType' => $refImg['mime_type'] ?? 'image/jpeg'
|
||||
];
|
||||
error_log("Added first frame for I2V generation");
|
||||
}
|
||||
}
|
||||
|
||||
// Add last frame for interpolation (when 2 images provided)
|
||||
if (count($referenceImages) >= 2 && isset($referenceImages[1])) {
|
||||
$lastImg = $referenceImages[1];
|
||||
$lastData = preg_replace('/\s+/', '', $lastImg['data']);
|
||||
|
||||
if (preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $lastData)) {
|
||||
$instance['lastFrame'] = [
|
||||
'bytesBase64Encoded' => $lastData,
|
||||
'mimeType' => $lastImg['mime_type'] ?? 'image/jpeg'
|
||||
];
|
||||
error_log("Added last frame for video interpolation");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build parameters
|
||||
$parameters = [
|
||||
'aspectRatio' => $aspectRatio,
|
||||
'sampleCount' => 1
|
||||
];
|
||||
|
||||
// Duration: Veo 3.1 supports 4, 6, or 8 seconds
|
||||
if (in_array(intval($duration), [4, 6, 8])) {
|
||||
$parameters['durationSeconds'] = intval($duration);
|
||||
} else {
|
||||
$parameters['durationSeconds'] = 4;
|
||||
}
|
||||
|
||||
// Note: generateAudio is handled automatically by Veo 3.1
|
||||
// The model generates audio natively based on the scene
|
||||
// No need to explicitly pass this parameter
|
||||
|
||||
$payload = [
|
||||
'instances' => [$instance],
|
||||
'parameters' => $parameters
|
||||
];
|
||||
|
||||
// Log payload structure (without full base64 data for readability)
|
||||
$logPayload = $payload;
|
||||
if (isset($logPayload['instances'][0]['image'])) {
|
||||
$logPayload['instances'][0]['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['image']['bytesBase64Encoded']) . '_bytes]';
|
||||
}
|
||||
if (isset($logPayload['instances'][0]['lastFrame'])) {
|
||||
$logPayload['instances'][0]['lastFrame']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['lastFrame']['bytesBase64Encoded']) . '_bytes]';
|
||||
}
|
||||
if (isset($logPayload['instances'][0]['referenceImages'])) {
|
||||
foreach ($logPayload['instances'][0]['referenceImages'] as $i => &$refImg) {
|
||||
$refImg['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['referenceImages'][$i]['image']['bytesBase64Encoded']) . '_bytes]';
|
||||
}
|
||||
}
|
||||
error_log("Video generation payload structure: " . json_encode($logPayload));
|
||||
|
||||
return $this->makeRequest($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API request to Veo predictLongRunning endpoint
|
||||
*/
|
||||
private function makeRequest($payload, $retryCount = 0) {
|
||||
// Veo uses predictLongRunning for async video generation
|
||||
$url = "{$this->baseUrl}/{$this->model}:predictLongRunning";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
'x-goog-api-key: ' . $this->apiKey
|
||||
],
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_TIMEOUT => 600 // 10 minute timeout for video generation
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
if (curl_errno($ch)) {
|
||||
$error = curl_error($ch);
|
||||
@curl_close($ch);
|
||||
throw new Exception('cURL error: ' . $error);
|
||||
}
|
||||
|
||||
@curl_close($ch);
|
||||
|
||||
error_log("Video API Response Code: $httpCode");
|
||||
error_log("Video API Response: " . substr($response, 0, 1000));
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
$errorData = json_decode($response, true);
|
||||
$errorMessage = $errorData['error']['message'] ?? "HTTP $httpCode";
|
||||
$errorStatus = $errorData['error']['status'] ?? 'UNKNOWN';
|
||||
|
||||
error_log("Video API Error - HTTP $httpCode (Status: $errorStatus)");
|
||||
error_log("Error message: " . $errorMessage);
|
||||
|
||||
// Handle specific error types
|
||||
if ($httpCode === 500 && stripos($errorMessage, 'internal') !== false && $retryCount < 2) {
|
||||
error_log("Retrying video request due to internal error (attempt " . ($retryCount + 1) . ")");
|
||||
sleep(5); // Wait 5 seconds before retry
|
||||
return $this->makeRequest($payload, $retryCount + 1);
|
||||
}
|
||||
|
||||
if ($httpCode === 429 || $errorStatus === 'RESOURCE_EXHAUSTED') {
|
||||
throw new Exception("API rate limit exceeded. Please wait a moment and try again. Video generation is expensive (~\$0.75/second).");
|
||||
}
|
||||
|
||||
if ($errorStatus === 'INVALID_ARGUMENT') {
|
||||
throw new Exception("Invalid request format. Check your prompt and settings.");
|
||||
}
|
||||
|
||||
if (stripos($errorMessage, 'model') !== false && stripos($errorMessage, 'not found') !== false) {
|
||||
throw new Exception("Veo 3.1 model not available. You may need to enable it in your Google AI Studio account.");
|
||||
}
|
||||
|
||||
throw new Exception("API error: $errorMessage (HTTP $httpCode, Status: $errorStatus)");
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract video data from API response
|
||||
*/
|
||||
public function extractVideoData($response) {
|
||||
error_log("Extracting video data from response: " . json_encode(array_keys($response)));
|
||||
|
||||
// Check for completed operation with video response (from polling)
|
||||
// IMPORTANT: Check this FIRST before the operation check, because completed
|
||||
// operations also have a 'name' field but we want to extract the video data
|
||||
if (isset($response['done']) && $response['done'] === true) {
|
||||
// Check for error in operation
|
||||
if (isset($response['error'])) {
|
||||
$errorMsg = $response['error']['message'] ?? 'Unknown error';
|
||||
throw new Exception("Video generation failed: $errorMsg");
|
||||
}
|
||||
|
||||
// Extract video from generateVideoResponse format
|
||||
// Structure: response.generateVideoResponse.generatedSamples[0].video.uri
|
||||
$videoResponse = $response['response'] ?? $response;
|
||||
error_log("videoResponse keys: " . json_encode(array_keys($videoResponse)));
|
||||
|
||||
$generateVideoResponse = $videoResponse['generateVideoResponse'] ?? null;
|
||||
error_log("generateVideoResponse: " . ($generateVideoResponse ? 'found' : 'null'));
|
||||
|
||||
if ($generateVideoResponse && isset($generateVideoResponse['generatedSamples'])) {
|
||||
$samples = $generateVideoResponse['generatedSamples'];
|
||||
error_log("samples count: " . count($samples));
|
||||
|
||||
if (!empty($samples) && isset($samples[0]['video'])) {
|
||||
$video = $samples[0]['video'];
|
||||
error_log("video keys: " . json_encode(array_keys($video)));
|
||||
|
||||
// Check for video URI
|
||||
if (isset($video['uri'])) {
|
||||
error_log("Found video URI: " . $video['uri']);
|
||||
return [
|
||||
'url' => $video['uri'],
|
||||
'mime_type' => 'video/mp4',
|
||||
'type' => 'uri'
|
||||
];
|
||||
}
|
||||
|
||||
// Check for inline base64 data
|
||||
if (isset($video['bytesBase64Encoded'])) {
|
||||
return [
|
||||
'base64' => $video['bytesBase64Encoded'],
|
||||
'mime_type' => $video['mimeType'] ?? 'video/mp4',
|
||||
'type' => 'inline'
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for long-running operation (initial response from predictLongRunning)
|
||||
// This is for when the operation is NOT yet complete
|
||||
if (isset($response['name']) && strpos($response['name'], 'operations/') !== false) {
|
||||
// Only return operation type if not done
|
||||
if (!isset($response['done']) || $response['done'] !== true) {
|
||||
return [
|
||||
'operationId' => $response['name'],
|
||||
'type' => 'operation',
|
||||
'done' => $response['done'] ?? false
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy format: Check for finish reasons that indicate content issues
|
||||
if (isset($response['candidates'][0]['finishReason'])) {
|
||||
$finishReason = $response['candidates'][0]['finishReason'];
|
||||
$finishMessage = $response['candidates'][0]['finishMessage'] ?? '';
|
||||
|
||||
if ($finishReason === 'SAFETY') {
|
||||
throw new Exception('Video generation blocked by safety filters. Please try a different prompt.');
|
||||
}
|
||||
|
||||
if ($finishReason !== 'STOP' && !empty($finishMessage)) {
|
||||
throw new Exception('Video generation failed: ' . $finishMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy format: Look for video data in candidates
|
||||
if (isset($response['candidates'][0]['content']['parts'])) {
|
||||
foreach ($response['candidates'][0]['content']['parts'] as $part) {
|
||||
if (isset($part['inline_data']['data'])) {
|
||||
return [
|
||||
'base64' => $part['inline_data']['data'],
|
||||
'mime_type' => $part['inline_data']['mime_type'] ?? 'video/mp4',
|
||||
'type' => 'inline'
|
||||
];
|
||||
}
|
||||
if (isset($part['videoMetadata']['videoUri'])) {
|
||||
return [
|
||||
'url' => $part['videoMetadata']['videoUri'],
|
||||
'mime_type' => 'video/mp4',
|
||||
'type' => 'uri'
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$errorDetails = "Response structure: " . json_encode(array_keys($response));
|
||||
if (isset($response['response'])) {
|
||||
$errorDetails .= " | Response keys: " . json_encode(array_keys($response['response']));
|
||||
}
|
||||
throw new Exception('No video data found in API response. ' . $errorDetails);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check status of a long-running operation
|
||||
*/
|
||||
public function checkOperationStatus($operationId) {
|
||||
$url = "https://generativelanguage.googleapis.com/v1beta/{$operationId}";
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'x-goog-api-key: ' . $this->apiKey
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_TIMEOUT => 30
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
@curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception("Failed to check operation status: HTTP $httpCode");
|
||||
}
|
||||
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save video to disk
|
||||
*/
|
||||
public function saveVideo($base64Data, $mimeType = 'video/mp4') {
|
||||
$videoDir = __DIR__ . '/generated_videos';
|
||||
if (!is_dir($videoDir)) {
|
||||
mkdir($videoDir, 0755, true);
|
||||
}
|
||||
|
||||
$extension = $mimeType === 'video/webm' ? 'webm' : 'mp4';
|
||||
$filename = 'video_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $extension;
|
||||
$filepath = $videoDir . '/' . $filename;
|
||||
|
||||
$decoded = base64_decode($base64Data);
|
||||
if ($decoded === false) {
|
||||
throw new Exception('Failed to decode video data');
|
||||
}
|
||||
|
||||
file_put_contents($filepath, $decoded);
|
||||
|
||||
return $filename;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle API requests
|
||||
try {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
throw new Exception('Invalid request method');
|
||||
}
|
||||
|
||||
$action = $_POST['action'] ?? null;
|
||||
|
||||
if (!$action) {
|
||||
throw new Exception('No action specified');
|
||||
}
|
||||
|
||||
// Check if API key is configured
|
||||
if (!defined('GEMINI_API_KEY') || empty(GEMINI_API_KEY)) {
|
||||
throw new Exception('API key not configured. Please set GEMINI_API_KEY in config.php');
|
||||
}
|
||||
|
||||
// Get model type (standard or fast)
|
||||
$modelType = $_POST['modelType'] ?? 'standard';
|
||||
if (!in_array($modelType, ['standard', 'fast'])) {
|
||||
$modelType = 'standard';
|
||||
}
|
||||
|
||||
$api = new VeoVideoAPI(GEMINI_API_KEY, $modelType);
|
||||
|
||||
// Handle generate action
|
||||
if ($action === 'generate') {
|
||||
$prompt = $_POST['prompt'] ?? null;
|
||||
$duration = intval($_POST['duration'] ?? 4);
|
||||
$aspectRatio = $_POST['aspectRatio'] ?? '16:9';
|
||||
$resolution = $_POST['resolution'] ?? '720p';
|
||||
$generateAudio = ($_POST['generateAudio'] ?? 'true') === 'true';
|
||||
$referenceMode = $_POST['referenceMode'] ?? 'frame';
|
||||
|
||||
// Validate reference mode
|
||||
if (!in_array($referenceMode, ['frame', 'subject'])) {
|
||||
$referenceMode = 'frame';
|
||||
}
|
||||
|
||||
// Collect reference images (up to 3)
|
||||
$referenceImages = [];
|
||||
$refCount = intval($_POST['referenceImageCount'] ?? 0);
|
||||
for ($i = 0; $i < min($refCount, 3); $i++) {
|
||||
if (isset($_POST["referenceImage_$i"])) {
|
||||
$referenceImages[] = [
|
||||
'data' => $_POST["referenceImage_$i"],
|
||||
'mime_type' => $_POST["referenceImageType_$i"] ?? 'image/jpeg'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$prompt) {
|
||||
throw new Exception('Prompt is required');
|
||||
}
|
||||
|
||||
// Validate duration
|
||||
if (!in_array($duration, [4, 6, 8])) {
|
||||
$duration = 4;
|
||||
}
|
||||
|
||||
// Validate aspect ratio
|
||||
if (!in_array($aspectRatio, ['16:9', '9:16'])) {
|
||||
$aspectRatio = '16:9';
|
||||
}
|
||||
|
||||
error_log("Starting video generation: duration=$duration, aspect=$aspectRatio, audio=$generateAudio, refMode=$referenceMode, refCount=" . count($referenceImages));
|
||||
|
||||
// Generate video
|
||||
$response = $api->generateVideo($prompt, $duration, $aspectRatio, $resolution, $generateAudio, $referenceImages, $referenceMode);
|
||||
$videoData = $api->extractVideoData($response);
|
||||
|
||||
// Handle different response types
|
||||
if ($videoData['type'] === 'operation') {
|
||||
// Long-running operation - return operation ID for polling
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'pending',
|
||||
'operationId' => $videoData['operationId'],
|
||||
'message' => 'Video generation started. Poll for status.'
|
||||
]);
|
||||
} else if ($videoData['type'] === 'inline') {
|
||||
// Direct response with video data
|
||||
$filename = $api->saveVideo($videoData['base64'], $videoData['mime_type']);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'complete',
|
||||
'video' => [
|
||||
'filename' => $filename,
|
||||
'mime_type' => $videoData['mime_type'],
|
||||
'url' => '/api/stream_video.php?file=' . urlencode($filename)
|
||||
]
|
||||
]);
|
||||
} else if ($videoData['type'] === 'uri') {
|
||||
// Video available at URI
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'complete',
|
||||
'video' => [
|
||||
'url' => $videoData['url'],
|
||||
'mime_type' => $videoData['mime_type']
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle check status action
|
||||
if ($action === 'check_status') {
|
||||
$operationId = $_POST['operationId'] ?? null;
|
||||
|
||||
if (!$operationId) {
|
||||
throw new Exception('Operation ID is required');
|
||||
}
|
||||
|
||||
$status = $api->checkOperationStatus($operationId);
|
||||
|
||||
if (isset($status['done']) && $status['done'] === true) {
|
||||
// Operation complete - extract video
|
||||
// Pass the full status object so extractVideoData can find the video data
|
||||
$videoData = $api->extractVideoData($status);
|
||||
|
||||
if ($videoData['type'] === 'inline') {
|
||||
$filename = $api->saveVideo($videoData['base64'], $videoData['mime_type']);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'complete',
|
||||
'video' => [
|
||||
'filename' => $filename,
|
||||
'mime_type' => $videoData['mime_type'],
|
||||
'url' => '/api/stream_video.php?file=' . urlencode($filename)
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'complete',
|
||||
'video' => [
|
||||
'url' => $videoData['url'] ?? null,
|
||||
'mime_type' => $videoData['mime_type']
|
||||
]
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// Still processing
|
||||
$progress = 0;
|
||||
if (isset($status['metadata']['progress'])) {
|
||||
$progress = floatval($status['metadata']['progress']) * 100;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'status' => 'pending',
|
||||
'progress' => $progress,
|
||||
'message' => 'Video generation in progress...'
|
||||
]);
|
||||
}
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
// Handle download_video action - proxy the video download with authentication
|
||||
if ($action === 'download_video') {
|
||||
$videoUrl = $_POST['videoUrl'] ?? $_GET['videoUrl'] ?? null;
|
||||
|
||||
if (!$videoUrl) {
|
||||
throw new Exception('Video URL is required');
|
||||
}
|
||||
|
||||
// Validate URL is from Google APIs
|
||||
if (strpos($videoUrl, 'generativelanguage.googleapis.com') === false) {
|
||||
throw new Exception('Invalid video URL');
|
||||
}
|
||||
|
||||
// Download the video with API key authentication
|
||||
$ch = curl_init($videoUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'x-goog-api-key: ' . GEMINI_API_KEY
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_TIMEOUT => 120
|
||||
]);
|
||||
|
||||
$videoData = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
@curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200 || !$videoData) {
|
||||
throw new Exception("Failed to download video: HTTP $httpCode");
|
||||
}
|
||||
|
||||
// Save the video locally
|
||||
$filename = 'video_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.mp4';
|
||||
$videoDir = __DIR__ . '/generated_videos';
|
||||
|
||||
if (!is_dir($videoDir)) {
|
||||
mkdir($videoDir, 0755, true);
|
||||
}
|
||||
|
||||
$filepath = $videoDir . '/' . $filename;
|
||||
file_put_contents($filepath, $videoData);
|
||||
|
||||
// Return local URL
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'video' => [
|
||||
'url' => '/api/stream_video.php?file=' . urlencode($filename),
|
||||
'filename' => $filename,
|
||||
'mime_type' => 'video/mp4'
|
||||
]
|
||||
]);
|
||||
|
||||
exit;
|
||||
}
|
||||
|
||||
throw new Exception('Invalid action');
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
|
||||
error_log("Exception in video_api.php: " . $e->getMessage());
|
||||
error_log("Stack trace: " . $e->getTraceAsString());
|
||||
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
'debug' => [
|
||||
'file' => basename($e->getFile()),
|
||||
'line' => $e->getLine(),
|
||||
'timestamp' => date('Y-m-d H:i:s')
|
||||
]
|
||||
]);
|
||||
exit;
|
||||
}
|
||||