714 lines
No EOL
26 KiB
JavaScript
714 lines
No EOL
26 KiB
JavaScript
import React, { useState } from 'react';
|
|
import {
|
|
Paper,
|
|
Typography,
|
|
TextField,
|
|
FormControl,
|
|
InputLabel,
|
|
Select,
|
|
MenuItem,
|
|
Button,
|
|
Box,
|
|
Grid,
|
|
Accordion,
|
|
AccordionSummary,
|
|
AccordionDetails,
|
|
FormControlLabel,
|
|
Checkbox,
|
|
Alert
|
|
} from '@mui/material';
|
|
import {
|
|
PlayArrowRounded,
|
|
ExpandMoreRounded,
|
|
SettingsRounded,
|
|
CloudUploadRounded,
|
|
ImageRounded,
|
|
DeleteRounded
|
|
} from '@mui/icons-material';
|
|
import { VIDEO_GENERATION_OPTIONS, IMAGE_UPLOAD_CONFIG, REFERENCE_IMAGE_CONFIG } from '../utils/constants';
|
|
|
|
const VideoForm = ({ onSubmit, isGenerating, userJobs = [] }) => {
|
|
const [formData, setFormData] = useState({
|
|
prompt: '',
|
|
video_length_sec: 8,
|
|
aspect_ratio: '16:9',
|
|
person_generation: 'allow_adult',
|
|
model_name: 'veo-3.1-generate-preview',
|
|
seed: '',
|
|
generate_audio: true,
|
|
sampleCount: 1
|
|
});
|
|
|
|
const [errors, setErrors] = useState({});
|
|
const [selectedImage, setSelectedImage] = useState(null);
|
|
const [imagePreview, setImagePreview] = useState(null);
|
|
const [selectedLastFrame, setSelectedLastFrame] = useState(null);
|
|
const [lastFramePreview, setLastFramePreview] = useState(null);
|
|
const [selectedReferenceImages, setSelectedReferenceImages] = useState([]);
|
|
const [referenceImagePreviews, setReferenceImagePreviews] = useState([]);
|
|
|
|
// Get model capabilities based on selected model
|
|
const selectedModelConfig = VIDEO_GENERATION_OPTIONS.models.find(m => m.value === formData.model_name) || {};
|
|
const modelCapabilities = {
|
|
supportsReferenceImages: selectedModelConfig.supportsReferenceImages || false,
|
|
supportsLastFrame: selectedModelConfig.supportsLastFrame || false,
|
|
supportsVideoExtension: selectedModelConfig.supportsVideoExtension || false
|
|
};
|
|
|
|
const handleChange = (field) => (event) => {
|
|
const value = event.target.type === 'checkbox' ? event.target.checked : event.target.value;
|
|
setFormData(prev => ({
|
|
...prev,
|
|
[field]: value
|
|
}));
|
|
|
|
// Clear error when user starts typing
|
|
if (errors[field]) {
|
|
setErrors(prev => ({
|
|
...prev,
|
|
[field]: ''
|
|
}));
|
|
}
|
|
};
|
|
|
|
const validateImage = (file) => {
|
|
const errors = [];
|
|
|
|
// Check file size
|
|
if (file.size > IMAGE_UPLOAD_CONFIG.maxSize) {
|
|
errors.push(`File too large. Maximum size: ${IMAGE_UPLOAD_CONFIG.maxSize / (1024 * 1024)}MB`);
|
|
}
|
|
|
|
// Check file type
|
|
if (!IMAGE_UPLOAD_CONFIG.supportedFormats.includes(file.type)) {
|
|
errors.push(`Unsupported format. Supported: ${IMAGE_UPLOAD_CONFIG.supportedExtensions.join(', ')}`);
|
|
}
|
|
|
|
return errors;
|
|
};
|
|
|
|
const handleImageSelect = (event) => {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const imageErrors = validateImage(file);
|
|
if (imageErrors.length > 0) {
|
|
setErrors(prev => ({
|
|
...prev,
|
|
image: imageErrors[0]
|
|
}));
|
|
return;
|
|
}
|
|
|
|
setSelectedImage(file);
|
|
setErrors(prev => ({
|
|
...prev,
|
|
image: ''
|
|
}));
|
|
|
|
// Create preview
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
setImagePreview(e.target.result);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
};
|
|
|
|
const handleImageRemove = () => {
|
|
setSelectedImage(null);
|
|
setImagePreview(null);
|
|
setErrors(prev => ({
|
|
...prev,
|
|
image: ''
|
|
}));
|
|
// Reset file input
|
|
const fileInput = document.getElementById('image-upload');
|
|
if (fileInput) fileInput.value = '';
|
|
};
|
|
|
|
const handleLastFrameSelect = (event) => {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
const imageErrors = validateImage(file);
|
|
if (imageErrors.length > 0) {
|
|
setErrors(prev => ({
|
|
...prev,
|
|
lastFrame: imageErrors[0]
|
|
}));
|
|
return;
|
|
}
|
|
|
|
setSelectedLastFrame(file);
|
|
setErrors(prev => ({
|
|
...prev,
|
|
lastFrame: ''
|
|
}));
|
|
|
|
// Create preview
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
setLastFramePreview(e.target.result);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
};
|
|
|
|
const handleLastFrameRemove = () => {
|
|
setSelectedLastFrame(null);
|
|
setLastFramePreview(null);
|
|
setErrors(prev => ({
|
|
...prev,
|
|
lastFrame: ''
|
|
}));
|
|
const fileInput = document.getElementById('last-frame-upload');
|
|
if (fileInput) fileInput.value = '';
|
|
};
|
|
|
|
const handleReferenceImageSelect = (event) => {
|
|
const file = event.target.files[0];
|
|
if (!file) return;
|
|
|
|
// Check if max reference images reached
|
|
if (selectedReferenceImages.length >= REFERENCE_IMAGE_CONFIG.maxReferenceImages) {
|
|
setErrors(prev => ({
|
|
...prev,
|
|
referenceImages: `Maximum ${REFERENCE_IMAGE_CONFIG.maxReferenceImages} reference images allowed`
|
|
}));
|
|
return;
|
|
}
|
|
|
|
const imageErrors = validateImage(file);
|
|
if (imageErrors.length > 0) {
|
|
setErrors(prev => ({
|
|
...prev,
|
|
referenceImages: imageErrors[0]
|
|
}));
|
|
return;
|
|
}
|
|
|
|
setSelectedReferenceImages(prev => [...prev, file]);
|
|
setErrors(prev => ({
|
|
...prev,
|
|
referenceImages: ''
|
|
}));
|
|
|
|
// Create preview
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
setReferenceImagePreviews(prev => [...prev, e.target.result]);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
|
|
// Reset file input for next selection
|
|
event.target.value = '';
|
|
};
|
|
|
|
const handleReferenceImageRemove = (index) => {
|
|
setSelectedReferenceImages(prev => prev.filter((_, i) => i !== index));
|
|
setReferenceImagePreviews(prev => prev.filter((_, i) => i !== index));
|
|
setErrors(prev => ({
|
|
...prev,
|
|
referenceImages: ''
|
|
}));
|
|
};
|
|
|
|
const validateForm = () => {
|
|
const newErrors = {};
|
|
|
|
if (!formData.prompt.trim()) {
|
|
newErrors.prompt = 'Prompt is required';
|
|
} else if (formData.prompt.trim().length < 10) {
|
|
newErrors.prompt = 'Prompt must be at least 10 characters long';
|
|
}
|
|
|
|
if (formData.video_length_sec < 4 || formData.video_length_sec > 8) {
|
|
newErrors.video_length_sec = 'Video length must be 4, 6, or 8 seconds';
|
|
}
|
|
|
|
if (formData.seed && formData.seed.trim() !== '' && (isNaN(formData.seed) || parseInt(formData.seed) < 0 || parseInt(formData.seed) > 4294967295)) {
|
|
newErrors.seed = 'Seed must be a number between 0 and 4294967295';
|
|
}
|
|
|
|
if (formData.sampleCount < 1 || formData.sampleCount > 4) {
|
|
newErrors.sampleCount = 'Sample count must be between 1 and 4';
|
|
}
|
|
|
|
// Veo 3.1 specific validations
|
|
if (selectedLastFrame && !selectedImage) {
|
|
newErrors.lastFrame = 'Last frame requires a first frame image';
|
|
}
|
|
|
|
if (selectedReferenceImages.length > REFERENCE_IMAGE_CONFIG.maxReferenceImages) {
|
|
newErrors.referenceImages = `Maximum ${REFERENCE_IMAGE_CONFIG.maxReferenceImages} reference images allowed`;
|
|
}
|
|
|
|
setErrors(newErrors);
|
|
return Object.keys(newErrors).length === 0;
|
|
};
|
|
|
|
const handleSubmit = (event) => {
|
|
event.preventDefault();
|
|
|
|
if (validateForm()) {
|
|
onSubmit({
|
|
...formData,
|
|
image: selectedImage,
|
|
lastFrame: selectedLastFrame,
|
|
referenceImages: selectedReferenceImages
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Paper elevation={3} sx={{ p: 4 }}>
|
|
<Typography variant="h4" component="h1" gutterBottom>
|
|
Generate Your Video
|
|
</Typography>
|
|
<Typography variant="body1" color="text.secondary" sx={{ mb: 3 }}>
|
|
Describe your video and customize the generation settings below.
|
|
</Typography>
|
|
|
|
<form onSubmit={handleSubmit}>
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={12}>
|
|
<TextField
|
|
fullWidth
|
|
multiline
|
|
rows={4}
|
|
label="Video Prompt"
|
|
placeholder="Describe the video you want to generate... (e.g., 'A stunning time-lapse of a flower blooming')"
|
|
value={formData.prompt}
|
|
onChange={handleChange('prompt')}
|
|
error={!!errors.prompt}
|
|
helperText={errors.prompt || 'Be as descriptive as possible for better results'}
|
|
required
|
|
/>
|
|
</Grid>
|
|
|
|
<Grid item xs={12}>
|
|
<Box sx={{ border: '2px dashed #ccc', borderRadius: 2, p: 3, textAlign: 'center' }}>
|
|
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
|
|
<ImageRounded />
|
|
First Frame Image (Optional)
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
Upload a reference image to guide video generation
|
|
</Typography>
|
|
|
|
{!imagePreview ? (
|
|
<Box>
|
|
<input
|
|
accept="image/*"
|
|
style={{ display: 'none' }}
|
|
id="image-upload"
|
|
type="file"
|
|
onChange={handleImageSelect}
|
|
/>
|
|
<label htmlFor="image-upload">
|
|
<Button
|
|
variant="outlined"
|
|
component="span"
|
|
startIcon={<CloudUploadRounded />}
|
|
sx={{ mt: 1 }}
|
|
>
|
|
Upload Image
|
|
</Button>
|
|
</label>
|
|
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
|
Supported: JPG, PNG • Max: 10MB • Min: 720p
|
|
<br />
|
|
<em>Tip: Smaller images (under 2MB) upload faster and are less likely to timeout</em>
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<Box>
|
|
<Box sx={{ position: 'relative', display: 'inline-block' }}>
|
|
<img
|
|
src={imagePreview}
|
|
alt="Preview"
|
|
style={{
|
|
maxWidth: '300px',
|
|
maxHeight: '200px',
|
|
borderRadius: '8px',
|
|
objectFit: 'cover'
|
|
}}
|
|
/>
|
|
<Button
|
|
variant="contained"
|
|
color="error"
|
|
size="small"
|
|
startIcon={<DeleteRounded />}
|
|
onClick={handleImageRemove}
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 8,
|
|
right: 8,
|
|
minWidth: 'auto'
|
|
}}
|
|
>
|
|
Remove
|
|
</Button>
|
|
</Box>
|
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
|
{selectedImage?.name}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
{errors.image && (
|
|
<Typography variant="body2" color="error" sx={{ mt: 1 }}>
|
|
{errors.image}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</Grid>
|
|
|
|
{/* Last Frame Upload - Veo 3.1 Only */}
|
|
{modelCapabilities.supportsLastFrame && (
|
|
<Grid item xs={12}>
|
|
<Box sx={{ border: '2px dashed #9c27b0', borderRadius: 2, p: 3, textAlign: 'center', bgcolor: '#f3e5f5' }}>
|
|
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, color: '#7b1fa2' }}>
|
|
<ImageRounded />
|
|
Last Frame Image (Veo 3.1 - Optional)
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
Upload a last frame for interpolation between first and last frames
|
|
</Typography>
|
|
<Alert severity="info" sx={{ mt: 1, mb: 2, textAlign: 'left' }}>
|
|
<strong>Frame Interpolation:</strong> When you provide both a first frame and last frame, Veo 3.1 will generate video content that smoothly transitions between them.
|
|
</Alert>
|
|
|
|
{!lastFramePreview ? (
|
|
<Box>
|
|
<input
|
|
accept="image/*"
|
|
style={{ display: 'none' }}
|
|
id="last-frame-upload"
|
|
type="file"
|
|
onChange={handleLastFrameSelect}
|
|
/>
|
|
<label htmlFor="last-frame-upload">
|
|
<Button
|
|
variant="outlined"
|
|
component="span"
|
|
startIcon={<CloudUploadRounded />}
|
|
sx={{ mt: 1 }}
|
|
color="secondary"
|
|
>
|
|
Upload Last Frame
|
|
</Button>
|
|
</label>
|
|
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
|
Supported: JPG, PNG • Max: 10MB
|
|
</Typography>
|
|
</Box>
|
|
) : (
|
|
<Box>
|
|
<Box sx={{ position: 'relative', display: 'inline-block' }}>
|
|
<img
|
|
src={lastFramePreview}
|
|
alt="Last Frame Preview"
|
|
style={{
|
|
maxWidth: '300px',
|
|
maxHeight: '200px',
|
|
borderRadius: '8px',
|
|
objectFit: 'cover'
|
|
}}
|
|
/>
|
|
<Button
|
|
variant="contained"
|
|
color="error"
|
|
size="small"
|
|
startIcon={<DeleteRounded />}
|
|
onClick={handleLastFrameRemove}
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 8,
|
|
right: 8,
|
|
minWidth: 'auto'
|
|
}}
|
|
>
|
|
Remove
|
|
</Button>
|
|
</Box>
|
|
<Typography variant="body2" sx={{ mt: 1 }}>
|
|
{selectedLastFrame?.name}
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
{errors.lastFrame && (
|
|
<Typography variant="body2" color="error" sx={{ mt: 1 }}>
|
|
{errors.lastFrame}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Reference Images - Veo 3.1 Only */}
|
|
{modelCapabilities.supportsReferenceImages && (
|
|
<Grid item xs={12}>
|
|
<Box sx={{ border: '2px dashed #1976d2', borderRadius: 2, p: 3, textAlign: 'center', bgcolor: '#e3f2fd' }}>
|
|
<Typography variant="h6" gutterBottom sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1, color: '#1565c0' }}>
|
|
<ImageRounded />
|
|
Reference Images (Veo 3.1 - Optional)
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
Upload up to 3 reference images to guide video content and preserve subject appearance
|
|
</Typography>
|
|
<Alert severity="info" sx={{ mt: 1, mb: 2, textAlign: 'left' }}>
|
|
<strong>Reference Images:</strong> Use these to maintain consistency of subjects, styles, or specific visual elements throughout the generated video.
|
|
</Alert>
|
|
|
|
<Box sx={{ display: 'flex', gap: 2, flexWrap: 'wrap', justifyContent: 'center', mt: 2 }}>
|
|
{referenceImagePreviews.map((preview, index) => (
|
|
<Box key={index} sx={{ position: 'relative', display: 'inline-block' }}>
|
|
<img
|
|
src={preview}
|
|
alt={`Reference ${index + 1}`}
|
|
style={{
|
|
width: '150px',
|
|
height: '150px',
|
|
borderRadius: '8px',
|
|
objectFit: 'cover',
|
|
border: '2px solid #1976d2'
|
|
}}
|
|
/>
|
|
<Button
|
|
variant="contained"
|
|
color="error"
|
|
size="small"
|
|
onClick={() => handleReferenceImageRemove(index)}
|
|
sx={{
|
|
position: 'absolute',
|
|
top: 4,
|
|
right: 4,
|
|
minWidth: 'auto',
|
|
padding: '4px 8px'
|
|
}}
|
|
>
|
|
<DeleteRounded fontSize="small" />
|
|
</Button>
|
|
<Typography variant="caption" display="block" sx={{ mt: 0.5, color: '#1565c0' }}>
|
|
Ref {index + 1}
|
|
</Typography>
|
|
</Box>
|
|
))}
|
|
</Box>
|
|
|
|
{selectedReferenceImages.length < REFERENCE_IMAGE_CONFIG.maxReferenceImages && (
|
|
<Box sx={{ mt: 2 }}>
|
|
<input
|
|
accept="image/*"
|
|
style={{ display: 'none' }}
|
|
id="reference-image-upload"
|
|
type="file"
|
|
onChange={handleReferenceImageSelect}
|
|
/>
|
|
<label htmlFor="reference-image-upload">
|
|
<Button
|
|
variant="outlined"
|
|
component="span"
|
|
startIcon={<CloudUploadRounded />}
|
|
color="primary"
|
|
>
|
|
Add Reference Image
|
|
</Button>
|
|
</label>
|
|
<Typography variant="caption" display="block" sx={{ mt: 1 }}>
|
|
{selectedReferenceImages.length}/{REFERENCE_IMAGE_CONFIG.maxReferenceImages} images • Supported: JPG, PNG • Max: 10MB each
|
|
</Typography>
|
|
</Box>
|
|
)}
|
|
|
|
{errors.referenceImages && (
|
|
<Typography variant="body2" color="error" sx={{ mt: 1 }}>
|
|
{errors.referenceImages}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
</Grid>
|
|
)}
|
|
|
|
<Grid item xs={12}>
|
|
<Accordion defaultExpanded>
|
|
<AccordionSummary expandIcon={<ExpandMoreRounded />}>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
<SettingsRounded />
|
|
<Typography variant="h6">Advanced Settings</Typography>
|
|
</Box>
|
|
</AccordionSummary>
|
|
<AccordionDetails>
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={12} sm={6}>
|
|
<FormControl fullWidth>
|
|
<InputLabel>Video Length</InputLabel>
|
|
<Select
|
|
value={formData.video_length_sec}
|
|
onChange={handleChange('video_length_sec')}
|
|
label="Video Length"
|
|
error={!!errors.video_length_sec}
|
|
>
|
|
{VIDEO_GENERATION_OPTIONS.videoLengths.map((option) => (
|
|
<MenuItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.video_length_sec && (
|
|
<Typography variant="caption" color="error" sx={{ mt: 0.5 }}>
|
|
{errors.video_length_sec}
|
|
</Typography>
|
|
)}
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
<Grid item xs={12} sm={6}>
|
|
<FormControl fullWidth>
|
|
<InputLabel>Number of Videos</InputLabel>
|
|
<Select
|
|
value={formData.sampleCount}
|
|
onChange={handleChange('sampleCount')}
|
|
label="Number of Videos"
|
|
error={!!errors.sampleCount}
|
|
>
|
|
{VIDEO_GENERATION_OPTIONS.sampleCounts.map((option) => (
|
|
<MenuItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
{errors.sampleCount && (
|
|
<Typography variant="caption" color="error" sx={{ mt: 0.5 }}>
|
|
{errors.sampleCount}
|
|
</Typography>
|
|
)}
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
<Grid item xs={12}>
|
|
<FormControl fullWidth>
|
|
<InputLabel>Model</InputLabel>
|
|
<Select
|
|
value={formData.model_name}
|
|
onChange={handleChange('model_name')}
|
|
label="Model"
|
|
>
|
|
{VIDEO_GENERATION_OPTIONS.models.map((model) => (
|
|
<MenuItem key={model.value} value={model.value}>
|
|
<Box>
|
|
<Typography variant="body1">
|
|
{model.label}
|
|
{model.recommended && (
|
|
<Typography component="span" variant="caption" color="primary" sx={{ ml: 1, fontWeight: 'bold' }}>
|
|
RECOMMENDED
|
|
</Typography>
|
|
)}
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
{model.description} • ${model.pricePerSecond}/sec • {model.speed}
|
|
</Typography>
|
|
</Box>
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
<Grid item xs={12} sm={6}>
|
|
<FormControl fullWidth>
|
|
<InputLabel>Aspect Ratio</InputLabel>
|
|
<Select
|
|
value={formData.aspect_ratio}
|
|
onChange={handleChange('aspect_ratio')}
|
|
label="Aspect Ratio"
|
|
>
|
|
{VIDEO_GENERATION_OPTIONS.aspectRatios.map((option) => (
|
|
<MenuItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
<Grid item xs={12} sm={6}>
|
|
<FormControl fullWidth>
|
|
<InputLabel>Person Generation</InputLabel>
|
|
<Select
|
|
value={formData.person_generation}
|
|
onChange={handleChange('person_generation')}
|
|
label="Person Generation"
|
|
>
|
|
{VIDEO_GENERATION_OPTIONS.personGeneration.map((option) => (
|
|
<MenuItem key={option.value} value={option.value}>
|
|
{option.label}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
</Grid>
|
|
|
|
<Grid item xs={12} sm={6}>
|
|
<TextField
|
|
fullWidth
|
|
type="number"
|
|
label="Seed (Optional)"
|
|
placeholder="Leave empty for random seed"
|
|
value={formData.seed}
|
|
onChange={handleChange('seed')}
|
|
error={!!errors.seed}
|
|
helperText={errors.seed || 'Optional: Enter a number (0-4294967295) for reproducible results, or leave empty for random'}
|
|
inputProps={{ min: 0, max: 4294967295 }}
|
|
/>
|
|
</Grid>
|
|
|
|
<Grid item xs={12} sm={6}>
|
|
<FormControlLabel
|
|
control={
|
|
<Checkbox
|
|
checked={formData.generate_audio}
|
|
onChange={handleChange('generate_audio')}
|
|
name="generate_audio"
|
|
/>
|
|
}
|
|
label="Generate Audio"
|
|
sx={{ mt: 1 }}
|
|
/>
|
|
<Typography variant="body2" color="text.secondary" sx={{ ml: 4, mt: -1 }}>
|
|
Include audio in the generated video
|
|
</Typography>
|
|
</Grid>
|
|
|
|
</Grid>
|
|
</AccordionDetails>
|
|
</Accordion>
|
|
</Grid>
|
|
|
|
<Grid item xs={12}>
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}>
|
|
<Button
|
|
type="submit"
|
|
variant="contained"
|
|
size="large"
|
|
startIcon={<PlayArrowRounded />}
|
|
sx={{ px: 4, py: 1.5 }}
|
|
>
|
|
{userJobs.some(job => ['starting', 'uploading_image', 'generating', 'processing', 'downloading', 'queued'].includes(job.status))
|
|
? 'Add to Queue'
|
|
: 'Generate Video'
|
|
}
|
|
</Button>
|
|
</Box>
|
|
<Typography variant="caption" color="text.secondary" sx={{ textAlign: 'center', display: 'block', mt: 1 }}>
|
|
Total Jobs: {userJobs.length}
|
|
</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
</form>
|
|
</Paper>
|
|
);
|
|
};
|
|
|
|
export default VideoForm; |