yhzion commited on
Commit
6ea9418
·
verified ·
1 Parent(s): db0c1c1

undefined - Initial Deployment

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +607 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Video Annotator
3
- emoji: 🔥
4
- colorFrom: red
5
- colorTo: green
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: video-annotator
3
+ emoji: 🐳
4
+ colorFrom: pink
5
+ colorTo: yellow
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,607 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Video Segment Annotator</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/lucide@latest"></script>
9
+ <style>
10
+ .waveform {
11
+ background: linear-gradient(to bottom, #1a202c, #2d3748);
12
+ height: 80px;
13
+ position: relative;
14
+ overflow: hidden;
15
+ }
16
+
17
+ .waveform-line {
18
+ position: absolute;
19
+ bottom: 0;
20
+ width: 2px;
21
+ background-color: #2ED9FF;
22
+ transition: height 0.1s ease;
23
+ }
24
+
25
+ .playhead {
26
+ position: absolute;
27
+ top: 0;
28
+ bottom: 0;
29
+ width: 2px;
30
+ background-color: #fff;
31
+ z-index: 10;
32
+ }
33
+
34
+ .selection-area {
35
+ position: absolute;
36
+ top: 0;
37
+ bottom: 0;
38
+ background-color: rgba(255, 213, 79, 0.6);
39
+ z-index: 5;
40
+ }
41
+
42
+ .handle {
43
+ position: absolute;
44
+ top: 0;
45
+ bottom: 0;
46
+ width: 8px;
47
+ background-color: #FFD54F;
48
+ cursor: ew-resize;
49
+ z-index: 15;
50
+ }
51
+
52
+ .marker {
53
+ position: absolute;
54
+ top: 0;
55
+ width: 2px;
56
+ height: 20px;
57
+ background-color: #f56565;
58
+ z-index: 8;
59
+ }
60
+
61
+ .time-ruler {
62
+ height: 20px;
63
+ background-color: #2d3748;
64
+ position: relative;
65
+ overflow: hidden;
66
+ }
67
+
68
+ .time-tick {
69
+ position: absolute;
70
+ bottom: 0;
71
+ width: 1px;
72
+ background-color: #718096;
73
+ }
74
+
75
+ .time-label {
76
+ position: absolute;
77
+ bottom: 0;
78
+ font-size: 10px;
79
+ color: #a0aec0;
80
+ transform: translateX(-50%);
81
+ }
82
+
83
+ .media-viewer {
84
+ background-color: #1a202c;
85
+ aspect-ratio: 16/9;
86
+ }
87
+
88
+ @media (max-width: 768px) {
89
+ .toolbar {
90
+ flex-wrap: wrap;
91
+ }
92
+ .toolbar button {
93
+ margin-bottom: 4px;
94
+ }
95
+ }
96
+ </style>
97
+ </head>
98
+ <body class="bg-gray-900 text-gray-200 font-sans">
99
+ <div class="container mx-auto px-4 py-6">
100
+ <header class="mb-6">
101
+ <h1 class="text-2xl font-semibold text-blue-400">Video Segment Annotator</h1>
102
+ <p class="text-gray-400">Professional timeline for precise video annotation</p>
103
+ </header>
104
+
105
+ <div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
106
+ <!-- Main content area -->
107
+ <div class="lg:col-span-3 space-y-4">
108
+ <!-- Media Viewer -->
109
+ <div class="media-viewer rounded-lg overflow-hidden relative flex items-center justify-center">
110
+ <video id="videoElement" class="w-full h-full" controls>
111
+ <source src="https://samplelib.com/lib/preview/mp4/sample-5s.mp4" type="video/mp4">
112
+ </video>
113
+ <div class="absolute bottom-4 left-4 bg-black bg-opacity-70 px-2 py-1 rounded text-sm">
114
+ <span id="currentTime">00:00:00:00</span> / <span id="duration">00:00:05:00</span>
115
+ </div>
116
+ </div>
117
+
118
+ <!-- Timeline Header -->
119
+ <div class="time-ruler rounded-t-lg" id="timeRuler">
120
+ <!-- Time ticks will be generated by JavaScript -->
121
+ </div>
122
+
123
+ <!-- Audio Waveform -->
124
+ <div class="waveform rounded-b-lg relative" id="waveformContainer">
125
+ <div class="playhead" id="playhead"></div>
126
+ <div class="selection-area" id="selectionArea"></div>
127
+ <div class="handle" id="inHandle"></div>
128
+ <div class="handle" id="outHandle"></div>
129
+ <!-- Waveform lines will be generated by JavaScript -->
130
+ <!-- Markers will be added here -->
131
+ </div>
132
+
133
+ <!-- Zoom Controls -->
134
+ <div class="flex items-center justify-between mt-2">
135
+ <div class="flex items-center space-x-2">
136
+ <button id="zoomIn" class="bg-gray-700 hover:bg-gray-600 p-2 rounded">
137
+ <i data-lucide="zoom-in" class="w-4 h-4"></i>
138
+ </button>
139
+ <button id="zoomOut" class="bg-gray-700 hover:bg-gray-600 p-2 rounded">
140
+ <i data-lucide="zoom-out" class="w-4 h-4"></i>
141
+ </button>
142
+ <input type="range" id="zoomSlider" min="1" max="100" value="50" class="w-32">
143
+ </div>
144
+ <div class="text-sm text-gray-400">
145
+ Zoom: <span id="zoomLevel">100%</span>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <!-- Properties Panel -->
151
+ <div class="lg:col-span-1 bg-gray-800 rounded-lg p-4 space-y-4">
152
+ <h2 class="text-lg font-semibold border-b border-gray-700 pb-2">Segment Properties</h2>
153
+
154
+ <div>
155
+ <label class="block text-sm font-medium text-gray-400 mb-1">Start Time</label>
156
+ <input type="text" id="startTime" value="00:00:01:00" class="w-full bg-gray-700 rounded px-3 py-2 text-sm">
157
+ </div>
158
+
159
+ <div>
160
+ <label class="block text-sm font-medium text-gray-400 mb-1">End Time</label>
161
+ <input type="text" id="endTime" value="00:00:03:00" class="w-full bg-gray-700 rounded px-3 py-2 text-sm">
162
+ </div>
163
+
164
+ <div>
165
+ <label class="block text-sm font-medium text-gray-400 mb-1">Label</label>
166
+ <select class="w-full bg-gray-700 rounded px-3 py-2 text-sm">
167
+ <option>Speech</option>
168
+ <option>Music</option>
169
+ <option>Sound Effect</option>
170
+ <option>Silence</option>
171
+ </select>
172
+ </div>
173
+
174
+ <div>
175
+ <label class="block text-sm font-medium text-gray-400 mb-1">Notes</label>
176
+ <textarea class="w-full bg-gray-700 rounded px-3 py-2 text-sm h-20"></textarea>
177
+ </div>
178
+
179
+ <div class="space-y-2">
180
+ <h3 class="text-sm font-medium text-gray-400">Keyboard Shortcuts</h3>
181
+ <div class="grid grid-cols-2 gap-2 text-xs">
182
+ <div class="bg-gray-700 p-2 rounded">Space: Play/Pause</div>
183
+ <div class="bg-gray-700 p-2 rounded">J-K-L: Shuttle</div>
184
+ <div class="bg-gray-700 p-2 rounded">I: Set In</div>
185
+ <div class="bg-gray-700 p-2 rounded">O: Set Out</div>
186
+ <div class="bg-gray-700 p-2 rounded">M: Add Marker</div>
187
+ <div class="bg-gray-700 p-2 rounded">← →: Frame Step</div>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </div>
192
+
193
+ <!-- Toolbar -->
194
+ <div class="mt-6 bg-gray-800 rounded-lg p-3 toolbar flex items-center space-x-2 overflow-x-auto">
195
+ <button class="bg-blue-600 hover:bg-blue-500 px-3 py-1 rounded flex items-center">
196
+ <i data-lucide="play" class="w-4 h-4 mr-1"></i> Play
197
+ </button>
198
+ <button class="bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded flex items-center">
199
+ <i data-lucide="pause" class="w-4 h-4 mr-1"></i> Pause
200
+ </button>
201
+ <button class="bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded flex items-center">
202
+ <i data-lucide="skip-back" class="w-4 h-4 mr-1"></i> Previous
203
+ </button>
204
+ <button class="bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded flex items-center">
205
+ <i data-lucide="skip-forward" class="w-4 h-4 mr-1"></i> Next
206
+ </button>
207
+ <div class="h-6 w-px bg-gray-600 mx-1"></div>
208
+ <button class="bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded flex items-center">
209
+ <i data-lucide="type" class="w-4 h-4 mr-1"></i> Add Label
210
+ </button>
211
+ <button class="bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded flex items-center">
212
+ <i data-lucide="scissors" class="w-4 h-4 mr-1"></i> Split
213
+ </button>
214
+ <button id="addMarkerBtn" class="bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded flex items-center">
215
+ <i data-lucide="map-pin" class="w-4 h-4 mr-1"></i> Marker
216
+ </button>
217
+ <div class="h-6 w-px bg-gray-600 mx-1"></div>
218
+ <button class="bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded flex items-center">
219
+ <i data-lucide="save" class="w-4 h-4 mr-1"></i> Save
220
+ </button>
221
+ <button class="bg-gray-700 hover:bg-gray-600 px-3 py-1 rounded flex items-center">
222
+ <i data-lucide="settings" class="w-4 h-4 mr-1"></i> Settings
223
+ </button>
224
+ </div>
225
+ </div>
226
+
227
+ <script>
228
+ // Initialize Lucide icons
229
+ lucide.createIcons();
230
+
231
+ // Video element and timeline controls
232
+ const video = document.getElementById('videoElement');
233
+ const currentTimeDisplay = document.getElementById('currentTime');
234
+ const durationDisplay = document.getElementById('duration');
235
+ const playhead = document.getElementById('playhead');
236
+ const waveformContainer = document.getElementById('waveformContainer');
237
+ const timeRuler = document.getElementById('timeRuler');
238
+ const zoomSlider = document.getElementById('zoomSlider');
239
+ const zoomLevel = document.getElementById('zoomLevel');
240
+ const zoomInBtn = document.getElementById('zoomIn');
241
+ const zoomOutBtn = document.getElementById('zoomOut');
242
+ const startTimeInput = document.getElementById('startTime');
243
+ const endTimeInput = document.getElementById('endTime');
244
+ const selectionArea = document.getElementById('selectionArea');
245
+ const inHandle = document.getElementById('inHandle');
246
+ const outHandle = document.getElementById('outHandle');
247
+ const addMarkerBtn = document.getElementById('addMarkerBtn');
248
+
249
+ // State variables
250
+ let isDraggingPlayhead = false;
251
+ let isDraggingInHandle = false;
252
+ let isDraggingOutHandle = false;
253
+ let zoomFactor = 1;
254
+ let videoDuration = 5; // seconds (sample video is 5s)
255
+ let markers = [];
256
+
257
+ // Initialize the video
258
+ video.addEventListener('loadedmetadata', function() {
259
+ videoDuration = video.duration;
260
+ durationDisplay.textContent = formatTime(videoDuration);
261
+
262
+ // Generate waveform (simulated for this demo)
263
+ generateWaveform();
264
+
265
+ // Generate time ruler
266
+ generateTimeRuler();
267
+
268
+ // Set initial selection
269
+ updateSelectionArea(1, 3); // 1s to 3s
270
+ });
271
+
272
+ // Update current time display
273
+ video.addEventListener('timeupdate', function() {
274
+ currentTimeDisplay.textContent = formatTime(video.currentTime);
275
+
276
+ // Update playhead position
277
+ if (!isDraggingPlayhead) {
278
+ const playheadPosition = (video.currentTime / videoDuration) * waveformContainer.offsetWidth;
279
+ playhead.style.left = `${playheadPosition}px`;
280
+ }
281
+ });
282
+
283
+ // Format time as HH:MM:SS:FF (frames)
284
+ function formatTime(seconds) {
285
+ const date = new Date(seconds * 1000);
286
+ const hh = date.getUTCHours().toString().padStart(2, '0');
287
+ const mm = date.getUTCMinutes().toString().padStart(2, '0');
288
+ const ss = date.getUTCSeconds().toString().padStart(2, '0');
289
+ const ff = Math.floor((seconds % 1) * 30).toString().padStart(2, '0'); // Assuming 30fps
290
+ return `${hh}:${mm}:${ss}:${ff}`;
291
+ }
292
+
293
+ // Generate simulated waveform
294
+ function generateWaveform() {
295
+ const containerWidth = waveformContainer.offsetWidth;
296
+ const lineCount = containerWidth / 2; // One line every 2px
297
+
298
+ // Clear existing waveform
299
+ waveformContainer.querySelectorAll('.waveform-line').forEach(el => el.remove());
300
+
301
+ // Generate random waveform lines
302
+ for (let i = 0; i < lineCount; i++) {
303
+ const line = document.createElement('div');
304
+ line.className = 'waveform-line';
305
+
306
+ // Position and random height
307
+ const leftPos = i * 2;
308
+ const height = Math.random() * 70 + 10; // Random height between 10-80px
309
+
310
+ line.style.left = `${leftPos}px`;
311
+ line.style.height = `${height}px`;
312
+
313
+ waveformContainer.appendChild(line);
314
+ }
315
+ }
316
+
317
+ // Generate time ruler ticks
318
+ function generateTimeRuler() {
319
+ const containerWidth = timeRuler.offsetWidth;
320
+ const totalSeconds = videoDuration;
321
+ const pixelsPerSecond = containerWidth / totalSeconds;
322
+
323
+ // Clear existing ticks
324
+ timeRuler.innerHTML = '';
325
+
326
+ // Add major ticks every second and minor ticks every 0.5 seconds
327
+ for (let i = 0; i <= totalSeconds; i += 0.5) {
328
+ const tick = document.createElement('div');
329
+ tick.className = 'time-tick';
330
+
331
+ const isMajorTick = i % 1 === 0;
332
+ const leftPos = i * pixelsPerSecond;
333
+
334
+ tick.style.left = `${leftPos}px`;
335
+ tick.style.height = isMajorTick ? '10px' : '5px';
336
+
337
+ timeRuler.appendChild(tick);
338
+
339
+ // Add labels for major ticks
340
+ if (isMajorTick) {
341
+ const label = document.createElement('div');
342
+ label.className = 'time-label';
343
+ label.textContent = i;
344
+ label.style.left = `${leftPos}px`;
345
+ timeRuler.appendChild(label);
346
+ }
347
+ }
348
+ }
349
+
350
+ // Update selection area between in and out points
351
+ function updateSelectionArea(startTime, endTime) {
352
+ const containerWidth = waveformContainer.offsetWidth;
353
+ const startPos = (startTime / videoDuration) * containerWidth;
354
+ const endPos = (endTime / videoDuration) * containerWidth;
355
+ const width = endPos - startPos;
356
+
357
+ selectionArea.style.left = `${startPos}px`;
358
+ selectionArea.style.width = `${width}px`;
359
+
360
+ inHandle.style.left = `${startPos}px`;
361
+ outHandle.style.left = `${endPos}px`;
362
+
363
+ startTimeInput.value = formatTime(startTime);
364
+ endTimeInput.value = formatTime(endTime);
365
+ }
366
+
367
+ // Handle zoom controls
368
+ zoomSlider.addEventListener('input', function() {
369
+ zoomFactor = 1 + (this.value / 100) * 4; // 1x to 5x zoom
370
+ zoomLevel.textContent = `${Math.round(zoomFactor * 100)}%`;
371
+
372
+ // In a real app, this would adjust the timeline scale
373
+ console.log(`Zoom changed to ${zoomFactor}x`);
374
+ });
375
+
376
+ zoomInBtn.addEventListener('click', function() {
377
+ zoomSlider.value = Math.min(parseInt(zoomSlider.value) + 10, 100);
378
+ zoomSlider.dispatchEvent(new Event('input'));
379
+ });
380
+
381
+ zoomOutBtn.addEventListener('click', function() {
382
+ zoomSlider.value = Math.max(parseInt(zoomSlider.value) - 10, 0);
383
+ zoomSlider.dispatchEvent(new Event('input'));
384
+ });
385
+
386
+ // Playhead dragging
387
+ playhead.addEventListener('mousedown', function(e) {
388
+ isDraggingPlayhead = true;
389
+ document.addEventListener('mousemove', movePlayhead);
390
+ document.addEventListener('mouseup', stopDraggingPlayhead);
391
+ e.preventDefault();
392
+ });
393
+
394
+ function movePlayhead(e) {
395
+ if (!isDraggingPlayhead) return;
396
+
397
+ const containerRect = waveformContainer.getBoundingClientRect();
398
+ let xPos = e.clientX - containerRect.left;
399
+ xPos = Math.max(0, Math.min(xPos, containerRect.width));
400
+
401
+ playhead.style.left = `${xPos}px`;
402
+
403
+ // Update video time
404
+ const time = (xPos / containerRect.width) * videoDuration;
405
+ video.currentTime = time;
406
+ }
407
+
408
+ function stopDraggingPlayhead() {
409
+ isDraggingPlayhead = false;
410
+ document.removeEventListener('mousemove', movePlayhead);
411
+ document.removeEventListener('mouseup', stopDraggingPlayhead);
412
+ }
413
+
414
+ // In/Out handle dragging
415
+ inHandle.addEventListener('mousedown', function(e) {
416
+ isDraggingInHandle = true;
417
+ document.addEventListener('mousemove', moveInHandle);
418
+ document.addEventListener('mouseup', stopDraggingInHandle);
419
+ e.preventDefault();
420
+ });
421
+
422
+ outHandle.addEventListener('mousedown', function(e) {
423
+ isDraggingOutHandle = true;
424
+ document.addEventListener('mousemove', moveOutHandle);
425
+ document.addEventListener('mouseup', stopDraggingOutHandle);
426
+ e.preventDefault();
427
+ });
428
+
429
+ function moveInHandle(e) {
430
+ if (!isDraggingInHandle) return;
431
+
432
+ const containerRect = waveformContainer.getBoundingClientRect();
433
+ let xPos = e.clientX - containerRect.left;
434
+ const outPos = parseFloat(outHandle.style.left || '0');
435
+
436
+ xPos = Math.max(0, Math.min(xPos, outPos - 10)); // Don't go past out handle
437
+
438
+ inHandle.style.left = `${xPos}px`;
439
+
440
+ // Update selection area and seek video
441
+ const startTime = (xPos / containerRect.width) * videoDuration;
442
+ const endTime = (parseFloat(outHandle.style.left) / containerRect.width) * videoDuration;
443
+ updateSelectionArea(startTime, endTime);
444
+ video.currentTime = startTime;
445
+ }
446
+
447
+ function moveOutHandle(e) {
448
+ if (!isDraggingOutHandle) return;
449
+
450
+ const containerRect = waveformContainer.getBoundingClientRect();
451
+ let xPos = e.clientX - containerRect.left;
452
+ const inPos = parseFloat(inHandle.style.left || '0');
453
+
454
+ xPos = Math.max(inPos + 10, Math.min(xPos, containerRect.width)); // Don't go before in handle
455
+
456
+ outHandle.style.left = `${xPos}px`;
457
+
458
+ // Update selection area and seek video
459
+ const startTime = (parseFloat(inHandle.style.left) / containerRect.width) * videoDuration;
460
+ const endTime = (xPos / containerRect.width) * videoDuration;
461
+ updateSelectionArea(startTime, endTime);
462
+ video.currentTime = endTime;
463
+ }
464
+
465
+ function stopDraggingInHandle() {
466
+ isDraggingInHandle = false;
467
+ document.removeEventListener('mousemove', moveInHandle);
468
+ document.removeEventListener('mouseup', stopDraggingInHandle);
469
+ }
470
+
471
+ function stopDraggingOutHandle() {
472
+ isDraggingOutHandle = false;
473
+ document.removeEventListener('mousemove', moveOutHandle);
474
+ document.removeEventListener('mouseup', stopDraggingOutHandle);
475
+ }
476
+
477
+ // Add marker at current time
478
+ addMarkerBtn.addEventListener('click', function() {
479
+ const markerTime = video.currentTime;
480
+ const markerId = Date.now();
481
+
482
+ markers.push({
483
+ id: markerId,
484
+ time: markerTime
485
+ });
486
+
487
+ addMarkerToTimeline(markerId, markerTime);
488
+ });
489
+
490
+ function addMarkerToTimeline(id, time) {
491
+ const marker = document.createElement('div');
492
+ marker.className = 'marker';
493
+ marker.dataset.id = id;
494
+
495
+ const containerWidth = waveformContainer.offsetWidth;
496
+ const leftPos = (time / videoDuration) * containerWidth;
497
+
498
+ marker.style.left = `${leftPos}px`;
499
+
500
+ // Add tooltip
501
+ marker.title = `Marker at ${formatTime(time)}`;
502
+
503
+ // Double click to remove
504
+ marker.addEventListener('dblclick', function() {
505
+ marker.remove();
506
+ markers = markers.filter(m => m.id !== id);
507
+ });
508
+
509
+ waveformContainer.appendChild(marker);
510
+ }
511
+
512
+ // Keyboard shortcuts
513
+ document.addEventListener('keydown', function(e) {
514
+ // Space - play/pause
515
+ if (e.code === 'Space') {
516
+ e.preventDefault();
517
+ if (video.paused) {
518
+ video.play();
519
+ } else {
520
+ video.pause();
521
+ }
522
+ }
523
+
524
+ // I - set in point
525
+ if (e.code === 'KeyI') {
526
+ e.preventDefault();
527
+ const inTime = video.currentTime;
528
+ const outTime = parseFloat(endTimeInput.value.split(':')[3]) / 30 +
529
+ parseFloat(endTimeInput.value.split(':')[2]) +
530
+ parseFloat(endTimeInput.value.split(':')[1]) * 60 +
531
+ parseFloat(endTimeInput.value.split(':')[0]) * 3600;
532
+
533
+ updateSelectionArea(inTime, Math.max(inTime + 0.1, outTime));
534
+ }
535
+
536
+ // O - set out point
537
+ if (e.code === 'KeyO') {
538
+ e.preventDefault();
539
+ const outTime = video.currentTime;
540
+ const inTime = parseFloat(startTimeInput.value.split(':')[3]) / 30 +
541
+ parseFloat(startTimeInput.value.split(':')[2]) +
542
+ parseFloat(startTimeInput.value.split(':')[1]) * 60 +
543
+ parseFloat(startTimeInput.value.split(':')[0]) * 3600;
544
+
545
+ updateSelectionArea(Math.min(inTime, outTime - 0.1), outTime);
546
+ }
547
+
548
+ // M - add marker
549
+ if (e.code === 'KeyM') {
550
+ e.preventDefault();
551
+ const markerTime = video.currentTime;
552
+ const markerId = Date.now();
553
+
554
+ markers.push({
555
+ id: markerId,
556
+ time: markerTime
557
+ });
558
+
559
+ addMarkerToTimeline(markerId, markerTime);
560
+ }
561
+
562
+ // Left/Right arrow - frame step
563
+ if (e.code === 'ArrowLeft') {
564
+ e.preventDefault();
565
+ video.currentTime = Math.max(0, video.currentTime - (1/30)); // Step back 1 frame (30fps)
566
+ }
567
+
568
+ if (e.code === 'ArrowRight') {
569
+ e.preventDefault();
570
+ video.currentTime = Math.min(videoDuration, video.currentTime + (1/30)); // Step forward 1 frame
571
+ }
572
+ });
573
+
574
+ // Make the timeline container responsive
575
+ window.addEventListener('resize', function() {
576
+ generateWaveform();
577
+ generateTimeRuler();
578
+
579
+ // Update playhead and selection positions
580
+ const currentTime = video.currentTime;
581
+ const playheadPosition = (currentTime / videoDuration) * waveformContainer.offsetWidth;
582
+ playhead.style.left = `${playheadPosition}px`;
583
+
584
+ const inTime = parseFloat(startTimeInput.value.split(':')[3]) / 30 +
585
+ parseFloat(startTimeInput.value.split(':')[2]) +
586
+ parseFloat(startTimeInput.value.split(':')[1]) * 60 +
587
+ parseFloat(startTimeInput.value.split(':')[0]) * 3600;
588
+
589
+ const outTime = parseFloat(endTimeInput.value.split(':')[3]) / 30 +
590
+ parseFloat(endTimeInput.value.split(':')[2]) +
591
+ parseFloat(endTimeInput.value.split(':')[1]) * 60 +
592
+ parseFloat(endTimeInput.value.split(':')[0]) * 3600;
593
+
594
+ updateSelectionArea(inTime, outTime);
595
+
596
+ // Reposition markers
597
+ markers.forEach(marker => {
598
+ const markerEl = waveformContainer.querySelector(`.marker[data-id="${marker.id}"]`);
599
+ if (markerEl) {
600
+ const leftPos = (marker.time / videoDuration) * waveformContainer.offsetWidth;
601
+ markerEl.style.left = `${leftPos}px`;
602
+ }
603
+ });
604
+ });
605
+ </script>
606
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=yhzion/video-annotator" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
607
+ </html>