| 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; |