Compare commits

..

14 commits

Author SHA1 Message Date
Manish Tanwar
e0cc56fc6e Merge main into feature/deployment: new features + conflict resolution
New features from main:
- Custom presets (create, edit, export/import)
- Identity protection (auto-translate real names to style descriptions)
- Video negative prompts (AI-suggested exclusions)
- Generate video from library image (one-click first frame)
- Expanded preview (maximize on images/videos)
- Move items between projects
- Preset badge in library
- 4K resolution option for Veo 3.1 (8s duration)
- LastFrame interpolation requires 8s (enforced in UI + API)

Conflict resolutions:
- main.jsx: kept feature/deployment AuthProvider+MsalProvider structure
- LoginPage.jsx: kept feature/deployment useAuth() context approach
- useProjects.js: kept feature/deployment AuthContext userId
- CinePromptStudio.jsx: kept both getApiUrl + useCustomPresets imports
- ProjectsTab.jsx: took main's expanded icon set
- VideoGenTab.jsx: took main's duration/resolution logic improvements
- video_api.php: took main's 8s constraint + better error logging

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-02-25 17:49:15 +05:30
Manish Tanwar
473aacd6af 7. Video download fix 2026-02-06 05:02:43 +05:30
Manish Tanwar
da2fdff1c4 7. Video player fix 2026-02-06 04:50:00 +05:30
Manish Tanwar
4cbf517417 6. Thumbnail and Video Streaming fix-5 2026-02-06 02:54:34 +05:30
Manish Tanwar
210e0ac1bf 6. Thumbnail and Video Streaming fix-4 2026-02-06 01:43:36 +05:30
Manish Tanwar
37be13e013 6. Thumbnail and Video Streaming fix-3- hardcoded the path 2026-02-06 01:27:20 +05:30
Manish Tanwar
9d4f128cef 6. Thumbnail and Video Streaming fix-2 2026-02-06 01:03:46 +05:30
Manish Tanwar
ce92aee238 6. Thumbnail and Video Streaming fix 2026-02-06 00:44:50 +05:30
Manish Tanwar
204cda6e75 5. Upload folder locations 2026-02-05 19:50:57 +05:30
Manish Tanwar
2589fba2ec 4. Deployment Dist Folder+API Url 2026-02-05 18:03:34 +05:30
Manish Tanwar
92fd1164b6 3. Deployment Dist Folder+Update Url 2026-02-05 17:15:25 +05:30
Manish Tanwar
119330e093 2. Deployment Dist Folder 2026-02-05 02:06:14 +05:30
Manish Tanwar
247a956df6 2. Deployment Dist Folder 2026-02-05 02:01:56 +05:30
Manish Tanwar
beaa401c3b 1. Minimum deployment codemarkdown for minimum changes 2026-02-05 00:24:03 +05:30
64 changed files with 6856 additions and 3671 deletions

View file

@ -29,7 +29,9 @@
"Bash(dos2unix:*)",
"WebSearch",
"WebFetch(domain:ai.google.dev)",
"Bash(cp:*)"
"Bash(xargs:*)",
"Bash(php -l:*)",
"Bash(git add -A)"
]
}
}

2
.gitignore vendored
View file

@ -5,7 +5,7 @@ backend/vendor/
backend/composer.lock
# Build output
frontend/dist/
# frontend/dist/
frontend/dist-ssr/
*.local

View file

@ -1,10 +1,32 @@
# ==============================================================================
# VIDEO OPTIMIZER - FRONTEND SECURITY CONFIGURATION
# LUX STUDIO - SPA ROUTING AND SECURITY CONFIGURATION
# ==============================================================================
# Location: /var/www/html/lux-studio/.htaccess
# Purpose: Security hardening for frontend static files
# Purpose: SPA routing, API passthrough, and security hardening
# ==============================================================================
# ------------------------------------------------------------------------------
# SPA ROUTING (Required for React Router)
# ------------------------------------------------------------------------------
RewriteEngine On
RewriteBase /lux-studio/
# Handle API requests - pass to PHP files directly
RewriteRule ^api/(.*)$ api/$1 [L]
# Serve generated videos/images directly
RewriteRule ^generated_videos/(.*)$ generated_videos/$1 [L]
RewriteRule ^generated_images/(.*)$ generated_images/$1 [L]
# Serve existing files and directories
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# SPA fallback - all other requests to index.html
RewriteRule ^ index.html [L]
# ------------------------------------------------------------------------------
# DIRECTORY PROTECTION
# ------------------------------------------------------------------------------
@ -22,12 +44,7 @@ ServerSignature Off
# FILE ACCESS CONTROL
# ------------------------------------------------------------------------------
# Default: Allow access to all files (will be restricted below)
<FilesMatch ".*">
Require all granted
</FilesMatch>
# Deny access to sensitive files and patterns
# Deny access to hidden files (dotfiles)
<FilesMatch "^\.">
Require all denied
</FilesMatch>
@ -47,74 +64,11 @@ ServerSignature Off
Require all denied
</FilesMatch>
# Deny access to PHP files (if any exist - security measure)
<FilesMatch "\.php$">
Require all denied
</FilesMatch>
# Deny access to Python files (should not be in frontend)
<FilesMatch "\.py$">
Require all denied
</FilesMatch>
# Deny access to README and documentation that shouldn't be public
<FilesMatch "^(README|INSTALL|CHANGELOG|LICENSE|CONTRIBUTING)">
Require all denied
</FilesMatch>
# ------------------------------------------------------------------------------
# ALLOWED FILE TYPES (Explicitly allow necessary files)
# ------------------------------------------------------------------------------
# Allow HTML files (main application pages)
<FilesMatch "\.(html|htm)$">
Require all granted
</FilesMatch>
# Allow JavaScript files
<FilesMatch "\.(js|mjs)$">
Require all granted
</FilesMatch>
# Allow CSS files
<FilesMatch "\.css$">
Require all granted
</FilesMatch>
# Allow images
<FilesMatch "\.(jpg|jpeg|png|gif|ico|svg|webp)$">
Require all granted
</FilesMatch>
# Allow fonts
<FilesMatch "\.(woff|woff2|ttf|otf|eot)$">
Require all granted
</FilesMatch>
# Allow JSON files (only if needed for app functionality)
<FilesMatch "\.json$">
Require all denied
</FilesMatch>
# ------------------------------------------------------------------------------
# ERROR DOCUMENTS
# ------------------------------------------------------------------------------
# Custom error pages (optional - create these files if needed)
# ErrorDocument 403 /video-optimizer/error/403.html
# ErrorDocument 404 /video-optimizer/error/404.html
# ErrorDocument 500 /video-optimizer/error/500.html
# ------------------------------------------------------------------------------
# ADDITIONAL SECURITY
# ------------------------------------------------------------------------------
# Prevent access to .htaccess itself
<Files ".htaccess">
Require all denied
</Files>
# ==============================================================================
# END OF CONFIGURATION
# ==============================================================================

685
AI_IMPLEMENTATION_GUIDE.md Normal file
View file

@ -0,0 +1,685 @@
# Nano Banana Pro - AI Implementation Guide
## How to Build an Iterative Image Generation & Editing System with Google Gemini
---
## 🎯 CRITICAL CONCEPT: This is NOT a standard image generation API
**⚠️ WATCHOUT #1:** Google Gemini's image generation works COMPLETELY differently from DALL-E, Stable Diffusion, or Midjourney.
### Key Differences:
1. **Uses `generateContent` endpoint** (not a dedicated image API)
2. **Images returned as base64 in JSON** (embedded in response)
3. **Editing = Sending previous image back** (as base64 in request)
4. **Very aggressive content filters** (IMAGE_RECITATION errors)
5. **No direct image URLs** (everything is base64)
---
## 📐 SYSTEM ARCHITECTURE
```
User Input (Prompt/Upload)
JavaScript Frontend (converts file to base64)
PHP Backend API (api.php)
Session Storage (stores base64 + MIME type)
Google Gemini API (processes with previous image if editing)
Extract base64 from response
Store in session
Display in browser (data URI)
```
---
## 🔑 CRITICAL IMPLEMENTATION DETAILS
### 1. THE REQUEST FORMAT (MOST IMPORTANT!)
**⚠️ WATCHOUT #2:** The request structure is VERY specific. Get this wrong and you get 500 errors.
#### For NEW image generation:
```json
{
"contents": [
{
"parts": [
{"text": "Your detailed creative prompt here"}
]
}
],
"generationConfig": {
"responseModalities": ["IMAGE"],
"imageConfig": {
"aspectRatio": "16:9",
"imageSize": "2K"
}
}
}
```
#### For EDITING existing image:
```json
{
"contents": [
{
"parts": [
{
"inline_data": {
"mime_type": "image/jpeg",
"data": "base64_string_here"
}
},
{"text": "Edit instruction prompt"}
]
}
],
"generationConfig": {
"responseModalities": ["IMAGE"],
"imageConfig": {
"aspectRatio": "16:9",
"imageSize": "2K"
}
}
}
```
**⚠️ CRITICAL:**
- Image MUST come BEFORE text in the parts array
- Use `inline_data` (snake_case) not `inlineData`
- MIME type should be `image/jpeg` (what Gemini returns)
- Base64 must be clean (no whitespace, no data URI prefix)
---
### 2. THE RESPONSE FORMAT
**⚠️ WATCHOUT #3:** The response structure has TWO possible formats!
#### Success Response (with image):
```json
{
"candidates": [
{
"content": {
"parts": [
{
"inlineData": {
"mimeType": "image/jpeg",
"data": "base64_image_data"
}
}
]
},
"finishReason": "STOP"
}
]
}
```
**⚠️ NOTICE:** Response uses `inlineData` (camelCase), but request uses `inline_data` (snake_case)!
#### Blocked Response (IMAGE_RECITATION):
```json
{
"candidates": [
{
"content": [],
"finishReason": "IMAGE_RECITATION",
"finishMessage": "Unable to show the generated image..."
}
]
}
```
**⚠️ CRITICAL:** ALWAYS check `finishReason` BEFORE trying to extract image data!
---
### 3. MIME TYPE HANDLING
**⚠️ WATCHOUT #4:** MIME type mismatches break image display!
```php
// WRONG - hardcoded PNG:
$_SESSION['current_image'] = $base64;
echo '<img src="data:image/png;base64,' . $base64 . '">';
// RIGHT - store and use actual MIME type:
$_SESSION['current_image'] = $base64;
$_SESSION['current_image_mime'] = $mimeType; // e.g., "image/jpeg"
echo '<img src="data:' . $mimeType . ';base64,' . $base64 . '">';
```
**Why this matters:**
- Gemini returns `image/jpeg`
- If you display as `image/png`, browser may fail to render
- Store BOTH base64 data AND MIME type
---
### 4. BASE64 DATA HANDLING
**⚠️ WATCHOUT #5:** Base64 data must be CLEAN!
```javascript
// WRONG - includes data URI prefix:
const base64 = reader.result; // "data:image/jpeg;base64,/9j/4AAQ..."
// RIGHT - strip the prefix:
const base64 = reader.result.split(',')[1]; // "/9j/4AAQ..."
```
**Validation:**
```php
// Clean whitespace
$inputImage = preg_replace('/\s+/', '', $inputImage);
// Validate format
if (!preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $inputImage)) {
throw new Exception("Invalid base64 format");
}
```
---
### 5. THE EDITING FLOW
**⚠️ WATCHOUT #6:** Session management is CRITICAL for editing to work!
```php
// Step 1: Generate first image
$response = $api->generateImage("cyberpunk city", "16:9", "2K", null);
$imageData = extractImageData($response);
$_SESSION['current_image'] = $imageData['base64'];
$_SESSION['current_image_mime'] = $imageData['mime_type'];
// Step 2: Edit existing image
$previousImage = $_SESSION['current_image']; // Get from session
$response = $api->generateImage("add rain", "16:9", "2K", $previousImage);
$imageData = extractImageData($response);
$_SESSION['current_image'] = $imageData['base64']; // Update session
```
**Flow:**
1. Store generated image in session
2. On edit request, retrieve from session
3. Send as `inline_data` in request
4. Store new result back to session
5. Repeat for each edit
---
### 6. ERROR HANDLING - THE TRICKY PART
**⚠️ WATCHOUT #7:** Multiple error types, each needs specific handling!
```php
// Check finishReason FIRST
if (isset($response['candidates'][0]['finishReason'])) {
$reason = $response['candidates'][0]['finishReason'];
if ($reason === 'IMAGE_RECITATION') {
throw new Exception('Blocked by content filter. Use more creative prompts.');
}
if ($reason === 'SAFETY') {
throw new Exception('Blocked by safety filters.');
}
// Only proceed if STOP
if ($reason !== 'STOP') {
throw new Exception('Generation failed: ' . $reason);
}
}
// Then extract image
foreach ($response['candidates'][0]['content']['parts'] as $part) {
if (isset($part['inlineData']['data'])) {
return $part['inlineData'];
}
}
```
**Common Errors:**
| Error | HTTP Code | Cause | Solution |
|-------|-----------|-------|----------|
| IMAGE_RECITATION | 200 | Prompt too generic | Use creative, detailed prompts |
| Internal error | 500 | API temporary issue | Retry with exponential backoff |
| RESOURCE_EXHAUSTED | 429 | Rate limit | Wait 30s between requests |
| INVALID_ARGUMENT | 400 | Bad request format | Check base64 encoding |
---
### 7. PROMPT ENGINEERING
**⚠️ WATCHOUT #8:** Simple prompts WILL fail!
```javascript
// ❌ WILL FAIL (IMAGE_RECITATION):
"a red circle"
"a blue square"
"a tree"
"a car"
// ✅ WILL WORK:
"a vintage red sports car racing through a neon-lit cyberpunk city at night"
"a magical forest with glowing blue mushrooms and fireflies at twilight"
"a futuristic cityscape with flying vehicles and holographic billboards"
```
**Rules:**
- Minimum 10 words
- Include adjectives (vintage, glowing, futuristic)
- Add context (at night, in rain, during sunset)
- Avoid single objects
- Be creative and specific
---
### 8. FILE UPLOAD HANDLING
**⚠️ WATCHOUT #9:** File conversion must be done client-side!
```javascript
// Convert file to base64 (client-side)
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
// CRITICAL: Remove data URI prefix!
const base64 = reader.result.split(',')[1];
resolve(base64);
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// Usage
const file = uploadInput.files[0];
const base64 = await fileToBase64(file);
formData.append('uploadedImage', base64);
formData.append('uploadedImageType', file.type);
```
**Backend handling:**
```php
if ($uploadedImage) {
// Store uploaded image
$_SESSION['current_image'] = $uploadedImage;
$_SESSION['current_image_mime'] = $uploadedImageType;
// If prompt provided, apply it
if ($prompt) {
$response = $api->generateImage($prompt, $aspectRatio, $imageSize, $uploadedImage);
// Update with edited version
}
}
```
---
### 9. SESSION MANAGEMENT
**⚠️ WATCHOUT #10:** Session structure is critical!
```php
// Initialize (MUST be done before any output)
session_start();
// Required session variables
$_SESSION['current_image'] = null; // Base64 string
$_SESSION['current_image_mime'] = 'image/png'; // MIME type
$_SESSION['conversation_history'] = []; // Array of prompts
$_SESSION['image_history'] = []; // Array of previous images
// Reset (clear everything)
$_SESSION['conversation_history'] = [];
$_SESSION['current_image'] = null;
$_SESSION['current_image_mime'] = 'image/png';
$_SESSION['image_history'] = [];
```
---
### 10. API CONFIGURATION
**⚠️ WATCHOUT #11:** Endpoint and model name are specific!
```php
// CORRECT endpoint:
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent";
// Header format:
'x-goog-api-key: YOUR_API_KEY' // NOT 'Authorization: Bearer'
// Timeout:
CURLOPT_TIMEOUT => 120 // 2 minutes - image generation is SLOW
```
**Model name:** `gemini-3-pro-image-preview`
- May change in future
- Check Google's docs if errors persist
---
## 🐛 DEBUGGING CHECKLIST
When things don't work, check IN THIS ORDER:
### 1. Is the request format correct?
```php
error_log("Request payload: " . json_encode($payload));
```
### 2. Is the response structure what you expect?
```php
error_log("Response structure: " . json_encode($response));
```
### 3. Check finishReason:
```php
$reason = $response['candidates'][0]['finishReason'] ?? 'UNKNOWN';
error_log("Finish reason: " . $reason);
```
### 4. Verify base64 data:
```php
error_log("Base64 length: " . strlen($base64));
error_log("First 50 chars: " . substr($base64, 0, 50));
```
### 5. Check MIME type matching:
```php
error_log("Stored MIME: " . $_SESSION['current_image_mime']);
error_log("Response MIME: " . $response['candidates'][0]['content']['parts'][0]['inlineData']['mimeType']);
```
---
## 🎓 COMMON MISTAKES TO AVOID
### Mistake #1: Wrong request structure
```json
// ❌ WRONG - text before image:
{"parts": [{"text": "..."}, {"inline_data": {...}}]}
// ✅ RIGHT - image before text:
{"parts": [{"inline_data": {...}}, {"text": "..."}]}
```
### Mistake #2: Not checking finishReason
```php
// ❌ WRONG - directly accessing parts:
$image = $response['candidates'][0]['content']['parts'][0]['inlineData']['data'];
// ✅ RIGHT - check finishReason first:
if ($response['candidates'][0]['finishReason'] === 'IMAGE_RECITATION') {
// Handle blocked content
}
```
### Mistake #3: Hardcoded MIME types
```php
// ❌ WRONG:
echo '<img src="data:image/png;base64,...">'; // Assumes PNG
// ✅ RIGHT:
echo '<img src="data:' . $mimeType . ';base64,...">'; // Uses actual type
```
### Mistake #4: Not cleaning base64
```javascript
// ❌ WRONG:
const base64 = reader.result; // Includes "data:image/png;base64,"
// ✅ RIGHT:
const base64 = reader.result.split(',')[1]; // Only base64 part
```
### Mistake #5: Missing error handling
```php
// ❌ WRONG:
$response = curl_exec($ch);
return json_decode($response);
// ✅ RIGHT:
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpCode !== 200) {
// Handle errors
}
```
---
## 📊 DATA FLOW DIAGRAM
```
┌─────────────────┐
│ User Action │
│ (Prompt/Upload)│
└────────┬────────┘
┌─────────────────┐
│ JavaScript │
│ - Validate │
│ - Convert file │
│ - Build FormData│
└────────┬────────┘
┌─────────────────┐
│ api.php │
│ - Get session │
│ - Build request │
│ - Call Gemini │
└────────┬────────┘
┌─────────────────┐
│ Gemini API │
│ - Process │
│ - Check filters │
│ - Generate │
└────────┬────────┘
┌─────────────────┐
│ Extract Response│
│ - Check finish │
│ - Get base64 │
│ - Get MIME type │
└────────┬────────┘
┌─────────────────┐
│ Store Session │
│ - current_image │
│ - image_mime │
│ - history │
└────────┬────────┘
┌─────────────────┐
│ Return to JS │
│ - Success flag │
│ - Reload page │
└────────┬────────┘
┌─────────────────┐
│ Display Image │
│ - Data URI │
│ - Correct MIME │
└─────────────────┘
```
---
## 🔧 TESTING STRATEGY
### Test 1: Basic Generation
```
Prompt: "A futuristic motorcycle in a neon-lit city"
Expected: Image generated successfully
```
### Test 2: Simple Edit
```
1. Generate: "A red sports car"
2. Edit: "add rain and reflections"
Expected: Car now has rain
```
### Test 3: Upload
```
1. Upload: photo.jpg
2. No prompt
Expected: Photo stored, ready for editing
```
### Test 4: Upload + Edit
```
1. Upload: landscape.jpg
2. Prompt: "make it look like a watercolor painting"
Expected: Transformed image
```
### Test 5: Error Handling
```
Prompt: "a blue square"
Expected: IMAGE_RECITATION error with helpful message
```
---
## 🚨 CRITICAL SUCCESS FACTORS
**You MUST get these right or the system will NOT work:**
1. ✅ **Request format** - Image before text, correct structure
2. ✅ **Response parsing** - Check finishReason first
3. ✅ **MIME type handling** - Store and use dynamically
4. ✅ **Base64 cleaning** - No whitespace, no prefixes
5. ✅ **Session management** - Store both data and MIME type
6. ✅ **Error handling** - Different errors need different responses
7. ✅ **Prompt quality** - Detailed, creative prompts only
8. ✅ **File upload** - Client-side base64 conversion
9. ✅ **API timeout** - 120 seconds minimum
10. ✅ **Retry logic** - For temporary API failures
---
## 📝 QUICK REFERENCE
### Essential Code Patterns
**Check finishReason:**
```php
$reason = $response['candidates'][0]['finishReason'] ?? null;
if ($reason !== 'STOP') {
// Handle error
}
```
**Extract image:**
```php
foreach ($response['candidates'][0]['content']['parts'] as $part) {
if (isset($part['inlineData']['data'])) {
return [
'base64' => $part['inlineData']['data'],
'mime_type' => $part['inlineData']['mimeType']
];
}
}
```
**Store in session:**
```php
$_SESSION['current_image'] = $imageData['base64'];
$_SESSION['current_image_mime'] = $imageData['mime_type'];
```
**Display image:**
```php
<img src="data:<?php echo $_SESSION['current_image_mime']; ?>;base64,<?php echo $_SESSION['current_image']; ?>">
```
---
## 🎯 IMPLEMENTATION CHECKLIST
Before considering the implementation complete:
- [ ] Image generation works with detailed prompts
- [ ] Image editing works (sends previous image)
- [ ] IMAGE_RECITATION errors handled gracefully
- [ ] MIME type stored and used correctly
- [ ] File upload converts to base64 properly
- [ ] Session persists across requests
- [ ] Error messages are helpful
- [ ] Debug panel shows request/response
- [ ] Simple prompts show helpful error
- [ ] Retry logic works for 500 errors
- [ ] Rate limiting handled
- [ ] Base64 data validated
- [ ] Conversation history tracked
- [ ] Reset clears session properly
---
## 💡 TIPS FOR AI ASSISTANTS
When helping users implement this:
1. **Show the request JSON first** - Most problems are here
2. **Emphasize finishReason checking** - Critical for error handling
3. **Explain MIME type importance** - Common source of display issues
4. **Warn about simple prompts** - Will trigger IMAGE_RECITATION
5. **Test with detailed prompts** - "red circle" will fail
6. **Check session management** - Editing requires proper storage
7. **Validate base64 format** - Clean data is essential
8. **Add debug logging** - Makes troubleshooting easier
9. **Handle all error types** - Different errors need different solutions
10. **Test the full flow** - Generate → Edit → Edit
---
## 📚 REFERENCES
- **API Endpoint:** `https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent`
- **Model:** `gemini-3-pro-image-preview`
- **Auth Header:** `x-goog-api-key: YOUR_KEY`
- **Response Format:** JSON with base64 in `inlineData`
- **Request Format:** JSON with `inline_data` for editing
---
## ⚡ FINAL NOTES
This implementation is **working and stable** when these rules are followed:
1. Use creative, detailed prompts (10+ words)
2. Check `finishReason` before extracting image
3. Store and use correct MIME types
4. Clean base64 data (no whitespace/prefixes)
5. Manage session properly for editing
6. Handle all error types specifically
7. Implement retry logic for temporary failures
8. Validate uploaded files before processing
**The system works reliably when these patterns are followed exactly.**
---
*Generated from working implementation - December 2024*

