Add web interface for VideoGen

Features:
- Modern web UI with all generation modes (T2V, I2V, T2I, I2I, V2V, Dub, Subtitles, Upscale)
- Real-time progress updates via WebSocket
- File upload for input images/videos/audio
- File download for generated content
- Background job processing with progress tracking
- Job management (cancel, retry, delete)
- Gallery for browsing generated files
- REST API for programmatic access
- Responsive design for desktop and mobile

Backend (webapp.py):
- Flask + Flask-SocketIO for real-time updates
- Background job processing with threading
- File upload/download handling
- Job state persistence
- REST API endpoints

Frontend:
- Modern dark theme UI
- Mode selection with visual cards
- Form with all options and settings
- Real-time progress modal with log streaming
- Toast notifications
- Keyboard shortcuts (Ctrl+Enter to submit, Escape to close)

Documentation:
- Updated README.md with web interface section
- Updated EXAMPLES.md with web interface usage
- Updated requirements.txt with web dependencies
parent 344cd12a
...@@ -8,24 +8,25 @@ This document contains comprehensive examples for using the VideoGen toolkit, co ...@@ -8,24 +8,25 @@ This document contains comprehensive examples for using the VideoGen toolkit, co
1. [Basic Usage](#basic-usage) 1. [Basic Usage](#basic-usage)
2. [Auto Mode](#auto-mode) 2. [Auto Mode](#auto-mode)
3. [Text-to-Video (T2V)](#text-to-video-t2v) 3. [Web Interface](#web-interface)
4. [Image-to-Video (I2V)](#image-to-video-i2v) 4. [Text-to-Video (T2V)](#text-to-video-t2v)
5. [Text-to-Image (T2I)](#text-to-image-t2i) 5. [Image-to-Video (I2V)](#image-to-video-i2v)
6. [Image-to-Image (I2I)](#image-to-image-i2i) 6. [Text-to-Image (T2I)](#text-to-image-t2i)
7. [Video-to-Video (V2V)](#video-to-video-v2v) 7. [Image-to-Image (I2I)](#image-to-image-i2i)
8. [Video-to-Image (V2I)](#video-to-image-v2i) 8. [Video-to-Video (V2V)](#video-to-video-v2v)
9. [2D-to-3D Conversion](#2d-to-3d-conversion) 9. [Video-to-Image (V2I)](#video-to-image-v2i)
10. [Audio Generation](#audio-generation) 10. [2D-to-3D Conversion](#2d-to-3d-conversion)
11. [Lip Sync](#lip-sync) 11. [Audio Generation](#audio-generation)
12. [Video Dubbing & Translation](#video-dubbing--translation) 12. [Lip Sync](#lip-sync)
13. [Subtitle Generation](#subtitle-generation) 13. [Video Dubbing & Translation](#video-dubbing--translation)
14. [Character Consistency](#character-consistency) 14. [Subtitle Generation](#subtitle-generation)
15. [Distributed Multi-GPU](#distributed-multi-gpu) 15. [Character Consistency](#character-consistency)
16. [Model Management](#model-management) 16. [Distributed Multi-GPU](#distributed-multi-gpu)
17. [VRAM Management](#vram-management) 17. [Model Management](#model-management)
18. [Upscaling](#upscaling) 18. [VRAM Management](#vram-management)
19. [NSFW Content](#nsfw-content) 19. [Upscaling](#upscaling)
20. [Advanced Combinations](#advanced-combinations) 20. [NSFW Content](#nsfw-content)
21. [Advanced Combinations](#advanced-combinations)
--- ---
...@@ -59,6 +60,145 @@ python3 videogen --auto --prefer-speed --prompt "quick animation test" ...@@ -59,6 +60,145 @@ python3 videogen --auto --prefer-speed --prompt "quick animation test"
--- ---
## Web Interface
VideoGen includes a modern web interface for easy access to all features without using the command line.
### Starting the Web Server
```bash
# Start on default port (5000)
python3 webapp.py
# Start on custom port
python3 webapp.py --port 8080
# Start accessible from network
python3 webapp.py --host 0.0.0.0 --port 5000
# Start with debug mode
python3 webapp.py --debug
```
### Web Interface Features
The web interface provides access to all VideoGen features:
1. **Generate Tab**
- Mode selection (T2V, I2V, T2I, I2I, V2V, Dub, Subtitles, Upscale)
- Prompt input with quick hints
- Model selection with auto-mode option
- File upload for input images/videos/audio
- Resolution, FPS, duration settings
- Audio options (TTS, music, sync, lip-sync)
- Translation/dubbing options
- Advanced settings (offloading, VRAM limit, debug)
2. **Jobs Tab**
- Real-time job monitoring
- Progress bars with status text
- Output log streaming
- Cancel/retry/delete jobs
- Download generated files
3. **Gallery Tab**
- Browse all generated files
- Preview videos and images
- Download files
- Delete files
4. **Settings Tab**
- Server configuration
- Default values
- About information
### Using the Web Interface
1. **Start the server:**
```bash
python3 webapp.py
```
2. **Open in browser:**
Navigate to `http://localhost:5000`
3. **Select a mode:**
Click on the desired generation mode (T2V, I2V, etc.)
4. **Enter prompt:**
Type your prompt or use the quick hints
5. **Configure settings:**
Select model, resolution, duration, etc.
6. **Upload files (if needed):**
For I2I, V2V, dubbing - upload input files
7. **Click Generate:**
Watch real-time progress in the modal
8. **Download result:**
From the modal, jobs list, or gallery
### Web Interface API
The web interface also provides a REST API:
```bash
# Get model list
curl http://localhost:5000/api/models
# Get TTS voices
curl http://localhost:5000/api/tts-voices
# Get languages
curl http://localhost:5000/api/languages
# Upload a file
curl -X POST -F "file=@image.png" -F "type=image" http://localhost:5000/api/upload
# Create a job
curl -X POST -H "Content-Type: application/json" \
-d '{"mode":"t2v","prompt":"a cat playing","model":"wan_1.3b_t2v"}' \
http://localhost:5000/api/jobs
# Get job status
curl http://localhost:5000/api/jobs/JOB_ID
# Cancel a job
curl -X POST http://localhost:5000/api/jobs/JOB_ID/cancel
# List outputs
curl http://localhost:5000/api/outputs
# Download a file
curl http://localhost:5000/api/download/filename.mp4 -o output.mp4
```
### WebSocket Events
For real-time updates, connect via Socket.IO:
```javascript
const socket = io('http://localhost:5000');
// Subscribe to job updates
socket.emit('subscribe_job', 'JOB_ID');
// Receive job updates
socket.on('job_update', (job) => {
console.log('Progress:', job.progress);
console.log('Status:', job.status);
});
// Receive log lines
socket.on('job_log', (data) => {
console.log('Log:', data.line);
});
```
---
## Auto Mode ## Auto Mode
Auto mode analyzes your prompt and automatically: Auto mode analyzes your prompt and automatically:
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
**Copyleft © 2026 Stefy <stefy@nexlab.net>** **Copyleft © 2026 Stefy <stefy@nexlab.net>**
A comprehensive, GPU-accelerated video generation toolkit supporting Text-to-Video (T2V), Image-to-Video (I2V), Text-to-Image (T2I), Image-to-Image (I2I), Video-to-Video (V2V), Video-to-Image (V2I), and 2D-to-3D conversion with audio synthesis, synchronization, and lip-sync capabilities. A comprehensive, GPU-accelerated video generation toolkit supporting Text-to-Video (T2V), Image-to-Video (I2V), Text-to-Image (T2I), Image-to-Image (I2I), Video-to-Video (V2V), Video-to-Image (V2I), and 2D-to-3D conversion with audio synthesis, synchronization, lip-sync, dubbing, and translation capabilities.
--- ---
...@@ -34,6 +34,13 @@ A comprehensive, GPU-accelerated video generation toolkit supporting Text-to-Vid ...@@ -34,6 +34,13 @@ A comprehensive, GPU-accelerated video generation toolkit supporting Text-to-Vid
- **Audio Sync**: Match audio duration to video (stretch, trim, pad, loop) - **Audio Sync**: Match audio duration to video (stretch, trim, pad, loop)
- **Lip Sync**: Wav2Lip and SadTalker integration - **Lip Sync**: Wav2Lip and SadTalker integration
### Video Dubbing & Translation
- **Video Dubbing**: Translate and dub videos while preserving original voice
- **Voice Cloning**: Preserve speaker's voice in translated video
- **Automatic Subtitles**: Generate subtitles using Whisper
- **Subtitle Translation**: Translate subtitles to 20+ languages
- **Subtitle Burning**: Burn subtitles directly into video
### Model Support ### Model Support
- **Small Models** (<16GB VRAM): Wan 1.3B, Zeroscope, ModelScope - **Small Models** (<16GB VRAM): Wan 1.3B, Zeroscope, ModelScope
- **Medium Models** (16-30GB VRAM): Wan 14B, CogVideoX, Mochi - **Medium Models** (16-30GB VRAM): Wan 14B, CogVideoX, Mochi
...@@ -44,12 +51,14 @@ A comprehensive, GPU-accelerated video generation toolkit supporting Text-to-Vid ...@@ -44,12 +51,14 @@ A comprehensive, GPU-accelerated video generation toolkit supporting Text-to-Vid
- **Auto Mode**: Automatic model selection and configuration - **Auto Mode**: Automatic model selection and configuration
- **NSFW Detection**: Automatic content classification - **NSFW Detection**: Automatic content classification
- **Prompt Splitting**: Intelligent I2V prompt separation - **Prompt Splitting**: Intelligent I2V prompt separation
- **Time Estimation**: Predict generation time before starting - **Time Estimation**: Hardware-aware generation time prediction
- **Multi-GPU**: Distributed generation across multiple GPUs - **Multi-GPU**: Distributed generation across multiple GPUs
- **Auto-Disable**: Models that fail 3 times are auto-disabled
### AI Integration ### User Interfaces
- **Command Line**: Full-featured CLI with all options
- **Web Interface**: Modern web UI with real-time progress updates
- **MCP Server**: Model Context Protocol wrapper for AI agents - **MCP Server**: Model Context Protocol wrapper for AI agents
- **Skill Documentation**: Comprehensive AI agent integration guide
--- ---
...@@ -77,6 +86,11 @@ pip install opencv-python face-recognition dlib --break-system-packages ...@@ -77,6 +86,11 @@ pip install opencv-python face-recognition dlib --break-system-packages
git clone https://github.com/Rudrabha/Wav2Lip.git git clone https://github.com/Rudrabha/Wav2Lip.git
``` ```
### Web Interface
```bash
pip install flask flask-cors flask-socketio eventlet
```
### MCP Server (For AI Agents) ### MCP Server (For AI Agents)
```bash ```bash
pip install mcp pip install mcp
...@@ -187,6 +201,38 @@ See [SKILL.md](SKILL.md) for comprehensive AI agent integration guide including: ...@@ -187,6 +201,38 @@ See [SKILL.md](SKILL.md) for comprehensive AI agent integration guide including:
--- ---
## Web Interface
VideoGen includes a modern web interface for easy access to all features:
### Starting the Web Server
```bash
python3 webapp.py --port 5000 --host 0.0.0.0
```
Then open `http://localhost:5000` in your browser.
### Web Interface Features
- **All Generation Modes**: T2V, I2V, T2I, I2I, V2V, Upscale, Dubbing, Subtitles
- **Real-time Progress**: Live progress updates with output log streaming
- **File Upload/Download**: Upload images, videos, audio; download generated content
- **Model Selection**: Browse and select from all available models
- **Job Management**: View, cancel, retry, and track all generation jobs
- **Gallery**: Browse and download all generated files
- **Responsive Design**: Works on desktop and mobile devices
### Web Interface Screenshots
The web interface provides:
1. **Generate Tab**: Main generation form with all options
2. **Jobs Tab**: Real-time job monitoring with progress bars
3. **Gallery Tab**: Browse and download generated content
4. **Settings Tab**: Configuration and about information
---
## Documentation ## Documentation
- **[EXAMPLES.md](EXAMPLES.md)**: Comprehensive command-line examples for all features - **[EXAMPLES.md](EXAMPLES.md)**: Comprehensive command-line examples for all features
...@@ -266,6 +312,12 @@ export CUDA_VISIBLE_DEVICES=0,1 # GPU selection ...@@ -266,6 +312,12 @@ export CUDA_VISIBLE_DEVICES=0,1 # GPU selection
``` ```
videogen/ videogen/
├── videogen # Main script ├── videogen # Main script
├── webapp.py # Web interface server
├── templates/ # HTML templates
│ └── index.html # Main web UI
├── static/ # Static assets
│ ├── css/style.css # Styles
│ └── js/app.js # JavaScript
├── videogen_mcp_server.py # MCP server for AI agents ├── videogen_mcp_server.py # MCP server for AI agents
├── README.md # This file ├── README.md # This file
├── EXAMPLES.md # Comprehensive examples ├── EXAMPLES.md # Comprehensive examples
......
...@@ -55,5 +55,16 @@ pydantic>=2.0.0 ...@@ -55,5 +55,16 @@ pydantic>=2.0.0
# Distributed Processing # Distributed Processing
# accelerate # Already listed above # accelerate # Already listed above
# Web Interface Dependencies (Optional - for webapp.py)
flask>=3.0.0
flask-cors>=4.0.0
flask-socketio>=5.3.0
eventlet>=0.33.0
python-socketio>=5.10.0
werkzeug>=3.0.0
# MCP Server Dependencies (Optional - for AI agent integration)
# mcp>=0.9.0 # Install with: pip install mcp
# Optional: NSFW Classification # Optional: NSFW Classification
# onnxruntime>=1.16.0 # onnxruntime>=1.16.0
\ No newline at end of file
/* VideoGen Web Interface Styles */
:root {
--primary: #6366f1;
--primary-dark: #4f46e5;
--primary-light: #818cf8;
--secondary: #64748b;
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
--info: #3b82f6;
--bg-dark: #0f172a;
--bg-darker: #020617;
--bg-card: #1e293b;
--bg-input: #334155;
--text-primary: #f8fafc;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--border-color: #334155;
--border-radius: 8px;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
line-height: 1.6;
}
/* Header */
.header {
background: var(--bg-darker);
border-bottom: 1px solid var(--border-color);
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
max-width: 1400px;
margin: 0 auto;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 1.5rem;
font-weight: 700;
}
.logo i {
color: var(--primary);
font-size: 1.75rem;
}
.logo .version {
font-size: 0.75rem;
color: var(--text-muted);
font-weight: 400;
margin-left: 0.5rem;
}
.nav {
display: flex;
gap: 0.5rem;
}
.nav-btn {
background: transparent;
border: none;
color: var(--text-secondary);
padding: 0.75rem 1.25rem;
border-radius: var(--border-radius);
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
transition: all 0.2s;
}
.nav-btn:hover {
background: var(--bg-card);
color: var(--text-primary);
}
.nav-btn.active {
background: var(--primary);
color: white;
}
.nav-btn .badge {
background: var(--danger);
color: white;
font-size: 0.7rem;
padding: 0.15rem 0.4rem;
border-radius: 10px;
margin-left: 0.25rem;
}
/* Main Content */
.main {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.tab {
display: none;
}
.tab.active {
display: block;
}
/* Mode Selector */
.mode-selector {
margin-bottom: 2rem;
}
.mode-selector h2 {
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.mode-buttons {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
}
.mode-btn {
background: var(--bg-card);
border: 2px solid var(--border-color);
border-radius: var(--border-radius);
padding: 1.25rem;
cursor: pointer;
text-align: center;
transition: all 0.2s;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.mode-btn i {
font-size: 1.5rem;
color: var(--primary);
}
.mode-btn span {
font-weight: 600;
color: var(--text-primary);
}
.mode-btn small {
color: var(--text-muted);
font-size: 0.75rem;
}
.mode-btn:hover {
border-color: var(--primary);
transform: translateY(-2px);
}
.mode-btn.active {
border-color: var(--primary);
background: rgba(99, 102, 241, 0.1);
}
/* Form Styles */
.generate-form {
background: var(--bg-card);
border-radius: var(--border-radius);
padding: 2rem;
}
.form-section {
margin-bottom: 1.5rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.form-section:last-of-type {
border-bottom: none;
}
.form-section h3 {
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text-primary);
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.form-group label {
font-size: 0.875rem;
color: var(--text-secondary);
font-weight: 500;
}
.form-group input,
.form-group select,
.form-group textarea {
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 0.75rem 1rem;
color: var(--text-primary);
font-size: 0.9rem;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: var(--primary);
}
.form-group textarea {
resize: vertical;
min-height: 80px;
}
/* Prompt Container */
.prompt-container {
margin-bottom: 1rem;
}
.prompt-container textarea {
width: 100%;
min-height: 100px;
font-size: 1rem;
}
.prompt-hints {
display: flex;
gap: 0.5rem;
margin-top: 0.5rem;
flex-wrap: wrap;
}
.hint {
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: 20px;
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
}
.hint:hover {
border-color: var(--primary);
color: var(--primary);
}
/* Checkbox */
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: var(--primary);
}
/* File Upload */
.file-uploads {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.file-upload {
position: relative;
}
.file-upload input[type="file"] {
position: absolute;
opacity: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.file-label {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 2rem;
background: var(--bg-input);
border: 2px dashed var(--border-color);
border-radius: var(--border-radius);
cursor: pointer;
transition: all 0.2s;
}
.file-label i {
font-size: 2rem;
color: var(--primary);
}
.file-label span {
font-weight: 500;
}
.file-label small {
color: var(--text-muted);
}
.file-upload:hover .file-label {
border-color: var(--primary);
background: rgba(99, 102, 241, 0.1);
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1.5rem;
border: none;
border-radius: var(--border-radius);
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-secondary {
background: var(--bg-input);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: var(--bg-card);
}
.btn-danger {
background: var(--danger);
color: white;
}
.btn-danger:hover {
background: #dc2626;
}
.btn-success {
background: var(--success);
color: white;
}
.btn-large {
padding: 1rem 2rem;
font-size: 1rem;
}
.btn-small {
padding: 0.5rem 0.75rem;
font-size: 0.8rem;
}
.form-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border-color);
}
/* Collapsible Section */
.collapsible h3 {
cursor: pointer;
user-select: none;
}
.collapsible .toggle-icon {
margin-left: auto;
transition: transform 0.3s;
}
.collapsible.collapsed .toggle-icon {
transform: rotate(-90deg);
}
.collapsible .section-content {
max-height: 500px;
overflow: hidden;
transition: max-height 0.3s;
}
.collapsible.collapsed .section-content {
max-height: 0;
}
/* Jobs List */
.jobs-container {
background: var(--bg-card);
border-radius: var(--border-radius);
padding: 2rem;
}
.jobs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.jobs-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.job-card {
background: var(--bg-input);
border-radius: var(--border-radius);
padding: 1.5rem;
border: 1px solid var(--border-color);
}
.job-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.job-id {
font-family: monospace;
color: var(--text-muted);
font-size: 0.8rem;
}
.job-status {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
}
.job-status.pending {
background: rgba(251, 191, 36, 0.2);
color: var(--warning);
}
.job-status.running {
background: rgba(59, 130, 246, 0.2);
color: var(--info);
}
.job-status.completed {
background: rgba(34, 197, 94, 0.2);
color: var(--success);
}
.job-status.failed {
background: rgba(239, 68, 68, 0.2);
color: var(--danger);
}
.job-status.cancelled {
background: rgba(100, 116, 139, 0.2);
color: var(--secondary);
}
.job-command {
background: var(--bg-darker);
padding: 0.75rem;
border-radius: 4px;
font-family: monospace;
font-size: 0.8rem;
color: var(--text-secondary);
margin-bottom: 1rem;
overflow-x: auto;
white-space: nowrap;
}
.job-progress {
margin-bottom: 1rem;
}
.job-progress-bar {
height: 8px;
background: var(--bg-darker);
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.job-progress-fill {
height: 100%;
background: var(--primary);
transition: width 0.3s;
}
.job-progress-text {
font-size: 0.8rem;
color: var(--text-muted);
}
.job-actions {
display: flex;
gap: 0.5rem;
}
/* Gallery */
.gallery-container {
background: var(--bg-card);
border-radius: var(--border-radius);
padding: 2rem;
}
.gallery-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 1.5rem;
}
.gallery-item {
background: var(--bg-input);
border-radius: var(--border-radius);
overflow: hidden;
border: 1px solid var(--border-color);
transition: transform 0.2s;
}
.gallery-item:hover {
transform: translateY(-4px);
}
.gallery-preview {
aspect-ratio: 16/9;
background: var(--bg-darker);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.gallery-preview img,
.gallery-preview video {
width: 100%;
height: 100%;
object-fit: cover;
}
.gallery-preview i {
font-size: 3rem;
color: var(--text-muted);
}
.gallery-info {
padding: 1rem;
}
.gallery-info h4 {
font-size: 0.9rem;
margin-bottom: 0.25rem;
word-break: break-all;
}
.gallery-info p {
font-size: 0.75rem;
color: var(--text-muted);
}
.gallery-actions {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
}
/* Modal */
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal.active {
display: flex;
}
.modal-content {
background: var(--bg-card);
border-radius: var(--border-radius);
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--border-color);
}
.modal-header h3 {
display: flex;
align-items: center;
gap: 0.5rem;
}
.close-btn {
background: none;
border: none;
color: var(--text-secondary);
font-size: 1.5rem;
cursor: pointer;
}
.modal-body {
padding: 1.5rem;
flex: 1;
overflow-y: auto;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
padding: 1.5rem;
border-top: 1px solid var(--border-color);
}
/* Progress Container */
.progress-container {
margin-bottom: 1.5rem;
}
.progress-bar {
height: 24px;
background: var(--bg-darker);
border-radius: 12px;
overflow: hidden;
margin-bottom: 0.75rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--primary-light));
transition: width 0.3s;
border-radius: 12px;
}
.progress-text {
font-size: 0.9rem;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
.progress-percent {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
}
/* Log Container */
.log-container {
background: var(--bg-darker);
border-radius: var(--border-radius);
overflow: hidden;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem 1rem;
background: var(--bg-input);
font-size: 0.8rem;
color: var(--text-secondary);
}
.log-content {
max-height: 200px;
overflow-y: auto;
padding: 1rem;
font-family: monospace;
font-size: 0.75rem;
line-height: 1.4;
}
.log-content .log-line {
color: var(--text-muted);
margin-bottom: 0.25rem;
}
.log-content .log-line.error {
color: var(--danger);
}
.log-content .log-line.success {
color: var(--success);
}
.log-content.collapsed {
max-height: 0;
padding: 0 1rem;
}
/* Toast Notifications */
#toast-container {
position: fixed;
bottom: 2rem;
right: 2rem;
z-index: 2000;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.toast {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
padding: 1rem 1.5rem;
display: flex;
align-items: center;
gap: 0.75rem;
box-shadow: var(--shadow-lg);
animation: slideIn 0.3s ease;
}
.toast.success {
border-color: var(--success);
}
.toast.success i {
color: var(--success);
}
.toast.error {
border-color: var(--danger);
}
.toast.error i {
color: var(--danger);
}
.toast.info {
border-color: var(--info);
}
.toast.info i {
color: var(--info);
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Empty State */
.empty-state {
text-align: center;
padding: 4rem 2rem;
color: var(--text-muted);
}
.empty-state i {
font-size: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state p {
font-size: 1.1rem;
}
/* Hidden */
.hidden {
display: none !important;
}
/* Responsive */
@media (max-width: 768px) {
.header-content {
flex-direction: column;
gap: 1rem;
}
.nav {
flex-wrap: wrap;
justify-content: center;
}
.main {
padding: 1rem;
}
.mode-buttons {
grid-template-columns: repeat(2, 1fr);
}
.form-row {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
}
.form-actions .btn {
width: 100%;
justify-content: center;
}
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-darker);
}
::-webkit-scrollbar-thumb {
background: var(--border-color);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--secondary);
}
/* Animations */
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
.pulse {
animation: pulse 2s infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.fa-spin {
animation: spin 1s linear infinite;
}
\ No newline at end of file
// VideoGen Web Interface - JavaScript Application
// Global state
let socket = null;
let currentJobId = null;
let models = [];
let ttsVoices = [];
let languages = [];
let currentMode = 't2v';
// Initialize application
document.addEventListener('DOMContentLoaded', () => {
initializeSocket();
loadModels();
loadTTSVoices();
loadLanguages();
setupEventListeners();
loadJobs();
refreshGallery();
// Set API endpoint display
document.getElementById('api-endpoint').value = window.location.origin;
});
// Socket.IO initialization
function initializeSocket() {
socket = io();
socket.on('connect', () => {
console.log('Connected to server');
showToast('Connected to server', 'success');
});
socket.on('disconnect', () => {
console.log('Disconnected from server');
showToast('Disconnected from server', 'error');
});
socket.on('job_update', (job) => {
updateJobInList(job);
updateProgressModal(job);
updateJobsBadge();
});
socket.on('job_log', (data) => {
appendLog(data.line);
});
}
// Load models from API
async function loadModels() {
try {
const response = await fetch('/api/models');
models = await response.json();
const modelSelect = document.getElementById('model');
const imageModelSelect = document.getElementById('image_model');
modelSelect.innerHTML = '<option value="">Select a model...</option>';
imageModelSelect.innerHTML = '<option value="">Select a model...</option>';
models.forEach(model => {
const option = document.createElement('option');
option.value = model.name;
option.textContent = `${model.name} (${model.type || 'video'})`;
option.dataset.type = model.type;
modelSelect.appendChild(option);
// Add image models to image model select
if (model.type === 'image' || model.type === 't2i') {
const imgOption = option.cloneNode(true);
imageModelSelect.appendChild(imgOption);
}
});
} catch (error) {
console.error('Error loading models:', error);
showToast('Failed to load models', 'error');
}
}
// Load TTS voices
async function loadTTSVoices() {
try {
const response = await fetch('/api/tts-voices');
ttsVoices = await response.json();
const select = document.getElementById('tts_voice');
select.innerHTML = '<option value="">Select a voice...</option>';
ttsVoices.forEach(voice => {
const option = document.createElement('option');
option.value = voice.id;
option.textContent = voice.name;
select.appendChild(option);
});
} catch (error) {
console.error('Error loading TTS voices:', error);
}
}
// Load languages
async function loadLanguages() {
try {
const response = await fetch('/api/languages');
languages = await response.json();
const sourceSelect = document.getElementById('source_lang');
const targetSelect = document.getElementById('target_lang');
languages.forEach(lang => {
const sourceOption = document.createElement('option');
sourceOption.value = lang.code;
sourceOption.textContent = lang.name;
sourceSelect.appendChild(sourceOption);
const targetOption = sourceOption.cloneNode(true);
targetSelect.appendChild(targetOption);
});
} catch (error) {
console.error('Error loading languages:', error);
}
}
// Setup event listeners
function setupEventListeners() {
// Tab navigation
document.querySelectorAll('.nav-btn').forEach(btn => {
btn.addEventListener('click', () => switchTab(btn.dataset.tab));
});
// Mode selection
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.addEventListener('click', () => switchMode(btn.dataset.mode));
});
// Form submission
document.getElementById('generate-form').addEventListener('submit', handleGenerate);
// Strength slider
document.getElementById('strength').addEventListener('input', (e) => {
document.getElementById('strength-value').textContent = e.target.value;
});
}
// Switch tab
function switchTab(tabName) {
document.querySelectorAll('.nav-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabName);
});
document.querySelectorAll('.tab').forEach(tab => {
tab.classList.toggle('active', tab.id === `tab-${tabName}`);
});
if (tabName === 'jobs') {
loadJobs();
} else if (tabName === 'gallery') {
refreshGallery();
}
}
// Switch generation mode
function switchMode(mode) {
currentMode = mode;
document.querySelectorAll('.mode-btn').forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
// Show/hide relevant sections
const i2vPrompts = document.getElementById('i2v-prompts');
const imageModelGroup = document.getElementById('image-model-group');
const imageUploadBox = document.getElementById('image-upload-box');
const videoUploadBox = document.getElementById('video-upload-box');
const audioUploadBox = document.getElementById('audio-upload-box');
const audioSection = document.getElementById('audio-section');
const dubbingSection = document.getElementById('dubbing-section');
const subtitleSection = document.getElementById('subtitle-section');
const strengthGroup = document.getElementById('strength-group');
// Reset all sections
i2vPrompts.classList.add('hidden');
imageModelGroup.classList.add('hidden');
imageUploadBox.classList.remove('hidden');
videoUploadBox.classList.add('hidden');
audioUploadBox.classList.add('hidden');
audioSection.classList.remove('hidden');
dubbingSection.classList.add('hidden');
subtitleSection.classList.add('hidden');
strengthGroup.classList.add('hidden');
// Configure for each mode
switch (mode) {
case 't2v':
imageUploadBox.classList.add('hidden');
break;
case 'i2v':
i2vPrompts.classList.remove('hidden');
imageModelGroup.classList.remove('hidden');
imageUploadBox.classList.add('hidden'); // Will generate image first
break;
case 't2i':
imageUploadBox.classList.add('hidden');
audioSection.classList.add('hidden');
break;
case 'i2i':
strengthGroup.classList.remove('hidden');
break;
case 'v2v':
imageUploadBox.classList.add('hidden');
videoUploadBox.classList.remove('hidden');
strengthGroup.classList.remove('hidden');
break;
case 'dub':
imageUploadBox.classList.add('hidden');
videoUploadBox.classList.remove('hidden');
audioSection.classList.add('hidden');
dubbingSection.classList.remove('hidden');
break;
case 'subtitles':
imageUploadBox.classList.add('hidden');
videoUploadBox.classList.remove('hidden');
audioSection.classList.add('hidden');
subtitleSection.classList.remove('hidden');
break;
case 'upscale':
imageUploadBox.classList.add('hidden');
videoUploadBox.classList.remove('hidden');
audioSection.classList.add('hidden');
break;
}
}
// Handle file upload
async function handleFileUpload(input, type) {
const file = input.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
formData.append('type', type);
try {
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await response.json();
if (response.ok) {
// Update the hidden input with the file path
const hiddenInput = document.getElementById(`input_${type}`);
if (hiddenInput) {
hiddenInput.value = data.path;
}
// Update the file name display
const fileNameDisplay = document.getElementById(`${type}-file-name`);
if (fileNameDisplay) {
fileNameDisplay.textContent = file.name;
}
showToast(`File uploaded: ${file.name}`, 'success');
} else {
showToast(`Upload failed: ${data.error}`, 'error');
}
} catch (error) {
console.error('Upload error:', error);
showToast('Upload failed', 'error');
}
}
// Handle form submission
async function handleGenerate(e) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
const params = {};
// Convert FormData to object
for (let [key, value] of formData.entries()) {
if (value !== '' && value !== null) {
params[key] = value;
}
}
// Add mode
params.mode = currentMode;
// Handle checkboxes
params.auto = form.querySelector('#auto')?.checked || false;
params.generate_audio = form.querySelector('#generate_audio')?.checked || false;
params.sync_audio = form.querySelector('#sync_audio')?.checked || false;
params.lip_sync = form.querySelector('#lip_sync')?.checked || false;
params.no_filter = form.querySelector('#no_filter')?.checked || false;
params.debug = form.querySelector('#debug')?.checked || false;
params.voice_clone = form.querySelector('#voice_clone')?.checked || false;
params.create_subtitles = form.querySelector('#create_subtitles')?.checked || false;
params.translate_subtitles = form.querySelector('#translate_subtitles')?.checked || false;
params.burn_subtitles = form.querySelector('#burn_subtitles')?.checked || false;
// Convert numeric values
params.width = parseInt(params.width) || 832;
params.height = parseInt(params.height) || 480;
params.fps = parseInt(params.fps) || 15;
params.length = parseFloat(params.length) || 5;
params.seed = parseInt(params.seed) || -1;
params.vram_limit = parseInt(params.vram_limit) || 22;
params.strength = parseFloat(params.strength) || 0.7;
// Mode-specific settings
if (currentMode === 'i2i' || currentMode === 'v2v') {
params.image_to_image = currentMode === 'i2i';
params.video_to_video = currentMode === 'v2v';
}
if (currentMode === 't2i') {
params.generate_image = true;
}
if (currentMode === 'dub') {
params.dub_video = true;
}
if (currentMode === 'upscale') {
params.upscale = true;
}
try {
const response = await fetch('/api/jobs', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(params)
});
const job = await response.json();
if (response.ok) {
currentJobId = job.id;
socket.emit('subscribe_job', job.id);
showProgressModal(job);
showToast('Generation started', 'info');
loadJobs();
} else {
showToast(`Failed to start job: ${job.error}`, 'error');
}
} catch (error) {
console.error('Error starting job:', error);
showToast('Failed to start generation', 'error');
}
}
// Load jobs
async function loadJobs() {
try {
const response = await fetch('/api/jobs');
const jobs = await response.json();
const jobsList = document.getElementById('jobs-list');
if (jobs.length === 0) {
jobsList.innerHTML = `
<div class="empty-state">
<i class="fas fa-inbox"></i>
<p>No jobs yet. Start generating!</p>
</div>
`;
return;
}
jobsList.innerHTML = jobs.map(job => createJobCard(job)).join('');
updateJobsBadge();
} catch (error) {
console.error('Error loading jobs:', error);
}
}
// Create job card HTML
function createJobCard(job) {
const statusIcons = {
pending: 'fa-clock',
running: 'fa-spinner fa-spin',
completed: 'fa-check',
failed: 'fa-times',
cancelled: 'fa-ban'
};
const createdDate = new Date(job.created_at).toLocaleString();
let actionsHtml = '';
if (job.status === 'running') {
actionsHtml = `<button class="btn btn-small btn-danger" onclick="cancelJob('${job.id}')">
<i class="fas fa-stop"></i> Cancel
</button>`;
} else if (job.status === 'failed' || job.status === 'cancelled') {
actionsHtml = `<button class="btn btn-small btn-primary" onclick="retryJob('${job.id}')">
<i class="fas fa-redo"></i> Retry
</button>`;
}
if (job.output_files && job.output_files.length > 0) {
actionsHtml += job.output_files.map(file => `
<a href="/api/download/${file}" class="btn btn-small btn-success" download>
<i class="fas fa-download"></i> ${file}
</a>
`).join('');
}
return `
<div class="job-card" id="job-${job.id}">
<div class="job-header">
<div>
<span class="job-id">Job #${job.id}</span>
<p style="color: var(--text-muted); font-size: 0.8rem;">${createdDate}</p>
</div>
<span class="job-status ${job.status}">
<i class="fas ${statusIcons[job.status]}"></i>
${job.status.toUpperCase()}
</span>
</div>
<div class="job-command">${escapeHtml(job.command)}</div>
${job.status === 'running' ? `
<div class="job-progress">
<div class="job-progress-bar">
<div class="job-progress-fill" style="width: ${job.progress}%"></div>
</div>
<div class="job-progress-text">${escapeHtml(job.progress_text || 'Processing...')}</div>
</div>
` : ''}
${job.error ? `<p style="color: var(--danger); font-size: 0.85rem;"><i class="fas fa-exclamation-triangle"></i> ${escapeHtml(job.error)}</p>` : ''}
<div class="job-actions">
${actionsHtml}
<button class="btn btn-small btn-secondary" onclick="deleteJob('${job.id}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
}
// Update job in list
function updateJobInList(job) {
const existingCard = document.getElementById(`job-${job.id}`);
if (existingCard) {
const newCard = document.createElement('div');
newCard.innerHTML = createJobCard(job);
existingCard.replaceWith(newCard.firstElementChild);
} else {
loadJobs();
}
}
// Update jobs badge
function updateJobsBadge() {
const runningJobs = Object.values(jobs || {}).filter(j => j.status === 'running' || j.status === 'pending').length;
document.getElementById('jobs-badge').textContent = runningJobs;
}
// Cancel job
async function cancelJob(jobId) {
if (!jobId) jobId = currentJobId;
if (!jobId) return;
try {
const response = await fetch(`/api/jobs/${jobId}/cancel`, {
method: 'POST'
});
if (response.ok) {
showToast('Job cancelled', 'warning');
closeProgressModal();
loadJobs();
}
} catch (error) {
console.error('Error cancelling job:', error);
}
}
// Retry job
async function retryJob(jobId) {
try {
const response = await fetch(`/api/jobs/${jobId}/retry`, {
method: 'POST'
});
const job = await response.json();
if (response.ok) {
currentJobId = job.id;
socket.emit('subscribe_job', job.id);
showProgressModal(job);
showToast('Job restarted', 'info');
loadJobs();
} else {
showToast(`Failed to retry: ${job.error}`, 'error');
}
} catch (error) {
console.error('Error retrying job:', error);
}
}
// Delete job
async function deleteJob(jobId) {
if (!confirm('Delete this job?')) return;
try {
await fetch(`/api/jobs/${jobId}`, {
method: 'DELETE'
});
loadJobs();
showToast('Job deleted', 'info');
} catch (error) {
console.error('Error deleting job:', error);
}
}
// Clear completed jobs
async function clearCompletedJobs() {
const jobs = await (await fetch('/api/jobs')).json();
for (const job of jobs) {
if (['completed', 'failed', 'cancelled'].includes(job.status)) {
await fetch(`/api/jobs/${job.id}`, { method: 'DELETE' });
}
}
loadJobs();
showToast('Completed jobs cleared', 'info');
}
// Progress Modal
function showProgressModal(job) {
const modal = document.getElementById('progress-modal');
modal.classList.add('active');
updateProgressModal(job);
}
function closeProgressModal() {
const modal = document.getElementById('progress-modal');
modal.classList.remove('active');
currentJobId = null;
}
function minimizeModal() {
closeProgressModal();
switchTab('jobs');
}
function updateProgressModal(job) {
if (!job) return;
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
const progressPercent = document.getElementById('progress-percent');
progressFill.style.width = `${job.progress}%`;
progressText.textContent = job.progress_text || 'Processing...';
progressPercent.textContent = `${Math.round(job.progress)}%`;
// Update modal header based on status
const modalHeader = document.querySelector('#progress-modal h3');
if (job.status === 'completed') {
modalHeader.innerHTML = '<i class="fas fa-check"></i> Completed!';
progressFill.style.background = 'var(--success)';
} else if (job.status === 'failed') {
modalHeader.innerHTML = '<i class="fas fa-times"></i> Failed';
progressFill.style.background = 'var(--danger)';
} else if (job.status === 'cancelled') {
modalHeader.innerHTML = '<i class="fas fa-ban"></i> Cancelled';
progressFill.style.background = 'var(--secondary)';
}
}
// Append log line
function appendLog(line) {
const logContent = document.getElementById('log-content');
const logLine = document.createElement('div');
logLine.className = 'log-line';
// Detect error/success lines
if (line.includes('❌') || line.includes('Error') || line.includes('Failed')) {
logLine.classList.add('error');
} else if (line.includes('✅') || line.includes('Success') || line.includes('Done')) {
logLine.classList.add('success');
}
logLine.textContent = line;
logContent.appendChild(logLine);
logContent.scrollTop = logContent.scrollHeight;
}
function toggleLogs() {
const logContent = document.getElementById('log-content');
logContent.classList.toggle('collapsed');
}
// Gallery
async function refreshGallery() {
try {
const response = await fetch('/api/outputs');
const files = await response.json();
const gallery = document.getElementById('gallery-grid');
if (files.length === 0) {
gallery.innerHTML = `
<div class="empty-state">
<i class="fas fa-photo-video"></i>
<p>No generated files yet.</p>
</div>
`;
return;
}
gallery.innerHTML = files.map(file => createGalleryItem(file)).join('');
} catch (error) {
console.error('Error loading gallery:', error);
}
}
function createGalleryItem(file) {
const ext = file.name.split('.').pop().toLowerCase();
const isVideo = ['mp4', 'mov', 'webm', 'avi', 'mkv'].includes(ext);
const isImage = ['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext);
const isAudio = ['mp3', 'wav', 'ogg', 'flac'].includes(ext);
let preview = '';
if (isVideo) {
preview = `<video src="/api/download/${file.name}" muted></video>`;
} else if (isImage) {
preview = `<img src="/api/download/${file.name}" alt="${file.name}">`;
} else if (isAudio) {
preview = `<i class="fas fa-music"></i>`;
} else {
preview = `<i class="fas fa-file"></i>`;
}
const size = formatFileSize(file.size);
const date = new Date(file.modified).toLocaleDateString();
return `
<div class="gallery-item">
<div class="gallery-preview">
${preview}
</div>
<div class="gallery-info">
<h4>${file.name}</h4>
<p>${size}${date}</p>
<div class="gallery-actions">
<a href="/api/download/${file.name}" class="btn btn-small btn-primary" download>
<i class="fas fa-download"></i> Download
</a>
<button class="btn btn-small btn-danger" onclick="deleteOutput('${file.name}')">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
`;
}
async function deleteOutput(filename) {
if (!confirm(`Delete ${filename}?`)) return;
try {
await fetch(`/api/outputs/${filename}`, {
method: 'DELETE'
});
refreshGallery();
showToast('File deleted', 'info');
} catch (error) {
console.error('Error deleting file:', error);
}
}
// Toast notifications
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
const icons = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
warning: 'fa-exclamation-triangle',
info: 'fa-info-circle'
};
toast.innerHTML = `
<i class="fas ${icons[type]}"></i>
<span>${message}</span>
`;
container.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 5000);
}
// Utility functions
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + ' GB';
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function addPromptHint(hint) {
const prompt = document.getElementById('prompt');
if (prompt.value && !prompt.value.endsWith(', ')) {
prompt.value += ', ';
}
prompt.value += hint;
prompt.focus();
}
function toggleSection(header) {
const section = header.parentElement;
section.classList.toggle('collapsed');
}
function toggleAudioOptions() {
const audioOptions = document.getElementById('audio-options');
const checkbox = document.getElementById('generate_audio');
audioOptions.classList.toggle('hidden', !checkbox.checked);
}
function toggleAudioType() {
const audioType = document.getElementById('audio_type').value;
const ttsVoiceGroup = document.getElementById('tts-voice-group');
const audioTextGroup = document.getElementById('audio-text-group');
const musicPromptGroup = document.getElementById('music-prompt-group');
if (audioType === 'tts') {
ttsVoiceGroup.classList.remove('hidden');
audioTextGroup.classList.remove('hidden');
musicPromptGroup.classList.add('hidden');
} else {
ttsVoiceGroup.classList.add('hidden');
audioTextGroup.classList.add('hidden');
musicPromptGroup.classList.remove('hidden');
}
}
function resetForm() {
document.getElementById('generate-form').reset();
document.getElementById('strength-value').textContent = '0.7';
// Clear file uploads
document.getElementById('input_image').value = '';
document.getElementById('input_video').value = '';
document.getElementById('input_audio').value = '';
document.getElementById('image-file-name').textContent = 'PNG, JPG, WEBP';
document.getElementById('video-file-name').textContent = 'MP4, MOV, WEBM';
document.getElementById('audio-file-name').textContent = 'MP3, WAV, OGG';
// Reset mode
switchMode('t2v');
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Escape to close modal
if (e.key === 'Escape') {
closeProgressModal();
}
// Ctrl+Enter to submit
if (e.ctrlKey && e.key === 'Enter') {
document.getElementById('generate-form').dispatchEvent(new Event('submit'));
}
});
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VideoGen - AI Video Generation Studio</title>
<link rel="stylesheet" href="/static/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div id="app">
<!-- Header -->
<header class="header">
<div class="header-content">
<div class="logo">
<i class="fas fa-video"></i>
<span>VideoGen</span>
<span class="version">Web Interface</span>
</div>
<nav class="nav">
<button class="nav-btn active" data-tab="generate">
<i class="fas fa-magic"></i> Generate
</button>
<button class="nav-btn" data-tab="jobs">
<i class="fas fa-tasks"></i> Jobs
<span class="badge" id="jobs-badge">0</span>
</button>
<button class="nav-btn" data-tab="gallery">
<i class="fas fa-images"></i> Gallery
</button>
<button class="nav-btn" data-tab="settings">
<i class="fas fa-cog"></i> Settings
</button>
</nav>
</div>
</header>
<!-- Main Content -->
<main class="main">
<!-- Generate Tab -->
<section id="tab-generate" class="tab active">
<div class="generate-container">
<!-- Mode Selection -->
<div class="mode-selector">
<h2><i class="fas fa-film"></i> Generation Mode</h2>
<div class="mode-buttons">
<button class="mode-btn active" data-mode="t2v">
<i class="fas fa-video"></i>
<span>Text to Video</span>
<small>Generate video from text</small>
</button>
<button class="mode-btn" data-mode="i2v">
<i class="fas fa-image"></i>
<span>Image to Video</span>
<small>Animate an image</small>
</button>
<button class="mode-btn" data-mode="t2i">
<i class="fas fa-paint-brush"></i>
<span>Text to Image</span>
<small>Generate static image</small>
</button>
<button class="mode-btn" data-mode="i2i">
<i class="fas fa-exchange-alt"></i>
<span>Image to Image</span>
<small>Transform image</small>
</button>
<button class="mode-btn" data-mode="v2v">
<i class="fas fa-film"></i>
<span>Video to Video</span>
<small>Transform video</small>
</button>
<button class="mode-btn" data-mode="dub">
<i class="fas fa-language"></i>
<span>Dub & Translate</span>
<small>Translate video</small>
</button>
<button class="mode-btn" data-mode="subtitles">
<i class="fas fa-closed-captioning"></i>
<span>Subtitles</span>
<small>Create subtitles</small>
</button>
<button class="mode-btn" data-mode="upscale">
<i class="fas fa-expand"></i>
<span>Upscale</span>
<small>Enhance quality</small>
</button>
</div>
</div>
<!-- Main Form -->
<form id="generate-form" class="generate-form">
<!-- Prompt Section -->
<div class="form-section">
<h3><i class="fas fa-pen"></i> Prompt</h3>
<div class="prompt-container">
<textarea id="prompt" name="prompt" placeholder="Describe what you want to generate..." rows="3"></textarea>
<div class="prompt-hints">
<span class="hint" onclick="addPromptHint('cinematic, 4k, detailed')">+cinematic</span>
<span class="hint" onclick="addPromptHint('smooth motion, fluid animation')">+smooth</span>
<span class="hint" onclick="addPromptHint('high quality, masterpiece')">+quality</span>
<span class="hint" onclick="addPromptHint('dramatic lighting, atmospheric')">+dramatic</span>
</div>
</div>
<!-- I2V Prompts (hidden by default) -->
<div id="i2v-prompts" class="i2v-prompts hidden">
<div class="form-group">
<label for="prompt_image">Image Prompt</label>
<textarea id="prompt_image" name="prompt_image" placeholder="Describe the initial image..." rows="2"></textarea>
</div>
<div class="form-group">
<label for="prompt_animation">Animation Prompt</label>
<textarea id="prompt_animation" name="prompt_animation" placeholder="Describe the motion/animation..." rows="2"></textarea>
</div>
</div>
</div>
<!-- Model Selection -->
<div class="form-section">
<h3><i class="fas fa-cube"></i> Model</h3>
<div class="form-row">
<div class="form-group">
<label for="model">Video Model</label>
<select id="model" name="model">
<option value="">Loading models...</option>
</select>
</div>
<div class="form-group" id="image-model-group">
<label for="image_model">Image Model (for I2V)</label>
<select id="image_model" name="image_model">
<option value="">Loading models...</option>
</select>
</div>
</div>
<div class="form-row">
<label class="checkbox-label">
<input type="checkbox" id="auto" name="auto">
<span>Auto-select best model</span>
</label>
</div>
</div>
<!-- Input Files -->
<div class="form-section" id="input-files-section">
<h3><i class="fas fa-upload"></i> Input Files</h3>
<div class="file-uploads">
<div class="file-upload" id="image-upload-box">
<input type="file" id="input_image_file" accept="image/*" onchange="handleFileUpload(this, 'image')">
<label for="input_image_file" class="file-label">
<i class="fas fa-image"></i>
<span>Upload Image</span>
<small id="image-file-name">PNG, JPG, WEBP</small>
</label>
<input type="hidden" id="input_image" name="input_image">
</div>
<div class="file-upload hidden" id="video-upload-box">
<input type="file" id="input_video_file" accept="video/*" onchange="handleFileUpload(this, 'video')">
<label for="input_video_file" class="file-label">
<i class="fas fa-video"></i>
<span>Upload Video</span>
<small id="video-file-name">MP4, MOV, WEBM</small>
</label>
<input type="hidden" id="input_video" name="input_video">
</div>
<div class="file-upload hidden" id="audio-upload-box">
<input type="file" id="input_audio_file" accept="audio/*" onchange="handleFileUpload(this, 'audio')">
<label for="input_audio_file" class="file-label">
<i class="fas fa-music"></i>
<span>Upload Audio</span>
<small id="audio-file-name">MP3, WAV, OGG</small>
</label>
<input type="hidden" id="input_audio" name="input_audio">
</div>
</div>
</div>
<!-- Resolution & Duration -->
<div class="form-section">
<h3><i class="fas fa-sliders-h"></i> Settings</h3>
<div class="form-row">
<div class="form-group">
<label for="width">Width</label>
<select id="width" name="width">
<option value="512">512</option>
<option value="640">640</option>
<option value="768">768</option>
<option value="832" selected>832</option>
<option value="1024">1024</option>
<option value="1280">1280</option>
</select>
</div>
<div class="form-group">
<label for="height">Height</label>
<select id="height" name="height">
<option value="360">360</option>
<option value="480" selected>480</option>
<option value="576">576</option>
<option value="720">720</option>
<option value="1024">1024</option>
</select>
</div>
<div class="form-group">
<label for="fps">FPS</label>
<select id="fps" name="fps">
<option value="12">12</option>
<option value="15" selected>15</option>
<option value="24">24</option>
<option value="30">30</option>
</select>
</div>
<div class="form-group">
<label for="length">Duration (s)</label>
<input type="number" id="length" name="length" value="5" min="1" max="60" step="0.5">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="seed">Seed (-1 for random)</label>
<input type="number" id="seed" name="seed" value="-1">
</div>
<div class="form-group" id="strength-group">
<label for="strength">Strength</label>
<input type="range" id="strength" name="strength" value="0.7" min="0.1" max="1.0" step="0.1">
<span id="strength-value">0.7</span>
</div>
</div>
</div>
<!-- Audio Options -->
<div class="form-section" id="audio-section">
<h3><i class="fas fa-volume-up"></i> Audio</h3>
<div class="form-row">
<label class="checkbox-label">
<input type="checkbox" id="generate_audio" name="generate_audio" onchange="toggleAudioOptions()">
<span>Generate Audio</span>
</label>
</div>
<div id="audio-options" class="hidden">
<div class="form-row">
<div class="form-group">
<label for="audio_type">Audio Type</label>
<select id="audio_type" name="audio_type" onchange="toggleAudioType()">
<option value="tts">Text-to-Speech</option>
<option value="music">Music Generation</option>
</select>
</div>
<div class="form-group" id="tts-voice-group">
<label for="tts_voice">TTS Voice</label>
<select id="tts_voice" name="tts_voice">
<option value="">Loading voices...</option>
</select>
</div>
</div>
<div class="form-group" id="audio-text-group">
<label for="audio_text">Text to Speak</label>
<textarea id="audio_text" name="audio_text" placeholder="Enter text for TTS..." rows="2"></textarea>
</div>
<div class="form-group hidden" id="music-prompt-group">
<label for="music_prompt">Music Prompt</label>
<textarea id="music_prompt" name="music_prompt" placeholder="Describe the music style..." rows="2"></textarea>
</div>
<div class="form-row">
<label class="checkbox-label">
<input type="checkbox" id="sync_audio" name="sync_audio">
<span>Sync Audio to Video</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="lip_sync" name="lip_sync">
<span>Lip Sync</span>
</label>
</div>
</div>
</div>
<!-- Dubbing Options -->
<div class="form-section hidden" id="dubbing-section">
<h3><i class="fas fa-language"></i> Translation & Dubbing</h3>
<div class="form-row">
<div class="form-group">
<label for="source_lang">Source Language</label>
<select id="source_lang" name="source_lang">
<option value="">Auto-detect</option>
</select>
</div>
<div class="form-group">
<label for="target_lang">Target Language</label>
<select id="target_lang" name="target_lang">
</select>
</div>
</div>
<div class="form-row">
<label class="checkbox-label">
<input type="checkbox" id="voice_clone" name="voice_clone" checked>
<span>Preserve Voice (Voice Cloning)</span>
</label>
</div>
</div>
<!-- Subtitle Options -->
<div class="form-section hidden" id="subtitle-section">
<h3><i class="fas fa-closed-captioning"></i> Subtitles</h3>
<div class="form-row">
<label class="checkbox-label">
<input type="checkbox" id="create_subtitles" name="create_subtitles" checked>
<span>Create Subtitles</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="translate_subtitles" name="translate_subtitles">
<span>Translate Subtitles</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="burn_subtitles" name="burn_subtitles">
<span>Burn into Video</span>
</label>
</div>
<div class="form-row">
<div class="form-group">
<label for="whisper_model">Whisper Model</label>
<select id="whisper_model" name="whisper_model">
<option value="tiny">Tiny (fastest)</option>
<option value="base" selected>Base</option>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large (best)</option>
</select>
</div>
</div>
</div>
<!-- Advanced Options -->
<div class="form-section collapsible">
<h3 onclick="toggleSection(this)">
<i class="fas fa-cogs"></i> Advanced Options
<i class="fas fa-chevron-down toggle-icon"></i>
</h3>
<div class="section-content">
<div class="form-row">
<div class="form-group">
<label for="offload_strategy">Offload Strategy</label>
<select id="offload_strategy" name="offload_strategy">
<option value="model">Model Offload (Recommended)</option>
<option value="sequential">Sequential</option>
<option value="group">Group</option>
<option value="none">None (Full GPU)</option>
</select>
</div>
<div class="form-group">
<label for="vram_limit">VRAM Limit (GB)</label>
<input type="number" id="vram_limit" name="vram_limit" value="22" min="4" max="100">
</div>
</div>
<div class="form-row">
<label class="checkbox-label">
<input type="checkbox" id="no_filter" name="no_filter">
<span>Disable NSFW Filter</span>
</label>
<label class="checkbox-label">
<input type="checkbox" id="debug" name="debug">
<span>Debug Mode</span>
</label>
</div>
</div>
</div>
<!-- Submit Button -->
<div class="form-actions">
<button type="submit" class="btn btn-primary btn-large">
<i class="fas fa-play"></i> Generate
</button>
<button type="button" class="btn btn-secondary" onclick="resetForm()">
<i class="fas fa-undo"></i> Reset
</button>
</div>
</form>
</div>
</section>
<!-- Jobs Tab -->
<section id="tab-jobs" class="tab">
<div class="jobs-container">
<div class="jobs-header">
<h2><i class="fas fa-tasks"></i> Generation Jobs</h2>
<button class="btn btn-secondary" onclick="clearCompletedJobs()">
<i class="fas fa-trash"></i> Clear Completed
</button>
</div>
<div id="jobs-list" class="jobs-list">
<div class="empty-state">
<i class="fas fa-inbox"></i>
<p>No jobs yet. Start generating!</p>
</div>
</div>
</div>
</section>
<!-- Gallery Tab -->
<section id="tab-gallery" class="tab">
<div class="gallery-container">
<div class="gallery-header">
<h2><i class="fas fa-images"></i> Generated Files</h2>
<button class="btn btn-secondary" onclick="refreshGallery()">
<i class="fas fa-sync"></i> Refresh
</button>
</div>
<div id="gallery-grid" class="gallery-grid">
<div class="empty-state">
<i class="fas fa-photo-video"></i>
<p>No generated files yet.</p>
</div>
</div>
</div>
</section>
<!-- Settings Tab -->
<section id="tab-settings" class="tab">
<div class="settings-container">
<h2><i class="fas fa-cog"></i> Settings</h2>
<div class="settings-section">
<h3>Server Configuration</h3>
<div class="form-group">
<label>API Endpoint</label>
<input type="text" value="" id="api-endpoint" readonly>
</div>
</div>
<div class="settings-section">
<h3>Default Values</h3>
<div class="form-row">
<div class="form-group">
<label>Default Width</label>
<select id="default-width">
<option value="832">832</option>
<option value="1024">1024</option>
<option value="1280">1280</option>
</select>
</div>
<div class="form-group">
<label>Default Height</label>
<select id="default-height">
<option value="480">480</option>
<option value="720">720</option>
</select>
</div>
</div>
</div>
<div class="settings-section">
<h3>About</h3>
<p>VideoGen Web Interface v1.0</p>
<p>Copyleft © 2026 Stefy <stefy@nexlab.net></p>
<p>Licensed under GNU General Public License v3.0</p>
</div>
</div>
</section>
</main>
<!-- Job Progress Modal -->
<div id="progress-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h3><i class="fas fa-spinner fa-spin"></i> Generating...</h3>
<button class="close-btn" onclick="closeProgressModal()">&times;</button>
</div>
<div class="modal-body">
<div class="progress-container">
<div class="progress-bar">
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
</div>
<div class="progress-text" id="progress-text">Initializing...</div>
<div class="progress-percent" id="progress-percent">0%</div>
</div>
<div class="log-container" id="log-container">
<div class="log-header">
<span>Output Log</span>
<button class="btn btn-small" onclick="toggleLogs()">
<i class="fas fa-chevron-down"></i>
</button>
</div>
<div class="log-content" id="log-content"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-danger" onclick="cancelJob()">
<i class="fas fa-stop"></i> Cancel
</button>
<button class="btn btn-secondary" onclick="minimizeModal()">
<i class="fas fa-minus"></i> Minimize
</button>
</div>
</div>
</div>
<!-- Toast Notifications -->
<div id="toast-container"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.5.4/socket.io.min.js"></script>
<script src="/static/js/app.js"></script>
</body>
</html>
\ No newline at end of file
#!/usr/bin/env python3
"""
VideoGen Web Interface
======================
A web interface for VideoGen that provides access to all features
including video generation, audio, dubbing, and more.
Usage:
python webapp.py --port 5000 --host 0.0.0.0
Copyleft © 2026 Stefy <stefy@nexlab.net>
Licensed under GNU General Public License v3.0 or later
"""
import os
import sys
import json
import uuid
import subprocess
import threading
import queue
import time
import re
import shutil
from pathlib import Path
from datetime import datetime
from typing import Dict, List, Optional, Any
from dataclasses import dataclass, asdict
import argparse
# Flask and extensions
from flask import Flask, request, jsonify, send_file, send_from_directory, Response
from flask_cors import CORS
from flask_socketio import SocketIO, emit
from werkzeug.utils import secure_filename
import eventlet
# Enable eventlet for async operations
eventlet.monkey_patch()
# Configuration
UPLOAD_FOLDER = Path.home() / ".config" / "videogen" / "webapp" / "uploads"
OUTPUT_FOLDER = Path.home() / ".config" / "videogen" / "webapp" / "outputs"
JOBS_FILE = Path.home() / ".config" / "videogen" / "webapp" / "jobs.json"
ALLOWED_EXTENSIONS = {
'image': {'png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp'},
'video': {'mp4', 'avi', 'mov', 'mkv', 'webm', 'gif'},
'audio': {'mp3', 'wav', 'ogg', 'flac', 'aac'},
'subtitle': {'srt', 'vtt', 'ass', 'ssa'},
}
# Ensure directories exist
UPLOAD_FOLDER.mkdir(parents=True, exist_ok=True)
OUTPUT_FOLDER.mkdir(parents=True, exist_ok=True)
# Flask app setup
app = Flask(__name__, static_folder='static', template_folder='templates')
app.config['UPLOAD_FOLDER'] = str(UPLOAD_FOLDER)
app.config['OUTPUT_FOLDER'] = str(OUTPUT_FOLDER)
app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500MB max upload
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'videogen-webapp-secret-key')
CORS(app)
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='eventlet')
# Job storage
@dataclass
class Job:
id: str
status: str # pending, running, completed, failed, cancelled
command: str
created_at: str
started_at: Optional[str] = None
completed_at: Optional[str] = None
progress: float = 0.0
progress_text: str = ""
output_files: List[str] = None
error: Optional[str] = None
logs: List[str] = None
def __post_init__(self):
if self.output_files is None:
self.output_files = []
if self.logs is None:
self.logs = []
# In-memory job storage (could be replaced with database)
jobs: Dict[str, Job] = {}
job_queues: Dict[str, queue.Queue] = {}
# Load existing jobs from file
def load_jobs():
global jobs
if JOBS_FILE.exists():
try:
with open(JOBS_FILE, 'r') as f:
data = json.load(f)
for job_id, job_data in data.items():
jobs[job_id] = Job(**job_data)
except Exception as e:
print(f"Error loading jobs: {e}")
def save_jobs():
try:
with open(JOBS_FILE, 'w') as f:
json.dump({k: asdict(v) for k, v in jobs.items()}, f, indent=2)
except Exception as e:
print(f"Error saving jobs: {e}")
# Helper functions
def allowed_file(filename: str, file_type: str) -> bool:
"""Check if file extension is allowed for the given type"""
if '.' not in filename:
return False
ext = filename.rsplit('.', 1)[1].lower()
return ext in ALLOWED_EXTENSIONS.get(file_type, set())
def get_model_list() -> List[Dict]:
"""Get list of available models from videogen"""
try:
result = subprocess.run(
['python3', 'videogen', '--model-list', '--json'],
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
return json.loads(result.stdout)
except Exception as e:
print(f"Error getting model list: {e}")
return []
def get_tts_voices() -> List[Dict]:
"""Get list of available TTS voices"""
return [
{"id": "edge_female_us", "name": "Edge TTS - Female (US)", "engine": "edge"},
{"id": "edge_male_us", "name": "Edge TTS - Male (US)", "engine": "edge"},
{"id": "edge_female_uk", "name": "Edge TTS - Female (UK)", "engine": "edge"},
{"id": "edge_male_uk", "name": "Edge TTS - Male (UK)", "engine": "edge"},
{"id": "edge_female_es", "name": "Edge TTS - Female (Spanish)", "engine": "edge"},
{"id": "edge_male_es", "name": "Edge TTS - Male (Spanish)", "engine": "edge"},
{"id": "edge_female_fr", "name": "Edge TTS - Female (French)", "engine": "edge"},
{"id": "edge_male_fr", "name": "Edge TTS - Male (French)", "engine": "edge"},
{"id": "edge_female_de", "name": "Edge TTS - Female (German)", "engine": "edge"},
{"id": "edge_male_de", "name": "Edge TTS - Male (German)", "engine": "edge"},
{"id": "edge_female_it", "name": "Edge TTS - Female (Italian)", "engine": "edge"},
{"id": "edge_male_it", "name": "Edge TTS - Male (Italian)", "engine": "edge"},
{"id": "edge_female_ja", "name": "Edge TTS - Female (Japanese)", "engine": "edge"},
{"id": "edge_male_ja", "name": "Edge TTS - Male (Japanese)", "engine": "edge"},
{"id": "edge_female_zh", "name": "Edge TTS - Female (Chinese)", "engine": "edge"},
{"id": "edge_male_zh", "name": "Edge TTS - Male (Chinese)", "engine": "edge"},
{"id": "bark_speaker_0", "name": "Bark - Speaker 0", "engine": "bark"},
{"id": "bark_speaker_1", "name": "Bark - Speaker 1", "engine": "bark"},
{"id": "bark_speaker_2", "name": "Bark - Speaker 2", "engine": "bark"},
{"id": "bark_speaker_3", "name": "Bark - Speaker 3", "engine": "bark"},
]
def get_languages() -> List[Dict]:
"""Get list of supported languages for translation"""
return [
{"code": "en", "name": "English"},
{"code": "es", "name": "Spanish"},
{"code": "fr", "name": "French"},
{"code": "de", "name": "German"},
{"code": "it", "name": "Italian"},
{"code": "pt", "name": "Portuguese"},
{"code": "ru", "name": "Russian"},
{"code": "zh", "name": "Chinese"},
{"code": "ja", "name": "Japanese"},
{"code": "ko", "name": "Korean"},
{"code": "ar", "name": "Arabic"},
{"code": "hi", "name": "Hindi"},
{"code": "nl", "name": "Dutch"},
{"code": "pl", "name": "Polish"},
{"code": "tr", "name": "Turkish"},
{"code": "vi", "name": "Vietnamese"},
{"code": "th", "name": "Thai"},
{"code": "id", "name": "Indonesian"},
{"code": "sv", "name": "Swedish"},
{"code": "uk", "name": "Ukrainian"},
]
def build_command(params: Dict) -> List[str]:
"""Build videogen command from parameters"""
cmd = ['python3', 'videogen']
# Mode selection
mode = params.get('mode', 't2v')
# Prompt
if params.get('prompt'):
cmd.extend(['--prompt', params['prompt']])
# Model selection
if params.get('model'):
cmd.extend(['--model', params['model']])
if params.get('image_model'):
cmd.extend(['--image-model', params['image_model']])
# Output
output_name = params.get('output', f"output_{uuid.uuid4().hex[:8]}")
cmd.extend(['--output', str(OUTPUT_FOLDER / output_name)])
# Resolution
if params.get('width'):
cmd.extend(['--width', str(params['width'])])
if params.get('height'):
cmd.extend(['--height', str(params['height'])])
# Duration and FPS
if params.get('length'):
cmd.extend(['--length', str(params['length'])])
if params.get('fps'):
cmd.extend(['--fps', str(params['fps'])])
# Seed
if params.get('seed'):
cmd.extend(['--seed', str(params['seed'])])
# Mode-specific options
if mode == 'i2v':
cmd.append('--image-to-video')
if params.get('prompt_image'):
cmd.extend(['--prompt-image', params['prompt_image']])
if params.get('prompt_animation'):
cmd.extend(['--prompt-animation', params['prompt_animation']])
elif mode == 't2i':
cmd.append('--generate-image')
if params.get('image_steps'):
cmd.extend(['--image-steps', str(params['image_steps'])])
elif mode == 'i2i':
cmd.append('--image-to-image')
if params.get('strength'):
cmd.extend(['--strength', str(params['strength'])])
# Input files
if params.get('input_image'):
cmd.extend(['--image', params['input_image']])
if params.get('input_video'):
cmd.extend(['--video', params['input_video']])
if params.get('input_audio'):
cmd.extend(['--audio-file', params['input_audio']])
# Audio options
if params.get('generate_audio'):
cmd.append('--generate-audio')
if params.get('audio_type'):
cmd.extend(['--audio-type', params['audio_type']])
if params.get('audio_text'):
cmd.extend(['--audio-text', params['audio_text']])
if params.get('tts_voice'):
cmd.extend(['--tts-voice', params['tts_voice']])
if params.get('music_prompt'):
cmd.extend(['--music-prompt', params['music_prompt']])
if params.get('sync_audio'):
cmd.append('--sync-audio')
if params.get('audio_sync_mode'):
cmd.extend(['--audio-sync-mode', params['audio_sync_mode']])
if params.get('lip_sync'):
cmd.append('--lip-sync')
if params.get('lip_sync_method'):
cmd.extend(['--lip-sync-method', params['lip_sync_method']])
# Dubbing/Translation
if params.get('dub_video'):
cmd.append('--dub-video')
if params.get('target_lang'):
cmd.extend(['--target-lang', params['target_lang']])
if params.get('source_lang'):
cmd.extend(['--source-lang', params['source_lang']])
if params.get('voice_clone'):
cmd.append('--voice-clone')
else:
cmd.append('--no-voice-clone')
# Subtitles
if params.get('create_subtitles'):
cmd.append('--create-subtitles')
if params.get('translate_subtitles'):
cmd.append('--translate-subtitles')
if params.get('burn_subtitles'):
cmd.append('--burn-subtitles')
if params.get('subtitle_style'):
cmd.extend(['--subtitle-style', params['subtitle_style']])
# Transcription
if params.get('transcribe'):
cmd.append('--transcribe')
if params.get('whisper_model'):
cmd.extend(['--whisper-model', params['whisper_model']])
# V2V options
if params.get('video_to_video'):
cmd.append('--video-to-video')
if params.get('v2v_strength'):
cmd.extend(['--v2v-strength', str(params['v2v_strength'])])
# 2D to 3D
if params.get('convert_3d'):
cmd.append('--convert-3d')
if params.get('depth_method'):
cmd.extend(['--depth-method', params['depth_method']])
# Upscale
if params.get('upscale'):
cmd.append('--upscale')
if params.get('upscale_factor'):
cmd.extend(['--upscale-factor', str(params['upscale_factor'])])
# Character consistency
if params.get('character_reference'):
cmd.extend(['--character-reference', params['character_reference']])
if params.get('character_strength'):
cmd.extend(['--character-strength', str(params['character_strength'])])
# NSFW
if params.get('no_filter'):
cmd.append('--no-filter')
# Auto mode
if params.get('auto'):
cmd.append('--auto')
# Offloading
if params.get('offload_strategy'):
cmd.extend(['--offload-strategy', params['offload_strategy']])
# VRAM limit
if params.get('vram_limit'):
cmd.extend(['--vram-limit', str(params['vram_limit'])])
# Debug
if params.get('debug'):
cmd.append('--debug')
return cmd
def run_job(job_id: str, cmd: List[str]):
"""Run a videogen job in background"""
job = jobs.get(job_id)
if not job:
return
job.status = 'running'
job.started_at = datetime.now().isoformat()
save_jobs()
socketio.emit('job_update', asdict(job), room=job_id)
try:
# Start subprocess
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
cwd=os.getcwd()
)
# Store process for cancellation
job_queues[job_id] = queue.Queue()
job_queues[job_id].put(process)
# Read output line by line
for line in iter(process.stdout.readline, ''):
if jobs.get(job_id, Job(id='', status='', command='', created_at='')).status == 'cancelled':
process.terminate()
break
line = line.rstrip()
job.logs.append(line)
# Parse progress from output
progress_match = re.search(r'(\d+(?:\.\d+)?)\s*%', line)
if progress_match:
job.progress = float(progress_match.group(1))
# Update progress text
if 'Loading' in line or 'loading' in line:
job.progress_text = line.strip()
elif 'Generating' in line or 'generating' in line:
job.progress_text = line.strip()
elif 'Saving' in line or 'saving' in line:
job.progress_text = line.strip()
elif '✅' in line or '✨' in line:
job.progress_text = line.strip()
# Emit update
socketio.emit('job_log', {'job_id': job_id, 'line': line}, room=job_id)
socketio.emit('job_update', asdict(job), room=job_id)
process.wait()
if process.returncode == 0:
job.status = 'completed'
job.progress = 100.0
job.progress_text = 'Generation completed successfully!'
# Find output files
output_base = cmd[cmd.index('--output') + 1] if '--output' in cmd else str(OUTPUT_FOLDER / 'output')
for ext in ['.mp4', '.png', '.wav', '.srt', '_dubbed.mp4', '_upscaled.mp4']:
potential_file = output_base + ext if not ext.startswith('_') else output_base.replace('.mp4', '') + ext
if os.path.exists(potential_file):
job.output_files.append(os.path.basename(potential_file))
# Also check for numbered outputs
for f in OUTPUT_FOLDER.glob(f"{os.path.basename(output_base)}*{ext}"):
if f.name not in job.output_files:
job.output_files.append(f.name)
else:
job.status = 'failed'
job.error = 'Process returned non-zero exit code'
except Exception as e:
job.status = 'failed'
job.error = str(e)
finally:
job.completed_at = datetime.now().isoformat()
save_jobs()
socketio.emit('job_update', asdict(job), room=job_id)
if job_id in job_queues:
del job_queues[job_id]
# Routes
@app.route('/')
def index():
"""Serve the main page"""
return send_from_directory('templates', 'index.html')
@app.route('/static/<path:filename>')
def static_files(filename):
"""Serve static files"""
return send_from_directory('static', filename)
@app.route('/api/models', methods=['GET'])
def api_models():
"""Get list of available models"""
models = get_model_list()
return jsonify(models)
@app.route('/api/tts-voices', methods=['GET'])
def api_tts_voices():
"""Get list of TTS voices"""
return jsonify(get_tts_voices())
@app.route('/api/languages', methods=['GET'])
def api_languages():
"""Get list of supported languages"""
return jsonify(get_languages())
@app.route('/api/upload', methods=['POST'])
def upload_file():
"""Upload a file"""
if 'file' not in request.files:
return jsonify({'error': 'No file provided'}), 400
file = request.files['file']
if file.filename == '':
return jsonify({'error': 'No file selected'}), 400
file_type = request.form.get('type', 'image')
if not allowed_file(file.filename, file_type):
return jsonify({'error': f'File type not allowed for {file_type}'}), 400
# Generate unique filename
ext = file.filename.rsplit('.', 1)[1].lower()
filename = f"{uuid.uuid4().hex}.{ext}"
filepath = UPLOAD_FOLDER / filename
file.save(filepath)
return jsonify({
'filename': filename,
'path': str(filepath),
'size': filepath.stat().st_size
})
@app.route('/api/jobs', methods=['GET'])
def api_jobs():
"""Get list of jobs"""
job_list = [asdict(job) for job in jobs.values()]
job_list.sort(key=lambda x: x['created_at'], reverse=True)
return jsonify(job_list)
@app.route('/api/jobs/<job_id>', methods=['GET'])
def api_job(job_id):
"""Get job details"""
job = jobs.get(job_id)
if not job:
return jsonify({'error': 'Job not found'}), 404
return jsonify(asdict(job))
@app.route('/api/jobs', methods=['POST'])
def create_job():
"""Create a new job"""
params = request.json
# Generate job ID
job_id = uuid.uuid4().hex[:8]
# Build command
cmd = build_command(params)
# Create job
job = Job(
id=job_id,
status='pending',
command=' '.join(cmd),
created_at=datetime.now().isoformat()
)
jobs[job_id] = job
save_jobs()
# Start job in background
thread = threading.Thread(target=run_job, args=(job_id, cmd))
thread.daemon = True
thread.start()
return jsonify(asdict(job))
@app.route('/api/jobs/<job_id>/cancel', methods=['POST'])
def cancel_job(job_id):
"""Cancel a running job"""
job = jobs.get(job_id)
if not job:
return jsonify({'error': 'Job not found'}), 404
if job.status == 'running':
job.status = 'cancelled'
job.error = 'Cancelled by user'
save_jobs()
socketio.emit('job_update', asdict(job), room=job_id)
return jsonify(asdict(job))
@app.route('/api/jobs/<job_id>/retry', methods=['POST'])
def retry_job(job_id):
"""Retry a failed job"""
job = jobs.get(job_id)
if not job:
return jsonify({'error': 'Job not found'}), 404
if job.status not in ['failed', 'cancelled']:
return jsonify({'error': 'Can only retry failed or cancelled jobs'}), 400
# Reset job
job.status = 'pending'
job.error = None
job.progress = 0.0
job.progress_text = ''
job.logs = []
job.output_files = []
job.started_at = None
job.completed_at = None
save_jobs()
# Rebuild command and start
cmd = job.command.split()
thread = threading.Thread(target=run_job, args=(job_id, cmd))
thread.daemon = True
thread.start()
return jsonify(asdict(job))
@app.route('/api/jobs/<job_id>', methods=['DELETE'])
def delete_job(job_id):
"""Delete a job"""
if job_id in jobs:
del jobs[job_id]
save_jobs()
return jsonify({'success': True})
return jsonify({'error': 'Job not found'}), 404
@app.route('/api/download/<filename>')
def download_file(filename):
"""Download an output file"""
filepath = OUTPUT_FOLDER / filename
if not filepath.exists():
# Check in uploads folder too
filepath = UPLOAD_FOLDER / filename
if not filepath.exists():
return jsonify({'error': 'File not found'}), 404
return send_file(filepath, as_attachment=True)
@app.route('/api/outputs')
def list_outputs():
"""List all output files"""
files = []
for f in OUTPUT_FOLDER.iterdir():
if f.is_file():
files.append({
'name': f.name,
'size': f.stat().st_size,
'modified': datetime.fromtimestamp(f.stat().st_mtime).isoformat()
})
files.sort(key=lambda x: x['modified'], reverse=True)
return jsonify(files)
@app.route('/api/outputs/<filename>', methods=['DELETE'])
def delete_output(filename):
"""Delete an output file"""
filepath = OUTPUT_FOLDER / filename
if filepath.exists():
filepath.unlink()
return jsonify({'success': True})
return jsonify({'error': 'File not found'}), 404
# WebSocket events
@socketio.on('connect')
def handle_connect():
print(f"Client connected: {request.sid}")
@socketio.on('disconnect')
def handle_disconnect():
print(f"Client disconnected: {request.sid}")
@socketio.on('subscribe_job')
def handle_subscribe_job(job_id):
"""Subscribe to job updates"""
from flask_socketio import join_room
join_room(job_id)
if job_id in jobs:
emit('job_update', asdict(jobs[job_id]))
# Main entry point
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='VideoGen Web Interface')
parser.add_argument('--host', default='0.0.0.0', help='Host to bind to')
parser.add_argument('--port', type=int, default=5000, help='Port to bind to')
parser.add_argument('--debug', action='store_true', help='Enable debug mode')
args = parser.parse_args()
# Load existing jobs
load_jobs()
print(f"""
╔══════════════════════════════════════════════════════════════╗
║ VideoGen Web Interface ║
╠══════════════════════════════════════════════════════════════╣
║ Starting server on http://{args.host}:{args.port} ║
║ Upload folder: {UPLOAD_FOLDER} ║
║ Output folder: {OUTPUT_FOLDER} ║
╚══════════════════════════════════════════════════════════════╝
""")
socketio.run(app, host=args.host, port=args.port, debug=args.debug)
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment