From 7c972e8cd82e4bbefe2b80516c404f13da736456 Mon Sep 17 00:00:00 2001 From: Manish Tanwar Date: Fri, 10 Oct 2025 18:00:36 +0530 Subject: [PATCH] Update: Video Number, Refresh button, Video Process View, Generate Button, Job Queue Process: The video queue limit goes for 4. Later once have to clean the queue to request the videos again. Video queue will be visible once history is created else it will take all requests for videos togther --- CLAUDE.md | 97 +++++- backend/routes/api.py | 6 +- frontend/src/components/QueueManager.jsx | 341 ++++++++++++++------- frontend/src/components/VideoForm.jsx | 31 +- frontend/src/components/VideoGenerator.jsx | 38 ++- frontend/src/utils/constants.js | 8 +- 6 files changed, 363 insertions(+), 158 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b3ca25d..d465298 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,11 +14,57 @@ This is a full-stack web application for integrating Google's Veo 3.0 video gene ### Key Features - Text-to-video and image-to-video generation - Dual model support (Veo 3.0 and Veo 3.0 Fast) +- Multi-process job queue system (max 2 concurrent jobs per instance) - Real-time progress tracking with job status polling - Azure AD SSO authentication (with dev mode bypass) - Automatic file cleanup and download management - Usage tracking via webhook integration +## Common Commands + +### Development +```bash +# Start both frontend and backend in development mode +./run-dev.sh + +# Start backend only +cd backend && python app.py + +# Start frontend only +cd frontend && npm run dev + +# Build frontend for production +cd frontend && npm run build + +# Frontend linting +cd frontend && npm run lint +``` + +### Backend Commands +```bash +# Install Python dependencies +cd backend && pip install -r requirements.txt + +# Create virtual environment +python -m venv venv && source venv/bin/activate # Linux/Mac +python -m venv venv && venv\Scripts\activate # Windows + +# Run backend in debug mode +cd backend && FLASK_DEBUG=True python app.py +``` + +### Production Deployment +```bash +# Backend systemd service management +sudo systemctl start veo-video-generator +sudo systemctl stop veo-video-generator +sudo systemctl restart veo-video-generator +sudo systemctl status veo-video-generator + +# View service logs +sudo journalctl -u veo-video-generator -f +``` + ## Project Structure ``` @@ -120,23 +166,52 @@ npm run dev - `@azure/msal-react==2.0.7` - Microsoft authentication - `vite==5.0.8` - Build tool +## Core Architecture + +### Job Queue System +The application implements a multi-process job queue system in `backend/video_generator.py`: +- **Global queue**: `job_queue` array stores pending job IDs +- **Processing jobs**: `processing_jobs` tracks active jobs (max 2 concurrent via `CONCURRENT_JOB_LIMIT`) +- **User limits**: `user_job_counts` enforces max 4 jobs per user (`MAX_QUEUE_SIZE_PER_USER`) +- **Job status**: In-memory `job_status` dictionary tracks all job states (consider Redis for production scaling) + +### Backend Request Flow +1. **Job creation**: `generate_video_task()` validates input, creates UUID, queues job +2. **Queue processing**: `process_job_queue()` thread picks jobs from queue +3. **Video generation**: `generate_video()` calls Google Gen AI SDK with threading +4. **Status updates**: Jobs transition through: queued → processing → polling → completed/failed +5. **File management**: Videos downloaded to `temp_downloads/`, cleaned up after user download + +### Authentication Architecture +- **Production**: Azure AD SSO via MSAL (`@azure/msal-react`) +- **Development**: Auth bypass when `VITE_DEV_MODE=true` +- **Backend validation**: No auth validation on backend (frontend-only) + +### Storage Architecture +- **Temporary images**: Uploaded to GCS `temp_images/` bucket path, cleaned up after processing +- **Generated videos**: Downloaded from GCS to local `temp_downloads/` directory +- **Automatic cleanup**: Configurable delays (15s small files, 30s large files) + ## Video Generation Flow 1. **User submits request** via React frontend (prompt + optional image + parameters) -2. **Backend creates job** with unique UUID and initializes status tracking -3. **Image processing** (if provided): validate, convert to JPEG, upload to GCS -4. **API call to Google** using Gen AI SDK with Veo 3.0 model -5. **Backend polls** long-running operation every 30 seconds -6. **Frontend polls** status endpoint every 2 seconds for progress updates -7. **Video download** from GCS to backend temp storage when complete -8. **User downloads** video and backend automatically cleans up temp files +2. **Job queuing**: Backend creates job with UUID, adds to global queue, enforces user limits +3. **Queue processing**: Background thread picks next job when slot available +4. **Image processing** (if provided): validate, convert to JPEG, upload to GCS +5. **API call to Google**: Gen AI SDK call with Veo 3.0 model in separate thread +6. **Backend polling**: Long-running operation polled every 30 seconds +7. **Frontend polling**: Status endpoint polled every 2 seconds for progress updates +8. **Video download**: Completed video downloaded from GCS to local temp storage +9. **User download**: Video served to user and temp files cleaned up automatically ## Important Implementation Details -- **Job tracking**: In-memory dictionary (`job_status`) stores generation status (consider Redis for production scaling) -- **Authentication**: Azure AD SSO in production, bypassed in dev mode (`VITE_DEV_MODE=true`) +- **Concurrent processing**: Max 2 simultaneous video generations per backend instance +- **Queue management**: FIFO queue with per-user limits to prevent abuse +- **Error handling**: Max 3 retries per job with exponential backoff - **File cleanup**: Automatic deletion after download, configurable cleanup endpoint -- **CORS**: Development allows localhost, production restricts to specific domain +- **CORS**: Development allows localhost, production restricts to specific domain - **Image formats**: All Pillow-supported formats accepted, automatically converted to JPEG - **Model selection**: UI allows choosing between Veo 3.0 (high-quality) and Veo 3.0 Fast (optimized) -- **Usage tracking**: Optional webhook integration sends generation data to external endpoint \ No newline at end of file +- **Usage tracking**: Optional webhook integration sends generation data to external endpoint +- **Threading**: Job processing and GCS operations use threading for non-blocking execution \ No newline at end of file diff --git a/backend/routes/api.py b/backend/routes/api.py index 6452a97..d3ab305 100644 --- a/backend/routes/api.py +++ b/backend/routes/api.py @@ -183,15 +183,15 @@ def generate_video(): print(f"DEBUG: Error cleaning up image: {e}") return jsonify({'error': 'Seed must be a number between 0 and 4294967295'}), 400 - # Validate sample count (increased limit to 10) - if not isinstance(sample_count, int) or sample_count < 1 or sample_count > 10: + # Validate sample count (limit to 4) + if not isinstance(sample_count, int) or sample_count < 1 or sample_count > 4: if image_path and os.path.exists(image_path): try: os.remove(image_path) os.rmdir(os.path.join(Config.TEMP_DOWNLOAD_PATH, f"job_{job_id}")) except Exception as e: print(f"DEBUG: Error cleaning up image: {e}") - return jsonify({'error': 'Sample count must be between 1 and 10'}), 400 + return jsonify({'error': 'Sample count must be between 1 and 4'}), 400 # Check queue limit for user if not can_add_to_queue(user_email): diff --git a/frontend/src/components/QueueManager.jsx b/frontend/src/components/QueueManager.jsx index 914fcb6..a1e411b 100644 --- a/frontend/src/components/QueueManager.jsx +++ b/frontend/src/components/QueueManager.jsx @@ -12,7 +12,8 @@ import { Divider, Alert, IconButton, - Tooltip + Tooltip, + GlobalStyles } from '@mui/material'; import { CheckCircleRounded, @@ -30,6 +31,7 @@ const QueueManager = ({ userEmail }) => { const [jobs, setJobs] = useState([]); const [queueStatus, setQueueStatus] = useState({}); const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); const [error, setError] = useState(''); const fetchUserJobs = async () => { @@ -106,6 +108,28 @@ const QueueManager = ({ userEmail }) => { return job.progress || 0; }; + const isActiveJob = (job) => { + const activeStatuses = [ + JOB_STATUS.STARTING, + JOB_STATUS.UPLOADING_IMAGE, + JOB_STATUS.GENERATING, + JOB_STATUS.PROCESSING, + JOB_STATUS.DOWNLOADING, + 'retry_1_of_3', + 'retry_2_of_3', + 'retry_3_of_3' + ]; + return activeStatuses.includes(job.status) || job.status.startsWith('retry_'); + }; + + const getMostRecentActiveJob = () => { + return jobs.find(job => isActiveJob(job)); + }; + + const getHistoricalJobs = () => { + return jobs.filter(job => !isActiveJob(job)); + }; + const renderDownloadButtons = (job) => { if (job.status !== JOB_STATUS.COMPLETED) return null; @@ -146,6 +170,140 @@ const QueueManager = ({ userEmail }) => { return {buttons}; }; + const renderJobItem = (job, isActive = false) => { + return ( + + + + {getStatusIcon(job.status)} + + {job.videos_requested} video{job.videos_requested > 1 ? 's' : ''} + + + {job.status === JOB_STATUS.QUEUED && job.queue_position > 0 && ( + + )} + + + + {job.can_cancel && job.status === JOB_STATUS.QUEUED && ( + + )} + + + + + {job.prompt.length > 100 ? `${job.prompt.substring(0, 100)}...` : job.prompt} + + + + {job.message || 'Processing...'} + + + {/* Enhanced Progress Bar for Active Jobs */} + + + {isActive && ( + + {getProgressValue(job)}% complete + + )} + + {/* Download Buttons */} + {renderDownloadButtons(job)} + + {/* Job Details */} + + + + Created: {new Date(job.created_at).toLocaleString()} + + + {job.started_at && ( + + + Started: {new Date(job.started_at).toLocaleString()} + + + )} + + + Model: {job.model_name?.includes('fast') ? 'Fast' : 'Standard'} + + + + + Length: {job.video_length_sec}s + + + + + {/* Retry Information */} + {job.retry_count > 0 && ( + + Retry attempt {job.retry_count} of {job.max_retries} +
+ {job.status.includes('retry') && job.message && ( + {job.message} + )} + {job.error && job.error.includes('timeout') && ( +
+ Note: This is likely due to network connectivity issues. The system will automatically retry with longer timeouts. +
+ )} +
+ )} + + {/* Final Failure Information */} + {job.final_failure && ( + + Failed after {job.retry_count} retries. {job.error} + + )} +
+ ); + }; + if (loading) { return ( @@ -156,12 +314,26 @@ const QueueManager = ({ userEmail }) => { } return ( - - + <> + + + Job Queue - { fetchUserJobs(); fetchQueueStatusData(); }}> - + { + setRefreshing(true); + await Promise.all([fetchUserJobs(), fetchQueueStatusData()]); + setRefreshing(false); + }} + disabled={refreshing} + > + @@ -181,123 +353,58 @@ const QueueManager = ({ userEmail }) => { )} - {jobs.length === 0 ? ( +{jobs.length === 0 ? ( No jobs found. Submit a video generation request to see it here. ) : ( - - {jobs.map((job, index) => ( - - - - - {getStatusIcon(job.status)} - - {job.videos_requested} video{job.videos_requested > 1 ? 's' : ''} - - - {job.status === JOB_STATUS.QUEUED && job.queue_position > 0 && ( - - )} - - - - {job.can_cancel && job.status === JOB_STATUS.QUEUED && ( - - )} - - - - - {job.prompt.length > 100 ? `${job.prompt.substring(0, 100)}...` : job.prompt} - - - - {job.message || 'Processing...'} - - - {/* Progress Bar */} - - - {/* Download Buttons */} - {renderDownloadButtons(job)} - - {/* Job Details */} - - - - Created: {new Date(job.created_at).toLocaleString()} - - - {job.started_at && ( - - - Started: {new Date(job.started_at).toLocaleString()} - - - )} - - - Model: {job.model_name?.includes('fast') ? 'Fast' : 'Standard'} - - - - - Length: {job.video_length_sec}s - - - - - {/* Retry Information */} - {job.retry_count > 0 && ( - - Retry attempt {job.retry_count} of {job.max_retries} -
- {job.status.includes('retry') && job.message && ( - {job.message} - )} - {job.error && job.error.includes('timeout') && ( -
- Note: This is likely due to network connectivity issues. The system will automatically retry with longer timeouts. -
- )} -
- )} - - {/* Final Failure Information */} - {job.final_failure && ( - - Failed after {job.retry_count} retries. {job.error} - - )} -
- {index < jobs.length - 1 && } -
- ))} -
+ <> + {/* Active Job Section */} + {getMostRecentActiveJob() && ( + <> + + Current Video Generation + + + {renderJobItem(getMostRecentActiveJob(), true)} + + + {getHistoricalJobs().length > 0 && ( + <> + + + Job History + + + )} + + )} + + {/* Historical Jobs Section */} + {getHistoricalJobs().length > 0 && ( + + {getHistoricalJobs().map((job, index) => ( + + {renderJobItem(job, false)} + {index < getHistoricalJobs().length - 1 && } + + ))} + + )} + )} -
+
+ ); }; diff --git a/frontend/src/components/VideoForm.jsx b/frontend/src/components/VideoForm.jsx index d1a3151..3755bf8 100644 --- a/frontend/src/components/VideoForm.jsx +++ b/frontend/src/components/VideoForm.jsx @@ -14,7 +14,8 @@ import { AccordionSummary, AccordionDetails, FormControlLabel, - Checkbox + Checkbox, + Alert } from '@mui/material'; import { PlayArrowRounded, @@ -26,7 +27,7 @@ import { } from '@mui/icons-material'; import { VIDEO_GENERATION_OPTIONS, IMAGE_UPLOAD_CONFIG } from '../utils/constants'; -const VideoForm = ({ onSubmit, isGenerating }) => { +const VideoForm = ({ onSubmit, isGenerating, userJobs = [], queueLimit = 4 }) => { const [formData, setFormData] = useState({ prompt: '', video_length_sec: 8, @@ -130,8 +131,13 @@ const VideoForm = ({ onSubmit, isGenerating }) => { newErrors.seed = 'Seed must be a number between 0 and 4294967295'; } - if (formData.sampleCount < 1 || formData.sampleCount > 10) { - newErrors.sampleCount = 'Sample count must be between 1 and 10'; + if (formData.sampleCount < 1 || formData.sampleCount > 4) { + newErrors.sampleCount = 'Sample count must be between 1 and 4'; + } + + // Check queue limit + if (userJobs.length >= queueLimit) { + newErrors.queue = `Maximum ${queueLimit} jobs allowed. Please wait for completion or cancel existing jobs.`; } @@ -410,18 +416,31 @@ const VideoForm = ({ onSubmit, isGenerating }) => { + {errors.queue && ( + + {errors.queue} + + )} + + Queue: {userJobs.length}/{queueLimit} jobs + diff --git a/frontend/src/components/VideoGenerator.jsx b/frontend/src/components/VideoGenerator.jsx index 0177996..d3da643 100644 --- a/frontend/src/components/VideoGenerator.jsx +++ b/frontend/src/components/VideoGenerator.jsx @@ -1,11 +1,11 @@ -import React from 'react'; +import React, { useState, useEffect } from 'react'; import { Box, Grid } from '@mui/material'; import { useIsAuthenticated, useMsal } from '@azure/msal-react'; import VideoForm from './VideoForm'; -import ProgressIndicator from './ProgressIndicator'; import QueueManager from './QueueManager'; import { useVideoGeneration } from '../hooks/useVideoGeneration'; import { useMockIsAuthenticated, useMockMsal } from './DevAuthWrapper'; +import { getUserJobs } from '../services/api'; const VideoGenerator = () => { const isDev = import.meta.env.VITE_DEV_MODE === 'true'; @@ -13,6 +13,8 @@ const VideoGenerator = () => { const isAuthenticated = isDev ? useMockIsAuthenticated() : useIsAuthenticated(); const { accounts } = isDev ? useMockMsal() : useMsal(); + const [userJobs, setUserJobs] = useState([]); + const { isGenerating, status, @@ -37,6 +39,24 @@ const VideoGenerator = () => { return 'anonymous'; }; + const fetchUserJobs = async () => { + try { + const userEmail = getUserEmail(); + const data = await getUserJobs(userEmail); + setUserJobs(data.jobs || []); + } catch (err) { + console.error('Failed to fetch user jobs:', err); + } + }; + + useEffect(() => { + fetchUserJobs(); + + // Poll for job updates every 2 seconds + const interval = setInterval(fetchUserJobs, 2000); + return () => clearInterval(interval); + }, []); + const handleFormSubmit = (videoConfig) => { const userEmail = getUserEmail(); startGeneration(videoConfig, userEmail); @@ -50,19 +70,9 @@ const VideoGenerator = () => { - - - - {/* Right Column - Queue Manager */} diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index e102703..e52f08d 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -37,13 +37,7 @@ export const VIDEO_GENERATION_OPTIONS = { { value: 1, label: '1 video' }, { value: 2, label: '2 videos' }, { value: 3, label: '3 videos' }, - { value: 4, label: '4 videos' }, - { value: 5, label: '5 videos' }, - { value: 6, label: '6 videos' }, - { value: 7, label: '7 videos' }, - { value: 8, label: '8 videos' }, - { value: 9, label: '9 videos' }, - { value: 10, label: '10 videos' } + { value: 4, label: '4 videos' } ] };