305
AUTH_README.md Normal file
View file

@ -0,0 +1,305 @@
# MSAL Authentication Setup Guide
## Overview
Nano Banana Pro now includes Microsoft Authentication Library (MSAL) / Azure AD Single Sign-On (SSO) authentication. The authentication can be **toggled on/off** via environment variable for seamless testing and deployment.
---
## Quick Start
### Local Development (No Authentication)
```bash
# 1. Ensure .env file exists with:
SSO_ENABLED=false
# 2. Run the app normally in MAMP
# All users get mock "Local Developer" credentials
# No login required
```
### Production (with SSO)
```bash
# 1. Update .env file:
SSO_ENABLED=true
SSO_TENANT_ID=your-azure-tenant-id
SSO_CLIENT_ID=your-azure-application-id
# 2. Deploy to server
# 3. Users must login with Microsoft account
```
---
## Installation Steps
### 1. Install Dependencies
```bash
cd /Users/daveporter/Desktop/CODING-2024/NANO-RESEARCH
composer install
```
This installs the Firebase JWT library required for token validation.
### 2. Configure Environment
```bash
# Copy example file
cp .env.example .env
# Edit .env and set:
SSO_ENABLED=false # Start with authentication disabled
```
### 3. Azure AD Setup (When Enabling SSO)
#### Create Azure AD App Registration:
1. Go to [Azure Portal](https://portal.azure.com)
2. Navigate to: **Azure Active Directory****App registrations** → **New registration**
3. Set name: "Nano Banana Pro"
4. Set redirect URI: `https://your-server-url.com/path/to/app/index.php`
5. Click **Register**
#### Get Credentials:
1. Copy **Application (client) ID** → This is your `SSO_CLIENT_ID`
2. Copy **Directory (tenant) ID** → This is your `SSO_TENANT_ID`
3. Go to **Authentication** → Enable **ID tokens** checkbox
4. Go to **API permissions** → Add: `openid`, `profile`, `email`
#### Update .env:
```bash
SSO_ENABLED=true
SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
```
---
## File Structure
### New Files Created:
```
/NANO-RESEARCH/
├── composer.json # PHP dependencies (Firebase JWT)
├── .env # Environment config (gitignored)
├── .env.example # Template for environment variables
├── env_loader.php # Loads .env file
├── JWTValidator.php # JWT token validation logic
├── AuthMiddleware.php # Auth orchestrator + login UI
├── auth.php # Auth API endpoint
├── auth-test.php # Debugging page
├── AUTH_README.md # This file
└── vendor/ # Composer dependencies (gitignored)
```
### Modified Files:
```
config.php # Added SSO constants
index.php # Added auth check, logout button
api.php # Added auth check
enhance_prompt.php # Added auth check
.gitignore # Added .env and vendor/
```
---
## How It Works
### When SSO_ENABLED=false (Testing Mode)
1. User visits app
2. AuthMiddleware returns mock "Local Developer" user
3. No login page shown
4. All features work normally
5. Perfect for local testing
### When SSO_ENABLED=true (Production Mode)
1. User visits app
2. AuthMiddleware checks for `auth_token` cookie
3. If no token → Show MSAL login page
4. User clicks "Sign In with Microsoft"
5. MSAL popup opens for Azure AD login
6. User authenticates
7. Token sent to `auth.php` for validation
8. JWT validated against Azure AD public keys
9. Token stored in httpOnly cookie (24 hours)
10. User redirected to app
11. Logout button visible in header
---
## Testing
### Test Authentication Status
Visit: `http://your-server/auth-test.php`
Shows:
- SSO configuration (enabled/disabled)
- Tenant ID and Client ID
- Current authentication status
- User information
- Cookie presence
### Test Locally (SSO Disabled)
```bash
# 1. Set SSO_ENABLED=false in .env
# 2. Open app in MAMP
# 3. Should see "Welcome, Local Developer" (if SSO was previously enabled)
# 4. App functions normally
# 5. No login/logout buttons
```
### Test on Server (SSO Enabled)
```bash
# NOTE: Cannot test locally - Azure AD requires exact redirect URI match
# 1. Deploy to production server
# 2. Set SSO_ENABLED=true in .env on server
# 3. Add Azure AD credentials to .env
# 4. Visit app URL
# 5. Should see login page
# 6. Click "Sign In with Microsoft"
# 7. Complete Microsoft login
# 8. Should redirect to app
# 9. Should see "Welcome, [Your Name]" and logout button
```
---
## Security Features
**httpOnly Cookies** - Prevents XSS attacks (JavaScript can't access token)
**SameSite=Lax** - Prevents CSRF attacks
**Secure Flag** - Cookie only sent over HTTPS in production
**JWT Validation** - Cryptographic verification of tokens
**Expiration Check** - Validates `exp` claim
**Not-Before Check** - Validates `nbf` claim
**Audience Validation** - Ensures token is for our app
**Issuer Validation** - Ensures token from Azure AD
**JWKS Verification** - Uses Azure AD public keys
**24-Hour Expiration** - Tokens expire after 1 day
---
## Troubleshooting
### Login Page Shows But Can't Login
- Check Azure AD app registration has correct redirect URI
- Ensure `SSO_TENANT_ID` and `SSO_CLIENT_ID` are correct
- Check browser console for MSAL errors
- Visit `auth-test.php` to verify configuration
### "Authentication Required" Error
- Check `auth_token` cookie exists (browser dev tools)
- Token may have expired (24-hour limit)
- Try logging out and back in
- Check `auth-test.php` for token status
### SSO Not Disabling
- Verify `.env` has `SSO_ENABLED=false` (not "false" in quotes)
- Clear browser cookies
- Restart PHP server/MAMP
- Check `auth-test.php` shows "SSO Enabled: NO"
### Token Validation Failing
- Check server can reach Azure AD endpoints
- Verify tenant ID and client ID match Azure AD
- Check token hasn't expired
- Review `error_log` for JWT validation details
---
## API Endpoints
### Login
```http
POST /auth.php
Content-Type: application/json
{
"action": "login",
"idToken": "eyJ0eXAiOiJKV1QiLCJhbGci...",
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGci..."
}
```
### Logout
```http
POST /auth.php
Content-Type: application/json
{
"action": "logout"
}
```
### Status Check
```http
POST /auth.php
Content-Type: application/json
{
"action": "status"
}
```
---
## Maintenance
### Rotating Credentials
1. Update Azure AD app registration
2. Update `.env` with new credentials
3. No code changes needed
4. Existing sessions remain valid until cookie expires
### Disabling SSO Temporarily
```bash
# In .env:
SSO_ENABLED=false
# Immediately disables SSO for all users
# No restart needed
# Users get mock "Local Developer" access
```
### Monitoring
- Check `error_log` for authentication failures
- Monitor Azure AD sign-in logs
- Track failed login attempts
- Review token validation errors
---
## Production Checklist
Before enabling SSO in production:
- [ ] Composer dependencies installed (`vendor/` directory exists)
- [ ] `.env` file configured with Azure AD credentials
- [ ] Azure AD app registration created
- [ ] Redirect URI matches production URL exactly
- [ ] ID tokens enabled in Azure AD app
- [ ] API permissions added (`openid`, `profile`, `email`)
- [ ] HTTPS enabled on production server
- [ ] `auth-test.php` shows correct configuration
- [ ] Test login/logout flow works
- [ ] Error logging enabled
---
## Support
For issues with:
- **MSAL errors**: Check [MSAL.js documentation](https://github.com/AzureAD/microsoft-authentication-library-for-js)
- **Azure AD setup**: Check [Azure AD app registration guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)
- **JWT validation**: Check Firebase JWT library logs in `error_log`
- **Configuration**: Run `auth-test.php` to see current setup
---
## Important Notes
- **Cannot test MSAL locally** - Azure AD requires exact URL match
- **Testing happens on server** after deployment
- **SSO toggle allows testing without auth** before enabling
- **httpOnly cookies** mean token not accessible via JavaScript
- **24-hour token expiration** - users must re-login daily
- **Mock user** (`dev@localhost`) used when SSO disabled

558
CLAUDE.md
View file

@ -4,39 +4,36 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
## Project Overview
**Lux Studio** is an AI-powered cinematography suite for professional image and video generation, combining physics-based prompt engineering with Google's Imagen 3 and Veo 3.1 APIs. The application uses a project-first workflow with local IndexedDB storage.
**Lux Studio** is an AI-powered cinematography suite for professional image and video generation. It combines physics-based prompt engineering with Google's Imagen 3 and Veo 3.1 APIs to enable iterative creative workflows with professional cinematographer tools.
**Key Architecture**: React frontend (Vite) + PHP backend + IndexedDB storage
## Directory Structure
```
cinema-studio-pro/
├── frontend/ # React frontend application
lux-studio-app/
├── frontend/ # React + Vite frontend application
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── hooks/ # Custom hooks (IndexedDB, projects)
│ │ ├── authConfig.js # MSAL authentication config
│ │ └── App.jsx # Root component
│ ├── .env # Frontend environment variables (gitignored)
│ ├── .env.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
│ │ ├── components/ # React components
│ │ ├── hooks/ # Custom React hooks (IndexedDB, projects)
│ │ └── assets/ # Static assets
│ ├── .env # Frontend configuration (ports, API keys)
│ ├── .env.example # Frontend config template
│ ├── vite.config.js # Vite configuration with dynamic proxy
│ └── package.json # Frontend dependencies
├── backend/ # PHP backend API
│ ├── api.php # Image generation endpoint (Imagen 3)
│ ├── video_api.php # Video generation endpoint (Veo 3.1)
│ ├── stream_video.php # Video streaming with Range support
│ ├── enhance_prompt.php # AI prompt optimization
│ ├── session_manager.php # Session management
│ ├── config.example.php # Backend config template
│ └── .env # Backend configuration (ports)
├── api.php.backup # Backup of original api.php
├── video_api.php.backup # Backup of original video_api.php
└── CLAUDE.md # This file
```
## Development Commands
@ -51,68 +48,113 @@ cinema-studio-pro/
```
### Frontend (React + Vite)
All frontend commands must be run from the [frontend/](frontend/) directory:
```bash
cd frontend
npm install # Install dependencies
npm run dev # Start dev server (port from .env, default: 3000)
npm run build # Build for production
npm run preview # Preview production build
npm run lint # Run ESLint
npm install # Install dependencies
npm run dev # Start dev server (port 3000, configurable via VITE_FRONTEND_PORT)
npm run build # Production build → dist/
npm run preview # Preview production build
npm run lint # ESLint code quality checks
```
### Backend (PHP)
Run from the [backend/](backend/) directory:
```bash
cd backend
composer install # Install PHP dependencies (Firebase JWT)
# Port is configured in .env (BACKEND_PORT, default: 5015)
php -S localhost:5015 # Start PHP development server
php -S localhost:5015 # Start PHP backend server (port configurable via BACKEND_PORT in .env)
```
### Full Development Setup
**Both servers must run simultaneously** - Vite proxies `/api/*` calls to the PHP backend (port 5015).
## Configuration
### API Key Setup
**Backend**: Copy example config and add your API key:
```bash
# Terminal 1: Backend (reads port from backend/.env)
cd backend && php -S localhost:5015
# Terminal 2: Frontend (reads port from frontend/.env)
cd frontend && npm run dev
# Visit: http://localhost:3000 (or your configured FRONTEND_PORT)
cd backend
cp config.example.php config.php
```
**Note:** Ports are configured via `.env` files. See Configuration section below.
Edit [backend/config.php](backend/config.php):
```php
define('GEMINI_API_KEY', 'your-api-key-here');
```
**Frontend**: Create `frontend/.env`:
```env
VITE_GEMINI_API_KEY=your_gemini_api_key
VITE_FRONTEND_PORT=3000
VITE_BACKEND_PORT=5015
```
Get your API key from [Google AI Studio](https://aistudio.google.com/app/apikey).
### Environment Files
**Frontend Configuration** - [frontend/.env.example](frontend/.env.example):
```env
VITE_GEMINI_API_KEY=your_key_here
VITE_FRONTEND_PORT=3000 # Frontend dev server port
VITE_BACKEND_PORT=5015 # Backend API port
```
**Backend Configuration** - [backend/.env](backend/.env):
```env
BACKEND_PORT=5015 # PHP server port
FRONTEND_PORT=3000 # For reference
```
- Neither `.env` files nor `config.php` should be committed (both gitignored)
- Ports are configurable via environment variables
- Frontend uses `VITE_BACKEND_PORT` to proxy API requests
### Authentication Status
**Current**: No authentication - uses placeholder `'local'` userId
**Future**: SSO-ready with `userId` field in projects schema
To add SSO, modify `getCurrentUserId()` in [useProjects.js](frontend/src/hooks/useProjects.js):
```javascript
const getCurrentUserId = () => {
// Replace with your auth provider
return authContext.user?.id || 'local';
};
```
## Architecture Overview
### Two-Part System
### Data Flow: Project-First Workflow
**Frontend (frontend/):**
- React 19 + Vite + Tailwind CSS
- MSAL authentication with Azure AD (toggleable)
- IndexedDB for local project/media storage via `useProjects.js` and `useIndexedDB.js` hooks
- Three main tabs: Projects, Image Gen, Video Gen
- No server-side session storage - all state in browser
- Runs on port 3000 (configurable via FRONTEND_PORT in .env)
1. **User selects/creates project** in [ProjectsTab.jsx](frontend/src/components/ProjectsTab.jsx)
2. **Navigates to Image/Video tab** via [App.jsx](frontend/src/App.jsx) state management
3. **Generates content** through tab components
4. **Auto-saves to IndexedDB** via [useProjects.js](frontend/src/hooks/useProjects.js)
5. **Cross-tab usage**: Extract video frames → use as image references
**Backend (backend/):**
- PHP 7.4+ stateless APIs
- Session-based user isolation (multi-user support via SessionManager)
- Direct API calls to Google's Gemini/Imagen/Veo services
- File-based storage in `backend/uploads/sessions/{session_id}/`
- Runs on port 5015 (configurable via BACKEND_PORT in .env)
### Frontend Architecture
### Data Flow Architecture
**State Management Hub**: [App.jsx](frontend/src/App.jsx)
- Manages `activeProjectId`, `activeTab`, `videoRerunData`, `imageEditData`
- Coordinates navigation between Projects/Image/Video tabs
- Passes data between tabs for edit workflows
**Project-First Workflow:**
1. User creates/selects project in ProjectsTab (stored in IndexedDB)
2. Image Gen and Video Gen tabs are DISABLED until project selected
3. All generations save to active project automatically
4. Projects contain: images, videos, metadata, storyboards
**Core Components**:
- [CinePromptStudio.jsx](frontend/src/components/CinePromptStudio.jsx) - Image generation with 27+ cinematography presets, 6 cameras, 7 lenses
- [VideoGenTab.jsx](frontend/src/components/VideoGenTab.jsx) - Video generation with Veo 3.1 (text-to-video and image-to-video)
- [ProjectsTab.jsx](frontend/src/components/ProjectsTab.jsx) - Project management and library browsing
- [StoryboardEditor.jsx](frontend/src/components/StoryboardEditor.jsx) - Multi-frame storyboard creation with PDF/PNG export
- [VideoPlayer.jsx](frontend/src/components/VideoPlayer.jsx) - Playback with frame extraction
- [TabNavigation.jsx](frontend/src/components/TabNavigation.jsx) - Tab switching UI
**Image Generation (Imagen 3):**
```
CinePromptStudio → /api/api.php (proxied to backend:5015) → Gemini API (Imagen 3) →
Base64 response → Server stores in backend/uploads/sessions/{id}/ → Frontend saves to IndexedDB project
```
**Custom Hooks**:
- [useProjects.js](frontend/src/hooks/useProjects.js) - Project and item CRUD operations with IndexedDB
- [useIndexedDB.js](frontend/src/hooks/useIndexedDB.js) - Low-level IndexedDB wrapper
**Video Generation (Veo 3.1):**
```
@ -136,7 +178,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 the Critical Patterns section below for the full request format rules.
See `MDFiles/AI_IMPLEMENTATION_GUIDE.md` for complete details on this critical pattern.
### Session Management (Multi-User)
@ -299,7 +341,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/api
VITE_API_URL=https://ai-sandbox.oliver.solutions/lux-studio-back
VITE_GEMINI_API_KEY=AIzaSyC...
VITE_SSO_ENABLED=true
VITE_SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
@ -347,7 +389,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 image editing parts order: image data MUST precede text (see Critical Patterns section)
4. Verify `AI_IMPLEMENTATION_GUIDE.md` request format rules
5. Check `finishReason` in API response (IMAGE_RECITATION = blocked)
6. Ensure prompts are creative/detailed (10+ words)
@ -365,11 +407,78 @@ Edit `VideoGenTab.jsx` - update `durationOptions` array, ensure constraints matc
### 1. API URL Configuration
**IMPORTANT:** All API calls MUST use the `getApiUrl()` helper function to avoid CORS issues:
```javascript
// At the top of component (after state hooks)
const getApiUrl = (endpoint) => {
// In development, use Vite proxy to avoid CORS
if (import.meta.env.DEV) {
return `/api/${endpoint}`;
Database: CinemaStudioPro
├── projects: {id, userId, name, createdAt, updatedAt}
├── items: {id, projectId, type, prompt, settings, data, mimeType, createdAt}
└── storyboards: {id, projectId, name, frames, createdAt, updatedAt}
```
**Storage Notes**:
- All data stored client-side (device-specific, doesn't sync)
- Images stored as base64 in IndexedDB
- Videos stored as URLs pointing to PHP backend
- Browser storage limits apply (typically 50-100MB+)
### Backend Architecture
**Image Generation Flow** ([backend/api.php](backend/api.php)):
1. Receives prompt + aspect ratio + image size + reference images
2. Calls Gemini 3 Pro Image Preview API
3. Returns base64 image embedded in JSON response
**Video Generation Flow** ([backend/video_api.php](backend/video_api.php)):
1. Receives prompt + model selection + reference images
2. Calls Veo 3.1 API (long-running operation)
3. Polls for completion via webhook
4. Returns video URL for frontend playback
**Critical Files**:
- [backend/api.php](backend/api.php) - Imagen 3 image generation endpoint
- [backend/video_api.php](backend/video_api.php) - Veo 3.1 video generation endpoint
- [backend/stream_video.php](backend/stream_video.php) - Video streaming with HTTP Range support
- [backend/session_manager.php](backend/session_manager.php) - Session and file storage management
- [backend/enhance_prompt.php](backend/enhance_prompt.php) - Gemini-powered prompt optimization
- [backend/webhook_logger.php](backend/webhook_logger.php) - Logs video generation webhook callbacks
**Backup Files** (for reference, do not modify):
- [api.php.backup](api.php.backup) - Original api.php
- [video_api.php.backup](video_api.php.backup) - Original video_api.php
**Other PHP Files**:
- [backend/index.php](backend/index.php) - Legacy standalone interface (not used in React app)
- [backend/auth.php](backend/auth.php), [backend/AuthMiddleware.php](backend/AuthMiddleware.php), [backend/JWTValidator.php](backend/JWTValidator.php) - Future auth infrastructure
- [backend/debug.php](backend/debug.php), [backend/debug_request.php](backend/debug_request.php) - Debugging utilities
- [backend/get_current_image.php](backend/get_current_image.php) - Image retrieval endpoint
- [backend/cleanup.php](backend/cleanup.php) - Session cleanup script
### Proxy Configuration
[vite.config.js](frontend/vite.config.js) proxies backend requests using configurable port (default: 5015):
```javascript
// Reads VITE_BACKEND_PORT from .env (default: 5015)
'/api/*' → localhost:5015
'/generated_videos/*' → localhost:5015
'/generated_images/*' → localhost:5015
```
The proxy configuration dynamically reads the backend port from environment variables, ensuring consistency across development environments.
## AI API Integration: Critical Details
### Gemini Image Generation (Imagen 3)
**Request Format** - Image editing requires specific structure:
```json
{
"contents": [{
"parts": [
{"inline_data": {"mime_type": "image/jpeg", "data": "base64"}},
{"text": "edit instruction"}
]
}],
"generationConfig": {
"responseModalities": ["IMAGE"],
"imageConfig": {"aspectRatio": "16:9", "imageSize": "2K"}
}
// In production, use full API URL
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015';
@ -410,18 +519,21 @@ if ((tabId === 'image' || tabId === 'video') && !activeProjectId) {
}
```
### 5. Video Generation Settings
All durations (4s, 6s, 8s) work for both T2V and I2V modes. Resolution (720p/1080p) is a UI-only setting - only aspectRatio and durationSeconds are sent to the Veo 3.1 API.
**Critical**: Image MUST come before text in parts array.
### 6. MIME Type Handling
Always store and use dynamic MIME type from API response:
**Response Handling**:
```php
$_SESSION['current_image'] = $base64;
$_SESSION['current_image_mime'] = $mimeType; // Don't hardcode 'image/png'
// ALWAYS check finishReason first
$reason = $response['candidates'][0]['finishReason'];
if ($reason === 'IMAGE_RECITATION') {
// Content filter triggered - prompt too generic
}
if ($reason === 'STOP') {
// Extract base64 from inlineData
}
```
### 7. IndexedDB Project Operations
Always use the `useProjects` hook for database operations - don't use `useIndexedDB` directly from components.
**MIME Type Handling**: Gemini returns `image/jpeg`, not `image/png`. Store and use dynamic MIME types.
### 8. ESLint Unused Variable Rule
`eslint.config.js` uses `varsIgnorePattern: '^[A-Z_]'` - uppercase-named vars (constants, React components) won't trigger `no-unused-vars`. This is intentional.
@ -431,101 +543,195 @@ The `lastFrame` reference image (second image in I2V mode) is only applicable wh
## Error Handling Patterns
### Imagen 3 Errors
- `IMAGE_RECITATION` - Prompt too generic/simple, needs creative detail
- `SAFETY` - Content filter triggered, try different prompt
- `finishReason !== 'STOP'` - Generation failed for reason specified
See [AI_IMPLEMENTATION_GUIDE.md](AI_IMPLEMENTATION_GUIDE.md) and [QUICK_REFERENCE.md](QUICK_REFERENCE.md) for comprehensive API details.
### Veo 3.1 Errors
- Operation stuck in `PENDING` - Check webhook logs, may need retry
- Operation `FAILED` - Check error message in operation response
- Timeout after 5 minutes polling - Display timeout error to user
### Video Generation (Veo 3.1)
### Session/Storage Errors
- Session files not saving - Check uploads/sessions/ permissions (755)
- IndexedDB quota exceeded - Prompt user to delete old projects
- CORS errors - Ensure PHP backend allows origin
**Models**:
- Standard: Higher quality, longer processing
- Fast: 50% cost savings, faster generation
## Testing Checklist
**Features**:
- Text-to-video and image-to-video (I2V)
- Native audio generation with dialogue support
- Polling-based completion (long-running operations)
- HTTP Range streaming for progressive playback
Before committing changes:
- [ ] Test with no project selected (Image/Video Gen should be disabled)
- [ ] Test image generation with creative prompt (10+ words)
- [ ] Test image editing (uses last generated image)
- [ ] Test video generation T2V and I2V modes
- [ ] Test video rerun from project library
- [ ] Test frame extraction from video
- [ ] Test storyboard drag-to-reorder
- [ ] Test project deletion (cleans up IndexedDB)
- [ ] Test with SSO_ENABLED=false (mock auth)
- [ ] Check PHP error_log for any errors
- [ ] Verify generated files land in correct session directory
**Prompt Optimization**: AI automatically infers camera movement in `[brackets]` and audio cues in `(Sound: [...])`.
## Authentication
The application uses MSAL (Microsoft Authentication Library) for Azure AD SSO:
- **Enabled by default** in development with provided credentials
- **Toggleable** via `VITE_SSO_ENABLED` in frontend/.env
- **LoginPage component** handles authentication UI
- **App.jsx** checks authentication status and shows login or main app
- **Logout button** in header when authenticated (top-right corner)
### SSO Flow:
1. User visits `localhost:3000`
2. If not authenticated, `LoginPage` is shown
3. User clicks "Sign in with Microsoft"
4. MSAL popup opens for Azure AD login
5. After successful login, main app renders
6. User info and logout button appear in header
## Documentation References
- `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
Example:
```
Input: "Woman crosses street"
Output: "Woman crosses a street. [Camera: tracking, follows woman] (Sound: [footsteps, distant traffic])"
```
**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
## Physics-Based Prompt Engineering
**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`)
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.

361
DEPLOYMENT.md Normal file
View file

@ -0,0 +1,361 @@
# 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 Normal file
View file

@ -0,0 +1,617 @@
# Nano Banana Pro - Server Installation Guide
Complete instructions for deploying Nano Banana Pro from git clone to production.
---
## Prerequisites
- **PHP 7.4+** (PHP 8.0+ recommended)
- **Composer** (PHP dependency manager)
- **Git**
- **Web server** (Apache/Nginx with PHP support)
- **HTTPS** (required for production with SSO)
- **Google Gemini API Key** ([Get here](https://aistudio.google.com/app/apikey))
- **Azure AD Tenant** (optional, for SSO)
---
## Installation Steps
### 1. Clone Repository
```bash
# Clone from Bitbucket
git clone git@bitbucket.org:zlalani/nano-pro.git
# Navigate to directory
cd nano-pro
```
### 2. Install PHP Dependencies
```bash
# Install Composer dependencies (Firebase JWT library)
composer install
# Verify vendor directory created
ls -la vendor/
```
**Expected output:**
```
vendor/
├── autoload.php
├── composer/
└── firebase/
└── php-jwt/
```
### 3. Configure Application
```bash
# Copy example config files
cp config.example.php config.php
cp .env.example .env
```
### 4. Add Google Gemini API Key
Edit `config.php`:
```php
// Line 13: Add your Gemini API key
define('GEMINI_API_KEY', 'YOUR_ACTUAL_GEMINI_API_KEY_HERE');
```
Get API key from: https://aistudio.google.com/app/apikey
### 5. Configure Authentication (Optional)
Edit `.env`:
**For Testing (No Authentication):**
```bash
SSO_ENABLED=false
SSO_TENANT_ID=
SSO_CLIENT_ID=
```
**For Production (With Azure AD SSO):**
```bash
SSO_ENABLED=true
SSO_TENANT_ID=your-azure-tenant-id
SSO_CLIENT_ID=your-azure-app-client-id
```
See `AUTH_README.md` for complete Azure AD setup instructions.
### 6. Set Directory Permissions
```bash
# Create uploads directory if not exists
mkdir -p uploads/sessions
# Set proper permissions
chmod 755 uploads
chmod 755 uploads/sessions
# Ensure web server can write to uploads
chown -R www-data:www-data uploads/ # Ubuntu/Debian
# OR
chown -R apache:apache uploads/ # CentOS/RHEL
# OR
chown -R _www:_www uploads/ # macOS
```
### 7. Configure Web Server
#### Apache (.htaccess already included)
Ensure `mod_rewrite` is enabled:
```bash
sudo a2enmod rewrite
sudo systemctl restart apache2
```
Set document root to the application directory in your virtual host config.
#### Nginx
Add to your server block:
```nginx
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
}
# Protect uploads directory
location /uploads/ {
deny all;
return 403;
}
```
### 8. Test Installation
Visit these URLs to verify:
1. **Main App:** `https://your-server.com/nano-pro/`
- Should load the image generator
- If SSO enabled: Shows login page
- If SSO disabled: Loads directly
2. **Auth Test:** `https://your-server.com/nano-pro/auth-test.php`
- Shows SSO configuration
- Shows authentication status
- Shows user info (if authenticated)
3. **Debug Info:** Click "Toggle Debug Panel" in app
- Shows session ID
- Shows current image status
- Shows conversation history count
### 10. Verify Everything Works
**Test Checklist:**
- [ ] Main page loads without errors
- [ ] Prompt Studio section displays
- [ ] Can select cameras/lenses/applications
- [ ] "Enhance Prompt with AI" button works
- [ ] Can generate images
- [ ] Images display correctly
- [ ] Image history shows recent generations
- [ ] Can download images
- [ ] Lightbox modal works (click main image)
- [ ] Conversation history displays
- [ ] Quick action buttons populate prompt
- [ ] If SSO enabled: Login/logout works
---
## Directory Structure
After installation:
```
nano-pro/
├── .env # Environment config (gitignored)
├── .env.example # Environment template
├── .gitignore # Git ignore rules
├── .htaccess # Apache configuration
├── api.php # Image generation API
├── auth.php # Authentication API
├── auth-test.php # Auth debugging page
├── AuthMiddleware.php # Auth orchestrator
├── AUTH_README.md # Auth setup guide
├── cleanup.php # Image cleanup script
├── clear_session.php # Clear session utility
├── composer.json # PHP dependencies
├── composer.lock # Dependency lock file
├── config.example.php # Config template
├── config.php # Main config (gitignored)
├── debug.php # Debug utilities
├── debug_request.php # Request debugging
├── enhance_prompt.php # Prompt enhancement API
├── env_loader.php # Environment loader
├── get_logs.php # Server logs API
├── index.php # Main application
├── INSTALL.md # This file
├── JWTValidator.php # JWT token validator
├── README.md # Project readme
├── session_manager.php # Session management
├── uploads/ # User image storage
│ ├── .htaccess # Protect uploads
│ └── sessions/ # Session directories
│ └── {session_id}/ # Per-user storage
│ └── images/ # User images
└── vendor/ # Composer dependencies
├── autoload.php
└── firebase/
└── php-jwt/
```
---
## Configuration Files
### config.php (Main Configuration)
```php
<?php
require_once __DIR__ . '/env_loader.php';
// Google Gemini API Key
define('GEMINI_API_KEY', 'YOUR_KEY_HERE');
// MSAL / Azure AD SSO
define('SSO_ENABLED', getenv('SSO_ENABLED') === 'true');
define('SSO_TENANT_ID', getenv('SSO_TENANT_ID') ?: '');
define('SSO_CLIENT_ID', getenv('SSO_CLIENT_ID') ?: '');
// Session configuration
ini_set('session.gc_maxlifetime', 3600);
ini_set('session.cookie_lifetime', 3600);
// Error reporting
error_reporting(E_ALL);
ini_set('display_errors', 1);
ini_set('log_errors', 1);
```
### .env (Environment Variables)
```bash
# Authentication toggle
SSO_ENABLED=false
# Azure AD credentials (when SSO enabled)
SSO_TENANT_ID=
SSO_CLIENT_ID=
```
---
## Troubleshooting
### "Composer not found"
```bash
# Install Composer
curl -sS https://getcomposer.org/installer | php
sudo mv composer.phar /usr/local/bin/composer
```
### "Permission denied" errors
```bash
# Fix uploads directory permissions
sudo chown -R www-data:www-data uploads/
sudo chmod -R 755 uploads/
```
### "Class 'Firebase\JWT\JWT' not found"
```bash
# Re-run composer install
composer install
# Verify autoload file exists
ls -la vendor/autoload.php
```
### Images not saving
```bash
# Check uploads directory exists and is writable
ls -la uploads/
ls -la uploads/sessions/
# Create if missing
mkdir -p uploads/sessions
chmod 755 uploads/sessions
```
### "API key not configured" error
```bash
# Verify config.php has valid API key
grep GEMINI_API_KEY config.php
# Should show:
# define('GEMINI_API_KEY', 'AIzaSy...');
```
### Authentication not working
```bash
# Check SSO configuration
php auth-test.php
# Verify .env file loaded
cat .env
# Check error logs
tail -f error_log
```
### Cleanup script not running
```bash
# Test manually first
php cleanup.php
# Should show:
# === Image Cleanup Report ===
# Timestamp: 2025-12-16 ...
# Images cleaned: X
```
---
## Production Deployment Checklist
Before going live:
**Required:**
- [ ] PHP 7.4+ installed on server
- [ ] Composer dependencies installed (`vendor/` exists)
- [ ] `config.php` created with valid Gemini API key
- [ ] `.env` file created and configured
- [ ] `uploads/sessions/` directory created with write permissions
- [ ] `.htaccess` protecting uploads directory
- [ ] Web server configured (Apache/Nginx)
- [ ] Cron job set up for `cleanup.php`
**If Using SSO:**
- [ ] HTTPS enabled on server
- [ ] Azure AD app registration created
- [ ] Redirect URI matches server URL exactly
- [ ] `.env` has correct `SSO_TENANT_ID` and `SSO_CLIENT_ID`
- [ ] `SSO_ENABLED=true` in `.env`
- [ ] Tested login/logout flow
**Security:**
- [ ] `config.php` is gitignored (contains API key)
- [ ] `.env` is gitignored (contains credentials)
- [ ] `vendor/` is gitignored
- [ ] `uploads/sessions/` is gitignored
- [ ] Error display disabled in production (`display_errors = 0`)
- [ ] HTTPS enforced for production
**Testing:**
- [ ] Visit main app - loads without errors
- [ ] Generate a test image - works
- [ ] Enhance prompt feature - works
- [ ] Image history displays - works
- [ ] Download images - works
- [ ] Lightbox modal - works
- [ ] Visit `auth-test.php` - shows correct config
---
## Quick Start (TL;DR)
```bash
# 1. Clone
git clone git@bitbucket.org:zlalani/nano-pro.git
cd nano-pro
# 2. Install dependencies
composer install
# 3. Configure
cp config.example.php config.php
cp .env.example .env
# Edit config.php - add your Gemini API key
nano config.php
# 4. Set permissions
mkdir -p uploads/sessions
chmod 755 uploads/sessions
# 5. Test
# Visit: https://your-server.com/nano-pro/
# Visit: https://your-server.com/nano-pro/auth-test.php
# 6. Done!
# Note: Images auto-cleanup on app launch (no cron needed)
```
---
## Updating the Application
```bash
# Pull latest changes
git pull origin master
# Update dependencies
composer install
# Clear PHP opcode cache if using OPcache
# (or restart PHP-FPM)
sudo systemctl restart php8.1-fpm
# No database migrations needed - uses file storage
```
---
## Backup & Restore
### Backup
```bash
# Backup user images (last 24 hours)
tar -czf backup-images-$(date +%Y%m%d).tar.gz uploads/sessions/
# Backup configuration
tar -czf backup-config-$(date +%Y%m%d).tar.gz config.php .env
```
### Restore
```bash
# Restore images
tar -xzf backup-images-YYYYMMDD.tar.gz
# Restore configuration
tar -xzf backup-config-YYYYMMDD.tar.gz
# Set permissions
chmod 755 uploads/sessions/
```
---
## Automatic Image Cleanup
**No cron job required!** The application automatically cleans up expired images:
- Cleanup runs automatically when users launch the app (~10% of sessions)
- Finds images older than 24 hours across all user sessions
- Deletes expired images and metadata files
- Removes empty session directories
- Logs cleanup activity to `error_log`
**Manual cleanup** (if needed):
```bash
php cleanup.php
```
---
## Monitoring
### Check Application Status
```bash
# Visit auth test page
curl https://your-server.com/nano-pro/auth-test.php
# Check PHP error logs
tail -f /var/log/apache2/error.log
# OR
tail -f /var/log/nginx/error.log
# Check cleanup logs
tail -f cleanup.log
```
### Monitor Disk Usage
```bash
# Check uploads directory size
du -sh uploads/sessions/
# Count active sessions
ls -1 uploads/sessions/ | wc -l
# List sessions older than 24 hours
find uploads/sessions/ -type d -mtime +1
```
---
## Support
- **Application issues:** Check `error_log` in application directory
- **Authentication issues:** Run `auth-test.php`
- **Image storage issues:** Check `uploads/sessions/` permissions
- **API errors:** Check browser console and PHP error logs
- **Cleanup issues:** Run `php cleanup.php` manually
---
## Security Notes
- Never commit `config.php` or `.env` to git (they're gitignored)
- Always use HTTPS in production for SSO
- httpOnly cookies protect against XSS
- Images auto-expire after 24 hours
- Upload directory protected by .htaccess
- Each user has isolated session storage
---
## Performance Optimization
### Enable OPcache (Recommended)
Add to `php.ini`:
```ini
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
```
### Increase PHP Limits for Large Images
Add to `php.ini`:
```ini
upload_max_filesize = 20M
post_max_size = 20M
max_execution_time = 120
memory_limit = 256M
```
### Adjust Session Lifetime
In `config.php`:
```php
ini_set('session.gc_maxlifetime', 86400); // 24 hours
ini_set('session.cookie_lifetime', 86400);
```
---
## Maintenance
### Weekly Tasks
- Check disk space: `df -h`
- Review error logs for issues
- Verify cleanup cron is running
### Monthly Tasks
- Update Composer dependencies: `composer update`
- Review and rotate API keys if needed
- Check Azure AD token expiration policies
### As Needed
- Clear old sessions: `php cleanup.php`
- Restart PHP-FPM after config changes
- Monitor API usage quotas (Gemini API)
---
## Common Issues & Solutions
### Issue: "No image data found in API response"
**Solution:**
- Check Gemini API key is valid
- Verify API quota not exceeded
- Check prompt doesn't violate content policies
### Issue: Authentication keeps redirecting
**Solution:**
- Verify Azure AD redirect URI matches exactly
- Check HTTPS is enabled
- Clear browser cookies and try again
### Issue: Images not displaying in history
**Solution:**
- Check file permissions on uploads/
- Verify cleanup script hasn't deleted images
- Check session_manager.php error logs
### Issue: Composer install fails
**Solution:**
```bash
# Update Composer
composer self-update
# Clear cache
composer clear-cache
# Try again
composer install
```
---
## Uninstallation
```bash
# Stop cron job
crontab -e
# Remove the cleanup.php line
# Remove application files
rm -rf /path/to/nano-pro
# Remove user data (optional)
# WARNING: This deletes all user images!
# rm -rf uploads/sessions/*
```
---
## Getting Help
1. Check `error_log` file in application directory
2. Visit `auth-test.php` for authentication status
3. Click "Toggle Debug Panel" in app for session info
4. Review `AUTH_README.md` for SSO setup
5. Check server error logs (`/var/log/apache2/` or `/var/log/nginx/`)

View file

@ -1,255 +0,0 @@
# 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 16 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

242
QUICK_REFERENCE.md Normal file
View file

@ -0,0 +1,242 @@
# Nano Banana Pro - Quick Reference Card
## 🚨 MOST CRITICAL MISTAKES TO AVOID
### 1. Wrong Request Order
```json
❌ WRONG: {"parts": [{"text": "..."}, {"inline_data": {...}}]}
✅ RIGHT: {"parts": [{"inline_data": {...}}, {"text": "..."}]}
```
**Image MUST come before text!**
### 2. Not Checking finishReason
```php
❌ WRONG: $image = $response['candidates'][0]['content']['parts'][0];
✅ RIGHT: if ($reason === 'IMAGE_RECITATION') { /* handle */ }
```
**ALWAYS check finishReason first!**
### 3. Hardcoded MIME Type
```php
❌ WRONG: <img src="data:image/png;base64,...">
✅ RIGHT: <img src="data:<?= $mime ?>;base64,...">
```
**Store and use dynamic MIME type!**
### 4. Dirty Base64
```javascript
❌ WRONG: const base64 = reader.result;
✅ RIGHT: const base64 = reader.result.split(',')[1];
```
**Remove data URI prefix!**
### 5. Simple Prompts
```
❌ WRONG: "a red circle"
✅ RIGHT: "a vintage red sports car in a neon city"
```
**10+ words, creative, detailed!**
---
## 📋 ESSENTIAL PATTERNS
### Request Structure (New Image)
```json
{
"contents": [{
"parts": [{"text": "detailed creative prompt"}]
}],
"generationConfig": {
"responseModalities": ["IMAGE"],
"imageConfig": {
"aspectRatio": "16:9",
"imageSize": "2K"
}
}
}
```
### Request Structure (Edit Image)
```json
{
"contents": [{
"parts": [
{
"inline_data": {
"mime_type": "image/jpeg",
"data": "base64_here"
}
},
{"text": "edit instruction"}
]
}],
"generationConfig": {
"responseModalities": ["IMAGE"],
"imageConfig": {
"aspectRatio": "16:9",
"imageSize": "2K"
}
}
}
```
### Extract Image from Response
```php
// 1. Check finishReason
$reason = $response['candidates'][0]['finishReason'];
if ($reason === 'IMAGE_RECITATION') {
throw new Exception('Content blocked');
}
// 2. Extract image
foreach ($response['candidates'][0]['content']['parts'] as $part) {
if (isset($part['inlineData']['data'])) {
return [
'base64' => $part['inlineData']['data'],
'mime_type' => $part['inlineData']['mimeType']
];
}
}
```
### Session Management
```php
// Store
$_SESSION['current_image'] = $base64;
$_SESSION['current_image_mime'] = $mimeType;
// Retrieve for editing
$previousImage = $_SESSION['current_image'];
// Reset
$_SESSION['current_image'] = null;
$_SESSION['current_image_mime'] = 'image/png';
```
### File Upload (Client)
```javascript
function fileToBase64(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
const base64 = reader.result.split(',')[1]; // Remove prefix!
resolve(base64);
};
reader.readAsDataURL(file);
});
}
```
---
## 🎯 ERROR HANDLING MAP
| finishReason | Action |
|--------------|--------|
| `IMAGE_RECITATION` | Prompt too generic - ask for creative prompt |
| `SAFETY` | Content filter - different prompt needed |
| `STOP` | Success - extract image |
| Empty content | Check for API error in response |
| HTTP Code | Meaning | Solution |
|-----------|---------|----------|
| 200 | Success | Extract image |
| 400 | Bad request | Check base64/format |
| 429 | Rate limit | Wait 30s |
| 500 | Internal error | Retry with backoff |
---
## 🔑 KEY DIFFERENCES FROM OTHER APIS
| Aspect | Gemini | DALL-E/SD |
|--------|--------|-----------|
| Endpoint | generateContent | dedicated image API |
| Response | base64 in JSON | URL to image |
| Editing | Send previous base64 | Separate edit endpoint |
| Filters | Very aggressive | More lenient |
| MIME handling | Dynamic (jpeg) | Fixed (png) |
| Auth header | x-goog-api-key | Authorization Bearer |
---
## ⚡ DEBUGGING STEPS
1. Log request payload
2. Log response structure
3. Check finishReason
4. Verify base64 length
5. Check MIME type match
6. Test with known-good prompt
7. Clear session and retry
8. Check server logs
---
## 📝 WORKING PROMPTS
✅ **These work:**
- "A futuristic motorcycle racing through a neon-lit cyberpunk city at night"
- "A magical forest with glowing blue mushrooms and fireflies at twilight"
- "A vintage red sports car on a winding mountain road during sunset"
- "An astronaut floating in deep space with colorful nebulas in background"
❌ **These fail:**
- "a motorcycle"
- "a forest"
- "a car"
- "a circle"
**Rule: 10+ words, creative, contextual**
---
## 🛠️ API CONFIG
```php
$url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-pro-image-preview:generateContent";
$headers = [
'Content-Type: application/json',
'x-goog-api-key: ' . $apiKey
];
$options = [
CURLOPT_TIMEOUT => 120 // 2 minutes!
];
```
---
## ✅ IMPLEMENTATION CHECKLIST
- [ ] Image before text in parts array
- [ ] finishReason checked first
- [ ] MIME type stored and used
- [ ] Base64 cleaned (no prefix)
- [ ] Session manages both data and MIME
- [ ] Detailed prompts required
- [ ] Error types handled specifically
- [ ] Retry logic for 500 errors
- [ ] File upload converts properly
- [ ] Display uses correct MIME type
---
## 🎓 GOTCHAS
1. Request uses `inline_data`, response uses `inlineData`
2. Gemini returns `image/jpeg`, not `png`
3. Simple prompts trigger `IMAGE_RECITATION`
4. HTTP 200 doesn't mean success (check finishReason)
5. Base64 in requests must be pristine
6. Image MUST come before text in parts
7. Session MUST store MIME type
8. 120s timeout minimum
9. Retry on 500 errors
10. Check content array isn't empty
---
*Keep this card handy when implementing!*

192
README.md
View file

@ -1,192 +0,0 @@
# 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 Normal file
View file

@ -0,0 +1,370 @@
<?php
// Suppress HTML error output to prevent breaking JSON responses
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
header('Content-Type: application/json');
// Load configuration, session manager, and webhook logger
require_once 'config.php';
require_once 'session_manager.php';
require_once 'webhook_logger.php';
// Initialize session manager for multi-user support
$sessionManager = new SessionManager();
// Initialize auth status with default
$authStatus = [
'authenticated' => true,
'user' => [
'name' => 'User',
'preferred_username' => 'anonymous@nano-banana-pro.com'
]
];
// Check authentication (with graceful fallback)
try {
if (file_exists(__DIR__ . '/AuthMiddleware.php')) {
require_once 'AuthMiddleware.php';
$auth = new AuthMiddleware();
$authStatus = $auth->isAuthenticated();
if (!$authStatus['authenticated']) {
http_response_code(401);
echo json_encode([
'success' => false,
'error' => 'Authentication required',
'requiresAuth' => true
]);
exit;
}
}
} catch (Exception $e) {
// Log error but continue without auth (for testing)
error_log("Auth check failed in api.php: " . $e->getMessage());
}
class NanoBananaProAPI {
private $apiKey;
private $baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
private $model = 'gemini-3-pro-image-preview';
public function __construct($apiKey) {
$this->apiKey = $apiKey;
}
public function generateImage($prompt, $aspectRatio = '16:9', $imageSize = '2K', $inputImage = null, $referenceImages = []) {
$parts = [];
// IMPORTANT: Input image (the one being edited) must come FIRST
// Gemini treats the first image as the primary subject to modify
if ($inputImage) {
error_log("Edit mode: Input image size = " . strlen($inputImage) . " chars");
// Clean any whitespace from base64 data
$inputImage = preg_replace('/\s+/', '', $inputImage);
// Basic validation - check if it looks like base64
if (!preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $inputImage)) {
error_log("Base64 validation failed for input image");
error_log("First 100 chars: " . substr($inputImage, 0, 100));
throw new Exception("Invalid image data format - not valid base64");
}
$parts[] = [
'inline_data' => [
'mime_type' => 'image/jpeg', // Use jpeg to match API output
'data' => $inputImage
]
];
error_log("Added input image FIRST to request (mime_type: image/jpeg)");
}
// Add reference images after input image (up to 14 allowed by Gemini 3 Pro)
if (!empty($referenceImages)) {
error_log("Adding " . count($referenceImages) . " reference image(s) to request");
foreach ($referenceImages as $index => $refImg) {
$data = preg_replace('/\s+/', '', $refImg['data']);
// Basic validation
if (!preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $data)) {
error_log("Base64 validation failed for reference image $index");
continue;
}
$parts[] = [
'inline_data' => [
'mime_type' => $refImg['mime_type'] ?? 'image/jpeg',
'data' => $data
]
];
error_log("Added reference image $index (mime_type: " . ($refImg['mime_type'] ?? 'image/jpeg') . ")");
}
}
if (!$inputImage && empty($referenceImages)) {
error_log("Generation mode: No input image or reference images");
}
// Add the text prompt
$parts[] = ['text' => $prompt];
$payload = [
'contents' => [
['parts' => $parts]
],
'generationConfig' => [
'responseModalities' => ['IMAGE'],
'imageConfig' => [
'aspectRatio' => $aspectRatio,
'imageSize' => $imageSize
]
]
];
return $this->makeRequest($payload);
}
private function makeRequest($payload, $retryCount = 0) {
$url = "{$this->baseUrl}/{$this->model}:generateContent";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-goog-api-key: ' . $this->apiKey
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 120 // 2 minute timeout for image generation
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
$error = curl_error($ch);
@curl_close($ch);
throw new Exception('cURL error: ' . $error);
}
@curl_close($ch);
if ($httpCode !== 200) {
$errorData = json_decode($response, true);
$errorMessage = $errorData['error']['message'] ?? "HTTP $httpCode";
$errorStatus = $errorData['error']['status'] ?? 'UNKNOWN';
// Log full error details for debugging
error_log("API Error - HTTP $httpCode (Status: $errorStatus)");
error_log("Error message: " . $errorMessage);
error_log("Full response: " . $response);
// Handle specific error types
if ($httpCode === 500 && stripos($errorMessage, 'internal') !== false && $retryCount < 2) {
// Retry on internal errors (up to 2 times)
error_log("Retrying request due to internal error (attempt " . ($retryCount + 1) . ")");
sleep(2); // Wait 2 seconds before retry
return $this->makeRequest($payload, $retryCount + 1);
}
if ($httpCode === 429 || $errorStatus === 'RESOURCE_EXHAUSTED') {
throw new Exception("API rate limit exceeded. Please wait a moment and try again.");
}
if ($errorStatus === 'INVALID_ARGUMENT') {
throw new Exception("Invalid request format. This might be due to corrupted image data. Try clicking 'Start New Image' and generating fresh.");
}
throw new Exception("API error: $errorMessage (HTTP $httpCode, Status: $errorStatus)");
}
return json_decode($response, true);
}
public function extractImageData($response) {
// Log the response for debugging
error_log("API Response: " . json_encode($response));
// Check for finish reasons that indicate content issues
if (isset($response['candidates'][0]['finishReason'])) {
$finishReason = $response['candidates'][0]['finishReason'];
$finishMessage = $response['candidates'][0]['finishMessage'] ?? '';
if ($finishReason === 'IMAGE_RECITATION') {
throw new Exception('Image generation blocked by content filter. Try a more creative and descriptive prompt. Avoid simple geometric shapes or common objects. Example: "A futuristic cityscape at sunset with flying cars" instead of "a red circle".');
}
if ($finishReason === 'SAFETY') {
throw new Exception('Image generation blocked by safety filters. Please try a different prompt.');
}
if ($finishReason !== 'STOP' && !empty($finishMessage)) {
throw new Exception('Image generation failed: ' . $finishMessage);
}
}
if (isset($response['candidates'][0]['content']['parts'])) {
foreach ($response['candidates'][0]['content']['parts'] as $part) {
if (isset($part['inline_data']['data'])) {
return [
'base64' => $part['inline_data']['data'],
'mime_type' => $part['inline_data']['mime_type'] ?? 'image/png'
];
}
// Check for inlineData (alternative format)
if (isset($part['inlineData']['data'])) {
return [
'base64' => $part['inlineData']['data'],
'mime_type' => $part['inlineData']['mimeType'] ?? 'image/png'
];
}
}
}
// Provide detailed error with response structure
$errorDetails = "Response structure: " . json_encode(array_keys($response));
if (isset($response['candidates'][0])) {
$errorDetails .= " | Candidate keys: " . json_encode(array_keys($response['candidates'][0]));
}
throw new Exception('No image data found in API response. ' . $errorDetails);
}
}
// Handle API requests
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('Invalid request method');
}
$action = $_POST['action'] ?? null;
if (!$action) {
throw new Exception('No action specified');
}
// Handle reset action
if ($action === 'reset') {
$sessionManager->reset();
echo json_encode(['success' => true]);
exit;
}
// Handle generate action
if ($action === 'generate') {
$prompt = $_POST['prompt'] ?? null;
$aspectRatio = $_POST['aspectRatio'] ?? '16:9';
$imageSize = $_POST['imageSize'] ?? '2K';
$uploadedImage = $_POST['uploadedImage'] ?? null;
$uploadedImageType = $_POST['uploadedImageType'] ?? null;
// Collect reference images (up to 14)
$referenceImages = [];
$refCount = intval($_POST['referenceImageCount'] ?? 0);
for ($i = 0; $i < min($refCount, 14); $i++) {
if (isset($_POST["referenceImage_$i"])) {
$referenceImages[] = [
'data' => $_POST["referenceImage_$i"],
'mime_type' => $_POST["referenceImageType_$i"] ?? 'image/jpeg'
];
}
}
if (!empty($referenceImages)) {
error_log("Received " . count($referenceImages) . " reference images from frontend");
}
// Check if API key is configured
if (!defined('GEMINI_API_KEY') || empty(GEMINI_API_KEY)) {
throw new Exception('API key not configured. Please set GEMINI_API_KEY in config.php');
}
// Regular generation/editing flow
if (!$prompt) {
throw new Exception('Prompt is required');
}
// Initialize API
$api = new NanoBananaProAPI(GEMINI_API_KEY);
// Determine input image for editing:
// 1. Frontend sends uploadedImage when editing from library or a displayed image
// 2. Fall back to session's current image for iterative edits
$inputImage = null;
if ($uploadedImage) {
// Frontend explicitly sent an image - use it (this is the source of truth)
$inputImage = $uploadedImage;
error_log("Using uploaded image from frontend for editing");
} else {
// No uploaded image - check session for iterative editing
$currentImage = $sessionManager->getCurrentImage();
$inputImage = $currentImage ? $currentImage['data'] : null;
if ($inputImage) {
error_log("Using session image for editing");
} else {
error_log("No input image - generating new image");
}
}
// Generate or edit image (with optional reference images)
$response = $api->generateImage($prompt, $aspectRatio, $imageSize, $inputImage, $referenceImages);
$imageData = $api->extractImageData($response);
// Save to disk
$filename = $sessionManager->saveImage($imageData['base64'], $imageData['mime_type']);
$sessionManager->setCurrentImage($filename, $imageData['mime_type']);
// Add to conversation history
$sessionManager->addToHistory($prompt, $inputImage ? 'edit' : 'generate');
// Log to webhook
try {
$userEmail = $authStatus['user']['preferred_username'] ?? $authStatus['user']['email'] ?? 'anonymous@nano-banana-pro.com';
$webhookSettings = [
'prompt' => $prompt,
'aspectRatio' => $aspectRatio,
'imageSize' => $imageSize,
'actionType' => $inputImage ? 'edit' : 'generate',
'model' => 'Google Imagen 3'
];
logImageGeneration($prompt, $imageData['base64'], $imageData['mime_type'], $webhookSettings, $userEmail, $inputImage ? 'edit' : 'generate');
} catch (Exception $e) {
// Don't fail if webhook fails
error_log("Webhook logging failed: " . $e->getMessage());
}
echo json_encode([
'success' => true,
'message' => 'Image generated successfully'
]);
exit;
}
throw new Exception('Invalid action');
} catch (Exception $e) {
http_response_code(500);
// Log detailed error info
error_log("Exception in api.php: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
echo json_encode([
'success' => false,
'error' => $e->getMessage(),
'debug' => [
'file' => basename($e->getFile()),
'line' => $e->getLine(),
'timestamp' => date('Y-m-d H:i:s')
]
]);
exit;
}

View file

@ -1,41 +0,0 @@
# ============================================================================
# Lux Studio Backend Environment Configuration
# ============================================================================
# Copy this file to .env and configure for your environment
# For production settings, see .env.production
# ============================================================================
# ----------------------------------------------------------------------------
# Backend Port Configuration
# ----------------------------------------------------------------------------
# Port on which the PHP 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

View file

@ -1,33 +0,0 @@
# ============================================================================
# Lux Studio Backend - LOCAL Development Environment Configuration
# ============================================================================
# This file is used by setup.sh for local development
# ============================================================================
# ----------------------------------------------------------------------------
# Backend Port Configuration
# ----------------------------------------------------------------------------
# Port on which the PHP 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

View file

@ -1,30 +0,0 @@
# ============================================================================
# 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

View file

@ -1,142 +0,0 @@
# ==============================================================================
# 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
# ==============================================================================

View file

@ -13,6 +13,9 @@ ini_set('memory_limit', '512M');
ini_set('post_max_size', '100M');
ini_set('upload_max_filesize', '100M');
// Increase execution time for long-running API calls
set_time_limit(180); // 3 minutes
header('Content-Type: application/json');
// Load configuration, session manager, and webhook logger
@ -150,7 +153,7 @@ class NanoBananaProAPI {
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 120 // 2 minute timeout for image generation
]);

View file

@ -12,7 +12,7 @@ if (file_exists(__DIR__ . '/env_loader.php')) {
// Google Gemini API Key (Nano Banana Pro)
// Get your API key from: https://aistudio.google.com/app/apikey
define('GEMINI_API_KEY', 'YOUR_API_KEY_HERE');
define('GEMINI_API_KEY', 'AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8');
// MSAL / Azure AD SSO Configuration
// Always define these constants with defaults if not set

View file

@ -221,7 +221,6 @@ try {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($requestData));
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disabled for MAMP development
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

View file

@ -1,119 +0,0 @@
<?php
/**
* Get Session File API
* Fetches a specific file from backend sessions and returns as base64
* Used for importing backend files to projects
*/
// Suppress HTML error output
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Handle OPTIONS preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
try {
// Get parameters
$sessionId = $_GET['session_id'] ?? '';
$fileType = $_GET['file_type'] ?? 'image'; // 'image' or 'video'
$filename = $_GET['filename'] ?? '';
// Validate parameters
if (empty($sessionId)) {
throw new Exception('Missing session_id parameter');
}
if (empty($filename)) {
throw new Exception('Missing filename parameter');
}
if (!in_array($fileType, ['image', 'video'])) {
throw new Exception('Invalid file_type parameter (must be "image" or "video")');
}
// Sanitize inputs to prevent directory traversal
$sessionId = basename($sessionId);
$filename = basename($filename);
// Construct file path
$uploadsDir = __DIR__ . '/uploads/sessions';
$subDir = $fileType === 'image' ? 'images' : 'videos';
$filePath = $uploadsDir . '/' . $sessionId . '/' . $subDir . '/' . $filename;
// Check if file exists
if (!file_exists($filePath)) {
http_response_code(404);
echo json_encode([
'success' => false,
'error' => 'File not found',
'message' => 'The requested file does not exist or has been deleted'
]);
exit;
}
// Check if file is still valid (not expired)
$createdAt = filectime($filePath);
$expiresAt = $createdAt + (24 * 3600);
if (time() > $expiresAt) {
http_response_code(410);
echo json_encode([
'success' => false,
'error' => 'File expired',
'message' => 'This file has expired and will be deleted soon'
]);
exit;
}
// Read file
$fileContent = file_get_contents($filePath);
if ($fileContent === false) {
throw new Exception('Failed to read file');
}
// Convert to base64
$base64Data = base64_encode($fileContent);
// Get MIME type from metadata if available
$mimeType = $fileType === 'image' ? 'image/jpeg' : 'video/mp4';
$metaPath = $filePath . '.meta';
if (file_exists($metaPath)) {
$metaContent = file_get_contents($metaPath);
$meta = json_decode($metaContent, true);
if ($meta && isset($meta['mime_type'])) {
$mimeType = $meta['mime_type'];
}
}
// Get file size info
$fileSize = strlen($fileContent);
echo json_encode([
'success' => true,
'data' => $base64Data,
'filename' => $filename,
'mime_type' => $mimeType,
'file_type' => $fileType,
'size_bytes' => $fileSize,
'size_kb' => round($fileSize / 1024, 2),
'size_mb' => round($fileSize / (1024 * 1024), 2),
'created_at' => $createdAt,
'expires_at' => $expiresAt,
'time_remaining' => max(0, $expiresAt - time())
]);
} catch (Exception $e) {
error_log("Error in get_session_file.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Failed to fetch file',
'message' => $e->getMessage()
]);
}

File diff suppressed because it is too large Load diff

View file

@ -1,169 +0,0 @@
<?php
/**
* List Session Files API
* Scans uploads/sessions/ directory and returns all available files
* Used for importing backend files to projects
*/
// Suppress HTML error output
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Handle OPTIONS preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
try {
$uploadsDir = __DIR__ . '/uploads/sessions';
// Check if directory exists
if (!is_dir($uploadsDir)) {
echo json_encode([
'success' => true,
'sessions' => [],
'message' => 'No sessions directory found'
]);
exit;
}
$sessions = [];
$sessionDirs = glob($uploadsDir . '/*', GLOB_ONLYDIR);
foreach ($sessionDirs as $sessionPath) {
$sessionId = basename($sessionPath);
$imagesDir = $sessionPath . '/images';
$videosDir = $sessionPath . '/videos';
$sessionData = [
'session_id' => $sessionId,
'images' => [],
'videos' => []
];
// Scan images directory
if (is_dir($imagesDir)) {
$imageFiles = glob($imagesDir . '/*.{jpg,jpeg,png,webp}', GLOB_BRACE);
foreach ($imageFiles as $imagePath) {
$filename = basename($imagePath);
$metaPath = $imagePath . '.meta';
// Get file stats
$fileSize = filesize($imagePath);
$createdAt = filectime($imagePath);
$expiresAt = $createdAt + (24 * 3600); // 24 hours from creation
// Read metadata if available
$mimeType = 'image/jpeg';
if (file_exists($metaPath)) {
$metaContent = file_get_contents($metaPath);
$meta = json_decode($metaContent, true);
if ($meta && isset($meta['mime_type'])) {
$mimeType = $meta['mime_type'];
}
}
$sessionData['images'][] = [
'filename' => $filename,
'path' => 'sessions/' . $sessionId . '/images/' . $filename,
'created_at' => $createdAt,
'expires_at' => $expiresAt,
'time_remaining' => max(0, $expiresAt - time()),
'mime_type' => $mimeType,
'size_kb' => round($fileSize / 1024, 2),
'size_bytes' => $fileSize
];
}
}
// Scan videos directory
if (is_dir($videosDir)) {
$videoFiles = glob($videosDir . '/*.{mp4,webm,mov}', GLOB_BRACE);
foreach ($videoFiles as $videoPath) {
$filename = basename($videoPath);
$metaPath = $videoPath . '.meta';
// Get file stats
$fileSize = filesize($videoPath);
$createdAt = filectime($videoPath);
$expiresAt = $createdAt + (24 * 3600);
// Read metadata if available
$mimeType = 'video/mp4';
if (file_exists($metaPath)) {
$metaContent = file_get_contents($metaPath);
$meta = json_decode($metaContent, true);
if ($meta && isset($meta['mime_type'])) {
$mimeType = $meta['mime_type'];
}
}
$sessionData['videos'][] = [
'filename' => $filename,
'path' => 'sessions/' . $sessionId . '/videos/' . $filename,
'created_at' => $createdAt,
'expires_at' => $expiresAt,
'time_remaining' => max(0, $expiresAt - time()),
'mime_type' => $mimeType,
'size_kb' => round($fileSize / 1024, 2),
'size_mb' => round($fileSize / (1024 * 1024), 2),
'size_bytes' => $fileSize
];
}
}
// Only include sessions that have files
if (count($sessionData['images']) > 0 || count($sessionData['videos']) > 0) {
$sessions[] = $sessionData;
}
}
// Sort sessions by most recent file
usort($sessions, function($a, $b) {
$aMax = 0;
$bMax = 0;
foreach ($a['images'] as $img) {
$aMax = max($aMax, $img['created_at']);
}
foreach ($a['videos'] as $vid) {
$aMax = max($aMax, $vid['created_at']);
}
foreach ($b['images'] as $img) {
$bMax = max($bMax, $img['created_at']);
}
foreach ($b['videos'] as $vid) {
$bMax = max($bMax, $vid['created_at']);
}
return $bMax - $aMax; // Descending order (newest first)
});
echo json_encode([
'success' => true,
'sessions' => $sessions,
'total_sessions' => count($sessions),
'total_files' => array_reduce($sessions, function($carry, $session) {
return $carry + count($session['images']) + count($session['videos']);
}, 0)
], JSON_PRETTY_PRINT);
} catch (Exception $e) {
error_log("Error in list_session_files.php: " . $e->getMessage());
http_response_code(500);
echo json_encode([
'success' => false,
'error' => 'Failed to list session files',
'message' => $e->getMessage()
]);
}

View file

@ -29,7 +29,7 @@ class SessionManager {
}
$this->sessionId = session_id();
$this->uploadDir = __DIR__ . '/uploads/sessions';
$this->uploadDir = dirname(__DIR__) . '/uploads/sessions';
$this->sessionDir = $this->uploadDir . '/' . $this->sessionId;
// Create session directory if it doesn't exist
@ -352,7 +352,7 @@ class SessionManager {
*/
public static function cleanupExpiredImages($uploadDir = null) {
if ($uploadDir === null) {
$uploadDir = __DIR__ . '/uploads/sessions';
$uploadDir = dirname(__DIR__) . '/uploads/sessions';
}
if (!is_dir($uploadDir)) {

View file

@ -1,9 +0,0 @@
<?php
// Simple test file to verify backend is accessible
header('Content-Type: application/json');
echo json_encode([
'status' => 'success',
'message' => 'Backend is running!',
'php_version' => phpversion(),
'timestamp' => date('Y-m-d H:i:s')
]);

View file

@ -54,12 +54,6 @@ class VeoVideoAPI {
'prompt' => $prompt
];
// Build parameters first
$parameters = [
'aspectRatio' => $aspectRatio,
'sampleCount' => 1
];
// Frame mode: first image is starting frame, optional second image is last frame for interpolation
if (!empty($referenceImages)) {
if (isset($referenceImages[0])) {
@ -67,7 +61,6 @@ class VeoVideoAPI {
$data = preg_replace('/\s+/', '', $refImg['data']);
if (preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $data)) {
// Use bytesBase64Encoded format (original working format)
$instance['image'] = [
'bytesBase64Encoded' => $data,
'mimeType' => $refImg['mime_type'] ?? 'image/jpeg'
@ -96,6 +89,12 @@ class VeoVideoAPI {
}
}
// Build parameters
$parameters = [
'aspectRatio' => $aspectRatio,
'sampleCount' => 1
];
// Duration: Veo 3.1 supports 4, 6, or 8 seconds
if (in_array(intval($duration), [4, 6, 8])) {
$parameters['durationSeconds'] = intval($duration);
@ -137,7 +136,7 @@ class VeoVideoAPI {
// Log payload structure (without full base64 data for readability)
$logPayload = $payload;
if (isset($logPayload['instances'][0]['image']['bytesBase64Encoded'])) {
if (isset($logPayload['instances'][0]['image'])) {
$logPayload['instances'][0]['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['image']['bytesBase64Encoded']) . '_bytes]';
}
if (isset($logPayload['instances'][0]['lastFrame']['bytesBase64Encoded'])) {
@ -145,12 +144,9 @@ class VeoVideoAPI {
}
if (isset($logPayload['instances'][0]['referenceImages'])) {
foreach ($logPayload['instances'][0]['referenceImages'] as $i => &$refImg) {
if (isset($refImg['image']['bytesBase64Encoded'])) {
$refImg['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['referenceImages'][$i]['image']['bytesBase64Encoded']) . '_bytes]';
}
$refImg['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['referenceImages'][$i]['image']['bytesBase64Encoded']) . '_bytes]';
}
}
error_log("Video generation request - Model: " . $this->model);
error_log("Video generation payload structure: " . json_encode($logPayload));
return $this->makeRequest($payload);
@ -172,7 +168,7 @@ class VeoVideoAPI {
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 600 // 10 minute timeout for video generation
]);
@ -210,12 +206,7 @@ class VeoVideoAPI {
}
if ($errorStatus === 'INVALID_ARGUMENT') {
// Check if error is about lastFrame feature
if (stripos($errorMessage, 'lastFrame') !== false) {
throw new Exception("The lastFrame feature may not be available for your API key yet. Google is still rolling out Veo 3.1 features. Try using single image-to-video instead, or check Google AI Studio for feature availability. Error: " . $errorMessage);
}
// Include the actual error message from Google API for debugging
throw new Exception("Invalid request format: " . $errorMessage);
throw new Exception("Invalid request format. Check your prompt and settings.");
}
if (stripos($errorMessage, 'model') !== false && stripos($errorMessage, 'not found') !== false) {
@ -481,7 +472,7 @@ class VeoVideoAPI {
'x-goog-api-key: ' . $this->apiKey
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 30
]);
@ -611,7 +602,7 @@ try {
'video' => [
'filename' => $filename,
'mime_type' => $videoData['mime_type'],
'url' => '/generated_videos/' . $filename
'url' => '/lux-studio/api/stream_video.php?file=' . urlencode($filename)
]
]);
} else if ($videoData['type'] === 'uri') {
@ -653,7 +644,7 @@ try {
'video' => [
'filename' => $filename,
'mime_type' => $videoData['mime_type'],
'url' => '/generated_videos/' . $filename
'url' => '/lux-studio/api/stream_video.php?file=' . urlencode($filename)
]
]);
} else {
@ -705,7 +696,7 @@ try {
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => false, // Disabled for MAMP development
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 120
]);
@ -729,11 +720,11 @@ try {
$filepath = $videoDir . '/' . $filename;
file_put_contents($filepath, $videoData);
// Return URL in /generated_videos/ format for frontend to handle
// Return local URL
echo json_encode([
'success' => true,
'video' => [
'url' => '/generated_videos/' . $filename,
'url' => '/api/stream_video.php?file=' . urlencode($filename),
'filename' => $filename,
'mime_type' => 'video/mp4'
]

84
composer.lock generated Normal file
View file

@ -0,0 +1,84 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "0f805d76d4ba7495b7d83f2ad0a9e263",
"packages": [
{
"name": "firebase/php-jwt",
"version": "v6.11.1",
"source": {
"type": "git",
"url": "https://github.com/firebase/php-jwt.git",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"reference": "d1e91ecf8c598d073d0995afa8cd5c75c6e19e66",
"shasum": ""
},
"require": {
"php": "^8.0"
},
"require-dev": {
"guzzlehttp/guzzle": "^7.4",
"phpspec/prophecy-phpunit": "^2.0",
"phpunit/phpunit": "^9.5",
"psr/cache": "^2.0||^3.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0"
},
"suggest": {
"ext-sodium": "Support EdDSA (Ed25519) signatures",
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
},
"type": "library",
"autoload": {
"psr-4": {
"Firebase\\JWT\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Neuman Vong",
"email": "neuman+pear@twilio.com",
"role": "Developer"
},
{
"name": "Anant Narayanan",
"email": "anant@php.net",
"role": "Developer"
}
],
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
"homepage": "https://github.com/firebase/php-jwt",
"keywords": [
"jwt",
"php"
],
"support": {
"issues": "https://github.com/firebase/php-jwt/issues",
"source": "https://github.com/firebase/php-jwt/tree/v6.11.1"
},
"time": "2025-04-09T20:32:01+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.4"
},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}

281
deploy.sh
View file

@ -1,281 +0,0 @@
#!/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 3060 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

View file

@ -1,40 +1,20 @@
# ============================================================================
# Lux Studio Backend - LOCAL Development Environment Configuration
# ============================================================================
# This file is used by setup.sh for local development
# ============================================================================
# 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
# ----------------------------------------------------------------------------
# Backend Port Configuration
# ----------------------------------------------------------------------------
# Port on which the PHP backend server runs locally
# Start backend with: php -S localhost:5015
BACKEND_PORT=5015
VITE_BACKEND_PORT=5015
# ----------------------------------------------------------------------------
# API Base Path (for video/file streaming URLs)
# ----------------------------------------------------------------------------
# For LOCAL development: Use /lux-studio-back (matches Vite proxy)
# For PRODUCTION: Set to your Apache proxy path (e.g., /lux-studio-back)
API_BASE_PATH=/
# 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
# ----------------------------------------------------------------------------
# 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
# 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

View file

@ -1,56 +1,11 @@
# ============================================================================
# Lux Studio Frontend - PRODUCTION Environment Configuration
# ============================================================================
# This file contains production-ready settings
# Copy relevant values to .env when building for production
# ============================================================================
# IMPORTANT: After changing this file, you MUST rebuild:
# cd frontend
# npm run build
# Then upload frontend/dist/* to the server
# ============================================================================
# ----------------------------------------------------------------------------
# NOTE: Port variables are NOT used in production!
# Production uses Apache on port 443 (HTTPS) to serve static files.
# Ports are only needed for local development with Vite dev server.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Base Path Configuration (REQUIRED for production)
# ----------------------------------------------------------------------------
# Base path for the application (production subdirectory)
VITE_BASE_PATH=/lux-studio/
# ----------------------------------------------------------------------------
# Backend API URL (REQUIRED)
# ----------------------------------------------------------------------------
# PRODUCTION: Use Apache proxy
VITE_API_URL=https://ai-sandbox.oliver.solutions/lux-studio/api
# ----------------------------------------------------------------------------
# Google Gemini API Key
# ----------------------------------------------------------------------------
# Used for client-side prompt enhancement # AIzaSyCMKLSJJYEx4c6-TtBACnjdULrLzsr_fts
VITE_GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
VITE_GEMINI_API_KEY=AIzaSyDs7EKdC9NLM5UqWlGUqeQO96TmSA-kos8
# ----------------------------------------------------------------------------
# Azure AD / MSAL SSO Configuration
# ----------------------------------------------------------------------------
# Enable SSO authentication
# Azure AD SSO Configuration - PRODUCTION
VITE_SSO_ENABLED=true
# Production credentials
VITE_SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
VITE_SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
# ----------------------------------------------------------------------------
# SSO Redirect URI (REQUIRED)
# ----------------------------------------------------------------------------
# PRODUCTION: Full application URL (MUST match Azure AD configuration exactly)
# IMPORTANT: Include trailing slash to match Azure AD portal configuration
VITE_SSO_REDIRECT_URI=https://ai-sandbox.oliver.solutions/lux-studio/
# ----------------------------------------------------------------------------
# Environment Mode
# ----------------------------------------------------------------------------
NODE_ENV=production
# API Base URL for production
VITE_API_BASE_URL=/lux-studio/api

2
frontend/.gitignore vendored
View file

@ -8,7 +8,7 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
dist
# dist
dist-ssr
*.local

View file

@ -1,57 +0,0 @@
# ==============================================================================
# Lux Studio Frontend - Minimal Security Configuration
# ==============================================================================
# Disable directory listing
Options -Indexes +FollowSymLinks
# ==============================================================================
# React Router - Client-Side Routing
# ==============================================================================
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /lux-studio/
# Don't rewrite requests for real files or directories
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
# Rewrite all other requests to index.html for client-side routing
RewriteRule ^ index.html [L]
</IfModule>
# ==============================================================================
# Security - Block Sensitive Files
# ==============================================================================
# Block access to hidden files (., .env, .git, etc.)
<FilesMatch "^\.">
Require all denied
</FilesMatch>
# Block access to source map files in production
<FilesMatch "\.map$">
Require all denied
</FilesMatch>
# Block access to package files
<FilesMatch "(package\.json|package-lock\.json|composer\.json|composer\.lock)$">
Require all denied
</FilesMatch>
# ==============================================================================
# Basic Security Headers
# ==============================================================================
<IfModule mod_headers.c>
# Prevent MIME type sniffing
Header set X-Content-Type-Options "nosniff"
# Prevent clickjacking
Header set X-Frame-Options "SAMEORIGIN"
</IfModule>
# ==============================================================================
# END CONFIGURATION
# ==============================================================================

168
frontend/README.md Normal file
View file

@ -0,0 +1,168 @@
# 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 Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

38
frontend/dist/LUX_STUDIO_LOGO.svg vendored Normal file
View file

@ -0,0 +1,38 @@
<?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>

After

Width:  |  Height:  |  Size: 2.1 KiB

231
frontend/dist/assets/index-B01LkE57.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

14
frontend/dist/index.html vendored Normal file
View file

@ -0,0 +1,14 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/lux-studio/LUX_STUDIO_LOGO.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lux Studio - AI Cinematography</title>
<script type="module" crossorigin src="/lux-studio/assets/index-B01LkE57.js"></script>
<link rel="stylesheet" crossorigin href="/lux-studio/assets/index-S7O61I6c.css">
</head>
<body class="bg-slate-950">
<div id="root"></div>
</body>
</html>

View file

@ -3,7 +3,6 @@
<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>

File diff suppressed because it is too large Load diff

View file

@ -10,21 +10,21 @@
"preview": "vite preview"
},
"dependencies": {
"@azure/msal-browser": "^3.7.1",
"@azure/msal-react": "^2.0.23",
"@azure/msal-browser": "^5.1.0",
"@azure/msal-react": "^5.0.3",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@google/generative-ai": "^0.24.1",
"html2canvas": "^1.4.1",
"jspdf": "^4.0.0",
"lucide-react": "^0.555.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.22",
"eslint": "^9.39.1",

View file

@ -1,9 +1,150 @@
import React from 'react';
import AppContent from './components/AppContent';
import React, { useState } from 'react';
import { LogOut } from 'lucide-react';
import TabNavigation from './components/TabNavigation';
import CinePromptStudio from './components/CinePromptStudio';
import VideoGenTab from './components/VideoGenTab';
import ProjectsTab from './components/ProjectsTab';
import LoginPage from './components/LoginPage';
import { useAuth } from './contexts/AuthContext';
function App() {
// Simple wrapper - AppContent handles all MSAL hooks safely
return <AppContent />;
// Auth state
const { isAuthenticated, isLoading, user, logout } = useAuth();
// Default to projects tab - project-first workflow
const [activeTab, setActiveTab] = useState('projects');
// Active project state
const [activeProjectId, setActiveProjectId] = useState(null);
const [activeProjectName, setActiveProjectName] = useState(null);
// Rerun data for video regeneration
const [videoRerunData, setVideoRerunData] = useState(null);
// Edit data for image editing from library
const [imageEditData, setImageEditData] = useState(null);
// Handler for video rerun from ProjectsTab
const handleRerunVideo = (data) => {
setVideoRerunData(data);
setActiveTab('video');
};
// Clear rerun data after it's been loaded
const handleRerunLoaded = () => {
setVideoRerunData(null);
};
// Handler for editing image in Image Gen from ProjectsTab
const handleEditInImageGen = (data) => {
setImageEditData(data);
setActiveTab('image');
};
// Clear edit data after it's been loaded
const handleEditLoaded = () => {
setImageEditData(null);
};
// Handler for project selection from ProjectsTab
const handleProjectSelect = (id, name) => {
setActiveProjectId(id);
setActiveProjectName(name);
};
// Handler for tab change with blocking logic
const handleTabChange = (tabId) => {
// Block Image Gen and Video Gen if no project selected
if ((tabId === 'image' || tabId === 'video') && !activeProjectId) {
return; // Do nothing - tabs are disabled
}
setActiveTab(tabId);
};
// Handle logout
const handleLogout = async () => {
try {
await logout();
} catch (err) {
console.error('Logout error:', err);
}
};
// Show login page if not authenticated
if (!isAuthenticated || isLoading) {
return <LoginPage />;
}
return (
<div className="min-h-screen bg-slate-950 text-slate-100">
{/* Header */}
<div className="max-w-7xl mx-auto px-6 pt-10 pb-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<img
src="/LUX_STUDIO_LOGO.svg"
alt="Lux Studio"
className="h-12 w-auto"
/>
<TabNavigation
activeTab={activeTab}
onTabChange={handleTabChange}
activeProjectId={activeProjectId}
/>
</div>
<div className="flex items-center gap-6">
{/* Project Info */}
{activeProjectName && (
<div className="text-sm text-slate-400">
Working in: <span className="text-cinema-gold font-medium">{activeProjectName}</span>
</div>
)}
{/* User Info */}
<div className="flex items-center gap-3">
<div className="text-right">
<div className="text-sm text-slate-200">{user?.name}</div>
<div className="text-xs text-slate-400">{user?.email}</div>
</div>
<button
onClick={handleLogout}
className="p-2 text-slate-400 hover:text-slate-200 hover:bg-slate-800 rounded-lg transition-colors"
title="Sign out"
>
<LogOut className="w-5 h-5" />
</button>
</div>
</div>
</div>
</div>
{/* Main Content - Tab Panels */}
<main className="max-w-7xl mx-auto px-6 pb-8">
{activeTab === 'projects' && (
<ProjectsTab
onProjectSelect={handleProjectSelect}
activeProjectId={activeProjectId}
onRerunVideo={handleRerunVideo}
onEditInImageGen={handleEditInImageGen}
/>
)}
{activeTab === 'image' && (
<CinePromptStudio
activeProjectId={activeProjectId}
editData={imageEditData}
onEditLoaded={handleEditLoaded}
/>
)}
{activeTab === 'video' && (
<VideoGenTab
activeProjectId={activeProjectId}
rerunData={videoRerunData}
onRerunLoaded={handleRerunLoaded}
/>
)}
</main>
</div>
);
}
export default App;

View file

@ -1,26 +0,0 @@
/**
* MSAL Authentication Configuration
* Uses environment variables for flexible deployment
*/
export const msalConfig = {
auth: {
clientId: import.meta.env.VITE_SSO_CLIENT_ID || '',
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_SSO_TENANT_ID || 'common'}`,
redirectUri: import.meta.env.VITE_SSO_REDIRECT_URI || window.location.origin,
navigateToLoginRequestUrl: false,
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
}
};
export const loginRequest = {
scopes: ['User.Read']
};
// Check if SSO is enabled
export const isSSOEnabled = () => {
return import.meta.env.VITE_SSO_ENABLED === 'true';
};

