Perilon's picture
Initial commit
df66a57
<!DOCTYPE html>
<html>
<head>
<title>Sign Language Alignment Tool</title>
<style>
body {
background: #0e1117;
color: white;
font-family: sans-serif;
margin: 0;
padding: 20px;
}
/* Container styles for alignment UI */
.container {
display: flex;
max-width: 1200px;
margin: 0 auto;
gap: 20px;
}
.left-panel {
flex: 1;
min-width: 0;
position: relative;
z-index: 10;
}
.right-panel {
flex: 1;
min-width: 0;
}
.video-container {
width: 100%;
background: #1a1a1a;
margin-bottom: 20px;
}
video {
width: 100%;
max-height: 60vh;
object-fit: contain;
}
.transcript-container {
background: #262730;
padding: 20px;
border-radius: 5px;
max-height: 60vh;
overflow-y: auto;
position: relative;
z-index: 1;
}
.word-boundary {
display: inline-block;
width: 4px;
height: 1em;
background: transparent;
margin: 0 1px;
cursor: pointer;
vertical-align: middle;
}
.word-boundary:hover {
background: #666;
}
.word-boundary.start-marker {
background: #4CAF50;
}
/* Dashed appearance for the start marker in adjustment mode */
.word-boundary.start-marker.adjusting {
background-color: transparent !important;
border-left: 4px dashed #4CAF50 !important;
}
.word-boundary.end-marker {
background: #f44336;
}
.word {
display: inline-block;
padding: 2px;
}
button {
background: #262730;
color: white;
border: 1px solid #555;
padding: 10px 20px;
cursor: pointer;
margin: 5px;
border-radius: 4px;
position: relative;
z-index: 10;
}
button:hover {
background: #363840;
}
.warning-button {
background: #863232;
color: white;
border: 1px solid #555;
padding: 10px 20px;
cursor: pointer;
margin: 5px;
border-radius: 4px;
position: relative;
z-index: 10;
}
.warning-button:hover {
background: #a13d3d;
}
.selected-text {
background: #262730;
padding: 15px;
margin: 10px 0;
border-radius: 5px;
min-height: 50px;
}
.skipped-clip {
background: #4a3434;
color: #ffcccc;
padding: 10px;
border-radius: 4px;
margin: 5px 0;
font-style: italic;
}
.clip-info {
font-family: monospace;
margin: 10px 0;
color: #aaa;
}
/* Progress bar styles */
#progress-container-wrapper {
width: 100%;
background: #0e1117;
padding: 20px;
text-align: center;
}
#progress-container, #transcription-progress-container {
width: 100%;
background-color: #444;
border-radius: 5px;
margin: 10px 0;
overflow: hidden;
}
#progress-bar {
height: 20px;
width: 0%;
background-color: #4CAF50;
}
#transcription-progress-bar {
height: 20px;
width: 0%;
background-color: #2196F3;
}
</style>
</head>
<body>
<!-- Progress container is shown first until both extraction and transcription complete -->
<div id="progress-container-wrapper">
<h1>Preparing Video Clips and Transcription...</h1>
<div id="extraction-progress-section">
<h3>Video Extraction Progress</h3>
<div id="progress-container">
<div id="progress-bar"></div>
</div>
<div id="progress-text">0% complete</div>
</div>
<div id="transcription-progress-section">
<h3>Transcription Progress</h3>
<div id="transcription-progress-container">
<div id="transcription-progress-bar"></div>
</div>
<div id="transcription-progress-text">0% complete</div>
</div>
</div>
<!-- Alignment UI is hidden until both processes are finished -->
<div id="alignment-ui" style="display: none;">
<div class="container">
<div class="left-panel">
<h1>Sign Language Alignment Tool</h1>
<button onclick="window.location.href='/player/' + videoId">
Return to Annotation Mode
</button>
<div class="video-container">
<video id="current-clip" controls>
<source id="clip-source" type="video/mp4">
</video>
</div>
<div class="clip-info">
<div>Current Clip: <span id="current-clip-number">1</span> of <span id="total-clips">0</span></div>
<div>Clip Time Range: <span id="clip-range">0.00 - 0.00</span></div>
</div>
<div>
<button id="prev-clip">Previous Clip</button>
<button id="next-clip">Next Clip</button>
<button id="play-clip">Play Current Clip</button>
</div>
<div>
<h3>Selected Text</h3>
<div class="selected-text" id="selected-text">No text selected</div>
<div class="button-group">
<button id="confirm-selection">Confirm Text Selection</button>
<button id="mark-no-text" class="warning-button">No English Text Matches This Signing</button>
</div>
</div>
<button id="save-alignments">Save All Alignments</button>
</div>
<div class="right-panel">
<div class="transcript-container" id="transcript-container">
<h3>Full Transcript</h3>
<div id="transcript-text"></div>
</div>
</div>
</div>
</div>
<script>
// Determine the video id from the URL
const pathSegments = window.location.pathname.split('/').filter(seg => seg !== '');
const videoId = pathSegments[pathSegments.length - 1];
let currentClipIndex = 0;
let clips = [];
let wordTimings = [];
let alignments = [];
let startMarker = null;
let endMarker = null;
let skippedClips = new Set();
let minStartAllowed = 0;
let adjustingStart = false;
let originalStartMarker = null;
function renderTranscript() {
const container = document.getElementById('transcript-text');
container.innerHTML = '';
const initialBoundary = document.createElement('span');
initialBoundary.className = 'word-boundary';
initialBoundary.dataset.index = '0';
container.appendChild(initialBoundary);
wordTimings.forEach((word, i) => {
const wordSpan = document.createElement('span');
wordSpan.className = 'word';
wordSpan.textContent = word.punctuated_word;
container.appendChild(wordSpan);
const boundary = document.createElement('span');
boundary.className = 'word-boundary';
boundary.dataset.index = (i + 1).toString();
container.appendChild(boundary);
});
document.querySelectorAll('.word-boundary').forEach(boundary => {
boundary.onclick = () => handleBoundaryClick(boundary);
});
updateMarkers();
}
function updateMarkers() {
document.querySelectorAll('.word-boundary').forEach(boundary => {
const index = parseInt(boundary.dataset.index);
boundary.classList.remove('start-marker', 'end-marker', 'adjusting');
if (index === startMarker) {
boundary.classList.add('start-marker');
if (adjustingStart) {
boundary.classList.add('adjusting');
}
}
if (index === endMarker) {
boundary.classList.add('end-marker');
}
});
}
function handleBoundaryClick(boundary) {
const clickedIndex = parseInt(boundary.dataset.index);
if (skippedClips.has(currentClipIndex)) {
skippedClips.delete(currentClipIndex);
updateSelectedText();
}
if (startMarker === null) {
if (clickedIndex < minStartAllowed) {
alert("Cannot set boundary earlier than the previous segment's boundary.");
return;
}
startMarker = clickedIndex;
endMarker = null;
} else if (adjustingStart) {
if (clickedIndex < minStartAllowed) {
alert("You cannot move the marker earlier than the previous segment's boundary.");
} else {
startMarker = clickedIndex;
endMarker = null;
}
adjustingStart = false;
originalStartMarker = null;
} else if (clickedIndex === startMarker) {
adjustingStart = !adjustingStart;
originalStartMarker = startMarker;
} else if (endMarker === null && clickedIndex > startMarker) {
endMarker = clickedIndex;
updateSelectedText();
} else if (clickedIndex > startMarker) {
endMarker = clickedIndex;
updateSelectedText();
} else {
alert("Boundary selection invalid. End marker must be after start marker.");
}
updateMarkers();
}
function updateSelectedText() {
const selectedText = document.getElementById('selected-text');
if (skippedClips.has(currentClipIndex)) {
selectedText.textContent = 'This clip has been marked as having no matching English text';
selectedText.className = 'selected-text skipped-clip';
return;
}
selectedText.className = 'selected-text';
if (startMarker === null || endMarker === null) {
selectedText.textContent = 'No text selected';
return;
}
const selectedWords = wordTimings.slice(startMarker, endMarker).map(w => w.punctuated_word);
selectedText.textContent = selectedWords.join(' ');
}
function loadClip(index) {
if (index < 0 || index >= clips.length) {
console.error("Invalid clip index:", index);
return;
}
currentClipIndex = index;
document.getElementById('current-clip-number').textContent = (index + 1);
document.getElementById('total-clips').textContent = clips.length;
const clip = clips[index];
const video = document.getElementById('current-clip');
const clipSource = document.getElementById('clip-source');
clipSource.src = clip.path;
video.load();
document.getElementById('clip-range').textContent =
`${clip.start.toFixed(2)} - ${clip.end.toFixed(2)}`;
if (index === 0) {
minStartAllowed = 0;
startMarker = null;
endMarker = null;
} else {
let lastValidAlignment = null;
for (let i = index - 1; i >= 0; i--) {
if (alignments[i]) {
lastValidAlignment = alignments[i];
break;
}
}
if (lastValidAlignment) {
minStartAllowed = lastValidAlignment.endIndex;
startMarker = minStartAllowed;
endMarker = null;
} else {
minStartAllowed = 0;
startMarker = null;
endMarker = null;
}
}
adjustingStart = false;
originalStartMarker = null;
updateMarkers();
updateSelectedText();
}
document.getElementById('mark-no-text').onclick = () => {
markNoText();
};
function markNoText() {
skippedClips.add(currentClipIndex);
startMarker = null;
endMarker = null;
if (alignments[currentClipIndex]) {
delete alignments[currentClipIndex];
}
updateMarkers();
updateSelectedText();
if (currentClipIndex < clips.length - 1) {
loadClip(currentClipIndex + 1);
}
}
document.getElementById('confirm-selection').onclick = () => {
if (startMarker === null || endMarker === null) {
alert('Please select text by clicking boundaries before confirming');
return;
}
if (endMarker <= startMarker) {
alert('End marker must be after start marker');
return;
}
const asrStart = wordTimings[startMarker].start_time;
const asrEnd = wordTimings[endMarker - 1].end_time;
const textRaw = wordTimings.slice(startMarker, endMarker).map(w => w.word).join(' ');
const textPunctuated = wordTimings.slice(startMarker, endMarker).map(w => w.punctuated_word).join(' ');
alignments[currentClipIndex] = {
clip: clips[currentClipIndex],
startIndex: startMarker,
endIndex: endMarker,
audio_start: asrStart,
audio_end: asrEnd,
text: textPunctuated,
text_raw: textRaw
};
if (currentClipIndex < clips.length - 1) {
loadClip(currentClipIndex + 1);
} else {
alert('This was the last clip! Please save your alignments.');
}
};
document.getElementById('prev-clip').onclick = () => {
if (currentClipIndex > 0) {
loadClip(currentClipIndex - 1);
}
};
document.getElementById('next-clip').onclick = () => {
if (currentClipIndex < clips.length - 1) {
loadClip(currentClipIndex + 1);
}
};
document.getElementById('play-clip').onclick = () => {
const video = document.getElementById('current-clip');
video.currentTime = 0;
video.play();
};
document.getElementById('save-alignments').onclick = () => {
const filteredAlignments = alignments.filter((alignment, index) => alignment && !skippedClips.has(index));
fetch('/api/save_alignments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
video_id: videoId,
alignments: filteredAlignments
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Alignments saved successfully!');
} else {
alert('Error: ' + data.message);
}
})
.catch(error => {
alert('Error saving alignments: ' + error);
});
};
function pollProgress() {
Promise.all([
fetch(`/api/clip_progress/${videoId}`).then(r => r.json()),
fetch(`/api/transcription_progress/${videoId}`).then(r => r.json())
]).then(results => {
const clipProgress = results[0];
const transcriptionProgress = results[1];
document.getElementById('progress-bar').style.width = clipProgress.percent + '%';
document.getElementById('progress-text').textContent =
`Video Extraction ${clipProgress.percent}% complete`;
document.getElementById('transcription-progress-bar').style.width = transcriptionProgress.percent + '%';
document.getElementById('transcription-progress-text').textContent =
`Transcription ${transcriptionProgress.percent}% complete`;
if (clipProgress.percent >= 100 && transcriptionProgress.percent >= 100) {
document.getElementById('progress-container-wrapper').style.display = 'none';
document.getElementById('alignment-ui').style.display = 'block';
loadAlignmentData();
} else {
setTimeout(pollProgress, 2000);
}
}).catch(error => {
console.error("Error polling progress", error);
setTimeout(pollProgress, 2000);
});
}
function loadAlignmentData() {
Promise.all([
fetch(`/api/word_timestamps/${videoId}`).then(r => r.json()),
fetch(`/api/clips/${videoId}`).then(r => r.json())
]).then(([wordData, clipsData]) => {
if (wordData.status === 'success') {
wordTimings = wordData.words;
renderTranscript();
} else {
console.error("Error loading word timestamps:", wordData.message);
}
if (clipsData.status === 'success') {
clips = clipsData.clips;
document.getElementById('total-clips').textContent = clips.length;
if (clips.length > 0) loadClip(0);
} else {
console.error("Error loading clips:", clipsData.message);
}
});
}
window.onload = function() {
// Trigger both clip extraction and transcription in one go.
fetch(`/api/extract_clips/${videoId}`)
.then(response => response.json())
.then(data => {
console.log("Extraction & Transcription triggered:", data);
pollProgress();
})
.catch(error => {
console.error("Error triggering extraction and transcription:", error);
pollProgress();
});
};
</script>
</body>
</html>