|
import React, { useState, useEffect, FormEvent, ChangeEvent } from 'react'; |
|
|
|
interface InputData { |
|
prompt: string; |
|
negativePrompt: string; |
|
base_model: string; |
|
steps: number; |
|
guidance_scale: number; |
|
frames: number; |
|
width: number; |
|
height: number; |
|
seed: number; |
|
zoom_in_motion_strength: number; |
|
zoom_out_motion_strength: number; |
|
pan_left_motion_strength: number; |
|
pan_right_motion_strength: number; |
|
pan_up_motion_strength: number; |
|
pan_down_motion_strength: number; |
|
rolling_clockwise_motion_strength: number; |
|
rolling_anticlockwise_motion_strength: number; |
|
output_format: string; |
|
} |
|
|
|
interface OutputData { |
|
prediction: any; |
|
timestamp: string; |
|
prompt: string; |
|
} |
|
const TruncatedText = ({ text }: { text: string }) => { |
|
const [isTruncated, setIsTruncated] = useState(true); |
|
|
|
const toggleTruncated = () => { |
|
setIsTruncated(!isTruncated); |
|
}; |
|
|
|
if (text.length < 34) { |
|
return <span>{text}</span>; |
|
} |
|
|
|
return ( |
|
<span> |
|
{isTruncated ? `${text.substring(0, 24)}...` : text} |
|
<button onClick={toggleTruncated}> |
|
{isTruncated ? 'Read More' : 'Read Less'} |
|
</button> |
|
</span> |
|
); |
|
}; |
|
|
|
const IndexPage = () => { |
|
const [inputData, setInputData] = useState<InputData>({ |
|
prompt: '', |
|
negativePrompt: '', |
|
base_model: 'realisticVisionV20_v20', |
|
steps: 25, |
|
guidance_scale: 7.5, |
|
frames: 16, |
|
width: 768, |
|
height: 512, |
|
seed: -1, |
|
zoom_in_motion_strength: 0, |
|
zoom_out_motion_strength: 0, |
|
pan_left_motion_strength: 0, |
|
pan_right_motion_strength: 0.75, |
|
pan_up_motion_strength: 0, |
|
pan_down_motion_strength: 0, |
|
rolling_clockwise_motion_strength: 0, |
|
rolling_anticlockwise_motion_strength: 0, |
|
output_format: 'mp4' |
|
}); |
|
|
|
const [output, setOutput] = useState<OutputData[]>([]); |
|
const [isLoading, setIsLoading] = useState(false); |
|
|
|
useEffect(() => { |
|
const savedOutput = localStorage.getItem('output'); |
|
if (savedOutput) { |
|
setOutput(JSON.parse(savedOutput)); |
|
} |
|
}, []); |
|
|
|
useEffect(() => { |
|
output.forEach(video => { |
|
console.log('Video URL:', video.prediction.output); |
|
}); |
|
}, [output]); |
|
|
|
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => { |
|
const { name, value } = e.target; |
|
setInputData((prevData) => ({ ...prevData, [name]: value })); |
|
}; |
|
|
|
const handleSubmit = async (e: FormEvent) => { |
|
e.preventDefault(); |
|
setIsLoading(true); |
|
|
|
|
|
const payload = Object.fromEntries( |
|
Object.entries(inputData).map(([key, value]) => { |
|
return [key, isNaN(Number(value)) ? value : Number(value)]; |
|
}) |
|
); |
|
|
|
try { |
|
const response = await fetch('/api/animatediff', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify(payload), |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (response.ok) { |
|
const timestamp = new Date().toISOString(); |
|
const newOutput = { |
|
prediction: result.prediction, |
|
timestamp, |
|
prompt: inputData.prompt, |
|
}; |
|
const updatedOutput = [...output, newOutput]; |
|
setOutput(updatedOutput); |
|
localStorage.setItem('output', JSON.stringify(updatedOutput)); |
|
setInputData({ |
|
prompt: '', |
|
negativePrompt: '', |
|
base_model: 'realisticVisionV20_v20', |
|
steps: 25, |
|
guidance_scale: 7.5, |
|
frames: 16, |
|
width: 768, |
|
height: 512, |
|
seed: -1, |
|
zoom_in_motion_strength: 0, |
|
zoom_out_motion_strength: 0, |
|
pan_left_motion_strength: 0, |
|
pan_right_motion_strength: 0.75, |
|
pan_up_motion_strength: 0, |
|
pan_down_motion_strength: 0, |
|
rolling_clockwise_motion_strength: 0, |
|
rolling_anticlockwise_motion_strength: 0, |
|
output_format: 'mp4' |
|
}); |
|
} |
|
} catch (error) { |
|
console.error("An error occurred:", error); |
|
} finally { |
|
setIsLoading(false); |
|
} |
|
}; |
|
|
|
|
|
return ( |
|
<div className="bg-gray-900 text-white p-4 container mx-auto" style={{ minHeight: '100vh' }}> |
|
<h1 className="text-2xl mb-4">AnimateDiff Generation</h1> |
|
<form onSubmit={handleSubmit} className="space-y-4"> |
|
{/* Video Settings */} |
|
<div className="bg-gray-800 p-4 rounded space-y-2"> |
|
<h2 className="text-lg font-bold">Video Settings</h2> |
|
<div className="flex space-x-4"> {/* Added flex container */} |
|
<textarea |
|
name="prompt" |
|
value={inputData.prompt} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-700 rounded border border-gray-600 w-1/2 h-24" |
|
placeholder="Enter video prompt..." |
|
></textarea> |
|
|
|
<textarea |
|
name="negativePrompt" |
|
value={inputData.negativePrompt} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-700 rounded border border-gray-600 w-1/2 h-24" |
|
placeholder="Enter negative prompt..." |
|
></textarea> |
|
</div></div> |
|
|
|
|
|
{/* Dimensions */} |
|
<div className="bg-gray-800 p-4 rounded space-y-2"> |
|
<label className="block text-lg">Base Settings</label> |
|
<div className="flex flex-wrap space-x-4"> |
|
<div className="w-1/5"> |
|
<label>Base Model:</label> |
|
<select |
|
name="base_model" |
|
value={inputData.base_model} |
|
onChange={handleChange} |
|
className="p-3 bg-gray-800 rounded border border-gray-600 text-lg w-full"> |
|
{/* Options */} |
|
<option value="realisticVisionV20_v20">realisticVisionV20_v20</option> |
|
<option value="lyriel_v16">lyriel_v16</option> |
|
<option value="majicmixRealistic_v5Preview">majicmixRealistic_v5Preview</option> |
|
<option value="rcnzCartoon3d_v10">rcnzCartoon3d_v10</option> |
|
<option value="toonyou_beta3">toonyou_beta3</option> |
|
</select> |
|
</div> |
|
{/* Numeric Inputs */} |
|
{['steps', 'guidance_scale', 'frames', 'width', 'height', 'seed'].map((key) => ( |
|
<div className="w-1/5" key={key}> |
|
<label>{key.replace('_', ' ').charAt(0).toUpperCase() + key.slice(1)}:</label> |
|
<input |
|
type="number" |
|
name={key} |
|
value={inputData[key as keyof InputData]} |
|
onChange={handleChange} |
|
className="p-2 bg-gray-800 rounded border border-gray-600 text-lg w-full" |
|
/> |
|
</div> |
|
))} |
|
</div> |
|
</div> |
|
|
|
|
|
|
|
|
|
{/* Motion Settings */} |
|
<div className="bg-gray-800 p-4 rounded space-y-2"> |
|
<h2 className="text-lg font-bold">Motion Settings</h2> |
|
<div className="flex space-x-4"> |
|
<label>Zoom In: <input type="range" min="0" max="1" step="0.01" name="zoom_in_motion_strength" value={inputData.zoom_in_motion_strength} onChange={handleChange} /><span>{inputData.zoom_in_motion_strength}</span></label> |
|
<label>Zoom Out: <input type="range" min="0" max="1" step="0.01" name="zoom_out_motion_strength" value={inputData.zoom_out_motion_strength} onChange={handleChange} /><span>{inputData.zoom_out_motion_strength}</span></label> |
|
</div> |
|
<label>Pan Left: <input type="range" min="0" max="1" step="0.01" name="pan_left_motion_strength" value={inputData.pan_left_motion_strength} onChange={handleChange} /><span>{inputData.pan_left_motion_strength}</span></label> |
|
<label>Pan Right: <input type="range" min="0" max="1" step="0.01" name="pan_right_motion_strength" value={inputData.pan_right_motion_strength} onChange={handleChange} /><span>{inputData.pan_right_motion_strength}</span></label> |
|
<label>Pan Up: <input type="range" min="0" max="1" step="0.01" name="pan_up_motion_strength" value={inputData.pan_up_motion_strength} onChange={handleChange} /><span>{inputData.pan_up_motion_strength}</span></label> |
|
<label>Pan Down: <input type="range" min="0" max="1" step="0.01" name="pan_down_motion_strength" value={inputData.pan_down_motion_strength} onChange={handleChange} /><span>{inputData.pan_down_motion_strength}</span></label> |
|
</div> |
|
|
|
{/* Output Settings */} |
|
<div className="bg-gray-800 p-4 rounded space-y-2"> |
|
<h2 className="text-lg font-bold">Output Settings</h2> |
|
<div className="flex space-x-4"> |
|
<input type="radio" id="mp4" name="output_format" value="mp4" checked={inputData.output_format === 'mp4'} onChange={handleChange} /> |
|
<label htmlFor="mp4">MP4</label> |
|
<input type="radio" id="gif" name="output_format" value="gif" checked={inputData.output_format === 'gif'} onChange={handleChange} /> |
|
<label htmlFor="gif">GIF</label> |
|
</div> |
|
</div> |
|
|
|
{/* Submit Button */} |
|
<button |
|
type="submit" |
|
className={`p-2 px-5 rounded ${isLoading ? 'bg-gray-600' : 'bg-green-600'} text-white`} |
|
disabled={isLoading} |
|
style={{ cursor: isLoading ? 'not-allowed' : 'pointer' }} |
|
> |
|
{isLoading ? 'Generating...' : 'Generate'} |
|
</button> |
|
</form> |
|
<div className="grid lg:grid-cols-3 grid-cols-1 gap-4 mt-8"> |
|
{output.map((video, index) => ( |
|
<div key={index} className="space-y-2"> |
|
<h2 className="text-xl">Generated Video {index + 1}:</h2> |
|
<div className="relative"> |
|
<video |
|
controls |
|
loop |
|
width="100%" |
|
onMouseOver={event => { |
|
event.currentTarget.play(); |
|
event.currentTarget.setAttribute("controlsList", "nodownload"); |
|
}} |
|
onMouseOut={event => { |
|
event.currentTarget.pause(); |
|
event.currentTarget.removeAttribute("controlsList"); |
|
}} |
|
> |
|
<source src={video.prediction.output} type="video/mp4" /> |
|
</video> |
|
|
|
{/* Download Button */} |
|
<button |
|
onClick={async () => { |
|
try { |
|
const response = await fetch(video.prediction.output); |
|
const blob = await response.blob(); |
|
const url = window.URL.createObjectURL(blob); |
|
const link = document.createElement('a'); |
|
link.href = url; |
|
link.download = 'video.mp4'; |
|
document.body.appendChild(link); |
|
link.click(); |
|
document.body.removeChild(link); |
|
window.URL.revokeObjectURL(url); |
|
} catch (error) { |
|
console.error("Failed to download the video", error); |
|
} |
|
}} |
|
className="absolute top-0 right-0 bg-green-600 text-white px-3 py-1 rounded" |
|
> |
|
Download |
|
</button> |
|
|
|
|
|
|
|
</div> |
|
<p>Date: {new Date(video.timestamp).toLocaleDateString()} Time: {new Date(video.timestamp).toLocaleTimeString()}</p> |
|
<TruncatedText text={video.prompt} /> |
|
</div> |
|
))} |
|
</div> |
|
</div> |
|
); |
|
}; |
|
|
|
export default IndexPage; |