View file

@ -1,157 +0,0 @@
import { useState } from 'react';
import { useIsAuthenticated, useMsal } from '@azure/msal-react';
import { LogOut } from 'lucide-react';
import { isSSOEnabled } from '../authConfig';
import TabNavigation from './TabNavigation';
import CinePromptStudio from './CinePromptStudio';
import VideoGenTab from './VideoGenTab';
import ProjectsTab from './ProjectsTab';
import LoginPage from './LoginPage';
function AppContent() {
// Check if SSO is enabled
const ssoEnabled = isSSOEnabled();
// MSAL hooks - always called unconditionally since MsalProvider always wraps this component
const isAuthenticated = useIsAuthenticated();
const { instance, accounts } = useMsal();
// State hooks
const [activeTab, setActiveTab] = useState('projects');
const [activeProjectId, setActiveProjectId] = useState(null);
const [activeProjectName, setActiveProjectName] = useState(null);
const [videoRerunData, setVideoRerunData] = useState(null);
const [imageEditData, setImageEditData] = useState(null);
// Show login page if SSO is enabled and user is not authenticated
if (ssoEnabled && !isAuthenticated) {
return <LoginPage />;
}
// Get user info for display
const userName = accounts.length > 0
? accounts[0].name || accounts[0].username
: ssoEnabled ? null : 'Local Developer';
// Logout handler
const handleLogout = () => {
if (instance) {
instance.logoutPopup();
}
};
// Handler for video rerun from ProjectsTab
const handleRerunVideo = (data) => {
setVideoRerunData(data);
setActiveTab('video');
};
// Clear rerun data after it's been loaded
const handleRerunLoaded = () => {
setVideoRerunData(null);
};
// Handler for editing image in Image Gen from ProjectsTab
const handleEditInImageGen = (data) => {
setImageEditData(data);
setActiveTab('image');
};
// Clear edit data after it's been loaded
const handleEditLoaded = () => {
setImageEditData(null);
};
// Handler for project selection from ProjectsTab
const handleProjectSelect = (id, name) => {
setActiveProjectId(id);
setActiveProjectName(name);
};
// Handler for tab change with blocking logic
const handleTabChange = (tabId) => {
// Block Image Gen and Video Gen if no project selected
if ((tabId === 'image' || tabId === 'video') && !activeProjectId) {
return; // Do nothing - tabs are disabled
}
setActiveTab(tabId);
};
return (
<div className="min-h-screen bg-slate-950 text-slate-100">
{/* Header */}
<div className="max-w-7xl mx-auto px-6 pt-10 pb-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-6">
<img
src={`${import.meta.env.BASE_URL}LUX_STUDIO_LOGO.svg`}
alt="Lux Studio"
className="h-12 w-auto"
/>
<TabNavigation
activeTab={activeTab}
onTabChange={handleTabChange}
activeProjectId={activeProjectId}
/>
</div>
<div className="flex items-center gap-4">
<div className="flex flex-col items-end gap-1">
{activeProjectName && (
<div className="text-sm text-slate-400">
Working in: <span className="text-cinema-gold font-medium">{activeProjectName}</span>
</div>
)}
<div className="text-xs text-slate-500">
Version 1.0
</div>
</div>
{userName && (
<div className="flex items-center gap-3 border-l border-slate-700 pl-4">
<div className="text-right">
<div className="text-sm text-slate-300">{userName}</div>
</div>
{ssoEnabled && (
<button
onClick={handleLogout}
className="p-2 hover:bg-slate-800 rounded-lg transition-colors"
title="Logout"
>
<LogOut className="w-5 h-5 text-slate-400" />
</button>
)}
</div>
)}
</div>
</div>
</div>
{/* Main Content - Tab Panels */}
<main className="max-w-7xl mx-auto px-6 pb-8">
{activeTab === 'projects' && (
<ProjectsTab
onProjectSelect={handleProjectSelect}
activeProjectId={activeProjectId}
onRerunVideo={handleRerunVideo}
onEditInImageGen={handleEditInImageGen}
/>
)}
{activeTab === 'image' && (
<CinePromptStudio
activeProjectId={activeProjectId}
editData={imageEditData}
onEditLoaded={handleEditLoaded}
/>
)}
{activeTab === 'video' && (
<VideoGenTab
activeProjectId={activeProjectId}
rerunData={videoRerunData}
onRerunLoaded={handleRerunLoaded}
/>
)}
</main>
</div>
);
}
export default AppContent;

