Compare commits
2 commits
feature/de
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25ab570aab | ||
|
|
ac275a9259 |
64 changed files with 3672 additions and 6857 deletions
|
|
@ -29,9 +29,7 @@
|
|||
"Bash(dos2unix:*)",
|
||||
"WebSearch",
|
||||
"WebFetch(domain:ai.google.dev)",
|
||||
"Bash(xargs:*)",
|
||||
"Bash(php -l:*)",
|
||||
"Bash(git add -A)"
|
||||
"Bash(cp:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -5,7 +5,7 @@ backend/vendor/
|
|||
backend/composer.lock
|
||||
|
||||
# Build output
|
||||
# frontend/dist/
|
||||
frontend/dist/
|
||||
frontend/dist-ssr/
|
||||
*.local
|
||||
|
||||
|
|
|
|||
96
.htaccess
96
.htaccess
|
|
@ -1,32 +1,10 @@
|
|||
# ==============================================================================
|
||||
# LUX STUDIO - SPA ROUTING AND SECURITY CONFIGURATION
|
||||
# VIDEO OPTIMIZER - FRONTEND SECURITY CONFIGURATION
|
||||
# ==============================================================================
|
||||
# Location: /var/www/html/lux-studio/.htaccess
|
||||
# Purpose: SPA routing, API passthrough, and security hardening
|
||||
# Purpose: Security hardening for frontend static files
|
||||
# ==============================================================================
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# 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
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
@ -44,7 +22,12 @@ ServerSignature Off
|
|||
# FILE ACCESS CONTROL
|
||||
# ------------------------------------------------------------------------------
|
||||
|
||||
# Deny access to hidden files (dotfiles)
|
||||
# Default: Allow access to all files (will be restricted below)
|
||||
<FilesMatch ".*">
|
||||
Require all granted
|
||||
</FilesMatch>
|
||||
|
||||
# Deny access to sensitive files and patterns
|
||||
<FilesMatch "^\.">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
|
@ -64,11 +47,74 @@ 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
|
||||
# ==============================================================================
|
||||
|
|
|
|||
|
|
@ -1,685 +0,0 @@
|
|||
# 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
305
AUTH_README.md
|
|
@ -1,305 +0,0 @@
|
|||
# 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
|
||||
558
CLAUDE.md
558
CLAUDE.md
|
|
@ -4,36 +4,39 @@ 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. 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
|
||||
**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.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
lux-studio-app/
|
||||
├── frontend/ # React + Vite frontend application
|
||||
cinema-studio-pro/
|
||||
├── frontend/ # React frontend application
|
||||
│ ├── src/
|
||||
│ │ ├── 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
|
||||
│ │ ├── components/ # React components
|
||||
│ │ ├── hooks/ # Custom hooks (IndexedDB, projects)
|
||||
│ │ ├── authConfig.js # MSAL authentication config
|
||||
│ │ └── App.jsx # Root component
|
||||
│ ├── .env # Frontend environment variables (gitignored)
|
||||
│ ├── .env.local # Local dev template
|
||||
│ ├── .env.production # Production 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 (required by api.php)
|
||||
│ ├── JWTValidator.php # Azure AD token validation
|
||||
│ ├── uploads/ # Temporary file storage
|
||||
│ ├── .env # Backend environment variables (gitignored)
|
||||
│ ├── .env.local # Local dev template
|
||||
│ ├── .env.production # Production template
|
||||
│ └── composer.json # PHP dependencies
|
||||
├── README.md # User-facing project overview
|
||||
├── NEW_DEPLOYMENT.md # Production deployment guide (FileZilla + SSH)
|
||||
└── CLAUDE.md # This file
|
||||
|
||||
```
|
||||
|
||||
## Development Commands
|
||||
|
|
@ -48,113 +51,68 @@ lux-studio-app/
|
|||
```
|
||||
|
||||
### 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 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
|
||||
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
|
||||
```
|
||||
|
||||
### Backend (PHP)
|
||||
|
||||
Run from the [backend/](backend/) directory:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
php -S localhost:5015 # Start PHP backend server (port configurable via BACKEND_PORT in .env)
|
||||
composer install # Install PHP dependencies (Firebase JWT)
|
||||
# Port is configured in .env (BACKEND_PORT, default: 5015)
|
||||
php -S localhost:5015 # Start PHP development server
|
||||
```
|
||||
|
||||
**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:
|
||||
### Full Development Setup
|
||||
```bash
|
||||
cd backend
|
||||
cp config.example.php config.php
|
||||
# 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)
|
||||
```
|
||||
|
||||
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';
|
||||
};
|
||||
```
|
||||
**Note:** Ports are configured via `.env` files. See Configuration section below.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Data Flow: Project-First Workflow
|
||||
### Two-Part System
|
||||
|
||||
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
|
||||
**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)
|
||||
|
||||
### Frontend Architecture
|
||||
**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)
|
||||
|
||||
**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
|
||||
### Data Flow Architecture
|
||||
|
||||
**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
|
||||
**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
|
||||
|
||||
**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
|
||||
**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
|
||||
```
|
||||
|
||||
**Video Generation (Veo 3.1):**
|
||||
```
|
||||
|
|
@ -178,7 +136,7 @@ The Imagen 3 API via Gemini has a VERY SPECIFIC request format that differs from
|
|||
- MIME type must match (typically `image/jpeg`)
|
||||
- Base64 must be clean (no whitespace, no data URI prefix)
|
||||
|
||||
See `MDFiles/AI_IMPLEMENTATION_GUIDE.md` for complete details on this critical pattern.
|
||||
See the Critical Patterns section below for the full request format rules.
|
||||
|
||||
### Session Management (Multi-User)
|
||||
|
||||
|
|
@ -341,7 +299,7 @@ NODE_ENV=development
|
|||
# NOTE: Port variables NOT used in production (only for local dev)
|
||||
# Production uses Apache on port 443 to serve built static files
|
||||
VITE_BASE_PATH=/lux-studio/
|
||||
VITE_API_URL=https://ai-sandbox.oliver.solutions/lux-studio-back
|
||||
VITE_API_URL=https://ai-sandbox.oliver.solutions/lux-studio/api
|
||||
VITE_GEMINI_API_KEY=AIzaSyC...
|
||||
VITE_SSO_ENABLED=true
|
||||
VITE_SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
|
|
@ -389,7 +347,7 @@ Edit `VideoGenTab.jsx` - update `durationOptions` array, ensure constraints matc
|
|||
1. **Check API URL configuration** - Verify `VITE_API_URL` in frontend/.env matches backend server
|
||||
2. Check browser console for API errors (404/500 = wrong API URL)
|
||||
3. Check PHP error_log in project root
|
||||
4. Verify `AI_IMPLEMENTATION_GUIDE.md` request format rules
|
||||
4. Verify image editing parts order: image data MUST precede text (see Critical Patterns section)
|
||||
5. Check `finishReason` in API response (IMAGE_RECITATION = blocked)
|
||||
6. Ensure prompts are creative/detailed (10+ words)
|
||||
|
||||
|
|
@ -407,78 +365,11 @@ 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
|
||||
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"}
|
||||
// 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}`;
|
||||
}
|
||||
// In production, use full API URL
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015';
|
||||
|
|
@ -519,21 +410,18 @@ if ((tabId === 'image' || tabId === 'video') && !activeProjectId) {
|
|||
}
|
||||
```
|
||||
|
||||
**Critical**: Image MUST come before text in parts array.
|
||||
### 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.
|
||||
|
||||
**Response Handling**:
|
||||
### 6. MIME Type Handling
|
||||
Always store and use dynamic MIME type from API response:
|
||||
```php
|
||||
// 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
|
||||
}
|
||||
$_SESSION['current_image'] = $base64;
|
||||
$_SESSION['current_image_mime'] = $mimeType; // Don't hardcode 'image/png'
|
||||
```
|
||||
|
||||
**MIME Type Handling**: Gemini returns `image/jpeg`, not `image/png`. Store and use dynamic MIME types.
|
||||
### 7. IndexedDB Project Operations
|
||||
Always use the `useProjects` hook for database operations - don't use `useIndexedDB` directly from components.
|
||||
|
||||
### 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.
|
||||
|
|
@ -543,195 +431,101 @@ The `lastFrame` reference image (second image in I2V mode) is only applicable wh
|
|||
|
||||
## Error Handling Patterns
|
||||
|
||||
See [AI_IMPLEMENTATION_GUIDE.md](AI_IMPLEMENTATION_GUIDE.md) and [QUICK_REFERENCE.md](QUICK_REFERENCE.md) for comprehensive API details.
|
||||
### 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
|
||||
|
||||
### Video Generation (Veo 3.1)
|
||||
### 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
|
||||
|
||||
**Models**:
|
||||
- Standard: Higher quality, longer processing
|
||||
- Fast: 50% cost savings, faster generation
|
||||
### 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
|
||||
|
||||
**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
|
||||
## Testing Checklist
|
||||
|
||||
**Prompt Optimization**: AI automatically infers camera movement in `[brackets]` and audio cues in `(Sound: [...])`.
|
||||
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
|
||||
|
||||
Example:
|
||||
```
|
||||
Input: "Woman crosses street"
|
||||
Output: "Woman crosses a street. [Camera: tracking, follows woman] (Sound: [footsteps, distant traffic])"
|
||||
## 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
|
||||
|
||||
- `README.md` - User-facing project overview, setup, and troubleshooting
|
||||
- `NEW_DEPLOYMENT.md` - Production deployment guide (FileZilla + SSH)
|
||||
|
||||
## 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
|
||||
|
||||
**Frontend build:**
|
||||
```bash
|
||||
cd frontend
|
||||
cp .env.production .env
|
||||
npm run build
|
||||
# Outputs to frontend/dist/
|
||||
# Verify: grep -o "lux-studio/api" frontend/dist/assets/*.js | head -3
|
||||
```
|
||||
|
||||
## Physics-Based Prompt Engineering
|
||||
**Deployment** (FileZilla + SSH — see `NEW_DEPLOYMENT.md` for the full guide):
|
||||
- Server: `ai-sandbox.oliver.solutions`
|
||||
- Frontend static files → `/var/www/html/lux-studio/`
|
||||
- Backend PHP files → `/var/www/html/lux-studio/api/`
|
||||
- Delete old `assets/` folder on server before uploading new one (hash-named files change each build)
|
||||
- Never overwrite the server's `/api/.env` (contains production secrets)
|
||||
- `AuthMiddleware.php` is REQUIRED in `/api/` — `api.php` will crash without it
|
||||
|
||||
The cinematographer's toolkit in [CinePromptStudio.jsx](frontend/src/components/CinePromptStudio.jsx) injects real camera/lens physics:
|
||||
|
||||
**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
|
||||
|
||||
**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.
|
||||
**Production checklist:**
|
||||
- PHP 7.4+ installed on server
|
||||
- `backend/.env` on server with valid GEMINI_API_KEY and `SSO_ENABLED=false`
|
||||
- `backend/uploads/sessions/` writable by web server (chmod 755)
|
||||
- HTTPS enabled (required for SSO redirect URI)
|
||||
- Frontend built with `VITE_API_URL=https://ai-sandbox.oliver.solutions/lux-studio/api`
|
||||
- CORS configured on backend for frontend domain (`FRONTEND_URL` in server `.env`)
|
||||
|
|
|
|||
361
DEPLOYMENT.md
361
DEPLOYMENT.md
|
|
@ -1,361 +0,0 @@
|
|||
# Lux Studio Production Deployment Guide
|
||||
|
||||
## Target
|
||||
- **Server URL**: `https://ai-sandbox.oliver.solutions/lux-studio/`
|
||||
- **Server Path**: `/var/www/html/lux-studio/`
|
||||
|
||||
---
|
||||
|
||||
## MSAL SSO Configuration (Important!)
|
||||
|
||||
### Environment Files Explained
|
||||
|
||||
Vite uses different `.env` files based on the build mode:
|
||||
|
||||
| File | Used When | Purpose |
|
||||
|------|-----------|---------|
|
||||
| `frontend/.env` | `npm run dev` | Local development |
|
||||
| `frontend/.env.production` | `npm run build` | Production build |
|
||||
|
||||
### Current Configuration
|
||||
|
||||
**Local Development** (`frontend/.env`):
|
||||
```env
|
||||
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
|
||||
VITE_API_BASE_URL=/api
|
||||
```
|
||||
|
||||
**Production** (`frontend/.env.production`):
|
||||
```env
|
||||
VITE_SSO_ENABLED=true
|
||||
VITE_SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
VITE_SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
|
||||
VITE_SSO_REDIRECT_URI=https://ai-sandbox.oliver.solutions/lux-studio/
|
||||
VITE_API_BASE_URL=/lux-studio/api
|
||||
```
|
||||
|
||||
### Key Differences
|
||||
|
||||
| Setting | Local | Production |
|
||||
|---------|-------|------------|
|
||||
| Client ID | `15c0c4e2-bac0-4564-a3a6-c2717f00a6d9` | `9079054c-9620-4757-a256-23413042f1ef` |
|
||||
| Redirect URI | `http://localhost:3000` | `https://ai-sandbox.oliver.solutions/lux-studio/` |
|
||||
|
||||
### Before Building for Production
|
||||
|
||||
1. **Verify `frontend/.env.production` exists** with correct production credentials
|
||||
2. **Update Azure AD App Registration** (see Azure AD Configuration section below)
|
||||
3. Run `npm run build` - Vite will automatically use `.env.production`
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Build Frontend Locally
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
This creates the `frontend/dist/` folder.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Server Directory Structure
|
||||
|
||||
Create the following directories on your server:
|
||||
|
||||
```
|
||||
/var/www/html/lux-studio/
|
||||
├── api/ # PHP files go here
|
||||
├── generated_videos/ # Video output (writable)
|
||||
├── generated_images/ # Image output (writable)
|
||||
└── uploads/ # Session uploads (writable)
|
||||
└── sessions/ # Auto-created per user session
|
||||
```
|
||||
|
||||
**Commands to create directories:**
|
||||
```bash
|
||||
sudo mkdir -p /var/www/html/lux-studio/api
|
||||
sudo mkdir -p /var/www/html/lux-studio/generated_videos
|
||||
sudo mkdir -p /var/www/html/lux-studio/generated_images
|
||||
sudo mkdir -p /var/www/html/lux-studio/uploads/sessions
|
||||
```
|
||||
|
||||
**Important**: PHP files use `dirname(__DIR__)` to store uploads/videos at `/var/www/html/lux-studio/` (NOT inside `/api/`). This keeps generated content separate from code.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Files to Upload
|
||||
|
||||
### Frontend Files (from `frontend/dist/`)
|
||||
|
||||
Upload **everything** from `frontend/dist/` to `/var/www/html/lux-studio/`:
|
||||
|
||||
| Local Path | Upload To |
|
||||
|------------|-----------|
|
||||
| `frontend/dist/index.html` | `/var/www/html/lux-studio/index.html` |
|
||||
| `frontend/dist/assets/` (entire folder) | `/var/www/html/lux-studio/assets/` |
|
||||
| `frontend/dist/LUX_STUDIO_LOGO.svg` | `/var/www/html/lux-studio/LUX_STUDIO_LOGO.svg` |
|
||||
| Any other files in `frontend/dist/` | `/var/www/html/lux-studio/` |
|
||||
|
||||
**Example scp command:**
|
||||
```bash
|
||||
scp -r frontend/dist/* user@server:/var/www/html/lux-studio/
|
||||
```
|
||||
|
||||
### Backend PHP Files (from `backend/`)
|
||||
|
||||
Upload these **8 PHP files** to `/var/www/html/lux-studio/api/`:
|
||||
|
||||
| Local Path | Upload To | Required By |
|
||||
|------------|-----------|-------------|
|
||||
| `backend/api.php` | `/var/www/html/lux-studio/api/` | Image generation |
|
||||
| `backend/video_api.php` | `/var/www/html/lux-studio/api/` | Video generation |
|
||||
| `backend/stream_video.php` | `/var/www/html/lux-studio/api/` | Video playback |
|
||||
| `backend/enhance_prompt.php` | `/var/www/html/lux-studio/api/` | Prompt optimization |
|
||||
| `backend/get_config.php` | `/var/www/html/lux-studio/api/` | Video prompt optimization |
|
||||
| `backend/session_manager.php` | `/var/www/html/lux-studio/api/` | api.php |
|
||||
| `backend/webhook_logger.php` | `/var/www/html/lux-studio/api/` | api.php, enhance_prompt.php |
|
||||
| `backend/env_loader.php` | `/var/www/html/lux-studio/api/` | config.php |
|
||||
|
||||
**Example scp commands:**
|
||||
```bash
|
||||
scp backend/api.php user@server:/var/www/html/lux-studio/api/
|
||||
scp backend/video_api.php user@server:/var/www/html/lux-studio/api/
|
||||
scp backend/stream_video.php user@server:/var/www/html/lux-studio/api/
|
||||
scp backend/enhance_prompt.php user@server:/var/www/html/lux-studio/api/
|
||||
scp backend/get_config.php user@server:/var/www/html/lux-studio/api/
|
||||
scp backend/session_manager.php user@server:/var/www/html/lux-studio/api/
|
||||
scp backend/webhook_logger.php user@server:/var/www/html/lux-studio/api/
|
||||
scp backend/env_loader.php user@server:/var/www/html/lux-studio/api/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Create Files on Server
|
||||
|
||||
### 4.1 Create `.htaccess`
|
||||
|
||||
Create `/var/www/html/lux-studio/.htaccess`:
|
||||
|
||||
```apache
|
||||
# SPA Routing and API Configuration
|
||||
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
|
||||
Options -Indexes
|
||||
Options +FollowSymLinks
|
||||
ServerSignature Off
|
||||
|
||||
# Deny access to hidden files (dotfiles)
|
||||
<FilesMatch "^\.">
|
||||
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 environment and configuration files
|
||||
<FilesMatch "^(\.env|\.env\.|config\.json|package\.json|package-lock\.json|composer\.json|composer\.lock)">
|
||||
Require all denied
|
||||
</FilesMatch>
|
||||
|
||||
# Prevent access to .htaccess itself
|
||||
<Files ".htaccess">
|
||||
Require all denied
|
||||
</Files>
|
||||
```
|
||||
|
||||
### 4.2 Create `config.php`
|
||||
|
||||
Create `/var/www/html/lux-studio/api/config.php`:
|
||||
|
||||
```php
|
||||
<?php
|
||||
define('GEMINI_API_KEY', 'AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Set Permissions
|
||||
|
||||
```bash
|
||||
cd /var/www/html/lux-studio
|
||||
|
||||
# Set ownership (adjust www-data if your Apache user is different)
|
||||
sudo chown -R www-data:www-data .
|
||||
|
||||
# Standard permissions
|
||||
sudo chmod 755 .
|
||||
sudo chmod -R 755 assets/
|
||||
sudo chmod -R 755 api/
|
||||
|
||||
# Writable directories for generated content
|
||||
sudo chown -R www-data:www-data generated_videos/
|
||||
sudo chown -R www-data:www-data generated_images/
|
||||
sudo chown -R www-data:www-data uploads/
|
||||
sudo chmod -R 775 generated_videos/
|
||||
sudo chmod -R 775 generated_images/
|
||||
sudo chmod -R 775 uploads/
|
||||
|
||||
# Protect config file
|
||||
sudo chmod 640 api/config.php
|
||||
```
|
||||
|
||||
**Note**: The `uploads/sessions/` subdirectories are created automatically by PHP when users start sessions.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Apache Configuration
|
||||
|
||||
### 6.1 Check if mod_rewrite is enabled
|
||||
```bash
|
||||
apache2ctl -M | grep rewrite
|
||||
```
|
||||
|
||||
If not enabled:
|
||||
```bash
|
||||
sudo a2enmod rewrite
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
### 6.2 Enable AllowOverride for lux-studio
|
||||
|
||||
Edit your Apache site config:
|
||||
```bash
|
||||
sudo nano /etc/apache2/sites-enabled/000-default.conf
|
||||
```
|
||||
|
||||
Add inside the `<VirtualHost>` block:
|
||||
```apache
|
||||
<Directory /var/www/html/lux-studio>
|
||||
AllowOverride All
|
||||
</Directory>
|
||||
```
|
||||
|
||||
Then restart Apache:
|
||||
```bash
|
||||
sudo systemctl restart apache2
|
||||
```
|
||||
|
||||
**Note**: This only affects the `/lux-studio/` directory and won't impact other apps on the server.
|
||||
|
||||
---
|
||||
|
||||
## Complete Directory Structure After Deployment
|
||||
|
||||
```
|
||||
/var/www/html/lux-studio/
|
||||
├── .htaccess # Apache rewrite rules
|
||||
├── index.html # From frontend/dist/
|
||||
├── LUX_STUDIO_LOGO.svg # From frontend/dist/
|
||||
├── assets/ # From frontend/dist/assets/
|
||||
│ ├── index-*.js
|
||||
│ ├── index-*.css
|
||||
│ └── ...
|
||||
├── api/ # Backend PHP files (9 files)
|
||||
│ ├── api.php # Image generation
|
||||
│ ├── video_api.php # Video generation
|
||||
│ ├── stream_video.php # Video streaming
|
||||
│ ├── enhance_prompt.php # Prompt optimization
|
||||
│ ├── get_config.php # API key for video prompt optimization
|
||||
│ ├── session_manager.php # Session management
|
||||
│ ├── webhook_logger.php # Webhook logging
|
||||
│ ├── env_loader.php # Environment loading
|
||||
│ └── config.php # Created on server
|
||||
├── generated_videos/ # Video output (writable, www-data owned)
|
||||
├── generated_images/ # Image output (writable, www-data owned)
|
||||
└── uploads/ # Session data (writable, www-data owned)
|
||||
└── sessions/ # Auto-created per-user session directories
|
||||
└── {session_id}/ # Created automatically
|
||||
└── images/ # User uploaded/generated images
|
||||
```
|
||||
|
||||
**Storage Paths** (configured in PHP files using `dirname(__DIR__)`):
|
||||
- Images: `/var/www/html/lux-studio/uploads/sessions/{session_id}/images/`
|
||||
- Videos: `/var/www/html/lux-studio/generated_videos/`
|
||||
|
||||
---
|
||||
|
||||
## Azure AD Configuration
|
||||
|
||||
You have **two separate Azure AD App Registrations**:
|
||||
|
||||
### Production App (for deployed site)
|
||||
- **Client ID**: `9079054c-9620-4757-a256-23413042f1ef`
|
||||
- **Redirect URI**: `https://ai-sandbox.oliver.solutions/lux-studio/`
|
||||
|
||||
### Local Development App (for localhost)
|
||||
- **Client ID**: `15c0c4e2-bac0-4564-a3a6-c2717f00a6d9`
|
||||
- **Redirect URI**: `http://localhost:3000`
|
||||
|
||||
### Verify Production App Redirect URI
|
||||
|
||||
1. Go to [Azure Portal](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps)
|
||||
2. Find your **production** app (Client ID: `9079054c-9620-4757-a256-23413042f1ef`)
|
||||
3. Go to **Authentication** → **Platform configurations** → **Web**
|
||||
4. Ensure redirect URI is set to: `https://ai-sandbox.oliver.solutions/lux-studio/`
|
||||
5. Save changes if needed
|
||||
|
||||
---
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
- [ ] Visit `https://ai-sandbox.oliver.solutions/lux-studio/` - see login page
|
||||
- [ ] No 404 errors in browser console for assets
|
||||
- [ ] Click "Sign in with Microsoft" - redirects to Microsoft login
|
||||
- [ ] After login, returns to app with user info in header
|
||||
- [ ] Create a new project
|
||||
- [ ] Generate an image - saves to library
|
||||
- [ ] Generate a video - plays and saves
|
||||
- [ ] Refresh page on any tab - app loads correctly (SPA routing works)
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| 404 on assets | Verify all files from `dist/` are uploaded, check `base` path in vite.config.js |
|
||||
| 404 on logo | Ensure `LUX_STUDIO_LOGO.svg` exists at `/var/www/html/lux-studio/LUX_STUDIO_LOGO.svg` |
|
||||
| SSO login fails | Verify redirect URI in Azure Portal matches exactly: `https://ai-sandbox.oliver.solutions/lux-studio/` |
|
||||
| SSO uses wrong Client ID | Ensure you built with `npm run build` (uses `.env.production`), not `npm run dev` |
|
||||
| SSO redirect loop | Clear browser localStorage, check Azure AD redirect URI configuration |
|
||||
| API 500 errors | Check PHP logs: `sudo tail -f /var/log/apache2/error.log` |
|
||||
| .htaccess not working | Ensure `mod_rewrite` is enabled and `AllowOverride All` is set for lux-studio directory |
|
||||
| Failed to save image | Create `uploads/sessions` directory and set ownership: `sudo mkdir -p /var/www/html/lux-studio/uploads/sessions && sudo chown -R www-data:www-data /var/www/html/lux-studio/uploads` |
|
||||
| Generation fails | Check directories exist and are writable by Apache user (www-data) |
|
||||
|
||||
### SSO Debug Tips
|
||||
|
||||
Check which Client ID is being used in browser:
|
||||
1. Open browser DevTools → Console
|
||||
2. Look for MSAL initialization logs
|
||||
3. Verify Client ID matches production: `9079054c-9620-4757-a256-23413042f1ef`
|
||||
|
||||
If wrong Client ID appears, rebuild:
|
||||
```bash
|
||||
cd frontend
|
||||
rm -rf dist
|
||||
npm run build
|
||||
```
|
||||
617
INSTALL.md
617
INSTALL.md
|
|
@ -1,617 +0,0 @@
|
|||
# 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/`)
|
||||
255
NEW_DEPLOYMENT.md
Normal file
255
NEW_DEPLOYMENT.md
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
# Lux Studio — Deployment Guide
|
||||
|
||||
**Server:** ai-sandbox.oliver.solutions
|
||||
**Frontend URL:** `https://ai-sandbox.oliver.solutions/lux-studio/`
|
||||
**Backend URL:** `https://ai-sandbox.oliver.solutions/lux-studio/api/`
|
||||
|
||||
---
|
||||
|
||||
## Deployment Method
|
||||
|
||||
### Option A — Automated (if project is cloned on server)
|
||||
|
||||
If you have cloned `cinema-studio-pro` to `/opt/cinema-studio-pro/` on the server:
|
||||
|
||||
```bash
|
||||
cd /opt/cinema-studio-pro
|
||||
git pull # get latest changes
|
||||
sudo ./deploy.sh # build + deploy everything automatically
|
||||
```
|
||||
|
||||
The script handles the full build, file copy, Apache config, permissions, and verification.
|
||||
|
||||
---
|
||||
|
||||
### Option B — Manual (FileZilla from your local machine)
|
||||
|
||||
Follow Steps 1–6 below.
|
||||
|
||||
---
|
||||
|
||||
## Server Architecture
|
||||
|
||||
```
|
||||
/var/www/html/lux-studio/
|
||||
├── index.html ← React app entry point
|
||||
├── assets/ ← Built JS/CSS bundles (hash-named)
|
||||
├── LOGO/ ← Logo assets
|
||||
├── LUX_STUDIO_LOGO.svg
|
||||
├── favicon.png
|
||||
├── logo.svg
|
||||
├── .htaccess ← React router + security rules
|
||||
├── generated_images/ ← (auto-created, do not touch)
|
||||
├── generated_videos/ ← (auto-created, do not touch)
|
||||
├── uploads/sessions/ ← (auto-created, do not touch)
|
||||
└── api/ ← PHP backend (Apache serves directly)
|
||||
├── .env ← Production secrets (never overwrite)
|
||||
├── api.php
|
||||
├── video_api.php
|
||||
├── enhance_prompt.php
|
||||
├── session_manager.php
|
||||
├── stream_video.php
|
||||
├── AuthMiddleware.php
|
||||
├── JWTValidator.php
|
||||
├── ... (all other .php files)
|
||||
└── generated_videos/ ← Videos saved by PHP
|
||||
```
|
||||
|
||||
Apache serves PHP files directly — no separate PHP service or proxy needed.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites (Local Machine)
|
||||
|
||||
- Node.js 18+ and npm
|
||||
- FileZilla (SFTP)
|
||||
- SSH client (optional, for verification)
|
||||
|
||||
---
|
||||
|
||||
## Every Time You Deploy
|
||||
|
||||
### Step 1 — Check `frontend/.env.production`
|
||||
|
||||
Open `frontend/.env.production` and confirm these values are set:
|
||||
|
||||
```env
|
||||
VITE_BASE_PATH=/lux-studio/
|
||||
VITE_API_URL=https://ai-sandbox.oliver.solutions/lux-studio/api
|
||||
VITE_SSO_ENABLED=true
|
||||
VITE_SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
VITE_SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
|
||||
VITE_SSO_REDIRECT_URI=https://ai-sandbox.oliver.solutions/lux-studio/
|
||||
```
|
||||
|
||||
> `VITE_API_URL` must point to `/lux-studio/api` — NOT `/lux-studio-back`.
|
||||
> If it says `/lux-studio-back`, change it before building.
|
||||
|
||||
---
|
||||
|
||||
### Step 2 — Build the frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
cp .env.production .env
|
||||
npm run build
|
||||
```
|
||||
|
||||
Output lands in `frontend/dist/`. Verify the correct API URL is baked in:
|
||||
|
||||
```bash
|
||||
grep -o "lux-studio/api" frontend/dist/assets/*.js | head -3
|
||||
# Should print: lux-studio/api
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3 — FileZilla: Upload frontend
|
||||
|
||||
Connect via SFTP: host `ai-sandbox.oliver.solutions`, port `22`.
|
||||
|
||||
**Remote path:** `/var/www/html/lux-studio/`
|
||||
|
||||
| What | Local source | Server destination | Notes |
|
||||
|------|--------------|--------------------|-------|
|
||||
| App entry | `frontend/dist/index.html` | `index.html` | Overwrite |
|
||||
| JS/CSS bundles | `frontend/dist/assets/` | `assets/` | **Delete old folder first**, then upload new |
|
||||
| Logos | `frontend/dist/LOGO/` | `LOGO/` | Overwrite folder |
|
||||
| SVG logo | `frontend/dist/LUX_STUDIO_LOGO.svg` | `LUX_STUDIO_LOGO.svg` | Overwrite |
|
||||
| Favicon | `frontend/dist/favicon.png` | `favicon.png` | Overwrite |
|
||||
| Logo | `frontend/dist/logo.svg` | `logo.svg` | Overwrite |
|
||||
| Apache rules | `frontend/.htaccess` | `.htaccess` | Upload from root (not from dist/) |
|
||||
|
||||
> **Why delete `assets/` first:** Each build produces differently hash-named files
|
||||
> (e.g. `index-DbAHmoFD.js`). Without deleting, old stale files accumulate.
|
||||
|
||||
---
|
||||
|
||||
### Step 4 — FileZilla: Upload backend
|
||||
|
||||
**Remote path:** `/var/www/html/lux-studio/api/`
|
||||
|
||||
Upload all files from `backend/` **except** those in the skip list below.
|
||||
|
||||
#### Files to upload (overwrite existing):
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `api.php` | Image generation API |
|
||||
| `video_api.php` | Video generation API |
|
||||
| `enhance_prompt.php` | Prompt optimization |
|
||||
| `session_manager.php` | Multi-user session isolation |
|
||||
| `stream_video.php` | Video streaming with range support |
|
||||
| `get_current_image.php` | Session image retrieval |
|
||||
| `get_config.php` | Config endpoint |
|
||||
| `env_loader.php` | .env file parser |
|
||||
| `webhook_logger.php` | API request logging |
|
||||
| `config.php` | PHP configuration loader |
|
||||
| `AuthMiddleware.php` | Auth orchestration (required by api.php) |
|
||||
| `JWTValidator.php` | JWT token validation (required by AuthMiddleware) |
|
||||
| `auth.php` | Auth endpoint |
|
||||
| `cleanup.php` | Session file cleanup |
|
||||
| `clear_session.php` | Clear session endpoint |
|
||||
| `get_logs.php` | Log viewer endpoint |
|
||||
| `get_session_file.php` | Session file endpoint |
|
||||
| `index.php` | Backend index |
|
||||
| `list_session_files.php` | Session file listing |
|
||||
| `server-check.php` | Health check endpoint |
|
||||
|
||||
#### DO NOT upload:
|
||||
|
||||
| File | Reason |
|
||||
|------|--------|
|
||||
| `backend/.env` | Contains production secrets — **never overwrite** |
|
||||
| `backend/test.php` | Debug tool, not for production |
|
||||
| `backend/config.example.php` | Template only |
|
||||
| `backend/.env.local` / `.env.production` / `.env.example` | Local/template files |
|
||||
| `backend/vendor/` | Not required (SSO is disabled — JWT lib not loaded) |
|
||||
| `backend/composer.json` | Only needed if running composer on server |
|
||||
|
||||
---
|
||||
|
||||
### Step 5 — Verify `.env` on server (SSH)
|
||||
|
||||
```bash
|
||||
ssh your-username@ai-sandbox.oliver.solutions
|
||||
cat /var/www/html/lux-studio/api/.env
|
||||
```
|
||||
|
||||
Expected content:
|
||||
|
||||
```env
|
||||
GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
|
||||
FRONTEND_URL=https://ai-sandbox.oliver.solutions/lux-studio
|
||||
SSO_ENABLED=false
|
||||
SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
|
||||
```
|
||||
|
||||
If the file is missing or needs updating:
|
||||
|
||||
```bash
|
||||
sudo nano /var/www/html/lux-studio/api/.env
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6 — Test in browser
|
||||
|
||||
1. Open `https://ai-sandbox.oliver.solutions/lux-studio/`
|
||||
2. Hard refresh: `Ctrl + Shift + R`
|
||||
3. Open F12 → Network tab
|
||||
4. Check that:
|
||||
- Login page loads with the Lux Studio logo
|
||||
- After login, the Projects/Image Gen/Video Gen tabs are visible
|
||||
- API calls go to `/lux-studio/api/api.php` (not `/lux-studio-back/`)
|
||||
- No 404 errors in the Network tab
|
||||
5. Create a project, generate an image with a 10+ word prompt, confirm it saves
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### API calls returning 404
|
||||
|
||||
Check the built JS has the correct URL:
|
||||
```bash
|
||||
grep -o "lux-studio/api" /var/www/html/lux-studio/assets/*.js | head -3
|
||||
```
|
||||
If empty or showing `/lux-studio-back`, rebuild with the correct `VITE_API_URL`.
|
||||
|
||||
### Old code still loading after upload
|
||||
|
||||
Browser cache. Force a hard refresh: `Ctrl + Shift + R`
|
||||
Or: DevTools → Network → check "Disable cache" → refresh.
|
||||
|
||||
### Image generation crashing (500 error)
|
||||
|
||||
`AuthMiddleware.php` must be present in `/api/`. If it was just added, confirm it uploaded.
|
||||
|
||||
### `.env` missing on server
|
||||
|
||||
Create it manually via SSH (see Step 5 above). Without it, `GEMINI_API_KEY` is undefined and all API calls will fail.
|
||||
|
||||
### Permissions error on uploads
|
||||
|
||||
```bash
|
||||
sudo chown -R www-data:www-data /var/www/html/lux-studio/api/uploads
|
||||
sudo chmod -R 777 /var/www/html/lux-studio/api/uploads
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| What changed | Action needed |
|
||||
|-------------|---------------|
|
||||
| Frontend only (JS/CSS/components) | Steps 2, 3 |
|
||||
| Backend only (PHP files) | Step 4 |
|
||||
| Both frontend and backend | Steps 2, 3, 4 |
|
||||
| New `.env` values needed | Step 5 (SSH) |
|
||||
|
||||
---
|
||||
|
||||
**Last deployed:** 2026-02-27
|
||||
**Server:** ai-sandbox.oliver.solutions
|
||||
|
|
@ -1,242 +0,0 @@
|
|||
# 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!*
|
||||
192
README.md
Normal file
192
README.md
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
# Lux Studio
|
||||
|
||||
AI-powered cinematography suite for professional image and video generation.
|
||||
|
||||
**Live:** https://ai-sandbox.oliver.solutions/lux-studio/
|
||||
|
||||
---
|
||||
|
||||
## What It Does
|
||||
|
||||
Lux Studio combines physics-based prompt engineering with Google's Imagen 3 and Veo 3.1 APIs in a project-first workflow. Create projects, generate cinematic images with professional camera and lens profiles, produce AI videos, annotate storyboards — all stored locally in your browser.
|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
### Image Generation (Imagen 3)
|
||||
- 40+ cinematic lighting presets
|
||||
- 8 cinema camera body profiles (sensor behaviour, dynamic range, grain)
|
||||
- 10 lens profiles (bokeh, flares, chromatic aberration)
|
||||
- Up to 14 reference images for style transfer
|
||||
- Edit mode — refine a generated image with follow-up prompts
|
||||
- Image upload for style transfer or editing
|
||||
|
||||
### Video Generation (Veo 3.1)
|
||||
- Text-to-video and image-to-video modes
|
||||
- First-frame and last-frame interpolation (I2V)
|
||||
- Standard and Fast model selection
|
||||
- Duration options: 4s, 6s, 8s
|
||||
- Frame extraction from generated videos
|
||||
|
||||
### Project Workspace
|
||||
- Project-first workflow — every image and video is tied to a project
|
||||
- Persistent local storage via IndexedDB (no server account needed)
|
||||
- Storyboard editor with drag-to-reorder panels and PDF export
|
||||
|
||||
### Authentication
|
||||
- Microsoft Azure AD SSO via MSAL (toggleable per environment)
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Layer | Technology |
|
||||
|-------|-----------|
|
||||
| Frontend | React 19, Vite, Tailwind CSS |
|
||||
| Auth | @azure/msal-browser + @azure/msal-react |
|
||||
| Backend | PHP 7.4+ (Apache, no separate service needed) |
|
||||
| AI — Images | Google Imagen 3 via Gemini API |
|
||||
| AI — Video | Google Veo 3.1 via Gemini API |
|
||||
| Storage | IndexedDB (frontend) + session files (backend, auto-cleaned 24 h) |
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Node.js 18+
|
||||
- PHP 7.4+
|
||||
- Composer
|
||||
- A Google Gemini API key — https://aistudio.google.com/app/apikey
|
||||
- (Optional) Azure AD app registration for SSO
|
||||
|
||||
---
|
||||
|
||||
## Local Development
|
||||
|
||||
### 1. Configure environment files
|
||||
|
||||
```bash
|
||||
cp backend/.env.local backend/.env
|
||||
cp frontend/.env.local frontend/.env
|
||||
```
|
||||
|
||||
Edit `backend/.env` and set your `GEMINI_API_KEY`.
|
||||
|
||||
### 2. Start both servers
|
||||
|
||||
**Recommended — automated:**
|
||||
```bash
|
||||
./setup.sh # Detects port conflicts, starts both servers, generates stop.sh
|
||||
./stop.sh # Stop all servers
|
||||
./status.sh # Check running status
|
||||
```
|
||||
|
||||
**Manual:**
|
||||
```bash
|
||||
# Terminal 1 — Backend
|
||||
cd backend
|
||||
composer install
|
||||
php -S localhost:5015
|
||||
|
||||
# Terminal 2 — Frontend
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. Open the app
|
||||
|
||||
http://localhost:3000
|
||||
|
||||
> SSO is enabled by default. To skip Azure AD login during development, set `VITE_SSO_ENABLED=false` in `frontend/.env` and restart.
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Backend (`backend/.env`)
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `GEMINI_API_KEY` | Google Gemini API key (required) |
|
||||
| `FRONTEND_URL` | CORS allowed origin — no trailing slash |
|
||||
| `SSO_ENABLED` | `true` / `false` — enable Azure AD auth |
|
||||
| `BACKEND_PORT` | PHP dev server port (default: `5015`) |
|
||||
| `SSO_TENANT_ID` | Azure AD tenant ID |
|
||||
| `SSO_CLIENT_ID` | Azure AD client ID |
|
||||
|
||||
### Frontend (`frontend/.env`)
|
||||
|
||||
| Variable | Description |
|
||||
|----------|-------------|
|
||||
| `VITE_API_URL` | Backend base URL (e.g. `http://localhost:5015` for local) |
|
||||
| `VITE_BASE_PATH` | App base path (`/` local, `/lux-studio/` production) |
|
||||
| `VITE_GEMINI_API_KEY` | Gemini key for client-side prompt enhancement |
|
||||
| `VITE_SSO_ENABLED` | `true` / `false` — show SSO login UI |
|
||||
| `VITE_SSO_TENANT_ID` | Azure AD tenant ID |
|
||||
| `VITE_SSO_CLIENT_ID` | Azure AD client ID |
|
||||
| `VITE_SSO_REDIRECT_URI` | OAuth redirect URI (must match Azure AD portal exactly) |
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment
|
||||
|
||||
See [NEW_DEPLOYMENT.md](NEW_DEPLOYMENT.md) for the full step-by-step guide (FileZilla + SSH).
|
||||
|
||||
**Server:** `ai-sandbox.oliver.solutions`
|
||||
**Frontend path:** `/var/www/html/lux-studio/`
|
||||
**Backend path:** `/var/www/html/lux-studio/api/`
|
||||
|
||||
Quick build summary:
|
||||
```bash
|
||||
cd frontend
|
||||
cp .env.production .env
|
||||
npm run build
|
||||
# Upload frontend/dist/* per NEW_DEPLOYMENT.md Step 3
|
||||
# Upload backend/*.php per NEW_DEPLOYMENT.md Step 4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Fix |
|
||||
|---------|-----|
|
||||
| 404 on all API calls | Check `VITE_API_URL` is correct and the build was made after the change |
|
||||
| Image generation 500 error | `AuthMiddleware.php` missing from `/api/` — upload it |
|
||||
| `IMAGE_RECITATION` error | Prompt too generic — use 10+ descriptive words |
|
||||
| Login redirect loop | `VITE_SSO_REDIRECT_URI` must match Azure AD portal exactly (include trailing slash) |
|
||||
| Video stuck in PENDING | Check `video_operations.json` on the server; retry generation |
|
||||
| Old code after deploy | Hard refresh: `Ctrl + Shift + R`, or delete `assets/` folder on server before re-uploading |
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
cinema-studio-pro/
|
||||
├── frontend/ # React + Vite app
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # UI components
|
||||
│ │ ├── hooks/ # useProjects, useIndexedDB
|
||||
│ │ ├── authConfig.js # MSAL config
|
||||
│ │ └── App.jsx
|
||||
│ ├── .env.local # Local dev template
|
||||
│ ├── .env.production # Production template
|
||||
│ └── package.json
|
||||
├── backend/ # PHP APIs
|
||||
│ ├── api.php # Image generation (Imagen 3)
|
||||
│ ├── video_api.php # Video generation (Veo 3.1)
|
||||
│ ├── enhance_prompt.php # AI prompt optimisation
|
||||
│ ├── stream_video.php # Video streaming with Range support
|
||||
│ ├── session_manager.php # Multi-user session isolation
|
||||
│ ├── AuthMiddleware.php # SSO middleware (required by api.php)
|
||||
│ ├── JWTValidator.php # Azure AD token validation
|
||||
│ ├── uploads/ # Temp file storage (auto-cleaned)
|
||||
│ ├── .env.local # Local dev template
|
||||
│ └── .env.production # Production template
|
||||
├── setup.sh # Start local dev servers
|
||||
├── NEW_DEPLOYMENT.md # Production deployment guide
|
||||
└── CLAUDE.md # AI assistant instructions
|
||||
```
|
||||
370
api.php.backup
370
api.php.backup
|
|
@ -1,370 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
41
backend/.env.example
Normal file
41
backend/.env.example
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# ============================================================================
|
||||
# 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 dev server runs locally
|
||||
# Start backend with: php -S localhost:${BACKEND_PORT}
|
||||
# NOTE: Not used in production — Apache serves PHP directly via mod_php
|
||||
BACKEND_PORT=5015
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 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
|
||||
33
backend/.env.local
Normal file
33
backend/.env.local
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# ============================================================================
|
||||
# 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 dev server runs locally
|
||||
# Start backend with: php -S localhost:5015
|
||||
BACKEND_PORT=5015
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 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
|
||||
30
backend/.env.production
Normal file
30
backend/.env.production
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# ============================================================================
|
||||
# Lux Studio Backend - PRODUCTION Environment Configuration
|
||||
# ============================================================================
|
||||
# This file contains production-ready settings
|
||||
# Deployed to: /var/www/html/lux-studio/api/.env (never overwritten by deploy.sh)
|
||||
# Apache serves PHP directly via mod_php — no standalone server or proxy needed
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 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
|
||||
142
backend/.htaccess
Normal file
142
backend/.htaccess
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# ==============================================================================
|
||||
# LUX STUDIO BACKEND - SECURITY CONFIGURATION
|
||||
# ==============================================================================
|
||||
# Location: /var/www/html/lux-studio/api/.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,9 +13,6 @@ 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
|
||||
|
|
@ -153,7 +150,7 @@ class NanoBananaProAPI {
|
|||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
|
||||
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', 'AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8');
|
||||
define('GEMINI_API_KEY', 'YOUR_API_KEY_HERE');
|
||||
|
||||
// MSAL / Azure AD SSO Configuration
|
||||
// Always define these constants with defaults if not set
|
||||
|
|
|
|||
|
|
@ -221,6 +221,7 @@ 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);
|
||||
|
|
|
|||
119
backend/get_session_file.php
Normal file
119
backend/get_session_file.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?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
1513
backend/index.php
File diff suppressed because it is too large
Load diff
169
backend/list_session_files.php
Normal file
169
backend/list_session_files.php
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?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 = dirname(__DIR__) . '/uploads/sessions';
|
||||
$this->uploadDir = __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 = dirname(__DIR__) . '/uploads/sessions';
|
||||
$uploadDir = __DIR__ . '/uploads/sessions';
|
||||
}
|
||||
|
||||
if (!is_dir($uploadDir)) {
|
||||
|
|
|
|||
9
backend/test.php
Normal file
9
backend/test.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?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,6 +54,12 @@ 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])) {
|
||||
|
|
@ -61,6 +67,7 @@ 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'
|
||||
|
|
@ -89,12 +96,6 @@ 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);
|
||||
|
|
@ -136,7 +137,7 @@ class VeoVideoAPI {
|
|||
|
||||
// Log payload structure (without full base64 data for readability)
|
||||
$logPayload = $payload;
|
||||
if (isset($logPayload['instances'][0]['image'])) {
|
||||
if (isset($logPayload['instances'][0]['image']['bytesBase64Encoded'])) {
|
||||
$logPayload['instances'][0]['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['image']['bytesBase64Encoded']) . '_bytes]';
|
||||
}
|
||||
if (isset($logPayload['instances'][0]['lastFrame']['bytesBase64Encoded'])) {
|
||||
|
|
@ -144,9 +145,12 @@ class VeoVideoAPI {
|
|||
}
|
||||
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]';
|
||||
if (isset($refImg['image']['bytesBase64Encoded'])) {
|
||||
$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);
|
||||
|
|
@ -168,7 +172,7 @@ class VeoVideoAPI {
|
|||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => json_encode($payload),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
|
||||
CURLOPT_TIMEOUT => 600 // 10 minute timeout for video generation
|
||||
]);
|
||||
|
||||
|
|
@ -206,7 +210,12 @@ class VeoVideoAPI {
|
|||
}
|
||||
|
||||
if ($errorStatus === 'INVALID_ARGUMENT') {
|
||||
throw new Exception("Invalid request format. Check your prompt and settings.");
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (stripos($errorMessage, 'model') !== false && stripos($errorMessage, 'not found') !== false) {
|
||||
|
|
@ -472,7 +481,7 @@ class VeoVideoAPI {
|
|||
'x-goog-api-key: ' . $this->apiKey
|
||||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
|
||||
CURLOPT_TIMEOUT => 30
|
||||
]);
|
||||
|
||||
|
|
@ -602,7 +611,7 @@ try {
|
|||
'video' => [
|
||||
'filename' => $filename,
|
||||
'mime_type' => $videoData['mime_type'],
|
||||
'url' => '/lux-studio/api/stream_video.php?file=' . urlencode($filename)
|
||||
'url' => '/generated_videos/' . $filename
|
||||
]
|
||||
]);
|
||||
} else if ($videoData['type'] === 'uri') {
|
||||
|
|
@ -644,7 +653,7 @@ try {
|
|||
'video' => [
|
||||
'filename' => $filename,
|
||||
'mime_type' => $videoData['mime_type'],
|
||||
'url' => '/lux-studio/api/stream_video.php?file=' . urlencode($filename)
|
||||
'url' => '/generated_videos/' . $filename
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
|
|
@ -696,7 +705,7 @@ try {
|
|||
],
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
|
||||
CURLOPT_TIMEOUT => 120
|
||||
]);
|
||||
|
||||
|
|
@ -720,11 +729,11 @@ try {
|
|||
$filepath = $videoDir . '/' . $filename;
|
||||
file_put_contents($filepath, $videoData);
|
||||
|
||||
// Return local URL
|
||||
// Return URL in /generated_videos/ format for frontend to handle
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'video' => [
|
||||
'url' => '/api/stream_video.php?file=' . urlencode($filename),
|
||||
'url' => '/generated_videos/' . $filename,
|
||||
'filename' => $filename,
|
||||
'mime_type' => 'video/mp4'
|
||||
]
|
||||
|
|
|
|||
84
composer.lock
generated
84
composer.lock
generated
|
|
@ -1,84 +0,0 @@
|
|||
{
|
||||
"_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"
|
||||
}
|
||||
281
deploy.sh
Normal file
281
deploy.sh
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
#!/bin/bash
|
||||
|
||||
################################################################################
|
||||
# Lux Studio — Production Deployment Script
|
||||
#
|
||||
# Usage:
|
||||
# cd /opt/cinema-studio-pro
|
||||
# sudo ./deploy.sh
|
||||
#
|
||||
# What it does:
|
||||
# 1. Builds the React frontend using frontend/.env.production
|
||||
# 2. Deploys built files → /var/www/html/lux-studio/
|
||||
# 3. Deploys backend PHP → /var/www/html/lux-studio/api/
|
||||
# 4. Verifies the deployment
|
||||
#
|
||||
# Apache serves PHP directly — no systemd service or proxy needed.
|
||||
# Apache configuration (AllowOverride, modules) is managed separately by the operator.
|
||||
# The backend .env on the server is never overwritten (preserves API keys).
|
||||
################################################################################
|
||||
|
||||
set -e
|
||||
|
||||
# ─── Colors ───────────────────────────────────────────────────────────────────
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
# ─── Configuration ─────────────────────────────────────────────────────────────
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
FRONTEND_SRC="$SCRIPT_DIR/frontend"
|
||||
BACKEND_SRC="$SCRIPT_DIR/backend"
|
||||
WEB_ROOT="/var/www/html/lux-studio"
|
||||
API_ROOT="$WEB_ROOT/api"
|
||||
|
||||
# PHP files that must never be deployed to the server
|
||||
SKIP_PHP=("test.php" "config.example.php")
|
||||
|
||||
# ─── Helper functions ─────────────────────────────────────────────────────────
|
||||
step() { echo ""; echo -e "${YELLOW}[$1] $2...${NC}"; }
|
||||
ok() { echo -e "${GREEN} ✓ $1${NC}"; }
|
||||
warn() { echo -e "${YELLOW} ⚠ $1${NC}"; }
|
||||
fail() { echo -e "${RED} Error: $1${NC}"; exit 1; }
|
||||
|
||||
# ─── Header ───────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e "${BLUE}========================================"
|
||||
echo -e " Lux Studio — Production Deployment"
|
||||
echo -e "========================================${NC}"
|
||||
echo ""
|
||||
echo " Source: $SCRIPT_DIR"
|
||||
echo " Frontend: $WEB_ROOT"
|
||||
echo " Backend: $API_ROOT"
|
||||
echo ""
|
||||
|
||||
# ─── Root check ───────────────────────────────────────────────────────────────
|
||||
[[ $EUID -ne 0 ]] && fail "This script must be run as root. Use: sudo ./deploy.sh"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
step "1/6" "Preflight checks"
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
[ -d "$FRONTEND_SRC" ] || fail "frontend/ directory not found in $SCRIPT_DIR"
|
||||
[ -d "$BACKEND_SRC" ] || fail "backend/ directory not found in $SCRIPT_DIR"
|
||||
|
||||
[ -f "$FRONTEND_SRC/.env.production" ] \
|
||||
|| fail "frontend/.env.production not found — cannot build for production"
|
||||
|
||||
[ -f "$BACKEND_SRC/.env.production" ] \
|
||||
|| warn "backend/.env.production not found — /api/.env will not be created automatically"
|
||||
|
||||
# Check required system commands
|
||||
for cmd in php node npm; do
|
||||
command -v "$cmd" &>/dev/null \
|
||||
&& ok "$cmd: $(command -v $cmd)" \
|
||||
|| fail "$cmd is not installed"
|
||||
done
|
||||
|
||||
ok "Preflight passed"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
step "2/6" "Building frontend"
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
cd "$FRONTEND_SRC"
|
||||
|
||||
# Always use .env.production for production builds — never use a cached .env
|
||||
cp .env.production .env
|
||||
ok "frontend/.env overwritten from .env.production"
|
||||
|
||||
# Install npm dependencies if node_modules is absent or package.json changed
|
||||
echo " Checking npm dependencies..."
|
||||
npm install --silent
|
||||
ok "npm dependencies ready"
|
||||
|
||||
echo " Building (this may take 30–60 seconds)..."
|
||||
npm run build
|
||||
ok "Build complete → frontend/dist/"
|
||||
|
||||
# Confirm the correct API path was baked into the bundle
|
||||
if grep -qo "lux-studio/api" dist/assets/*.js 2>/dev/null; then
|
||||
ok "API URL verified in bundle: lux-studio/api"
|
||||
else
|
||||
fail "Bundle does not contain 'lux-studio/api'. Check VITE_API_URL in frontend/.env.production"
|
||||
fi
|
||||
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
step "3/6" "Deploying frontend to $WEB_ROOT"
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
mkdir -p "$WEB_ROOT"
|
||||
|
||||
# Delete old assets/ first — Vite produces differently hash-named files each
|
||||
# build. Leaving old files causes stale JS to be served.
|
||||
if [ -d "$WEB_ROOT/assets" ]; then
|
||||
rm -rf "$WEB_ROOT/assets"
|
||||
ok "Old assets/ removed (hash names change each build)"
|
||||
fi
|
||||
|
||||
# Copy built output
|
||||
cp "$FRONTEND_SRC/dist/index.html" "$WEB_ROOT/index.html"
|
||||
cp -r "$FRONTEND_SRC/dist/assets" "$WEB_ROOT/assets"
|
||||
ok "index.html and assets/ deployed"
|
||||
|
||||
# Copy static public assets (logos, favicon — not always present in all builds)
|
||||
for item in LUX_STUDIO_LOGO.svg LUX_STUDIO_LOGO.png; do
|
||||
src="$FRONTEND_SRC/dist/$item"
|
||||
[ -e "$src" ] && cp "$src" "$WEB_ROOT/$item"
|
||||
done
|
||||
ok "Static assets deployed (LUX_STUDIO_LOGO.svg, LUX_STUDIO_LOGO.png)"
|
||||
|
||||
# .htaccess is NOT copied into dist/ by Vite — must be taken from source
|
||||
if [ -f "$FRONTEND_SRC/.htaccess" ]; then
|
||||
cp "$FRONTEND_SRC/.htaccess" "$WEB_ROOT/.htaccess"
|
||||
ok ".htaccess deployed from frontend/.htaccess (not from dist/)"
|
||||
else
|
||||
warn "frontend/.htaccess not found — React Router deep links will break"
|
||||
fi
|
||||
|
||||
chown -R www-data:www-data "$WEB_ROOT"
|
||||
chmod -R 755 "$WEB_ROOT"
|
||||
ok "Permissions set (www-data:www-data, 755)"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
step "4/6" "Deploying backend PHP to $API_ROOT"
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
mkdir -p "$API_ROOT"
|
||||
|
||||
# Copy all .php files from backend/ except those in SKIP_PHP
|
||||
PHP_COUNT=0
|
||||
for php_file in "$BACKEND_SRC"/*.php; do
|
||||
filename=$(basename "$php_file")
|
||||
skip=false
|
||||
for skip_name in "${SKIP_PHP[@]}"; do
|
||||
[ "$filename" = "$skip_name" ] && skip=true && break
|
||||
done
|
||||
if [ "$skip" = false ]; then
|
||||
cp "$php_file" "$API_ROOT/$filename"
|
||||
PHP_COUNT=$((PHP_COUNT + 1))
|
||||
fi
|
||||
done
|
||||
ok "$PHP_COUNT PHP files deployed (skipped: ${SKIP_PHP[*]})"
|
||||
|
||||
# Copy backend .htaccess (security rules, blocks .env/vendor/direct class access)
|
||||
if [ -f "$BACKEND_SRC/.htaccess" ]; then
|
||||
cp "$BACKEND_SRC/.htaccess" "$API_ROOT/.htaccess"
|
||||
ok "Backend .htaccess deployed"
|
||||
fi
|
||||
|
||||
# ── .env handling ──────────────────────────────────────────────────────────────
|
||||
# The production .env contains the real GEMINI_API_KEY.
|
||||
# NEVER overwrite it — only create it if it does not exist yet.
|
||||
if [ -f "$API_ROOT/.env" ]; then
|
||||
ok "/api/.env already exists — not overwritten (production secrets preserved)"
|
||||
else
|
||||
if [ -f "$BACKEND_SRC/.env.production" ]; then
|
||||
cp "$BACKEND_SRC/.env.production" "$API_ROOT/.env"
|
||||
ok "Created /api/.env from backend/.env.production"
|
||||
echo ""
|
||||
warn "ACTION REQUIRED: Verify GEMINI_API_KEY and FRONTEND_URL in /api/.env:"
|
||||
warn " sudo nano $API_ROOT/.env"
|
||||
echo ""
|
||||
else
|
||||
warn "backend/.env.production not found — /api/.env was NOT created"
|
||||
warn "Create it manually: sudo nano $API_ROOT/.env"
|
||||
warn "Required keys: GEMINI_API_KEY, FRONTEND_URL, SSO_ENABLED"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create uploads directory (PHP writes generated images/videos here)
|
||||
mkdir -p "$API_ROOT/uploads/sessions"
|
||||
|
||||
# Set ownership before chmod so chmod applies correctly
|
||||
chown -R www-data:www-data "$API_ROOT"
|
||||
chmod -R 755 "$API_ROOT"
|
||||
# www-data must be able to write generated files
|
||||
chmod -R 777 "$API_ROOT/uploads"
|
||||
ok "Permissions set (uploads/: 777 for www-data writes)"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
step "5/6" "Verifying deployment"
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
ERRORS=0
|
||||
|
||||
check() {
|
||||
# $1 = path to check, $2 = label, $3 = "warn" (non-fatal) or empty (fatal)
|
||||
if [ -e "$1" ]; then
|
||||
ok "$2"
|
||||
elif [ "$3" = "warn" ]; then
|
||||
warn "$2 not found"
|
||||
else
|
||||
echo -e "${RED} ✗ $2 — MISSING${NC}"
|
||||
ERRORS=$((ERRORS+1))
|
||||
fi
|
||||
}
|
||||
|
||||
# Frontend
|
||||
check "$WEB_ROOT/index.html" "index.html"
|
||||
check "$WEB_ROOT/assets" "assets/ directory"
|
||||
check "$WEB_ROOT/.htaccess" ".htaccess (frontend)" warn
|
||||
|
||||
# Backend
|
||||
check "$API_ROOT/api.php" "api.php"
|
||||
check "$API_ROOT/AuthMiddleware.php" "AuthMiddleware.php ← api.php requires this"
|
||||
check "$API_ROOT/video_api.php" "video_api.php"
|
||||
check "$API_ROOT/enhance_prompt.php" "enhance_prompt.php"
|
||||
check "$API_ROOT/.htaccess" ".htaccess (backend)"
|
||||
check "$API_ROOT/uploads/sessions" "uploads/sessions/"
|
||||
|
||||
if [ -f "$API_ROOT/.env" ]; then
|
||||
ok "/api/.env present"
|
||||
else
|
||||
echo -e "${RED} ✗ /api/.env MISSING — all API calls will fail without it${NC}"
|
||||
ERRORS=$((ERRORS+1))
|
||||
fi
|
||||
|
||||
# Confirm bundle has the right API URL
|
||||
if grep -qo "lux-studio/api" "$WEB_ROOT"/assets/*.js 2>/dev/null; then
|
||||
ok "Bundle contains correct API path (lux-studio/api)"
|
||||
else
|
||||
warn "Could not verify API path in deployed bundle"
|
||||
fi
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
step "6/6" "Summary"
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE} Deployed:${NC}"
|
||||
echo " Frontend → $WEB_ROOT"
|
||||
echo " Backend → $API_ROOT"
|
||||
echo ""
|
||||
echo -e "${BLUE} Live URL:${NC}"
|
||||
echo " https://ai-sandbox.oliver.solutions/lux-studio/"
|
||||
echo ""
|
||||
echo -e "${BLUE} Useful commands:${NC}"
|
||||
echo " Check .env: sudo cat $API_ROOT/.env"
|
||||
echo " Apache errors: sudo tail -f /var/log/apache2/error.log"
|
||||
echo " Reload Apache: sudo systemctl reload apache2"
|
||||
echo " Re-deploy: cd $SCRIPT_DIR && sudo ./deploy.sh"
|
||||
echo ""
|
||||
echo -e "${BLUE} Test checklist:${NC}"
|
||||
echo " 1. Hard refresh in browser: Ctrl + Shift + R"
|
||||
echo " 2. F12 → Network: API calls go to /lux-studio/api/ (not /lux-studio-back/)"
|
||||
echo " 3. No 404 errors in the Network tab"
|
||||
echo " 4. Create a project and generate an image with a 10+ word prompt"
|
||||
echo ""
|
||||
|
||||
if [ $ERRORS -gt 0 ]; then
|
||||
echo -e "${YELLOW} ⚠ $ERRORS check(s) failed — review the output above before testing${NC}"
|
||||
echo ""
|
||||
exit 1
|
||||
else
|
||||
echo -e "${GREEN} ✓ All checks passed — deployment successful!${NC}"
|
||||
echo ""
|
||||
fi
|
||||
|
|
@ -1,20 +1,40 @@
|
|||
# Google Gemini API Key
|
||||
# Get your API key from: https://makersuite.google.com/app/apikey
|
||||
VITE_GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
|
||||
|
||||
# Frontend Port Configuration
|
||||
VITE_FRONTEND_PORT=3000
|
||||
# ============================================================================
|
||||
# Lux Studio Backend - LOCAL Development Environment Configuration
|
||||
# ============================================================================
|
||||
# This file is used by setup.sh for local development
|
||||
# ============================================================================
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Backend Port Configuration
|
||||
VITE_BACKEND_PORT=5015
|
||||
# ----------------------------------------------------------------------------
|
||||
# Port on which the PHP backend server runs locally
|
||||
# Start backend with: php -S localhost:5015
|
||||
BACKEND_PORT=5015
|
||||
|
||||
# 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
|
||||
# ----------------------------------------------------------------------------
|
||||
# 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=/
|
||||
|
||||
# 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
|
||||
# ----------------------------------------------------------------------------
|
||||
# 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,11 +1,56 @@
|
|||
# Google Gemini API Key
|
||||
VITE_GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
|
||||
# ============================================================================
|
||||
# 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
|
||||
# ============================================================================
|
||||
|
||||
# Azure AD SSO Configuration - PRODUCTION
|
||||
# ----------------------------------------------------------------------------
|
||||
# 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/api
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Google Gemini API Key
|
||||
# ----------------------------------------------------------------------------
|
||||
# Used for client-side prompt enhancement # AIzaSyCMKLSJJYEx4c6-TtBACnjdULrLzsr_fts
|
||||
VITE_GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Azure AD / MSAL SSO Configuration
|
||||
# ----------------------------------------------------------------------------
|
||||
# Enable SSO authentication
|
||||
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/
|
||||
|
||||
# API Base URL for production
|
||||
VITE_API_BASE_URL=/lux-studio/api
|
||||
# ----------------------------------------------------------------------------
|
||||
# Environment Mode
|
||||
# ----------------------------------------------------------------------------
|
||||
NODE_ENV=production
|
||||
|
|
|
|||
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
|
|
@ -8,7 +8,7 @@ pnpm-debug.log*
|
|||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
# dist
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
|
|
|
|||
57
frontend/.htaccess
Normal file
57
frontend/.htaccess
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
# ==============================================================================
|
||||
# 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
|
||||
# ==============================================================================
|
||||
|
|
@ -1,168 +0,0 @@
|
|||
# Lux Studio - AI Cinematography Suite
|
||||
|
||||
## Overview
|
||||
|
||||
Lux Studio is a comprehensive AI-powered creative suite for filmmakers and visual artists. It combines professional cinematography prompt generation, AI video generation (via Google Veo 3.1), and project management with storyboarding capabilities.
|
||||
|
||||
## Key Features
|
||||
|
||||
### Image Generation
|
||||
- **27 Cinematography Presets**: Documentary, Drama, Auteur, and Commercial styles
|
||||
- **Professional Equipment**: 6 cinema cameras, 7 cinema lenses with compatibility matrix
|
||||
- **Film Stock & Texture Engine**: Deep texture generation for photorealistic materials
|
||||
- **AI-Optimized Prompts**: Powered by Google Gemini for precise prompt engineering
|
||||
|
||||
### Video Generation
|
||||
- **Google Veo 3.1 Integration**: Generate 4-8 second AI videos
|
||||
- **Model Options**: Standard (higher quality) and Fast (50% cost savings)
|
||||
- **Image-to-Video (I2V)**: Use reference images to guide video generation
|
||||
- **Audio Generation**: Optional AI-generated audio tracks
|
||||
- **Frame Extraction**: Extract frames from videos and save to library
|
||||
|
||||
### Project Management
|
||||
- **Organize Work**: Create projects to organize generated images and videos
|
||||
- **Library**: Store and browse all generated assets
|
||||
- **Re-run Videos**: Regenerate videos with saved settings and reference images
|
||||
|
||||
### Storyboarding
|
||||
- **Create Storyboards**: Select images from library to build visual sequences
|
||||
- **Drag-to-Reorder**: Arrange frames with intuitive drag-and-drop
|
||||
- **Frame Annotations**: Add scene notes and descriptions
|
||||
- **Generate Video per Frame**: Send frames directly to Video Gen
|
||||
- **Export Options**: PDF and PNG export for sharing
|
||||
|
||||
## Data Storage
|
||||
|
||||
### Where is data stored?
|
||||
|
||||
All project data is stored locally in your browser using **IndexedDB**, a client-side database. This means:
|
||||
|
||||
- **Projects, images, and videos** are stored on your device
|
||||
- **Data persists** between browser sessions
|
||||
- **No server uploads** for library content (videos are generated server-side but stored locally)
|
||||
- **Device-specific**: Data doesn't sync between devices
|
||||
|
||||
### Database Structure
|
||||
|
||||
```
|
||||
IndexedDB: CinemaStudioPro
|
||||
├── projects # Project metadata (name, dates, userId)
|
||||
├── items # Images and videos (base64 data, prompts, settings)
|
||||
└── storyboards # Storyboard configurations (frames, annotations)
|
||||
```
|
||||
|
||||
### Storage Considerations
|
||||
|
||||
- Large video files are stored as URLs pointing to the server
|
||||
- Images are stored as base64-encoded data
|
||||
- Browser storage limits apply (typically 50-100MB+)
|
||||
- Clear browser data will remove all projects
|
||||
|
||||
### Production Migration Path
|
||||
|
||||
The current IndexedDB storage is ideal for development but not suitable for production deployment. For hosted environments, migrate to server-side storage:
|
||||
|
||||
**Recommended Architecture:**
|
||||
|
||||
```
|
||||
Development (Current):
|
||||
Browser IndexedDB → base64 images stored locally
|
||||
|
||||
Production:
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Frontend │ ──► │ Backend │ ──► │ S3/Cloud │
|
||||
│ (React) │ │ API │ │ Storage │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ PostgreSQL │
|
||||
│ (metadata) │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
**Migration Steps:**
|
||||
|
||||
1. **Cloud Storage**: Store images/videos in AWS S3, Google Cloud Storage, or similar
|
||||
2. **Backend API**: Create REST endpoints for CRUD operations
|
||||
3. **Database**: PostgreSQL/MySQL for project metadata, user data, and asset references
|
||||
4. **Update Hooks**: Modify `useProjects.js` to call API endpoints instead of IndexedDB
|
||||
|
||||
**API Endpoint Examples:**
|
||||
```
|
||||
POST /api/projects → createProject()
|
||||
GET /api/projects → loadProjects()
|
||||
DELETE /api/projects/:id → deleteProject()
|
||||
POST /api/projects/:id/items → addItemToProject()
|
||||
GET /api/projects/:id/items → getProjectItems()
|
||||
```
|
||||
|
||||
The hook abstraction (`useProjects`, `useIndexedDB`) makes this migration clean - swap the implementation without changing UI components.
|
||||
|
||||
## User Isolation (SSO Ready)
|
||||
|
||||
The application is prepared for multi-user support via SSO integration:
|
||||
|
||||
- Each project includes a `userId` field
|
||||
- Projects are filtered by user ID when loading
|
||||
- Currently uses `'local'` as placeholder until SSO is implemented
|
||||
|
||||
### SSO Integration
|
||||
|
||||
To add SSO authentication, update the `getCurrentUserId()` function in `src/hooks/useProjects.js`:
|
||||
|
||||
```javascript
|
||||
const getCurrentUserId = () => {
|
||||
// Replace with your auth provider
|
||||
return authContext.user?.id || 'local';
|
||||
};
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
- Node.js 18.0+
|
||||
- Google Gemini API key
|
||||
- PHP backend for video generation (Veo 3.1 API)
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Install dependencies**
|
||||
```bash
|
||||
cd Prompt_Studio
|
||||
npm install
|
||||
```
|
||||
|
||||
2. **Configure environment**
|
||||
```env
|
||||
VITE_GEMINI_API_KEY=your_gemini_api_key
|
||||
```
|
||||
|
||||
3. **Start development server**
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
4. **Build for production**
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Frontend**: React 18, Vite, Tailwind CSS
|
||||
- **AI Services**: Google Gemini (prompts), Google Veo 3.1 (video)
|
||||
- **Storage**: IndexedDB (client-side)
|
||||
- **Drag & Drop**: @dnd-kit
|
||||
- **Export**: jsPDF, html2canvas
|
||||
|
||||
## API Keys Required
|
||||
|
||||
| Service | Purpose | Environment Variable |
|
||||
|---------|---------|---------------------|
|
||||
| Google Gemini | Prompt optimization | `VITE_GEMINI_API_KEY` |
|
||||
| Google Veo 3.1 | Video generation | Configured in PHP backend |
|
||||
|
||||
## License
|
||||
|
||||
Proprietary - All rights reserved
|
||||
BIN
frontend/dist/LUX_STUDIO_LOGO.png
vendored
BIN
frontend/dist/LUX_STUDIO_LOGO.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 5.1 KiB |
38
frontend/dist/LUX_STUDIO_LOGO.svg
vendored
38
frontend/dist/LUX_STUDIO_LOGO.svg
vendored
|
|
@ -1,38 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 118.87 50.32">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #f90;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: none;
|
||||
stroke: #f90;
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<g>
|
||||
<g>
|
||||
<path class="cls-2" d="M16.77,14.11h2.01l-2.83,20.12h4.53l-.24,1.67h-6.54l3.06-21.79Z"/>
|
||||
<path class="cls-2" d="M31.75,14.11h2.01l-2.35,16.82c-.37,2.45.1,3.61,1.19,3.61s1.87-1.16,2.25-3.61l2.35-16.82h2.01l-2.28,16.31c-.54,3.85-2.18,5.79-4.56,5.79s-3.44-1.94-2.89-5.79l2.28-16.31Z"/>
|
||||
<path class="cls-2" d="M51.93,24.9l1.33,11h-2.01l-.88-9.16-3.4,9.16h-2.01l4.43-10.96-1.06-10.83h2.01l.58,8.85,3.06-8.85h2.01l-4.05,10.79Z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-1" d="M63.2,22.14c.95,0,1.39.71,1.21,2l-1.04.33.07-.57c.09-.64-.03-.93-.34-.93-.37,0-.63.43-.63,1.09,0,1.39,1.4,2.01,1.4,3.63,0,1.24-.63,2.09-1.73,2.09-.94,0-1.38-.71-1.2-2l1.04-.33-.07.57c-.09.64.02.93.34.93.37,0,.59-.42.59-1.07,0-1.32-1.4-1.96-1.4-3.62,0-1.21.66-2.12,1.75-2.12Z"/>
|
||||
<path class="cls-1" d="M72.83,23.07h-.96l-.93,6.61h-1l.93-6.61h-.96l.13-.82h2.91l-.12.82Z"/>
|
||||
<path class="cls-1" d="M78.54,22.24h1l-.8,5.78c-.09.64.02.93.34.93s.51-.29.6-.93l.81-5.78h1l-.78,5.5c-.2,1.38-.82,2.03-1.75,2.03s-1.38-.65-1.18-2.03l.77-5.5Z"/>
|
||||
<path class="cls-1" d="M89.96,24.59l-.38,2.74c-.21,1.57-.94,2.35-2.16,2.35h-1.06l1.04-7.43h1.06c1.22,0,1.71.78,1.5,2.35ZM89.02,24.18c.1-.77-.13-1.11-.59-1.11h-.16l-.8,5.78h.15c.46,0,.8-.35.91-1.11l.5-3.55Z"/>
|
||||
<path class="cls-1" d="M97.31,23.07l-.81,5.78h.5l-.12.82h-2l.12-.82h.5l.81-5.78h-.5l.12-.82h2l-.12.82h-.5Z"/>
|
||||
<path class="cls-1" d="M105.05,22.14c.93,0,1.38.65,1.18,2.03l-.5,3.58c-.2,1.38-.82,2.03-1.75,2.03s-1.38-.65-1.18-2.03l.5-3.58c.19-1.38.82-2.03,1.75-2.03ZM104.93,22.96c-.3,0-.51.29-.6.93l-.57,4.13c-.09.64.02.93.34.93s.51-.29.6-.93l.57-4.13c.1-.64-.02-.93-.34-.93Z"/>
|
||||
</g>
|
||||
<rect class="cls-3" x=".5" y=".5" width="117.87" height="49.32"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
231
frontend/dist/assets/index-B01LkE57.js
vendored
231
frontend/dist/assets/index-B01LkE57.js
vendored
File diff suppressed because one or more lines are too long
1
frontend/dist/assets/index-S7O61I6c.css
vendored
1
frontend/dist/assets/index-S7O61I6c.css
vendored
File diff suppressed because one or more lines are too long
5
frontend/dist/assets/index.es-DVsbFmJ6.js
vendored
5
frontend/dist/assets/index.es-DVsbFmJ6.js
vendored
File diff suppressed because one or more lines are too long
2
frontend/dist/assets/purify.es-Bzr520pe.js
vendored
2
frontend/dist/assets/purify.es-Bzr520pe.js
vendored
File diff suppressed because one or more lines are too long
14
frontend/dist/index.html
vendored
14
frontend/dist/index.html
vendored
|
|
@ -1,14 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/LUX_STUDIO_LOGO.svg" />
|
||||
<link rel="icon" type="image/png" href="/LUX_STUDIO_LOGO.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Lux Studio - AI Cinematography</title>
|
||||
</head>
|
||||
|
|
|
|||
1272
frontend/package-lock.json
generated
1272
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -10,21 +10,21 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^5.1.0",
|
||||
"@azure/msal-react": "^5.0.3",
|
||||
"@azure/msal-browser": "^3.7.1",
|
||||
"@azure/msal-react": "^2.0.23",
|
||||
"@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": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.1",
|
||||
"@types/react": "^19.2.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^5.1.1",
|
||||
"autoprefixer": "^10.4.22",
|
||||
"eslint": "^9.39.1",
|
||||
|
|
|
|||
|
|
@ -1,150 +1,9 @@
|
|||
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';
|
||||
import React from 'react';
|
||||
import AppContent from './components/AppContent';
|
||||
|
||||
function App() {
|
||||
// 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>
|
||||
);
|
||||
// Simple wrapper - AppContent handles all MSAL hooks safely
|
||||
return <AppContent />;
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
26
frontend/src/authConfig.js
Normal file
26
frontend/src/authConfig.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* 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';
|
||||
};
|
||||
157
frontend/src/components/AppContent.jsx
Normal file
157
frontend/src/components/AppContent.jsx
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
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,10 +2,20 @@ 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();
|
||||
|
||||
|
|
@ -1203,7 +1213,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 PHP server is running on port ${import.meta.env.VITE_BACKEND_PORT || '5015'}.`);
|
||||
setImageError(`Network error: ${err.message}. Make sure backend service is running.`);
|
||||
} finally {
|
||||
setIsGeneratingImage(false);
|
||||
}
|
||||
|
|
@ -1891,7 +1901,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 PHP backend running on port {import.meta.env.VITE_BACKEND_PORT || '5015'}</p>
|
||||
<p className="text-slate-600 text-xs mt-2">Requires backend service running</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
64
frontend/src/components/ErrorBoundary.jsx
Normal file
64
frontend/src/components/ErrorBoundary.jsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
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,98 +1,57 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import React from 'react';
|
||||
import { useMsal } from '@azure/msal-react';
|
||||
import { loginRequest } from '../authConfig';
|
||||
|
||||
const LoginPage = () => {
|
||||
const { login, isLoading } = useAuth();
|
||||
const [error, setError] = useState('');
|
||||
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
||||
const { instance } = useMsal();
|
||||
|
||||
const handleLogin = async () => {
|
||||
setError('');
|
||||
setIsLoggingIn(true);
|
||||
try {
|
||||
await login();
|
||||
} catch (err) {
|
||||
setError(err.message || 'Login failed. Please try again.');
|
||||
} finally {
|
||||
setIsLoggingIn(false);
|
||||
await instance.loginRedirect(loginRequest);
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
}
|
||||
};
|
||||
|
||||
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-4">
|
||||
<div className="min-h-screen bg-slate-950 flex items-center justify-center px-6">
|
||||
<div className="max-w-md w-full">
|
||||
{/* Logo and Title */}
|
||||
<div className="text-center mb-10">
|
||||
<div className="text-center mb-8">
|
||||
<img
|
||||
src="/LUX_STUDIO_LOGO.svg"
|
||||
src={`${import.meta.env.BASE_URL}LUX_STUDIO_LOGO.svg`}
|
||||
alt="Lux Studio"
|
||||
className="h-16 mx-auto mb-6"
|
||||
className="h-16 w-auto 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
|
||||
AI-powered cinematography suite for professional image and video generation
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
<div className="bg-slate-900 rounded-lg p-8 shadow-xl border border-slate-800">
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
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"
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
</>
|
||||
)}
|
||||
<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
|
||||
</button>
|
||||
|
||||
<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 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>
|
||||
</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 className="mt-6 text-center text-sm text-slate-500">
|
||||
<p>Powered by Google Imagen 3 & Veo 3.1</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -132,6 +132,13 @@ 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) {
|
||||
|
|
@ -216,6 +223,120 @@ 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 {
|
||||
|
|
@ -734,6 +855,20 @@ 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" />
|
||||
|
||||
|
|
@ -791,6 +926,157 @@ 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,22 +3,19 @@ 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'
|
||||
|
|
@ -150,7 +147,7 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
|
|||
// Check if already added
|
||||
if (referenceImages.some(img => img.projectItemId === item.id)) return;
|
||||
|
||||
// Auto-set constraints when adding first reference image (I2V requirements)
|
||||
// Auto-set aspect ratio when adding first reference image
|
||||
if (referenceImages.length === 0) {
|
||||
setAspectRatio('16:9');
|
||||
}
|
||||
|
|
@ -222,7 +219,9 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
|
|||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
setReferenceImages(prev => {
|
||||
// Auto-set constraints when adding first reference image (I2V requirements)
|
||||
const newLength = prev.length + 1;
|
||||
|
||||
// Auto-set aspect ratio when adding first reference image
|
||||
if (prev.length === 0) {
|
||||
setAspectRatio('16:9');
|
||||
}
|
||||
|
|
@ -231,6 +230,7 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
|
|||
if (newLength === 2) {
|
||||
setDuration(8);
|
||||
}
|
||||
|
||||
return [...prev, {
|
||||
data: reader.result.split(',')[1],
|
||||
mime_type: file.type,
|
||||
|
|
@ -252,10 +252,19 @@ 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;
|
||||
|
|
@ -277,15 +286,23 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
|
|||
}
|
||||
};
|
||||
|
||||
video.onerror = () => {
|
||||
console.error('Video load error for thumbnail');
|
||||
video.onerror = (err) => {
|
||||
console.error('Video load error for thumbnail:', err);
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
// Timeout fallback
|
||||
setTimeout(() => resolve(null), 5000);
|
||||
|
||||
video.src = videoUrl;
|
||||
// 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.load();
|
||||
});
|
||||
};
|
||||
|
|
@ -626,10 +643,14 @@ 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();
|
||||
|
|
@ -652,6 +673,20 @@ 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',
|
||||
|
|
@ -662,9 +697,11 @@ 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');
|
||||
|
||||
await addItemToProject(activeProjectId, {
|
||||
const itemData = {
|
||||
type: 'video',
|
||||
prompt: prompt,
|
||||
settings: { modelType, duration, aspectRatio, resolution, generateAudio, dialogue, referenceMode, negativePrompt },
|
||||
|
|
@ -672,7 +709,12 @@ 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);
|
||||
}
|
||||
|
|
@ -702,20 +744,22 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
Model
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{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>
|
||||
))}
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -726,25 +770,19 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
</label>
|
||||
<div className="flex gap-2">
|
||||
{durationOptions.map((opt) => {
|
||||
const isDisabledByI2V = referenceImages.length > 0 && opt.value !== 8;
|
||||
const isDisabledByInterpolation = referenceImages.length === 2 && opt.value !== 8;
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
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' : ''}
|
||||
onClick={() => !isDisabledByInterpolation && setDuration(opt.value)}
|
||||
disabled={isDisabledByInterpolation}
|
||||
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>
|
||||
|
|
@ -768,13 +806,7 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
{['16:9', '9:16'].map((ratio) => (
|
||||
<button
|
||||
key={ratio}
|
||||
onClick={() => {
|
||||
setAspectRatio(ratio);
|
||||
if (ratio === '9:16') {
|
||||
// Auto-downgrade resolution (1080p not supported in portrait)
|
||||
setResolution('720p');
|
||||
}
|
||||
}}
|
||||
onClick={() => setAspectRatio(ratio)}
|
||||
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-all ${
|
||||
aspectRatio === ratio
|
||||
? 'bg-cinema-gold text-slate-950'
|
||||
|
|
@ -1160,7 +1192,7 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
|
|||
<button
|
||||
onClick={() => {
|
||||
const link = document.createElement('a');
|
||||
link.href = convertVideoUrl(generatedVideo.url);
|
||||
link.href = generatedVideo.url;
|
||||
link.download = generatedVideo.filename || `lux-studio-video-${Date.now()}.mp4`;
|
||||
link.click();
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
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
|
||||
|
|
@ -14,15 +13,48 @@ const VideoPlayer = ({ src, onFrameExtract, onSaveToProject, className = '', aut
|
|||
const videoRef = useRef(null);
|
||||
const canvasRef = useRef(null);
|
||||
|
||||
// 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'}/`);
|
||||
}
|
||||
// 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);
|
||||
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
|
|
@ -60,6 +92,19 @@ 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);
|
||||
|
|
@ -67,6 +112,7 @@ 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);
|
||||
|
|
@ -75,18 +121,22 @@ 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) return;
|
||||
if (!video || !videoSrc) return;
|
||||
|
||||
if (isPlaying) {
|
||||
video.pause();
|
||||
} else {
|
||||
video.play();
|
||||
video.play().catch(err => {
|
||||
console.error('Video play failed:', err);
|
||||
setIsPlaying(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
/**
|
||||
* 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',
|
||||
};
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
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,9 +1,12 @@
|
|||
import { useState, useEffect, useCallback } from 'react';
|
||||
import useIndexedDB from './useIndexedDB';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
// Get current user ID from auth context
|
||||
// Falls back to 'local' for development/testing
|
||||
// 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';
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook for project management
|
||||
|
|
@ -12,8 +15,7 @@ import { useAuth } from '../contexts/AuthContext';
|
|||
*/
|
||||
const useProjects = () => {
|
||||
const { isReady, error: dbError, add, put, get, getAll, getByIndex, remove } = useIndexedDB();
|
||||
const { user } = useAuth();
|
||||
const userId = user?.id || 'local';
|
||||
const userId = getCurrentUserId();
|
||||
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
|
@ -237,14 +239,36 @@ const useProjects = () => {
|
|||
// Get all items for a project
|
||||
const getProjectItems = useCallback(async (projectId) => {
|
||||
try {
|
||||
const items = await getByIndex('items', 'projectId', projectId);
|
||||
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;
|
||||
});
|
||||
|
||||
// Sort by createdAt descending (newest first)
|
||||
return items.sort((a, b) => b.createdAt - a.createdAt);
|
||||
} catch (err) {
|
||||
setError(err.message);
|
||||
throw err;
|
||||
}
|
||||
}, [getByIndex]);
|
||||
}, [getByIndex, put]);
|
||||
|
||||
// Get a single project with its items
|
||||
const getProjectWithItems = useCallback(async (projectId) => {
|
||||
|
|
|
|||
|
|
@ -2,23 +2,44 @@ 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'
|
||||
|
||||
// Initialize MSAL instance
|
||||
const msalInstance = new PublicClientApplication(msalConfig);
|
||||
console.log('🚀 Main.jsx loading...');
|
||||
console.log('SSO Enabled:', isSSOEnabled());
|
||||
|
||||
// MSAL v3+ requires async initialization before rendering
|
||||
msalInstance.initialize().then(() => {
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<StrictMode>
|
||||
<MsalProvider instance={msalInstance}>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</MsalProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
});
|
||||
// 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>
|
||||
`;
|
||||
}
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -1,13 +0,0 @@
|
|||
/**
|
||||
* 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,27 +3,41 @@ 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(), '')
|
||||
const backendPort = env.VITE_BACKEND_PORT || '5015'
|
||||
const frontendPort = parseInt(env.VITE_FRONTEND_PORT || '3000')
|
||||
|
||||
// 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 || '/'
|
||||
|
||||
return {
|
||||
plugins: [react()],
|
||||
base: mode === 'production' ? '/lux-studio/' : '/',
|
||||
base: basePath,
|
||||
server: {
|
||||
port: frontendPort,
|
||||
proxy: {
|
||||
'/lux-studio-back': {
|
||||
target: backendUrl,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/lux-studio-back/, '')
|
||||
},
|
||||
'/api': {
|
||||
target: `http://localhost:${backendPort}`,
|
||||
target: backendUrl,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
},
|
||||
'/generated_videos': {
|
||||
target: `http://localhost:${backendPort}`,
|
||||
target: backendUrl,
|
||||
changeOrigin: true
|
||||
},
|
||||
'/generated_images': {
|
||||
target: `http://localhost:${backendPort}`,
|
||||
target: backendUrl,
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
|
|
|
|||
34
lux-studio-apache.conf
Normal file
34
lux-studio-apache.conf
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# ==============================================================================
|
||||
# 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
|
||||
# ==============================================================================
|
||||
43
lux-studio-backend.service
Normal file
43
lux-studio-backend.service
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[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
Normal file
6
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "cinema-studio-pro",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
422
setup.sh
Executable file
422
setup.sh
Executable file
|
|
@ -0,0 +1,422 @@
|
|||
#!/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
Normal file
108
status.sh
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
#!/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 ""
|
||||
|
|
@ -1,591 +0,0 @@
|
|||
<?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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue