Spaces:
Running
Running
Upload index.html
Browse files- templates/index.html +279 -0
templates/index.html
ADDED
@@ -0,0 +1,279 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<head>
|
4 |
+
<meta charset="UTF-8">
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Sketch2Video with Gemini & Veo 3</title>
|
7 |
+
<style>
|
8 |
+
body { font-family: sans-serif; display: flex; flex-direction: column; align-items: center; padding: 20px; background-color: #f4f4f4; min-height: 100vh; box-sizing: border-box; }
|
9 |
+
h1 { color: #333; text-align: center; margin-bottom: 25px; }
|
10 |
+
h2 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.2em; color: #444; }
|
11 |
+
.attribution { text-align: center; font-size: 1.2em; color: #667; margin-top: -15px; margin-bottom: 25px; }
|
12 |
+
.attribution .heart { display: inline-block; vertical-align: middle; }
|
13 |
+
.attribution .dev-logo-img { display: inline-block; width: 1.5em; height: auto; vertical-align: middle; margin: 0 0.2em; position: relative; top: -0.1em; }
|
14 |
+
.main-container { display: flex; justify-content: space-around; align-items: center; width: 95%; max-width: 1100px; gap: 30px; margin-top: 20px; flex-wrap: wrap; }
|
15 |
+
.sketch-area, .output-container { display: flex; flex-direction: column; align-items: center; width: auto; flex-basis: 512px; flex-grow: 0; box-sizing: border-box; }
|
16 |
+
.canvas-wrapper, .output-video-wrapper {
|
17 |
+
width: 100%;
|
18 |
+
aspect-ratio: 16 / 9;
|
19 |
+
background-color: #e9e9e9;
|
20 |
+
display: flex;
|
21 |
+
align-items: center;
|
22 |
+
justify-content: center;
|
23 |
+
border: 1px solid #ccc;
|
24 |
+
box-shadow: 2px 2px 5px rgba(0,0,0,0.05);
|
25 |
+
overflow: hidden;
|
26 |
+
}
|
27 |
+
canvas {
|
28 |
+
display: block;
|
29 |
+
background-color: #fff;
|
30 |
+
width: 100%;
|
31 |
+
height: 100%;
|
32 |
+
cursor: crosshair;
|
33 |
+
touch-action: none;
|
34 |
+
}
|
35 |
+
canvas.erase-mode { cursor: cell; }
|
36 |
+
.output-video-wrapper video { width: 100%; height: 100%; display: block; }
|
37 |
+
.output-video-wrapper .placeholder, .output-video-wrapper .output-error {
|
38 |
+
width: 100%;
|
39 |
+
height: 100%;
|
40 |
+
background-color: #fff;
|
41 |
+
color: #888;
|
42 |
+
text-align: center;
|
43 |
+
padding: 20px;
|
44 |
+
display: flex;
|
45 |
+
align-items: center;
|
46 |
+
justify-content: center;
|
47 |
+
box-sizing: border-box;
|
48 |
+
}
|
49 |
+
.output-video-wrapper .output-error { color: red; font-weight: bold; word-break: break-word; }
|
50 |
+
.canvas-controls-container { display: flex; justify-content: center; align-items: center; gap: 15px; margin-top: 25px; margin-bottom: 15px; flex-wrap: wrap; padding: 10px; background-color: #e9e9e9; border-radius: 5px; width: auto; max-width: 90%; }
|
51 |
+
.canvas-controls-container button { padding: 8px 12px; font-size: 0.9em; cursor: pointer; border: 1px solid #ccc; border-radius: 4px; background-color: #f8f8f8; }
|
52 |
+
.canvas-controls-container button:hover { background-color: #e0e0e0; border-color: #bbb; }
|
53 |
+
.canvas-controls-container button.active-tool { background-color: #d0e7ff; border-color: #007bff; font-weight: bold; }
|
54 |
+
label { font-size: 0.9em; color: #555; }
|
55 |
+
#colorPicker { width: 40px; height: 30px; border: 1px solid #ccc; padding: 2px; border-radius: 4px; cursor: pointer; background-color: #fff; vertical-align: middle; }
|
56 |
+
|
57 |
+
/* --- NEW CSS: A small addition for better alignment of the new slider --- */
|
58 |
+
.thickness-control { display: flex; align-items: center; gap: 5px; }
|
59 |
+
.thickness-control input[type="range"] { vertical-align: middle; }
|
60 |
+
|
61 |
+
.prompt-container { display: flex; flex-direction: column; align-items: center; width: 100%; max-width: 800px; margin-bottom: 15px; }
|
62 |
+
#promptInput { width: 100%; padding: 8px 10px; border: 1px solid #000000; border-radius: 8px; font-size: 1.2em; min-height: 40px; resize: vertical; line-height: 1.4; box-sizing: border-box; }
|
63 |
+
.action-buttons { display: flex; justify-content: center; gap: 15px; width: 100%; margin-top: 0; }
|
64 |
+
.action-buttons button { padding: 12px 20px; font-size: 1.1em; cursor: pointer; border: none; border-radius: 5px; color: white; transition: background-color 0.2s; min-width: 120px; }
|
65 |
+
.action-buttons button:disabled { background-color: #cccccc; cursor: not-allowed; }
|
66 |
+
#generateBtn { background-color: #007bff; }
|
67 |
+
#generateBtn:hover:not(:disabled) { background-color: #0056b3; }
|
68 |
+
#clearBtn { background-color: #dc3545; }
|
69 |
+
#clearBtn:hover:not(:disabled) { background-color: #c82333; }
|
70 |
+
.status-area { margin-top: 20px; font-weight: bold; min-height: 1.2em; text-align: center; width: 100%; padding-bottom: 20px; }
|
71 |
+
.status-area.error { color: red; }
|
72 |
+
.status-area.loading { color: #0056b3; }
|
73 |
+
</style>
|
74 |
+
</head>
|
75 |
+
<body>
|
76 |
+
<h1>Sketch2Video with Gemini & Veo 3</h1>
|
77 |
+
<h3 class="attribution">
|
78 |
+
Made with <span class="heart">🧡</span> by
|
79 |
+
<img src="{{ url_for('static', filename='images/dev-logo.png') }}" alt="Google Developers logo" class="dev-logo-img">
|
80 |
+
Nitin Tiwari
|
81 |
+
</h2>
|
82 |
+
|
83 |
+
<div class="main-container">
|
84 |
+
<div class="sketch-area">
|
85 |
+
<h2>Your Sketch</h2>
|
86 |
+
<div class="canvas-wrapper">
|
87 |
+
<canvas id="sketchCanvas" width="512" height="288"></canvas>
|
88 |
+
</div>
|
89 |
+
</div>
|
90 |
+
<div class="output-container">
|
91 |
+
<h2>Generated Video</h2>
|
92 |
+
<div id="outputWrapper" class="output-video-wrapper">
|
93 |
+
<video id="outputVideo" controls autoplay muted loop playsinline style="display: none;"></video>
|
94 |
+
<div id="outputPlaceholder" class="placeholder">Video will appear here...</div>
|
95 |
+
<div id="outputError" class="output-error" style="display: none;"></div>
|
96 |
+
</div>
|
97 |
+
</div>
|
98 |
+
</div>
|
99 |
+
|
100 |
+
<div class="canvas-controls-container">
|
101 |
+
<button id="drawBtn" class="active-tool">Draw</button>
|
102 |
+
<button id="eraseBtn">Erase</button>
|
103 |
+
<label for="colorPicker">Color:</label>
|
104 |
+
<input type="color" id="colorPicker" value="#000000">
|
105 |
+
|
106 |
+
<!-- +++ NEW HTML: Brush thickness slider +++ -->
|
107 |
+
<div class="thickness-control">
|
108 |
+
<label for="thicknessSlider">Thickness:</label>
|
109 |
+
<input type="range" id="thicknessSlider" min="1" max="50" value="3">
|
110 |
+
<span id="thicknessValue">3</span>
|
111 |
+
</div>
|
112 |
+
<!-- +++ END OF NEW HTML +++ -->
|
113 |
+
|
114 |
+
</div>
|
115 |
+
<div class="prompt-container">
|
116 |
+
<textarea id="promptInput" rows="1" placeholder="Describe the video animation (e.g., camera zooms in, car drives away)"></textarea>
|
117 |
+
</div>
|
118 |
+
<div class="action-buttons">
|
119 |
+
<button id="clearBtn">Clear Sketch & Prompt</button>
|
120 |
+
<button id="generateBtn">Generate Video</button>
|
121 |
+
</div>
|
122 |
+
<div id="status" class="status-area"></div>
|
123 |
+
<script>
|
124 |
+
const promptInput = document.getElementById('promptInput');
|
125 |
+
const canvas = document.getElementById('sketchCanvas');
|
126 |
+
const ctx = canvas.getContext('2d');
|
127 |
+
const clearBtn = document.getElementById('clearBtn');
|
128 |
+
const generateBtn = document.getElementById('generateBtn');
|
129 |
+
const eraseBtn = document.getElementById('eraseBtn');
|
130 |
+
const drawBtn = document.getElementById('drawBtn');
|
131 |
+
const colorPicker = document.getElementById('colorPicker');
|
132 |
+
|
133 |
+
// +++ NEW JS: Get the new slider elements +++
|
134 |
+
const thicknessSlider = document.getElementById('thicknessSlider');
|
135 |
+
const thicknessValue = document.getElementById('thicknessValue');
|
136 |
+
|
137 |
+
const outputWrapper = document.getElementById('outputWrapper');
|
138 |
+
const outputVideo = document.getElementById('outputVideo');
|
139 |
+
const outputPlaceholder = document.getElementById('outputPlaceholder');
|
140 |
+
const outputError = document.getElementById('outputError');
|
141 |
+
const statusDiv = document.getElementById('status');
|
142 |
+
let isDrawing = false;
|
143 |
+
let lastX = 0;
|
144 |
+
let lastY = 0;
|
145 |
+
let currentMode = 'draw';
|
146 |
+
let drawColor = '#000000';
|
147 |
+
|
148 |
+
// +++ MODIFIED JS: Change drawLineWidth from const to let so it can be updated +++
|
149 |
+
let drawLineWidth = 3;
|
150 |
+
const eraseLineWidth = 15;
|
151 |
+
|
152 |
+
function initializeCanvas() {
|
153 |
+
ctx.fillStyle = "white";
|
154 |
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
155 |
+
drawColor = '#000000';
|
156 |
+
colorPicker.value = '#000000';
|
157 |
+
|
158 |
+
// +++ NEW JS: Reset the slider to its default state +++
|
159 |
+
drawLineWidth = 3;
|
160 |
+
thicknessSlider.value = 3;
|
161 |
+
thicknessValue.textContent = 3;
|
162 |
+
|
163 |
+
setDrawMode();
|
164 |
+
outputVideo.style.display = 'none';
|
165 |
+
outputVideo.src = '';
|
166 |
+
outputPlaceholder.style.display = 'flex';
|
167 |
+
outputPlaceholder.textContent = 'Video will appear here...';
|
168 |
+
outputError.style.display = 'none';
|
169 |
+
statusDiv.textContent = '';
|
170 |
+
statusDiv.className = 'status-area';
|
171 |
+
generateBtn.disabled = false;
|
172 |
+
clearBtn.disabled = false;
|
173 |
+
eraseBtn.disabled = false;
|
174 |
+
drawBtn.disabled = false;
|
175 |
+
colorPicker.disabled = false;
|
176 |
+
promptInput.value = '';
|
177 |
+
promptInput.disabled = false;
|
178 |
+
}
|
179 |
+
|
180 |
+
function setDrawMode() { currentMode = 'draw'; ctx.globalCompositeOperation = 'source-over'; ctx.strokeStyle = drawColor; ctx.lineWidth = drawLineWidth; canvas.classList.remove('erase-mode'); drawBtn.classList.add('active-tool'); eraseBtn.classList.remove('active-tool'); }
|
181 |
+
function setEraseMode() { currentMode = 'erase'; ctx.globalCompositeOperation = 'destination-out'; ctx.lineWidth = eraseLineWidth; canvas.classList.add('erase-mode'); eraseBtn.classList.add('active-tool'); drawBtn.classList.remove('active-tool'); }
|
182 |
+
|
183 |
+
function getPos(evt) {
|
184 |
+
const rect = canvas.getBoundingClientRect();
|
185 |
+
const scaleX = canvas.width / rect.width;
|
186 |
+
const scaleY = canvas.height / rect.height;
|
187 |
+
const clientX = evt.touches ? evt.touches[0].clientX : evt.clientX;
|
188 |
+
const clientY = evt.touches ? evt.touches[0].clientY : evt.clientY;
|
189 |
+
return {
|
190 |
+
x: (clientX - rect.left) * scaleX,
|
191 |
+
y: (clientY - rect.top) * scaleY
|
192 |
+
};
|
193 |
+
}
|
194 |
+
|
195 |
+
function startDrawing(e) { if (e.touches) e.preventDefault(); isDrawing = true; const pos = getPos(e); [lastX, lastY] = [pos.x, pos.y]; ctx.beginPath(); ctx.moveTo(lastX, lastY); ctx.fillStyle = (currentMode === 'draw') ? ctx.strokeStyle : 'rgba(0,0,0,1)'; ctx.arc(lastX, lastY, ctx.lineWidth / 2, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.moveTo(lastX, lastY); }
|
196 |
+
function draw(e) { if (!isDrawing) return; if (e.touches) e.preventDefault(); const pos = getPos(e); ctx.lineTo(pos.x, pos.y); ctx.stroke(); [lastX, lastY] = [pos.x, pos.y]; }
|
197 |
+
function stopDrawing() { if (isDrawing) { isDrawing = false; ctx.beginPath(); } }
|
198 |
+
|
199 |
+
canvas.addEventListener('mousedown', startDrawing);
|
200 |
+
canvas.addEventListener('mousemove', draw);
|
201 |
+
canvas.addEventListener('mouseup', stopDrawing);
|
202 |
+
canvas.addEventListener('mouseout', stopDrawing);
|
203 |
+
canvas.addEventListener('touchstart', startDrawing, { passive: false });
|
204 |
+
canvas.addEventListener('touchmove', draw, { passive: false });
|
205 |
+
canvas.addEventListener('touchend', stopDrawing);
|
206 |
+
canvas.addEventListener('touchcancel', stopDrawing);
|
207 |
+
|
208 |
+
clearBtn.addEventListener('click', () => { initializeCanvas(); });
|
209 |
+
drawBtn.addEventListener('click', setDrawMode);
|
210 |
+
eraseBtn.addEventListener('click', setEraseMode);
|
211 |
+
colorPicker.addEventListener('input', (event) => { drawColor = event.target.value; if (currentMode === 'draw') { ctx.strokeStyle = drawColor; } setDrawMode(); });
|
212 |
+
|
213 |
+
// +++ NEW JS: Event listener for the thickness slider +++
|
214 |
+
thicknessSlider.addEventListener('input', (event) => {
|
215 |
+
// Update the variable, the display number, and the context
|
216 |
+
drawLineWidth = event.target.value;
|
217 |
+
thicknessValue.textContent = drawLineWidth;
|
218 |
+
ctx.lineWidth = drawLineWidth;
|
219 |
+
// For good UX, switch back to draw mode when changing thickness
|
220 |
+
setDrawMode();
|
221 |
+
});
|
222 |
+
|
223 |
+
// The generateBtn logic is unchanged and correct
|
224 |
+
generateBtn.addEventListener('click', async () => {
|
225 |
+
generateBtn.disabled = true; clearBtn.disabled = true; eraseBtn.disabled = true;
|
226 |
+
drawBtn.disabled = true; colorPicker.disabled = true; promptInput.disabled = true;
|
227 |
+
statusDiv.textContent = 'Generating video... this may take a few minutes.';
|
228 |
+
statusDiv.className = 'status-area loading';
|
229 |
+
outputVideo.style.display = 'none';
|
230 |
+
outputError.style.display = 'none';
|
231 |
+
outputPlaceholder.style.display = 'flex';
|
232 |
+
outputPlaceholder.textContent = 'Generating...';
|
233 |
+
const imageDataUrl = canvas.toDataURL('image/png');
|
234 |
+
const userPrompt = promptInput.value.trim();
|
235 |
+
const payload = { image_data: imageDataUrl };
|
236 |
+
if (userPrompt) { payload.prompt = userPrompt; }
|
237 |
+
try {
|
238 |
+
const response = await fetch('/generate', {
|
239 |
+
method: 'POST',
|
240 |
+
headers: { 'Content-Type': 'application/json', },
|
241 |
+
body: JSON.stringify(payload),
|
242 |
+
});
|
243 |
+
const result = await response.json();
|
244 |
+
if (response.ok && result.generated_video_url) {
|
245 |
+
outputVideo.src = result.generated_video_url;
|
246 |
+
outputVideo.load();
|
247 |
+
outputVideo.style.display = 'block';
|
248 |
+
outputPlaceholder.style.display = 'none';
|
249 |
+
outputError.style.display = 'none';
|
250 |
+
statusDiv.textContent = 'Video generated successfully!';
|
251 |
+
statusDiv.className = 'status-area';
|
252 |
+
} else {
|
253 |
+
const errorMsg = result.error || response.statusText || `Failed with status ${response.status}`;
|
254 |
+
console.error('Generation failed:', errorMsg);
|
255 |
+
statusDiv.textContent = `Error: ${errorMsg}`;
|
256 |
+
statusDiv.className = 'status-area error';
|
257 |
+
outputError.textContent = `Generation Failed: ${errorMsg}`;
|
258 |
+
outputError.style.display = 'flex';
|
259 |
+
outputPlaceholder.style.display = 'none';
|
260 |
+
outputVideo.style.display = 'none';
|
261 |
+
}
|
262 |
+
} catch (error) {
|
263 |
+
console.error('Network or fetch error:', error);
|
264 |
+
const errorText = `Network error or failed to fetch: ${error.message || String(error)}`;
|
265 |
+
statusDiv.textContent = `Error: ${errorText}`;
|
266 |
+
statusDiv.className = 'status-area error';
|
267 |
+
outputError.textContent = `Error: ${errorText}`;
|
268 |
+
outputError.style.display = 'flex';
|
269 |
+
outputPlaceholder.style.display = 'none';
|
270 |
+
outputVideo.style.display = 'none';
|
271 |
+
} finally {
|
272 |
+
generateBtn.disabled = false; clearBtn.disabled = false; eraseBtn.disabled = false;
|
273 |
+
drawBtn.disabled = false; colorPicker.disabled = false; promptInput.disabled = false;
|
274 |
+
}
|
275 |
+
});
|
276 |
+
window.onload = initializeCanvas;
|
277 |
+
</script>
|
278 |
+
</body>
|
279 |
+
</html>
|