View file

@ -2,20 +2,10 @@ import React, { useState, useEffect } from 'react';
import { Sparkles, Copy, Film, Camera, Sun, Check, Loader2, HelpCircle, Info, Sliders, X, Image, Download, RefreshCw, Upload, Plus, FolderOpen, Settings, Trash2, Pencil, Shield, Maximize2, Minimize2 } from 'lucide-react';
import { GoogleGenerativeAI } from '@google/generative-ai';
import useProjects from '../hooks/useProjects';
import { getApiUrl } from '../utils/api';
import useCustomPresets from '../hooks/useCustomPresets';
const CinePromptStudio = ({ activeProjectId, editData, onEditLoaded }) => {
// API URL helper - uses Vite proxy in dev, direct URL in production
const getApiUrl = (endpoint) => {
// In development, use Vite proxy to avoid CORS
if (import.meta.env.DEV) {
return `/api/${endpoint}`;
}
// In production, use full API URL
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015';
return `${apiUrl}/${endpoint}`;
};
// Projects hook for auto-save and fetching project images
const { addItemToProject, getProjectWithItems, isReady: dbReady } = useProjects();
@ -1213,7 +1203,7 @@ Return ONLY the final prompt text. No markdown, no preamble, no "Here is your pr
setImageError(result.error || 'Image generation failed');
}
} catch (err) {
setImageError(`Network error: ${err.message}. Make sure backend service is running.`);
setImageError(`Network error: ${err.message}. Make sure PHP server is running on port ${import.meta.env.VITE_BACKEND_PORT || '5015'}.`);
} finally {
setIsGeneratingImage(false);
}
@ -1901,7 +1891,7 @@ Return ONLY the final prompt text. No markdown, no preamble, no "Here is your pr
<div className="text-center p-8">
<Image className="w-12 h-12 text-slate-700 mx-auto mb-3" />
<p className="text-slate-500">Generate a prompt first, then click "Generate Image"</p>
<p className="text-slate-600 text-xs mt-2">Requires backend service running</p>
<p className="text-slate-600 text-xs mt-2">Requires PHP backend running on port {import.meta.env.VITE_BACKEND_PORT || '5015'}</p>
</div>
)}
</div>

