Perilon's picture
Initial commit
df66a57
raw
history blame
16.1 kB
<!DOCTYPE html>
<html>
<head>
<title>Sign Language Boundary Annotation Tool</title>
<style>
body {
background: #0e1117;
color: white;
font-family: sans-serif;
margin: 0;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.video-container {
width: 100%;
max-height: 60vh;
margin: 20px 0;
position: relative;
}
video {
width: 100%;
max-height: 60vh;
object-fit: contain;
}
.timeline {
width: 100%;
height: 60px;
background: #262730;
position: relative;
margin: 20px 0;
cursor: default;
}
.marker {
position: absolute;
width: 4px;
height: 100%;
background: red;
cursor: ew-resize;
transition: background 0.2s;
}
.marker:hover {
background: #ff6666;
}
.marker.selected {
background: #00ff00;
width: 6px;
box-shadow: 0 0 5px rgba(0, 255, 0, 0.5);
}
button {
background: #262730;
color: white;
border: 1px solid #555;
padding: 10px 20px;
cursor: pointer;
margin: 5px;
}
button:hover {
background: #363840;
}
#timestamps {
color: white;
margin: 10px 0;
word-wrap: break-word;
font-family: monospace;
}
.error {
color: #ff4444;
}
.success {
color: #44ff44;
}
#current-time {
margin: 10px 0;
font-family: monospace;
}
.adjustment-controls {
display: none;
margin: 20px 0;
padding: 10px;
background: #262730;
border-radius: 5px;
}
.adjustment-controls.visible {
display: block;
}
.control-group {
margin: 10px 0;
}
.control-group button {
margin: 0 5px;
}
.mode-buttons {
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="container">
<h1>Sign Language Boundary Annotation Tool</h1>
<div id="error-message" class="error"></div>
<div class="mode-buttons">
<button id="toggle-stage">Switch to Adjustment Mode</button>
<button id="alignment-mode">Switch to Alignment Mode</button>
</div>
<div class="video-container">
<video id="video" controls>
<source id="video-source" src="{% if video_id %}/video/{{ video_id }}.mp4{% endif %}" type="video/mp4">
</video>
</div>
<div class="timeline" id="timeline"></div>
<div id="current-time">Current Time: 0.00</div>
<div class="adjustment-controls" id="adjustment-controls">
<div class="control-group">
<button id="add-marker">Add New Marker</button>
<button id="delete-marker">Delete Selected Marker</button>
</div>
<div class="control-group">
<button id="back-second">-1 second</button>
<button id="forward-second">+1 second</button>
</div>
<div class="control-group">
<button id="back-frame">-1 frame</button>
<button id="forward-frame">+1 frame</button>
</div>
</div>
<button id="mark-button">Mark Boundary</button>
<div id="timestamps"></div>
<button id="save-button">Save Annotations</button>
<div id="save-status"></div>
</div>
<script>
// Use the provided template video_id if available; it should be the base ID (without .mp4)
const templateVideoId = "{{ video_id|default('') }}";
let currentVideo = "";
if (templateVideoId) {
currentVideo = templateVideoId;
} else {
// Fallback: use /videos API and remove the .mp4 extension
fetch('/videos')
.then(response => response.json())
.then(videos => {
if (videos.error) {
document.getElementById('error-message').textContent = videos.error;
return;
}
if (videos.length > 0) {
currentVideo = videos[0].replace(/\.mp4$/, "");
document.getElementById('video-source').src = `/video/${videos[0]}`;
document.getElementById('video').load();
}
})
.catch(error => {
document.getElementById('error-message').textContent = 'Error loading videos: ' + error;
});
}
let timestamps = [];
let isAdjustmentMode = false;
let isDragging = false;
let currentMarker = null;
let selectedMarker = null;
let isAddingMarker = false;
let keyRepeatInterval = null;
const FRAME_DURATION = 1/30;
const KEY_REPEAT_DELAY = 33;
const video = document.getElementById('video');
const videoSource = document.getElementById('video-source');
const timeline = document.getElementById('timeline');
const markButton = document.getElementById('mark-button');
const saveButton = document.getElementById('save-button');
const errorMessage = document.getElementById('error-message');
const saveStatus = document.getElementById('save-status');
const toggleStage = document.getElementById('toggle-stage');
const currentTimeDisplay = document.getElementById('current-time');
const adjustmentControls = document.getElementById('adjustment-controls');
const deleteMarkerBtn = document.getElementById('delete-marker');
const backSecondBtn = document.getElementById('back-second');
const forwardSecondBtn = document.getElementById('forward-second');
const backFrameBtn = document.getElementById('back-frame');
const forwardFrameBtn = document.getElementById('forward-frame');
const addMarkerBtn = document.getElementById('add-marker');
function loadExistingAnnotations() {
fetch(`/get_annotations/${currentVideo}`)
.then(response => response.json())
.then(data => {
if (data.timestamps) {
timestamps = data.timestamps;
updateTimestamps();
timeline.innerHTML = '';
timestamps.forEach(time => addMarker(time));
}
})
.catch(error => console.error('Error loading annotations:', error));
}
video.addEventListener('loadedmetadata', () => {
if (currentVideo) loadExistingAnnotations();
});
toggleStage.onclick = () => {
isAdjustmentMode = !isAdjustmentMode;
toggleStage.textContent = isAdjustmentMode ? "Switch to Marking Mode" : "Switch to Adjustment Mode";
markButton.style.display = isAdjustmentMode ? 'none' : 'block';
adjustmentControls.style.display = isAdjustmentMode ? 'block' : 'none';
deselectMarker();
};
document.getElementById('alignment-mode').onclick = () => {
window.location.href = `/alignment/${currentVideo}`;
};
markButton.onclick = () => {
const time = video.currentTime;
timestamps.push(time);
timestamps.sort((a, b) => a - b);
updateTimestamps();
timeline.innerHTML = '';
timestamps.forEach(time => addMarker(time));
};
function addMarker(time) {
const marker = document.createElement('div');
marker.className = 'marker';
marker.style.left = (time / video.duration * 100) + '%';
timeline.appendChild(marker);
initializeMarkerDrag(marker);
}
function initializeMarkerDrag(marker) {
marker.addEventListener('click', (e) => {
e.stopPropagation();
selectMarker(marker);
});
marker.addEventListener('mousedown', (e) => {
isDragging = true;
currentMarker = marker;
document.addEventListener('mousemove', handleDrag);
document.addEventListener('mouseup', () => {
isDragging = false;
document.removeEventListener('mousemove', handleDrag);
});
});
}
function handleDrag(e) {
if (!isDragging || !currentMarker) return;
const timelineRect = timeline.getBoundingClientRect();
let position = (e.clientX - timelineRect.left) / timelineRect.width;
position = Math.max(0, Math.min(1, position));
const newTime = position * video.duration;
currentMarker.style.left = `${position * 100}%`;
const index = Array.from(timeline.children).indexOf(currentMarker);
timestamps[index] = newTime;
video.currentTime = newTime;
updateTimestamps();
}
function selectMarker(marker) {
if (selectedMarker === marker) {
deselectMarker();
return;
}
if (selectedMarker) {
selectedMarker.classList.remove('selected');
}
selectedMarker = marker;
marker.classList.add('selected');
adjustmentControls.classList.add('visible');
const index = Array.from(timeline.children).indexOf(marker);
const timestamp = timestamps[index];
video.currentTime = timestamp;
}
function deselectMarker() {
if (selectedMarker) {
selectedMarker.classList.remove('selected');
selectedMarker = null;
adjustmentControls.classList.remove('visible');
}
}
function deleteSelectedMarker() {
if (!selectedMarker) return;
const index = Array.from(timeline.children).indexOf(selectedMarker);
timestamps.splice(index, 1);
selectedMarker.remove();
deselectMarker();
updateTimestamps();
}
function adjustMarkerTime(adjustment) {
if (!selectedMarker) return;
const index = Array.from(timeline.children).indexOf(selectedMarker);
let newTime = timestamps[index] + adjustment;
newTime = Math.max(0, Math.min(video.duration, newTime));
timestamps[index] = newTime;
selectedMarker.style.left = (newTime / video.duration * 100) + '%';
video.currentTime = newTime;
updateTimestamps();
}
function updateTimestamps() {
const div = document.getElementById('timestamps');
div.textContent = timestamps.map(t => t.toFixed(2)).join(', ');
}
video.addEventListener('timeupdate', () => {
currentTimeDisplay.textContent = `Current Time: ${video.currentTime.toFixed(2)}`;
});
document.addEventListener('keydown', (e) => {
if (selectedMarker) {
switch (e.key) {
case 'Delete':
deleteSelectedMarker();
break;
case 'ArrowLeft':
if (!keyRepeatInterval) {
adjustMarkerTime(-FRAME_DURATION);
keyRepeatInterval = setInterval(() => {
adjustMarkerTime(-FRAME_DURATION);
}, KEY_REPEAT_DELAY);
}
break;
case 'ArrowRight':
if (!keyRepeatInterval) {
adjustMarkerTime(FRAME_DURATION);
keyRepeatInterval = setInterval(() => {
adjustMarkerTime(FRAME_DURATION);
}, KEY_REPEAT_DELAY);
}
break;
}
}
});
document.addEventListener('keyup', (e) => {
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && keyRepeatInterval) {
clearInterval(keyRepeatInterval);
keyRepeatInterval = null;
}
});
addMarkerBtn.onclick = (e) => {
e.stopPropagation();
isAddingMarker = true;
timeline.style.cursor = 'crosshair';
deselectMarker();
};
timeline.addEventListener('click', (e) => {
if (isAddingMarker) {
const timelineRect = timeline.getBoundingClientRect();
const position = (e.clientX - timelineRect.left) / timelineRect.width;
const newTime = position * video.duration;
timestamps.push(newTime);
timestamps.sort((a, b) => a - b);
timeline.innerHTML = '';
timestamps.forEach(t => addMarker(t));
const newMarker = timeline.children[timestamps.indexOf(newTime)];
selectMarker(newMarker);
isAddingMarker = false;
timeline.style.cursor = 'default';
video.currentTime = newTime;
updateTimestamps();
}
});
deleteMarkerBtn.onclick = (e) => {
e.stopPropagation();
deleteSelectedMarker();
};
backSecondBtn.onclick = (e) => {
e.stopPropagation();
adjustMarkerTime(-1);
};
forwardSecondBtn.onclick = (e) => {
e.stopPropagation();
adjustMarkerTime(1);
};
backFrameBtn.onclick = (e) => {
e.stopPropagation();
adjustMarkerTime(-FRAME_DURATION);
};
forwardFrameBtn.onclick = (e) => {
e.stopPropagation();
adjustMarkerTime(FRAME_DURATION);
};
document.addEventListener('click', (e) => {
if (!e.target.closest('.timeline') && !e.target.closest('.adjustment-controls')) {
isAddingMarker = false;
timeline.style.cursor = 'default';
}
if (e.target.closest('.adjustment-controls') ||
e.target.classList.contains('marker') ||
e.target.closest('.control-group')) {
return;
}
deselectMarker();
});
saveButton.onclick = () => {
fetch('/save_annotations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
video: currentVideo,
timestamps: timestamps
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
saveStatus.className = 'success';
saveStatus.textContent = 'Annotations saved successfully!';
} else {
saveStatus.className = 'error';
saveStatus.textContent = data.message;
}
})
.catch(error => {
saveStatus.className = 'error';
saveStatus.textContent = 'Error saving annotations: ' + error;
});
};
</script>
</body>
</html>