View file

@ -1,64 +0,0 @@
import React from 'react';
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null, errorInfo: null };
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
console.error('🔴 Error caught by boundary:', error);
console.error('Error info:', errorInfo);
this.setState({
error,
errorInfo
});
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: '40px', fontFamily: 'monospace', maxWidth: '800px', margin: '0 auto' }}>
<h1 style={{ color: '#ef4444' }}> Something went wrong</h1>
<details style={{ whiteSpace: 'pre-wrap', background: '#1e293b', padding: '20px', borderRadius: '8px', color: '#e2e8f0' }}>
<summary style={{ cursor: 'pointer', marginBottom: '10px', fontSize: '16px', fontWeight: 'bold' }}>
Error Details (click to expand)
</summary>
<div style={{ marginTop: '10px' }}>
<strong>Error:</strong>
<pre style={{ marginTop: '5px', color: '#fca5a5' }}>{this.state.error && this.state.error.toString()}</pre>
<strong style={{ marginTop: '20px', display: 'block' }}>Stack Trace:</strong>
<pre style={{ marginTop: '5px', fontSize: '12px', color: '#cbd5e1' }}>
{this.state.errorInfo && this.state.errorInfo.componentStack}
</pre>
</div>
</details>
<button
onClick={() => window.location.reload()}
style={{
marginTop: '20px',
padding: '10px 20px',
background: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '6px',
cursor: 'pointer',
fontSize: '14px'
}}
>
Reload Page
</button>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View file

@ -1,57 +1,98 @@
import React from 'react';
import { useMsal } from '@azure/msal-react';
import { loginRequest } from '../authConfig';
import React, { useState } from 'react';
import { Loader2 } from 'lucide-react';
import { useAuth } from '../contexts/AuthContext';
const LoginPage = () => {
const { instance } = useMsal();
const { login, isLoading } = useAuth();
const [error, setError] = useState('');
const [isLoggingIn, setIsLoggingIn] = useState(false);
const handleLogin = async () => {
setError('');
setIsLoggingIn(true);
try {
await instance.loginRedirect(loginRequest);
} catch (error) {
console.error('Login failed:', error);
await login();
} catch (err) {
setError(err.message || 'Login failed. Please try again.');
} finally {
setIsLoggingIn(false);
}
};
if (isLoading) {
return (
<div className="min-h-screen bg-slate-950 flex items-center justify-center">
<div className="text-center">
<Loader2 className="w-12 h-12 animate-spin text-cinema-gold mx-auto mb-4" />
<p className="text-slate-400">Loading...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-slate-950 flex items-center justify-center px-6">
<div className="min-h-screen bg-slate-950 flex items-center justify-center px-4">
<div className="max-w-md w-full">
<div className="text-center mb-8">
{/* Logo and Title */}
<div className="text-center mb-10">
<img
src={`${import.meta.env.BASE_URL}LUX_STUDIO_LOGO.svg`}
src="/LUX_STUDIO_LOGO.svg"
alt="Lux Studio"
className="h-16 w-auto mx-auto mb-6"
className="h-16 mx-auto mb-6"
/>
<h1 className="text-3xl font-bold text-slate-100 mb-2">
Welcome to Lux Studio
</h1>
<p className="text-slate-400">
AI-powered cinematography suite for professional image and video generation
AI-Powered Cinematography Suite
</p>
</div>
<div className="bg-slate-900 rounded-lg p-8 shadow-xl border border-slate-800">
{/* Login Card */}
<div className="bg-slate-900/50 border border-slate-800 rounded-xl p-8">
<h2 className="text-xl font-semibold text-slate-200 text-center mb-6">
Sign in to continue
</h2>
{error && (
<div className="mb-6 p-4 bg-red-950/50 border border-red-800 rounded-lg text-red-400 text-sm">
{error}
</div>
)}
<button
onClick={handleLogin}
className="w-full bg-cinema-gold hover:bg-yellow-500 text-slate-950 font-semibold py-3 px-6 rounded-lg transition-colors flex items-center justify-center gap-3"
disabled={isLoggingIn}
className="w-full flex items-center justify-center gap-3 px-6 py-4 bg-[#2F2F2F] hover:bg-[#3F3F3F] text-white font-medium rounded-lg transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
<svg className="w-5 h-5" viewBox="0 0 21 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h10v10H0V0z" fill="#f25022"/>
<path d="M11 0h10v10H11V0z" fill="#7fba00"/>
<path d="M0 11h10v10H0V11z" fill="#00a4ef"/>
<path d="M11 11h10v10H11V11z" fill="#ffb900"/>
</svg>
Sign in with Microsoft
{isLoggingIn ? (
<>
<Loader2 className="w-5 h-5 animate-spin" />
<span>Signing in...</span>
</>
) : (
<>
{/* Microsoft Logo */}
<svg className="w-5 h-5" viewBox="0 0 21 21" fill="none">
<rect x="1" y="1" width="9" height="9" fill="#F25022" />
<rect x="11" y="1" width="9" height="9" fill="#7FBA00" />
<rect x="1" y="11" width="9" height="9" fill="#00A4EF" />
<rect x="11" y="11" width="9" height="9" fill="#FFB900" />
</svg>
<span>Sign in with Microsoft</span>
</>
)}
</button>
<p className="text-xs text-slate-500 text-center mt-6">
By signing in, you agree to use this application in accordance with your organization's policies.
<p className="mt-6 text-center text-xs text-slate-500">
By signing in, you agree to our Terms of Service and Privacy Policy
</p>
</div>
<div className="mt-6 text-center text-sm text-slate-500">
<p>Powered by Google Imagen 3 & Veo 3.1</p>
</div>
{/* Footer */}
<p className="mt-8 text-center text-xs text-slate-600">
Version 1.0 &middot; Powered by Google Imagen 3 & Veo 3.1
</p>
</div>
</div>
);

View file

@ -132,13 +132,6 @@ const ProjectsTab = ({ onProjectSelect, activeProjectId, onRerunVideo, onEditInI
const fileInputRef = useRef(null);
const [isUploading, setIsUploading] = useState(false);
// Import from backend state
const [showImportModal, setShowImportModal] = useState(false);
const [availableSessions, setAvailableSessions] = useState([]);
const [selectedFiles, setSelectedFiles] = useState([]);
const [importing, setImporting] = useState(false);
const [importProgress, setImportProgress] = useState({ current: 0, total: 0 });
// Load items and storyboards when project is selected
useEffect(() => {
if (selectedProject) {
@ -223,120 +216,6 @@ const ProjectsTab = ({ onProjectSelect, activeProjectId, onRerunVideo, onEditInI
}
};
// API URL helper for import endpoints
const getApiUrl = (endpoint) => {
if (import.meta.env.DEV) {
return `/api/${endpoint}`;
}
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015';
return `${apiUrl}/${endpoint}`;
};
// Fetch available backend session files
const fetchAvailableSessions = async () => {
try {
const response = await fetch(getApiUrl('list_session_files.php'));
const data = await response.json();
if (data.success) {
setAvailableSessions(data.sessions);
return data.sessions;
} else {
throw new Error(data.error || 'Failed to fetch sessions');
}
} catch (err) {
console.error('Error fetching sessions:', err);
setError('Failed to load backend files: ' + err.message);
return [];
}
};
// Handle opening import modal
const handleOpenImportModal = async () => {
if (!selectedProject) {
setError('Please select a project first');
return;
}
setShowImportModal(true);
setSelectedFiles([]);
await fetchAvailableSessions();
};
// Toggle file selection
const toggleFileSelection = (sessionId, fileType, filename) => {
const fileKey = `${sessionId}:${fileType}:${filename}`;
setSelectedFiles(prev => {
if (prev.includes(fileKey)) {
return prev.filter(f => f !== fileKey);
} else {
return [...prev, fileKey];
}
});
};
// Import selected files
const handleImportFiles = async () => {
if (selectedFiles.length === 0 || !selectedProject) return;
setImporting(true);
setError('');
setImportProgress({ current: 0, total: selectedFiles.length });
try {
for (let i = 0; i < selectedFiles.length; i++) {
const fileKey = selectedFiles[i];
const [sessionId, fileType, filename] = fileKey.split(':');
setImportProgress({ current: i + 1, total: selectedFiles.length });
// Fetch file from backend
const response = await fetch(
getApiUrl(`get_session_file.php?session_id=${encodeURIComponent(sessionId)}&file_type=${fileType}&filename=${encodeURIComponent(filename)}`)
);
const data = await response.json();
if (!data.success) {
console.error(`Failed to fetch ${filename}:`, data.error);
continue;
}
// Add to project
if (fileType === 'image') {
await addItemToProject(selectedProject.id, {
type: 'image',
prompt: `Imported: ${filename}`,
settings: {},
data: data.data, // Base64
mimeType: data.mime_type,
thumbnail: data.data
});
} else if (fileType === 'video') {
// Skip videos for now - they need to be in generated_videos folder
// TODO: Implement proper video import (copy to generated_videos or create session streaming endpoint)
console.warn(`Skipping video import for ${filename} - not yet supported`);
continue;
}
}
// Reload items
await loadProjectItems(selectedProject.id);
// Close modal
setShowImportModal(false);
setSelectedFiles([]);
setAvailableSessions([]);
alert(`Successfully imported ${selectedFiles.length} file(s)`);
} catch (err) {
setError('Import failed: ' + err.message);
console.error('Import error:', err);
} finally {
setImporting(false);
setImportProgress({ current: 0, total: 0 });
}
};
// Open a storyboard for editing
const handleOpenStoryboard = async (storyboardId) => {
try {
@ -855,20 +734,6 @@ const ProjectsTab = ({ onProjectSelect, activeProjectId, onRerunVideo, onEditInI
className="hidden"
/>
{/* Import from Backend button */}
<button
onClick={handleOpenImportModal}
disabled={importing}
className="flex items-center gap-2 px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-400 hover:text-slate-200 rounded-lg text-sm transition-all disabled:opacity-50"
>
{importing ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Database className="w-4 h-4" />
)}
{importing ? `Importing ${importProgress.current}/${importProgress.total}...` : 'Import'}
</button>
{/* Separator */}
<div className="w-px h-6 bg-slate-700" />
@ -926,157 +791,6 @@ const ProjectsTab = ({ onProjectSelect, activeProjectId, onRerunVideo, onEditInI
</div>
)}
{/* Import from Backend Modal */}
{showImportModal && (
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
<div className="bg-slate-900 border border-slate-700 rounded-xl p-6 max-w-4xl w-full max-h-[80vh] overflow-y-auto">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-slate-200">Import from Backend</h2>
<button
onClick={() => setShowImportModal(false)}
className="p-2 hover:bg-slate-800 rounded-lg transition-colors"
>
<X className="w-5 h-5 text-slate-400" />
</button>
</div>
{availableSessions.length === 0 ? (
<div className="text-center py-12 text-slate-500">
<Database className="w-16 h-16 mx-auto mb-4 opacity-30" />
<p className="text-lg">No backend files found</p>
<p className="text-sm mt-2">Generate some images or videos to see them here</p>
</div>
) : (
<>
<p className="text-sm text-slate-400 mb-4">
Found {availableSessions.reduce((acc, s) => acc + s.images.length, 0)} image(s) in {availableSessions.length} session(s).
Files auto-delete after 24 hours.
{availableSessions.reduce((acc, s) => acc + s.videos.length, 0) > 0 && (
<span className="block text-amber-400 text-xs mt-1">
Note: Video import not yet supported (videos must be in generated_videos folder)
</span>
)}
</p>
{/* Session list */}
<div className="space-y-4">
{availableSessions.map((session) => (
<div key={session.session_id} className="bg-slate-800/50 border border-slate-700 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-slate-300">
Session: {session.session_id.substring(0, 8)}...
</h3>
<span className="text-xs text-slate-500">
{session.images.length} image(s)
</span>
</div>
{/* Images */}
{session.images.length > 0 && (
<div className="space-y-2">
<h4 className="text-xs font-medium text-slate-400 uppercase">Images</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{session.images.map((img) => {
const fileKey = `${session.session_id}:image:${img.filename}`;
const isSelected = selectedFiles.includes(fileKey);
const expiresIn = Math.floor(img.time_remaining / 3600);
return (
<div
key={img.filename}
onClick={() => toggleFileSelection(session.session_id, 'image', img.filename)}
className={`relative p-2 rounded-lg border-2 cursor-pointer transition-all ${
isSelected
? 'border-cinema-gold bg-cinema-gold/10'
: 'border-slate-700 hover:border-slate-600'
}`}
>
<div className="aspect-square bg-slate-700/50 rounded flex items-center justify-center mb-2">
<Image className="w-8 h-8 text-slate-500" />
</div>
<p className="text-xs text-slate-400 truncate">{img.filename}</p>
<p className="text-xs text-slate-500">
{img.size_kb} KB {expiresIn}h left
</p>
{isSelected && (
<div className="absolute top-1 right-1 bg-cinema-gold rounded-full p-1">
<Check className="w-3 h-3 text-slate-950" />
</div>
)}
</div>
);
})}
</div>
</div>
)}
{/* Videos - Hidden for now since import not supported */}
{false && session.videos.length > 0 && (
<div className="space-y-2 mt-4">
<h4 className="text-xs font-medium text-slate-400 uppercase">Videos (Import Not Supported)</h4>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{session.videos.map((vid) => {
const expiresIn = Math.floor(vid.time_remaining / 3600);
return (
<div
key={vid.filename}
className="relative p-2 rounded-lg border-2 border-slate-700 opacity-50 cursor-not-allowed"
>
<div className="aspect-square bg-slate-700/50 rounded flex items-center justify-center mb-2">
<Video className="w-8 h-8 text-slate-500" />
</div>
<p className="text-xs text-slate-400 truncate">{vid.filename}</p>
<p className="text-xs text-slate-500">
{vid.size_mb} MB {expiresIn}h left
</p>
</div>
);
})}
</div>
</div>
)}
</div>
))}
</div>
{/* Action buttons */}
<div className="flex items-center justify-between mt-6 pt-4 border-t border-slate-700">
<p className="text-sm text-slate-400">
{selectedFiles.length} file(s) selected
</p>
<div className="flex gap-2">
<button
onClick={() => setShowImportModal(false)}
className="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm transition-all"
>
Cancel
</button>
<button
onClick={handleImportFiles}
disabled={selectedFiles.length === 0 || importing}
className="flex items-center gap-2 px-4 py-2 bg-cinema-gold hover:bg-amber-400 disabled:bg-slate-700 disabled:text-slate-500 text-slate-950 rounded-lg text-sm font-medium transition-all"
>
{importing ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
Importing {importProgress.current}/{importProgress.total}
</>
) : (
<>
<Download className="w-4 h-4" />
Import {selectedFiles.length} File(s)
</>
)}
</button>
</div>
</div>
</>
)}
</div>
</div>
)}
{/* LIBRARY TAB CONTENT */}
{activeSubTab === 'library' && (
<>

View file

@ -3,19 +3,22 @@ import { Video, Sparkles, Loader2, Download, RefreshCw, Plus, X, Volume2, Volume
import { GoogleGenerativeAI } from '@google/generative-ai';
import VideoPlayer from './VideoPlayer';
import useProjects from '../hooks/useProjects';
import { getApiUrl } from '../utils/api';
// Helper to convert old video URLs to production path
const convertVideoUrl = (url) => {
if (!url) return url;
if (url.startsWith('/generated_videos/')) {
return `${getApiUrl('stream_video.php')}?file=${encodeURIComponent(url.replace('/generated_videos/', ''))}`;
} else if (url.startsWith('/api/stream_video.php')) {
return url.replace('/api/', `${import.meta.env.VITE_API_BASE_URL || '/api'}/`);
}
return url;
};
const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
// API URL helper - uses Vite proxy in dev, direct URL in production
const getApiUrl = (endpoint) => {
// In development, use Vite proxy to avoid CORS
if (import.meta.env.DEV) {
return `/api/${endpoint}`;
}
// In production, use full API URL
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015';
return `${apiUrl}/${endpoint}`;
};
// Video Generation Settings (keep these)
const [sceneDescription, setSceneDescription] = useState('');
const [modelType, setModelType] = useState('fast'); // 'standard' or 'fast'
@ -147,7 +150,7 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
// Check if already added
if (referenceImages.some(img => img.projectItemId === item.id)) return;
// Auto-set aspect ratio when adding first reference image
// Auto-set constraints when adding first reference image (I2V requirements)
if (referenceImages.length === 0) {
setAspectRatio('16:9');
}
@ -219,9 +222,7 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
const reader = new FileReader();
reader.onload = () => {
setReferenceImages(prev => {
const newLength = prev.length + 1;
// Auto-set aspect ratio when adding first reference image
// Auto-set constraints when adding first reference image (I2V requirements)
if (prev.length === 0) {
setAspectRatio('16:9');
}
@ -230,7 +231,6 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
if (newLength === 2) {
setDuration(8);
}
return [...prev, {
data: reader.result.split(',')[1],
mime_type: file.type,
@ -252,19 +252,10 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
// Extract thumbnail from video URL
const extractVideoThumbnail = (videoUrl) => {
return new Promise((resolve) => {
if (!videoUrl) {
resolve(null);
return;
}
const video = document.createElement('video');
video.crossOrigin = 'anonymous';
video.muted = true;
// Only set crossOrigin for non-data URIs
if (!videoUrl.startsWith('data:')) {
video.crossOrigin = 'anonymous';
}
video.onloadeddata = () => {
// Seek to 0.5 seconds for a better frame than the very first
video.currentTime = 0.5;
@ -286,23 +277,15 @@ const VideoGenTab = ({ activeProjectId, rerunData, onRerunLoaded }) => {
}
};
video.onerror = (err) => {
console.error('Video load error for thumbnail:', err);
video.onerror = () => {
console.error('Video load error for thumbnail');
resolve(null);
};
// Timeout fallback
setTimeout(() => resolve(null), 5000);
// Convert URL if needed (same logic as VideoPlayer)
let finalUrl = videoUrl;
if (videoUrl.startsWith('/generated_videos/')) {
const filename = videoUrl.replace('/generated_videos/', '');
finalUrl = getApiUrl(`stream_video.php?file=${encodeURIComponent(filename)}`);
console.log(`Thumbnail extraction URL conversion: ${videoUrl}${finalUrl}`);
}
video.src = finalUrl;
video.src = videoUrl;
video.load();
});
};
@ -643,14 +626,10 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
setGenerationProgress(95);
console.log('Video result from backend:', videoResult);
// If the URL is from Google API, download through our proxy
let finalVideoUrl = videoResult.url;
let finalFilename = videoResult.filename;
console.log('Initial video URL:', finalVideoUrl);
if (videoResult.url && videoResult.url.includes('generativelanguage.googleapis.com')) {
// Download through proxy to handle authentication
const downloadFormData = new FormData();
@ -673,20 +652,6 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
}
setGenerationProgress(100);
// Validate video URL before proceeding
if (!finalVideoUrl) {
console.error('Video URL is empty after generation');
throw new Error('Backend did not return a valid video URL');
}
if (!finalVideoUrl.startsWith('/generated_videos/') && !finalVideoUrl.startsWith('data:') && !finalVideoUrl.startsWith('http')) {
console.error('Unexpected video URL format:', finalVideoUrl);
throw new Error(`Invalid video URL format: ${finalVideoUrl}`);
}
console.log('Video generated with URL:', finalVideoUrl);
setGeneratedVideo({
url: finalVideoUrl,
mime_type: videoResult.mime_type || 'video/mp4',
@ -697,11 +662,9 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
if (activeProjectId) {
try {
// Extract thumbnail from the video
console.log('Extracting thumbnail for:', finalVideoUrl);
const thumbnail = await extractVideoThumbnail(finalVideoUrl);
console.log('Thumbnail extracted:', thumbnail ? 'success' : 'failed');
const itemData = {
await addItemToProject(activeProjectId, {
type: 'video',
prompt: prompt,
settings: { modelType, duration, aspectRatio, resolution, generateAudio, dialogue, referenceMode, negativePrompt },
@ -709,12 +672,7 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
thumbnail: thumbnail,
data: finalVideoUrl,
mimeType: 'video/mp4'
};
console.log('Saving video to project with data.url:', itemData.data);
await addItemToProject(activeProjectId, itemData);
console.log('Video saved to project successfully');
});
} catch (saveErr) {
console.error('Failed to save to project:', saveErr);
}
@ -744,22 +702,20 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
Model
</label>
<div className="flex gap-2">
{modelOptions.map((opt) => {
return (
<button
key={opt.value}
onClick={() => setModelType(opt.value)}
className={`flex-1 px-3 py-2 rounded-lg font-medium transition-all text-sm ${
modelType === opt.value
? 'bg-cinema-gold text-slate-950'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
}`}
>
<div>{opt.label}</div>
<div className="text-xs opacity-70">{opt.description}</div>
</button>
);
})}
{modelOptions.map((opt) => (
<button
key={opt.value}
onClick={() => setModelType(opt.value)}
className={`flex-1 px-3 py-2 rounded-lg font-medium transition-all text-sm ${
modelType === opt.value
? 'bg-cinema-gold text-slate-950'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
}`}
>
<div>{opt.label}</div>
<div className="text-xs opacity-70">{opt.description}</div>
</button>
))}
</div>
</div>
@ -770,19 +726,25 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
</label>
<div className="flex gap-2">
{durationOptions.map((opt) => {
const isDisabledByInterpolation = referenceImages.length === 2 && opt.value !== 8;
const isDisabledByI2V = referenceImages.length > 0 && opt.value !== 8;
return (
<button
key={opt.value}
onClick={() => !isDisabledByInterpolation && setDuration(opt.value)}
disabled={isDisabledByInterpolation}
onClick={() => {
if (isDisabledByI2V) return;
setDuration(opt.value);
// Auto-downgrade resolution if not 8s (1080p requires 8s)
if (opt.value !== 8 && resolution === '1080p') {
setResolution('720p');
}
}}
disabled={isDisabledByI2V}
title={isDisabledByI2V ? 'I2V requires 8s duration' : ''}
className={`flex-1 px-3 py-2 rounded-lg font-medium transition-all text-sm ${
duration === opt.value
? 'bg-cinema-gold text-slate-950'
: isDisabledByInterpolation
? 'bg-slate-800/50 text-slate-600 cursor-not-allowed'
: 'bg-slate-800 text-slate-400 hover:bg-slate-700'
}`}
} ${isDisabledByI2V ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<div>{opt.label}</div>
<div className="text-xs opacity-70">{getDurationCost(opt.value)}</div>
@ -806,7 +768,13 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
{['16:9', '9:16'].map((ratio) => (
<button
key={ratio}
onClick={() => setAspectRatio(ratio)}
onClick={() => {
setAspectRatio(ratio);
if (ratio === '9:16') {
// Auto-downgrade resolution (1080p not supported in portrait)
setResolution('720p');
}
}}
className={`flex-1 px-4 py-2 rounded-lg font-medium transition-all ${
aspectRatio === ratio
? 'bg-cinema-gold text-slate-950'
@ -1192,7 +1160,7 @@ OUTPUT: Two sections separated by --- only. No explanations or labels.`;
<button
onClick={() => {
const link = document.createElement('a');
link.href = generatedVideo.url;
link.href = convertVideoUrl(generatedVideo.url);
link.download = generatedVideo.filename || `lux-studio-video-${Date.now()}.mp4`;
link.click();
}}

View file

@ -1,5 +1,6 @@
import React, { useRef, useState, useEffect } from 'react';
import { Play, Pause, SkipBack, SkipForward, Volume2, VolumeX, Download, Image, Scissors, ChevronLeft, ChevronRight, FolderPlus, Check } from 'lucide-react';
import { getApiUrl } from '../utils/api';
/**
* VideoPlayer component with frame extraction capability
@ -13,48 +14,15 @@ const VideoPlayer = ({ src, onFrameExtract, onSaveToProject, className = '', aut
const videoRef = useRef(null);
const canvasRef = useRef(null);
// API URL helper - uses Vite proxy in dev, direct URL in production
const getApiUrl = (endpoint) => {
// In development, use Vite proxy to avoid CORS
if (import.meta.env.DEV) {
return `/api/${endpoint}`;
}
// In production, use full API URL
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:5015';
return `${apiUrl}/${endpoint}`;
};
// Convert video source URLs based on type
const getVideoSrc = () => {
// Defensive check for empty/undefined src
if (!src) {
console.warn('VideoPlayer: src is empty or undefined');
return '';
}
console.log('VideoPlayer: Processing src:', src);
// If it's a data URI (base64), return as-is
if (src.startsWith('data:')) {
console.log('VideoPlayer: Using data URI');
return src;
}
// If it's a /generated_videos/ path, convert to streaming endpoint
if (src.startsWith('/generated_videos/')) {
const filename = src.replace('/generated_videos/', '');
const convertedUrl = getApiUrl(`stream_video.php?file=${encodeURIComponent(filename)}`);
console.log(`VideoPlayer URL conversion: ${src}${convertedUrl}`);
return convertedUrl;
}
// Return other URLs as-is
console.log(`VideoPlayer using URL as-is: ${src}`);
return src;
};
const videoSrc = getVideoSrc();
console.log('VideoPlayer: Final videoSrc:', videoSrc);
// Convert old URLs to production-aware streaming endpoint
let videoSrc = src;
if (src?.startsWith('/generated_videos/')) {
// Old direct video URLs
videoSrc = `${getApiUrl('stream_video.php')}?file=${encodeURIComponent(src.replace('/generated_videos/', ''))}`;
} else if (src?.startsWith('/api/stream_video.php')) {
// Old streaming URLs - update to production path
videoSrc = src.replace('/api/', `${import.meta.env.VITE_API_BASE_URL || '/api'}/`);
}
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
@ -92,19 +60,6 @@ const VideoPlayer = ({ src, onFrameExtract, onSaveToProject, className = '', aut
setVideoAspect('square');
}
};
const handleError = (err) => {
console.error('Video load error:', {
error: err,
src: src,
videoSrc: videoSrc,
currentSrc: video?.currentSrc,
networkState: video?.networkState,
readyState: video?.readyState,
error_code: video?.error?.code,
error_message: video?.error?.message
});
setIsPlaying(false);
};
video.addEventListener('timeupdate', handleTimeUpdate);
video.addEventListener('durationchange', handleDurationChange);
@ -112,7 +67,6 @@ const VideoPlayer = ({ src, onFrameExtract, onSaveToProject, className = '', aut
video.addEventListener('pause', handlePause);
video.addEventListener('ended', handleEnded);
video.addEventListener('loadedmetadata', handleLoadedMetadata);
video.addEventListener('error', handleError);
return () => {
video.removeEventListener('timeupdate', handleTimeUpdate);
@ -121,22 +75,18 @@ const VideoPlayer = ({ src, onFrameExtract, onSaveToProject, className = '', aut
video.removeEventListener('pause', handlePause);
video.removeEventListener('ended', handleEnded);
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
video.removeEventListener('error', handleError);
};
}, []);
// Toggle play/pause
const togglePlay = () => {
const video = videoRef.current;
if (!video || !videoSrc) return;
if (!video) return;
if (isPlaying) {
video.pause();
} else {
video.play().catch(err => {
console.error('Video play failed:', err);
setIsPlaying(false);
});
video.play();
}
};

View file

@ -0,0 +1,26 @@
/**
* MSAL Configuration for Azure AD SSO
*/
export const msalConfig = {
auth: {
clientId: import.meta.env.VITE_SSO_CLIENT_ID,
authority: `https://login.microsoftonline.com/${import.meta.env.VITE_SSO_TENANT_ID}`,
redirectUri: import.meta.env.VITE_SSO_REDIRECT_URI || window.location.origin,
postLogoutRedirectUri: import.meta.env.VITE_SSO_REDIRECT_URI || window.location.origin,
},
cache: {
cacheLocation: 'localStorage',
storeAuthStateInCookie: false,
},
};
// Scopes for Microsoft Graph API
export const loginRequest = {
scopes: ['User.Read'],
};
// Scopes for silent token acquisition
export const graphConfig = {
graphMeEndpoint: 'https://graph.microsoft.com/v1.0/me',
};

View file

@ -0,0 +1,89 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { useMsal, useIsAuthenticated } from '@azure/msal-react';
import { InteractionStatus } from '@azure/msal-browser';
import { loginRequest } from '../config/msalConfig';
const AuthContext = createContext(null);
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthProvider = ({ children }) => {
const { instance, accounts, inProgress } = useMsal();
const isAuthenticated = useIsAuthenticated();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
// Handle redirect response on page load
useEffect(() => {
instance.handleRedirectPromise()
.then((response) => {
if (response) {
// User just logged in via redirect
console.log('Redirect login successful');
}
})
.catch((error) => {
console.error('Redirect error:', error);
});
}, [instance]);
useEffect(() => {
if (inProgress === InteractionStatus.None) {
if (accounts.length > 0) {
const account = accounts[0];
setUser({
id: account.localAccountId,
name: account.name || account.username,
email: account.username,
});
} else {
setUser(null);
}
setIsLoading(false);
}
}, [accounts, inProgress]);
// Use redirect-based login (in-browser, no popup)
const login = async () => {
try {
await instance.loginRedirect(loginRequest);
} catch (error) {
console.error('Login failed:', error);
throw error;
}
};
// Use redirect-based logout
const logout = async () => {
try {
await instance.logoutRedirect({
postLogoutRedirectUri: window.location.origin,
});
} catch (error) {
console.error('Logout failed:', error);
throw error;
}
};
const value = {
user,
isAuthenticated,
isLoading: isLoading || inProgress !== InteractionStatus.None,
login,
logout,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
export default AuthContext;

View file

@ -1,12 +1,9 @@
import { useState, useEffect, useCallback } from 'react';
import useIndexedDB from './useIndexedDB';
import { useAuth } from '../contexts/AuthContext';
// Current user ID - returns 'local' which shows ALL projects in local dev mode
// In production with SSO, this would return the actual user ID for isolation
const getCurrentUserId = () => {
// For local development, return 'local' which will show all projects
return 'local';
};
// Get current user ID from auth context
// Falls back to 'local' for development/testing
/**
* Custom hook for project management
@ -15,7 +12,8 @@ const getCurrentUserId = () => {
*/
const useProjects = () => {
const { isReady, error: dbError, add, put, get, getAll, getByIndex, remove } = useIndexedDB();
const userId = getCurrentUserId();
const { user } = useAuth();
const userId = user?.id || 'local';
const [projects, setProjects] = useState([]);
const [isLoading, setIsLoading] = useState(true);
@ -239,36 +237,14 @@ const useProjects = () => {
// Get all items for a project
const getProjectItems = useCallback(async (projectId) => {
try {
let items = await getByIndex('items', 'projectId', projectId);
// Migration: Fix old video URL format
let hasUpdates = false;
items = items.map(item => {
if (item.type === 'video' && item.data && item.data.startsWith('/stream_video.php?file=')) {
// Extract filename from old format
const match = item.data.match(/file=([^&]+)/);
if (match) {
const filename = decodeURIComponent(match[1]);
item.data = `/generated_videos/${filename}`;
hasUpdates = true;
console.log(`Migrated video URL: ${filename}`);
// Update item in IndexedDB with new URL
put('items', item).catch(err => {
console.error('Failed to update video URL:', err);
});
}
}
return item;
});
const items = await getByIndex('items', 'projectId', projectId);
// Sort by createdAt descending (newest first)
return items.sort((a, b) => b.createdAt - a.createdAt);
} catch (err) {
setError(err.message);
throw err;
}
}, [getByIndex, put]);
}, [getByIndex]);
// Get a single project with its items
const getProjectWithItems = useCallback(async (projectId) => {

View file

@ -2,44 +2,23 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { PublicClientApplication } from '@azure/msal-browser'
import { MsalProvider } from '@azure/msal-react'
import { msalConfig } from './config/msalConfig'
import { AuthProvider } from './contexts/AuthContext'
import './index.css'
import App from './App.jsx'
import ErrorBoundary from './components/ErrorBoundary.jsx'
import { msalConfig, isSSOEnabled } from './authConfig'
console.log('🚀 Main.jsx loading...');
console.log('SSO Enabled:', isSSOEnabled());
// Initialize MSAL instance
const msalInstance = new PublicClientApplication(msalConfig);
// MSAL v3 requires initialize() before rendering
(async () => {
let msalInstance = null;
try {
console.log('Initializing MSAL with config:', msalConfig);
msalInstance = new PublicClientApplication(msalConfig);
await msalInstance.initialize();
console.log('✅ MSAL instance initialized');
} catch (error) {
console.error('❌ Error initializing MSAL instance:', error);
}
try {
createRoot(document.getElementById('root')).render(
<StrictMode>
<ErrorBoundary>
<MsalProvider instance={msalInstance}>
<App />
</MsalProvider>
</ErrorBoundary>
</StrictMode>,
)
console.log('✅ React app rendered');
} catch (error) {
console.error('❌ Error rendering app:', error);
document.getElementById('root').innerHTML = `
<div style="padding: 20px; color: red; font-family: monospace;">
<h1>Error Loading App</h1>
<pre>${error.message}\n${error.stack}</pre>
</div>
`;
}
})();
// MSAL v3+ requires async initialization before rendering
msalInstance.initialize().then(() => {
createRoot(document.getElementById('root')).render(
<StrictMode>
<MsalProvider instance={msalInstance}>
<AuthProvider>
<App />
</AuthProvider>
</MsalProvider>
</StrictMode>,
)
});

13
frontend/src/utils/api.js Normal file
View file

@ -0,0 +1,13 @@
/**
* API URL helper for environment-aware endpoint construction
*/
/**
* Get the full API URL for an endpoint
* @param {string} endpoint - The API endpoint (e.g., 'api.php', 'video_api.php')
* @returns {string} The full API URL
*/
export const getApiUrl = (endpoint) => {
const baseUrl = import.meta.env.VITE_API_BASE_URL || '/api';
return `${baseUrl}/${endpoint}`;
};

View file

@ -3,41 +3,27 @@ import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
// Load env file based on `mode` in the current working directory.
const env = loadEnv(mode, process.cwd(), '')
// Get port from environment or use defaults
const frontendPort = parseInt(env.FRONTEND_PORT || '3000')
const backendPort = parseInt(env.BACKEND_PORT || '5015')
const backendUrl = env.VITE_API_URL || `http://localhost:${backendPort}`
// Base path for production deployment vs local development
// Local: '/' (root)
// Production: '/lux-studio/' (subdirectory)
const basePath = env.VITE_BASE_PATH || '/'
const backendPort = env.VITE_BACKEND_PORT || '5015'
const frontendPort = parseInt(env.VITE_FRONTEND_PORT || '3000')
return {
plugins: [react()],
base: basePath,
base: mode === 'production' ? '/lux-studio/' : '/',
server: {
port: frontendPort,
proxy: {
'/lux-studio-back': {
target: backendUrl,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/lux-studio-back/, '')
},
'/api': {
target: backendUrl,
target: `http://localhost:${backendPort}`,
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
'/generated_videos': {
target: backendUrl,
target: `http://localhost:${backendPort}`,
changeOrigin: true
},
'/generated_images': {
target: backendUrl,
target: `http://localhost:${backendPort}`,
changeOrigin: true
}
}

View file

@ -1,34 +0,0 @@
# ==============================================================================
# LUX STUDIO - Apache Configuration Snippet
# ==============================================================================
# Add these lines to /etc/apache2/apache2.conf (around line 290)
# After existing service configurations
# ==============================================================================
# Backend API Proxy
ProxyPass /lux-studio-back/ http://localhost:5015/
ProxyPassReverse /lux-studio-back/ http://localhost:5015/
# Frontend Directory
<Directory /var/www/html/lux-studio>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
# Security - Block backend source access
<Directory /opt/lux-studio-back>
Require all denied
</Directory>
# CORS Headers
<Location /lux-studio-back>
Header set Access-Control-Allow-Origin "https://ai-sandbox.oliver.solutions"
Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
Header set Access-Control-Allow-Credentials "true"
</Location>
# ==============================================================================
# END LUX STUDIO
# ==============================================================================

View file

@ -1,43 +0,0 @@
[Unit]
Description=Lux Studio Backend API Service
After=network.target network-online.target
Documentation=https://github.com/your-repo/cinema-studio-pro
Wants=network-online.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/lux-studio-back
# Start PHP built-in server on port 5015
# Backend loads environment from /opt/lux-studio-back/.env via env_loader.php
ExecStart=/usr/bin/php -S 0.0.0.0:5015 -t /opt/lux-studio-back
# Restart policy - always restart if it crashes
Restart=always
RestartSec=10
# Logging - view with: journalctl -u lux-studio-backend -f
StandardOutput=journal
StandardError=journal
SyslogIdentifier=lux-studio-backend
# Resource limits
LimitNOFILE=65536
TimeoutStopSec=30
TimeoutStartSec=30
# Environment variables for PHP
Environment="PATH=/usr/local/bin:/usr/bin:/bin"
Environment="PHP_CLI_SERVER_WORKERS=4"
EnvironmentFile=/opt/lux-studio-back/.env
# Security hardening (optional - uncomment if needed)
# ProtectSystem=full
# ProtectHome=yes
# NoNewPrivileges=true
# PrivateTmp=yes
[Install]
WantedBy=multi-user.target

6
package-lock.json generated
View file

@ -1,6 +0,0 @@
{
"name": "cinema-studio-pro",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

422
setup.sh
View file

@ -1,422 +0,0 @@
#!/bin/bash
# ============================================================================
# Lux Studio - Local Development Setup & Start Script
# ============================================================================
# This script sets up AND starts both frontend and backend for LOCAL development
# For production deployment, use deploy.sh instead
# ============================================================================
set -e # Exit on any error
echo "🎬 Setting up Lux Studio for Local Development..."
echo ""
# Colors for output
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
# Get the absolute path of the project root
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# ============================================================================
# 1. CHECK PREREQUISITES
# ============================================================================
echo "${BLUE}Step 1: Checking prerequisites...${NC}"
if ! command -v node &> /dev/null; then
echo "${RED}✗ Node.js not found. Please install Node.js 18+ first.${NC}"
exit 1
fi
if ! command -v npm &> /dev/null; then
echo "${RED}✗ npm not found. Please install npm first.${NC}"
exit 1
fi
if ! command -v php &> /dev/null; then
echo "${RED}✗ PHP not found. Please install PHP 7.4+ first.${NC}"
exit 1
fi
if ! command -v composer &> /dev/null; then
echo "${RED}✗ Composer not found. Please install Composer first.${NC}"
exit 1
fi
echo "${GREEN}✓ All prerequisites installed${NC}"
echo " Node.js: $(node --version)"
echo " npm: $(npm --version)"
echo " PHP: $(php --version | head -n 1)"
echo " Composer: $(composer --version | head -n 1)"
echo ""
# ============================================================================
# 2. SETUP BACKEND
# ============================================================================
echo "${BLUE}Step 2: Setting up backend...${NC}"
cd backend
# Create .env from .env.local
if [ ! -f ".env" ]; then
if [ -f ".env.local" ]; then
echo " → Creating backend/.env from .env.local..."
cp .env.local .env
echo "${GREEN} ✓ backend/.env created with local development settings${NC}"
else
echo "${RED} ✗ Error: .env.local not found${NC}"
echo " Please ensure backend/.env.local exists"
exit 1
fi
else
echo "${GREEN} ✓ backend/.env already exists${NC}"
fi
# Install PHP dependencies
echo " → Installing PHP dependencies via Composer..."
composer install --quiet
if [ $? -eq 0 ]; then
echo "${GREEN}✓ Backend setup complete${NC}"
else
echo "${RED}✗ Backend setup failed${NC}"
exit 1
fi
cd ..
echo ""
# ============================================================================
# 3. SETUP FRONTEND
# ============================================================================
echo "${BLUE}Step 3: Setting up frontend...${NC}"
cd frontend
# Create .env from .env.local
if [ ! -f ".env" ]; then
if [ -f ".env.local" ]; then
echo " → Creating frontend/.env from .env.local..."
cp .env.local .env
echo "${GREEN} ✓ frontend/.env created with local development settings${NC}"
else
echo "${RED} ✗ Error: .env.local not found${NC}"
echo " Please ensure frontend/.env.local exists"
exit 1
fi
else
echo "${GREEN} ✓ frontend/.env already exists${NC}"
fi
# Install npm dependencies
echo " → Installing npm dependencies (this may take a few minutes)..."
npm install --silent
if [ $? -eq 0 ]; then
echo "${GREEN}✓ Frontend setup complete${NC}"
else
echo "${RED}✗ Frontend setup failed${NC}"
exit 1
fi
cd ..
echo ""
# ============================================================================
# 4. CREATE REQUIRED DIRECTORIES
# ============================================================================
echo "${BLUE}Step 4: Creating required directories...${NC}"
# Create backend upload directory with proper permissions
mkdir -p backend/uploads/sessions
chmod -R 755 backend/uploads 2>/dev/null || true
echo "${GREEN}✓ Required directories created${NC}"
echo ""
# ============================================================================
# 5. READ PORT CONFIGURATION
# ============================================================================
# Read ports from .env files
BACKEND_PORT=5015 # Default
FRONTEND_PORT=3000 # Default
if [ -f "backend/.env" ]; then
BACKEND_PORT=$(grep -E "^BACKEND_PORT=" backend/.env | cut -d'=' -f2 | tr -d ' ')
fi
if [ -f "frontend/.env" ]; then
FRONTEND_PORT=$(grep -E "^FRONTEND_PORT=" frontend/.env | cut -d'=' -f2 | tr -d ' ')
fi
# Use defaults if not found
BACKEND_PORT=${BACKEND_PORT:-5015}
FRONTEND_PORT=${FRONTEND_PORT:-3000}
# ============================================================================
# 6. CHECK FOR PORT CONFLICTS
# ============================================================================
echo "${BLUE}Step 5: Checking for port conflicts...${NC}"
# Function to check if a port is in use
check_port() {
local port=$1
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1 ; then
return 0 # Port is in use
else
return 1 # Port is free
fi
}
# Function to kill process on port
kill_port() {
local port=$1
local pid=$(lsof -ti:$port)
if [ ! -z "$pid" ]; then
echo " → Killing process $pid on port $port..."
kill -9 $pid 2>/dev/null || true
sleep 1
fi
}
# Check backend port
if check_port $BACKEND_PORT; then
echo "${YELLOW} ⚠ Port $BACKEND_PORT is already in use${NC}"
read -p " Kill the process and continue? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
kill_port $BACKEND_PORT
echo "${GREEN} ✓ Port $BACKEND_PORT is now free${NC}"
else
echo "${RED} ✗ Cannot start backend - port in use${NC}"
exit 1
fi
else
echo "${GREEN} ✓ Port $BACKEND_PORT is available${NC}"
fi
# Check frontend port
if check_port $FRONTEND_PORT; then
echo "${YELLOW} ⚠ Port $FRONTEND_PORT is already in use${NC}"
read -p " Kill the process and continue? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
kill_port $FRONTEND_PORT
echo "${GREEN} ✓ Port $FRONTEND_PORT is now free${NC}"
else
echo "${RED} ✗ Cannot start frontend - port in use${NC}"
exit 1
fi
else
echo "${GREEN} ✓ Port $FRONTEND_PORT is available${NC}"
fi
echo ""
# ============================================================================
# 7. START BACKEND SERVER
# ============================================================================
echo "${BLUE}Step 6: Starting backend server...${NC}"
cd "$PROJECT_ROOT/backend"
# Start PHP server in background
nohup php -S localhost:${BACKEND_PORT} > "${PROJECT_ROOT}/backend-server.log" 2>&1 &
BACKEND_PID=$!
# Wait a moment and check if server started
sleep 2
if ps -p $BACKEND_PID > /dev/null; then
echo "${GREEN}✓ Backend server started on port ${BACKEND_PORT}${NC}"
echo " PID: $BACKEND_PID"
echo " Logs: ${PROJECT_ROOT}/backend-server.log"
# Save PID for later shutdown
echo $BACKEND_PID > "${PROJECT_ROOT}/.backend.pid"
else
echo "${RED}✗ Failed to start backend server${NC}"
echo " Check ${PROJECT_ROOT}/backend-server.log for errors"
exit 1
fi
cd "$PROJECT_ROOT"
echo ""
# ============================================================================
# 8. START FRONTEND SERVER
# ============================================================================
echo "${BLUE}Step 7: Starting frontend dev server...${NC}"
cd "$PROJECT_ROOT/frontend"
# Start Vite dev server in background
nohup npm run dev > "${PROJECT_ROOT}/frontend-server.log" 2>&1 &
FRONTEND_PID=$!
# Wait for Vite to start (it takes a bit longer)
echo " → Waiting for Vite dev server to start..."
sleep 5
if ps -p $FRONTEND_PID > /dev/null; then
echo "${GREEN}✓ Frontend server started on port ${FRONTEND_PORT}${NC}"
echo " PID: $FRONTEND_PID"
echo " Logs: ${PROJECT_ROOT}/frontend-server.log"
# Save PID for later shutdown
echo $FRONTEND_PID > "${PROJECT_ROOT}/.frontend.pid"
else
echo "${RED}✗ Failed to start frontend server${NC}"
echo " Check ${PROJECT_ROOT}/frontend-server.log for errors"
# Clean up backend server
kill $BACKEND_PID 2>/dev/null || true
exit 1
fi
cd "$PROJECT_ROOT"
echo ""
# ============================================================================
# 9. VERIFY SERVERS ARE RUNNING
# ============================================================================
echo "${BLUE}Step 8: Verifying servers...${NC}"
# Wait a moment for servers to fully initialize
sleep 3
# Check if backend responds
if curl -s http://localhost:${BACKEND_PORT}/server-check.php > /dev/null 2>&1; then
echo "${GREEN} ✓ Backend is responding${NC}"
else
echo "${YELLOW} ⚠ Backend may not be fully ready yet${NC}"
fi
# Check if frontend responds
if curl -s http://localhost:${FRONTEND_PORT} > /dev/null 2>&1; then
echo "${GREEN} ✓ Frontend is responding${NC}"
else
echo "${YELLOW} ⚠ Frontend may not be fully ready yet${NC}"
fi
echo ""
# ============================================================================
# 10. CREATE STOP SCRIPT
# ============================================================================
# Create a stop script for easy shutdown
cat > "${PROJECT_ROOT}/stop.sh" << 'EOF'
#!/bin/bash
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
echo -e "${YELLOW}🛑 Stopping Lux Studio servers...${NC}"
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Stop backend
if [ -f "${PROJECT_ROOT}/.backend.pid" ]; then
BACKEND_PID=$(cat "${PROJECT_ROOT}/.backend.pid")
if ps -p $BACKEND_PID > /dev/null 2>&1; then
kill $BACKEND_PID 2>/dev/null
echo -e "${GREEN}✓ Backend server stopped (PID: $BACKEND_PID)${NC}"
else
echo -e "${YELLOW}⚠ Backend server not running${NC}"
fi
rm "${PROJECT_ROOT}/.backend.pid"
fi
# Stop frontend
if [ -f "${PROJECT_ROOT}/.frontend.pid" ]; then
FRONTEND_PID=$(cat "${PROJECT_ROOT}/.frontend.pid")
if ps -p $FRONTEND_PID > /dev/null 2>&1; then
kill $FRONTEND_PID 2>/dev/null
echo -e "${GREEN}✓ Frontend server stopped (PID: $FRONTEND_PID)${NC}"
else
echo -e "${YELLOW}⚠ Frontend server not running${NC}"
fi
rm "${PROJECT_ROOT}/.frontend.pid"
fi
# Also kill any remaining processes on the ports (cleanup)
BACKEND_PORT=$(grep -E "^BACKEND_PORT=" backend/.env 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
FRONTEND_PORT=$(grep -E "^FRONTEND_PORT=" frontend/.env 2>/dev/null | cut -d'=' -f2 | tr -d ' ')
BACKEND_PORT=${BACKEND_PORT:-5015}
FRONTEND_PORT=${FRONTEND_PORT:-3000}
# Kill any remaining processes on ports
for port in $BACKEND_PORT $FRONTEND_PORT; do
pid=$(lsof -ti:$port 2>/dev/null)
if [ ! -z "$pid" ]; then
kill -9 $pid 2>/dev/null || true
echo -e "${GREEN}✓ Cleaned up process on port $port${NC}"
fi
done
echo -e "${GREEN}✅ All servers stopped${NC}"
EOF
chmod +x "${PROJECT_ROOT}/stop.sh"
# ============================================================================
# 11. SUCCESS MESSAGE
# ============================================================================
echo "${GREEN}════════════════════════════════════════════════════════════${NC}"
echo "${GREEN}🎉 Lux Studio is Running!${NC}"
echo "${GREEN}════════════════════════════════════════════════════════════${NC}"
echo ""
echo "${BLUE}🌐 Application URLs:${NC}"
echo " • Frontend: ${GREEN}http://localhost:${FRONTEND_PORT}${NC}"
echo " • Backend API: ${GREEN}http://localhost:${BACKEND_PORT}${NC}"
echo ""
echo "${BLUE}📊 Server Status:${NC}"
echo " • Backend PID: $BACKEND_PID (port $BACKEND_PORT)"
echo " • Frontend PID: $FRONTEND_PID (port $FRONTEND_PORT)"
echo ""
echo "${BLUE}📝 Log Files:${NC}"
echo " • Backend: ${PROJECT_ROOT}/backend-server.log"
echo " • Frontend: ${PROJECT_ROOT}/frontend-server.log"
echo " • View backend: ${GREEN}tail -f ${PROJECT_ROOT}/backend-server.log${NC}"
echo " • View frontend: ${GREEN}tail -f ${PROJECT_ROOT}/frontend-server.log${NC}"
echo ""
echo "${BLUE}🛑 Stop Servers:${NC}"
echo " ${GREEN}./stop.sh${NC}"
echo ""
echo "${BLUE}⚙️ Configuration:${NC}"
echo " • SSO: Enabled with LOCAL credentials"
echo " • SSO Client ID: 15c0c4e2-bac0-4564-a3a6-c2717f00a6d9 (local dev)"
echo " • API Key: Configured in .env files"
echo ""
echo "${BLUE}📚 Documentation:${NC}"
echo " • CLAUDE.md - Developer guide"
echo " • MDFiles/README.md - User guide"
echo " • MDFiles/AUTH_README.md - SSO guide"
echo ""
echo "${YELLOW}💡 Tip: Open http://localhost:${FRONTEND_PORT} in your browser!${NC}"
echo ""
echo "Happy coding! 🚀"

108
status.sh
View file

@ -1,108 +0,0 @@
#!/bin/bash
# ============================================================================
# Lux Studio - Server Status Check Script
# ============================================================================
# Colors
GREEN='\033[0;32m'
BLUE='\033[0;34m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# Read ports from .env files
BACKEND_PORT=5015
FRONTEND_PORT=3000
if [ -f "backend/.env" ]; then
BACKEND_PORT=$(grep -E "^BACKEND_PORT=" backend/.env | cut -d'=' -f2 | tr -d ' ')
fi
if [ -f "frontend/.env" ]; then
FRONTEND_PORT=$(grep -E "^FRONTEND_PORT=" frontend/.env | cut -d'=' -f2 | tr -d ' ')
fi
BACKEND_PORT=${BACKEND_PORT:-5015}
FRONTEND_PORT=${FRONTEND_PORT:-3000}
echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}"
echo -e "${BLUE}📊 Lux Studio Server Status${NC}"
echo -e "${BLUE}════════════════════════════════════════════════════════════${NC}"
echo ""
# Check Backend
echo -e "${BLUE}🔧 Backend Server (Port ${BACKEND_PORT}):${NC}"
if [ -f "${PROJECT_ROOT}/.backend.pid" ]; then
BACKEND_PID=$(cat "${PROJECT_ROOT}/.backend.pid")
if ps -p $BACKEND_PID > /dev/null 2>&1; then
echo -e " Status: ${GREEN}✓ Running${NC}"
echo -e " PID: $BACKEND_PID"
if curl -s http://localhost:${BACKEND_PORT}/server-check.php > /dev/null 2>&1; then
echo -e " Health: ${GREEN}✓ Responding${NC}"
else
echo -e " Health: ${YELLOW}⚠ Not responding${NC}"
fi
else
echo -e " Status: ${RED}✗ Not running (stale PID file)${NC}"
fi
else
echo -e " Status: ${RED}✗ Not running${NC}"
fi
echo ""
# Check Frontend
echo -e "${BLUE}🎨 Frontend Server (Port ${FRONTEND_PORT}):${NC}"
if [ -f "${PROJECT_ROOT}/.frontend.pid" ]; then
FRONTEND_PID=$(cat "${PROJECT_ROOT}/.frontend.pid")
if ps -p $FRONTEND_PID > /dev/null 2>&1; then
echo -e " Status: ${GREEN}✓ Running${NC}"
echo -e " PID: $FRONTEND_PID"
if curl -s http://localhost:${FRONTEND_PORT} > /dev/null 2>&1; then
echo -e " Health: ${GREEN}✓ Responding${NC}"
else
echo -e " Health: ${YELLOW}⚠ Not responding${NC}"
fi
else
echo -e " Status: ${RED}✗ Not running (stale PID file)${NC}"
fi
else
echo -e " Status: ${RED}✗ Not running${NC}"
fi
echo ""
# URLs
echo -e "${BLUE}🌐 Application URLs:${NC}"
echo -e " • Frontend: ${GREEN}http://localhost:${FRONTEND_PORT}${NC}"
echo -e " • Backend API: ${GREEN}http://localhost:${BACKEND_PORT}${NC}"
echo ""
# Log files
echo -e "${BLUE}📝 Log Files:${NC}"
if [ -f "${PROJECT_ROOT}/backend-server.log" ]; then
echo -e " • Backend: ${GREEN}${PROJECT_ROOT}/backend-server.log${NC}"
echo -e " Last 3 lines:"
tail -3 "${PROJECT_ROOT}/backend-server.log" | sed 's/^/ /'
else
echo -e " • Backend: ${YELLOW}No log file${NC}"
fi
echo ""
if [ -f "${PROJECT_ROOT}/frontend-server.log" ]; then
echo -e " • Frontend: ${GREEN}${PROJECT_ROOT}/frontend-server.log${NC}"
echo -e " Last 3 lines:"
tail -3 "${PROJECT_ROOT}/frontend-server.log" | sed 's/^/ /'
else
echo -e " • Frontend: ${YELLOW}No log file${NC}"
fi
echo ""
# Commands
echo -e "${BLUE}💡 Useful Commands:${NC}"
echo -e " • View backend logs: ${GREEN}tail -f backend-server.log${NC}"
echo -e " • View frontend logs: ${GREEN}tail -f frontend-server.log${NC}"
echo -e " • Stop servers: ${GREEN}./stop.sh${NC}"
echo -e " • Restart: ${GREEN}./stop.sh && ./setup.sh${NC}"
echo ""

591
video_api.php.backup Normal file
View file

@ -0,0 +1,591 @@
<?php
/**
* Video Generation API for Cinema Studio Pro
* Handles Veo 3.1 video generation via Gemini API
*/
// Suppress HTML error output to prevent breaking JSON responses
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
// Handle preflight requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// Load configuration
require_once 'config.php';
class VeoVideoAPI {
private $apiKey;
private $baseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
private $model;
// Available models
private static $models = [
'standard' => 'veo-3.1-generate-preview',
'fast' => 'veo-3.1-fast-generate-preview'
];
// Storage for pending operations
private $operationsFile;
public function __construct($apiKey, $modelType = 'standard') {
$this->apiKey = $apiKey;
$this->model = self::$models[$modelType] ?? self::$models['standard'];
$this->operationsFile = __DIR__ . '/video_operations.json';
}
/**
* Generate a video using Veo 3.1
* Returns an operation ID for async polling
*/
public function generateVideo($prompt, $duration = 4, $aspectRatio = '16:9', $resolution = '720p', $generateAudio = true, $referenceImages = [], $referenceMode = 'frame') {
// Build the instance object
$instance = [
'prompt' => $prompt
];
// Frame mode: first image is starting frame, optional second image is last frame for interpolation
if (!empty($referenceImages)) {
if (isset($referenceImages[0])) {
$refImg = $referenceImages[0];
$data = preg_replace('/\s+/', '', $refImg['data']);
if (preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $data)) {
$instance['image'] = [
'bytesBase64Encoded' => $data,
'mimeType' => $refImg['mime_type'] ?? 'image/jpeg'
];
error_log("Added first frame for I2V generation");
}
}
// Add last frame for interpolation (when 2 images provided)
if (count($referenceImages) >= 2 && isset($referenceImages[1])) {
$lastImg = $referenceImages[1];
$lastData = preg_replace('/\s+/', '', $lastImg['data']);
if (preg_match('/^[A-Za-z0-9+\/]+={0,2}$/', $lastData)) {
$instance['lastFrame'] = [
'bytesBase64Encoded' => $lastData,
'mimeType' => $lastImg['mime_type'] ?? 'image/jpeg'
];
error_log("Added last frame for video interpolation");
}
}
}
// Build parameters
$parameters = [
'aspectRatio' => $aspectRatio,
'sampleCount' => 1
];
// Duration: Veo 3.1 supports 4, 6, or 8 seconds
if (in_array(intval($duration), [4, 6, 8])) {
$parameters['durationSeconds'] = intval($duration);
} else {
$parameters['durationSeconds'] = 4;
}
// Note: generateAudio is handled automatically by Veo 3.1
// The model generates audio natively based on the scene
// No need to explicitly pass this parameter
$payload = [
'instances' => [$instance],
'parameters' => $parameters
];
// Log payload structure (without full base64 data for readability)
$logPayload = $payload;
if (isset($logPayload['instances'][0]['image'])) {
$logPayload['instances'][0]['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['image']['bytesBase64Encoded']) . '_bytes]';
}
if (isset($logPayload['instances'][0]['lastFrame'])) {
$logPayload['instances'][0]['lastFrame']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['lastFrame']['bytesBase64Encoded']) . '_bytes]';
}
if (isset($logPayload['instances'][0]['referenceImages'])) {
foreach ($logPayload['instances'][0]['referenceImages'] as $i => &$refImg) {
$refImg['image']['bytesBase64Encoded'] = '[BASE64_DATA_' . strlen($payload['instances'][0]['referenceImages'][$i]['image']['bytesBase64Encoded']) . '_bytes]';
}
}
error_log("Video generation payload structure: " . json_encode($logPayload));
return $this->makeRequest($payload);
}
/**
* Make API request to Veo predictLongRunning endpoint
*/
private function makeRequest($payload, $retryCount = 0) {
// Veo uses predictLongRunning for async video generation
$url = "{$this->baseUrl}/{$this->model}:predictLongRunning";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-goog-api-key: ' . $this->apiKey
],
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 600 // 10 minute timeout for video generation
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if (curl_errno($ch)) {
$error = curl_error($ch);
@curl_close($ch);
throw new Exception('cURL error: ' . $error);
}
@curl_close($ch);
error_log("Video API Response Code: $httpCode");
error_log("Video API Response: " . substr($response, 0, 1000));
if ($httpCode !== 200) {
$errorData = json_decode($response, true);
$errorMessage = $errorData['error']['message'] ?? "HTTP $httpCode";
$errorStatus = $errorData['error']['status'] ?? 'UNKNOWN';
error_log("Video API Error - HTTP $httpCode (Status: $errorStatus)");
error_log("Error message: " . $errorMessage);
// Handle specific error types
if ($httpCode === 500 && stripos($errorMessage, 'internal') !== false && $retryCount < 2) {
error_log("Retrying video request due to internal error (attempt " . ($retryCount + 1) . ")");
sleep(5); // Wait 5 seconds before retry
return $this->makeRequest($payload, $retryCount + 1);
}
if ($httpCode === 429 || $errorStatus === 'RESOURCE_EXHAUSTED') {
throw new Exception("API rate limit exceeded. Please wait a moment and try again. Video generation is expensive (~\$0.75/second).");
}
if ($errorStatus === 'INVALID_ARGUMENT') {
throw new Exception("Invalid request format. Check your prompt and settings.");
}
if (stripos($errorMessage, 'model') !== false && stripos($errorMessage, 'not found') !== false) {
throw new Exception("Veo 3.1 model not available. You may need to enable it in your Google AI Studio account.");
}
throw new Exception("API error: $errorMessage (HTTP $httpCode, Status: $errorStatus)");
}
return json_decode($response, true);
}
/**
* Extract video data from API response
*/
public function extractVideoData($response) {
error_log("Extracting video data from response: " . json_encode(array_keys($response)));
// Check for completed operation with video response (from polling)
// IMPORTANT: Check this FIRST before the operation check, because completed
// operations also have a 'name' field but we want to extract the video data
if (isset($response['done']) && $response['done'] === true) {
// Check for error in operation
if (isset($response['error'])) {
$errorMsg = $response['error']['message'] ?? 'Unknown error';
throw new Exception("Video generation failed: $errorMsg");
}
// Extract video from generateVideoResponse format
// Structure: response.generateVideoResponse.generatedSamples[0].video.uri
$videoResponse = $response['response'] ?? $response;
error_log("videoResponse keys: " . json_encode(array_keys($videoResponse)));
$generateVideoResponse = $videoResponse['generateVideoResponse'] ?? null;
error_log("generateVideoResponse: " . ($generateVideoResponse ? 'found' : 'null'));
if ($generateVideoResponse && isset($generateVideoResponse['generatedSamples'])) {
$samples = $generateVideoResponse['generatedSamples'];
error_log("samples count: " . count($samples));
if (!empty($samples) && isset($samples[0]['video'])) {
$video = $samples[0]['video'];
error_log("video keys: " . json_encode(array_keys($video)));
// Check for video URI
if (isset($video['uri'])) {
error_log("Found video URI: " . $video['uri']);
return [
'url' => $video['uri'],
'mime_type' => 'video/mp4',
'type' => 'uri'
];
}
// Check for inline base64 data
if (isset($video['bytesBase64Encoded'])) {
return [
'base64' => $video['bytesBase64Encoded'],
'mime_type' => $video['mimeType'] ?? 'video/mp4',
'type' => 'inline'
];
}
}
}
}
// Check for long-running operation (initial response from predictLongRunning)
// This is for when the operation is NOT yet complete
if (isset($response['name']) && strpos($response['name'], 'operations/') !== false) {
// Only return operation type if not done
if (!isset($response['done']) || $response['done'] !== true) {
return [
'operationId' => $response['name'],
'type' => 'operation',
'done' => $response['done'] ?? false
];
}
}
// Legacy format: Check for finish reasons that indicate content issues
if (isset($response['candidates'][0]['finishReason'])) {
$finishReason = $response['candidates'][0]['finishReason'];
$finishMessage = $response['candidates'][0]['finishMessage'] ?? '';
if ($finishReason === 'SAFETY') {
throw new Exception('Video generation blocked by safety filters. Please try a different prompt.');
}
if ($finishReason !== 'STOP' && !empty($finishMessage)) {
throw new Exception('Video generation failed: ' . $finishMessage);
}
}
// Legacy format: Look for video data in candidates
if (isset($response['candidates'][0]['content']['parts'])) {
foreach ($response['candidates'][0]['content']['parts'] as $part) {
if (isset($part['inline_data']['data'])) {
return [
'base64' => $part['inline_data']['data'],
'mime_type' => $part['inline_data']['mime_type'] ?? 'video/mp4',
'type' => 'inline'
];
}
if (isset($part['videoMetadata']['videoUri'])) {
return [
'url' => $part['videoMetadata']['videoUri'],
'mime_type' => 'video/mp4',
'type' => 'uri'
];
}
}
}
$errorDetails = "Response structure: " . json_encode(array_keys($response));
if (isset($response['response'])) {
$errorDetails .= " | Response keys: " . json_encode(array_keys($response['response']));
}
throw new Exception('No video data found in API response. ' . $errorDetails);
}
/**
* Check status of a long-running operation
*/
public function checkOperationStatus($operationId) {
$url = "https://generativelanguage.googleapis.com/v1beta/{$operationId}";
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'x-goog-api-key: ' . $this->apiKey
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
@curl_close($ch);
if ($httpCode !== 200) {
throw new Exception("Failed to check operation status: HTTP $httpCode");
}
return json_decode($response, true);
}
/**
* Save video to disk
*/
public function saveVideo($base64Data, $mimeType = 'video/mp4') {
$videoDir = __DIR__ . '/generated_videos';
if (!is_dir($videoDir)) {
mkdir($videoDir, 0755, true);
}
$extension = $mimeType === 'video/webm' ? 'webm' : 'mp4';
$filename = 'video_' . time() . '_' . bin2hex(random_bytes(4)) . '.' . $extension;
$filepath = $videoDir . '/' . $filename;
$decoded = base64_decode($base64Data);
if ($decoded === false) {
throw new Exception('Failed to decode video data');
}
file_put_contents($filepath, $decoded);
return $filename;
}
}
// Handle API requests
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('Invalid request method');
}
$action = $_POST['action'] ?? null;
if (!$action) {
throw new Exception('No action specified');
}
// Check if API key is configured
if (!defined('GEMINI_API_KEY') || empty(GEMINI_API_KEY)) {
throw new Exception('API key not configured. Please set GEMINI_API_KEY in config.php');
}
// Get model type (standard or fast)
$modelType = $_POST['modelType'] ?? 'standard';
if (!in_array($modelType, ['standard', 'fast'])) {
$modelType = 'standard';
}
$api = new VeoVideoAPI(GEMINI_API_KEY, $modelType);
// Handle generate action
if ($action === 'generate') {
$prompt = $_POST['prompt'] ?? null;
$duration = intval($_POST['duration'] ?? 4);
$aspectRatio = $_POST['aspectRatio'] ?? '16:9';
$resolution = $_POST['resolution'] ?? '720p';
$generateAudio = ($_POST['generateAudio'] ?? 'true') === 'true';
$referenceMode = $_POST['referenceMode'] ?? 'frame';
// Validate reference mode
if (!in_array($referenceMode, ['frame', 'subject'])) {
$referenceMode = 'frame';
}
// Collect reference images (up to 3)
$referenceImages = [];
$refCount = intval($_POST['referenceImageCount'] ?? 0);
for ($i = 0; $i < min($refCount, 3); $i++) {
if (isset($_POST["referenceImage_$i"])) {
$referenceImages[] = [
'data' => $_POST["referenceImage_$i"],
'mime_type' => $_POST["referenceImageType_$i"] ?? 'image/jpeg'
];
}
}
if (!$prompt) {
throw new Exception('Prompt is required');
}
// Validate duration
if (!in_array($duration, [4, 6, 8])) {
$duration = 4;
}
// Validate aspect ratio
if (!in_array($aspectRatio, ['16:9', '9:16'])) {
$aspectRatio = '16:9';
}
error_log("Starting video generation: duration=$duration, aspect=$aspectRatio, audio=$generateAudio, refMode=$referenceMode, refCount=" . count($referenceImages));
// Generate video
$response = $api->generateVideo($prompt, $duration, $aspectRatio, $resolution, $generateAudio, $referenceImages, $referenceMode);
$videoData = $api->extractVideoData($response);
// Handle different response types
if ($videoData['type'] === 'operation') {
// Long-running operation - return operation ID for polling
echo json_encode([
'success' => true,
'status' => 'pending',
'operationId' => $videoData['operationId'],
'message' => 'Video generation started. Poll for status.'
]);
} else if ($videoData['type'] === 'inline') {
// Direct response with video data
$filename = $api->saveVideo($videoData['base64'], $videoData['mime_type']);
echo json_encode([
'success' => true,
'status' => 'complete',
'video' => [
'filename' => $filename,
'mime_type' => $videoData['mime_type'],
'url' => '/api/stream_video.php?file=' . urlencode($filename)
]
]);
} else if ($videoData['type'] === 'uri') {
// Video available at URI
echo json_encode([
'success' => true,
'status' => 'complete',
'video' => [
'url' => $videoData['url'],
'mime_type' => $videoData['mime_type']
]
]);
}
exit;
}
// Handle check status action
if ($action === 'check_status') {
$operationId = $_POST['operationId'] ?? null;
if (!$operationId) {
throw new Exception('Operation ID is required');
}
$status = $api->checkOperationStatus($operationId);
if (isset($status['done']) && $status['done'] === true) {
// Operation complete - extract video
// Pass the full status object so extractVideoData can find the video data
$videoData = $api->extractVideoData($status);
if ($videoData['type'] === 'inline') {
$filename = $api->saveVideo($videoData['base64'], $videoData['mime_type']);
echo json_encode([
'success' => true,
'status' => 'complete',
'video' => [
'filename' => $filename,
'mime_type' => $videoData['mime_type'],
'url' => '/api/stream_video.php?file=' . urlencode($filename)
]
]);
} else {
echo json_encode([
'success' => true,
'status' => 'complete',
'video' => [
'url' => $videoData['url'] ?? null,
'mime_type' => $videoData['mime_type']
]
]);
}
} else {
// Still processing
$progress = 0;
if (isset($status['metadata']['progress'])) {
$progress = floatval($status['metadata']['progress']) * 100;
}
echo json_encode([
'success' => true,
'status' => 'pending',
'progress' => $progress,
'message' => 'Video generation in progress...'
]);
}
exit;
}
// Handle download_video action - proxy the video download with authentication
if ($action === 'download_video') {
$videoUrl = $_POST['videoUrl'] ?? $_GET['videoUrl'] ?? null;
if (!$videoUrl) {
throw new Exception('Video URL is required');
}
// Validate URL is from Google APIs
if (strpos($videoUrl, 'generativelanguage.googleapis.com') === false) {
throw new Exception('Invalid video URL');
}
// Download the video with API key authentication
$ch = curl_init($videoUrl);
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => [
'x-goog-api-key: ' . GEMINI_API_KEY
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 120
]);
$videoData = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$contentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
@curl_close($ch);
if ($httpCode !== 200 || !$videoData) {
throw new Exception("Failed to download video: HTTP $httpCode");
}
// Save the video locally
$filename = 'video_' . date('Ymd_His') . '_' . bin2hex(random_bytes(4)) . '.mp4';
$videoDir = __DIR__ . '/generated_videos';
if (!is_dir($videoDir)) {
mkdir($videoDir, 0755, true);
}
$filepath = $videoDir . '/' . $filename;
file_put_contents($filepath, $videoData);
// Return local URL
echo json_encode([
'success' => true,
'video' => [
'url' => '/api/stream_video.php?file=' . urlencode($filename),
'filename' => $filename,
'mime_type' => 'video/mp4'
]
]);
exit;
}
throw new Exception('Invalid action');
} catch (Exception $e) {
http_response_code(500);
error_log("Exception in video_api.php: " . $e->getMessage());
error_log("Stack trace: " . $e->getTraceAsString());
echo json_encode([
'success' => false,
'error' => $e->getMessage(),
'debug' => [
'file' => basename($e->getFile()),
'line' => $e->getLine(),
'timestamp' => date('Y-m-d H:i:s')
]
]);
exit;
}