dhravani / templates /validate.html
coild's picture
Upload 52 files
70b77f4 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Validate Recordings - COILD Dhravani</title>
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Google+Sans:wght@400;500;700&display=swap" rel="stylesheet">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='validate.css') }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="{{ url_for('static', filename='country-states.js') }}"></script>
</head>
<body>
<div class="container-fluid px-3">
<!-- Navigation bar -->
<div class="d-flex justify-content-between align-items-center pt-3 pb-3">
<h4 class="mb-0" style="color: #1a73e8;">COILD Dhravani</h4>
<div class="d-flex align-items-center gap-3">
<a href="{{ url_for('index') }}" class="btn btn-outline-primary">Back</a>
{% if enable_auth and session.get('user') %}
<a href="{{ url_for('logout') }}" class="btn btn-outline-secondary">Logout</a>
{% endif %}
</div>
</div>
<!-- Filters Panel - Moved to top -->
<div class="card mb-4 border-0">
<div class="card-body p-3">
<div class="row align-items-center">
<div class="col-auto d-none d-md-block">
<h5 class="filter-title mb-0">
<i class="fas fa-sliders me-2"></i>Filters
</h5>
</div>
<!-- Mobile filter toggle button -->
<div class="col-12 d-md-none mb-2">
<button class="btn btn-outline-primary w-100 d-flex justify-content-between align-items-center"
type="button"
data-bs-toggle="collapse"
data-bs-target="#filterCollapse"
aria-expanded="false"
aria-controls="filterCollapse">
<span><i class="fas fa-sliders me-2"></i>Filters</span>
<i class="fas fa-chevron-down filter-chevron"></i>
</button>
</div>
<div class="col-12 col-md-auto flex-grow-1">
<!-- Collapsible container for mobile -->
<div class="collapse d-md-block" id="filterCollapse">
<form id="filterForm" class="row g-3 align-items-center flex-nowrap mobile-filter-form">
<!-- Language Filter -->
<div class="col filter-item">
<label class="filter-mobile-label d-md-none">Language</label>
<div class="filter-select">
<select class="form-control" id="language" name="language" required>
<option value="">Select Language *</option>
{% for lang in languages %}
<option value="{{ lang.code }}">{{ lang.name }}</option>
{% endfor %}
</select>
<i class="fas fa-chevron-down select-arrow"></i>
</div>
</div>
<!-- Add Domain Filter -->
<div class="col filter-item">
<label class="filter-mobile-label d-md-none">Domain</label>
<div class="filter-select">
<select class="form-control" id="domain" name="domain">
<!-- Will be populated by JavaScript -->
</select>
<i class="fas fa-chevron-down select-arrow"></i>
</div>
</div>
<!-- Add Subdomain Filter -->
<div class="col filter-item">
<label class="filter-mobile-label d-md-none">Subdomain</label>
<div class="filter-select">
<select class="form-control" id="subdomain" name="subdomain" disabled>
<option value="">Select Domain First</option>
</select>
<i class="fas fa-chevron-down select-arrow"></i>
</div>
</div>
<div class="col-12 col-md-auto mt-3 mt-md-0">
<button type="submit" class="btn btn-primary w-100">
<i class="fas fa-filter me-2"></i>Apply
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="card border-0">
<div id="validateRecordings">
<!-- Single Recording View -->
<div id="singleRecordingView" class="single-recording-view">
<div id="noRecordingMessage" class="no-recordings" style="display: none;">
<i class="fas fa-check-circle"></i>
<h5>No recordings to validate</h5>
<p>All recordings have been reviewed for the selected language.</p>
</div>
<div id="recordingContent" style="display: none;">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="speaker-info-container">
<span class="badge bg-primary" id="speakerInfo"></span>
<span class="badge bg-info" id="genderInfo"></span>
<span class="badge bg-secondary" id="ageInfo"></span>
<span class="badge bg-light text-dark" id="stateInfo"></span>
<span class="badge bg-accent" id="accentInfo"></span>
<span class="badge bg-success" id="domainInfo"></span>
<span class="badge bg-warning text-dark" id="subdomainInfo"></span>
</div>
</div>
<!-- Add tooltip to audio player -->
<div class="audio-player mb-4">
<audio controls class="w-100" preload="auto"
data-bs-toggle="tooltip" data-bs-placement="top"
title="Press 'Space' to Play/Pause"></audio>
</div>
<div class="transcript-box mb-4">
<div class="p-2 bg-light rounded" id="transcriptionText"></div>
</div>
<div class="validation-actions">
<button class="btn btn-success" onclick="verifyRecording(true)"
title="Accept Recording (A)">
<i class="fas fa-check me-2"></i>Accept
</button>
<button class="btn btn-danger" onclick="verifyRecording(false)"
title="Reject Recording (R)">
<i class="fas fa-times me-2"></i>Reject
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Add keyboard shortcuts help modal -->
<div class="modal fade" id="shortcutsModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Keyboard Shortcuts</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="shortcuts-list">
<div class="shortcut-item">
<kbd>Space</kbd>
<span>Play/Pause audio</span>
</div>
<div class="shortcut-item">
<kbd>A</kbd>
<span>Accept recording</span>
</div>
<div class="shortcut-item">
<kbd>R</kbd>
<span>Reject recording</span>
</div>
<div class="shortcut-item">
<kbd>?</kbd>
<span>Show/hide keyboard shortcuts</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Include Bootstrap JS for modal -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
<script>
let currentRecording = null;
// Initialize tooltips
document.addEventListener('DOMContentLoaded', function() {
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl, {
trigger: 'hover' // Show tooltip on hover
})
})
});
// Update keyboard event listener
document.addEventListener('keydown', function(event) {
if (!currentRecording) return;
// Ignore keyboard shortcuts if user is typing in an input or textarea
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') return;
// Ignore if focus is on a button
if (event.target.tagName === 'BUTTON') return;
const audio = document.querySelector('#recordingContent audio');
switch(event.key.toLowerCase()) {
case ' ': // Space key
event.preventDefault(); // Prevent space from triggering buttons
if (audio.paused) {
audio.play();
} else {
audio.pause();
}
break;
case 'a': // Accept
event.preventDefault();
verifyRecording(true);
break;
case 'r': // Reject
event.preventDefault();
verifyRecording(false);
break;
}
});
// Add diagnostic logging function
function logFilterSelection(language, domain, subdomain) {
console.log(`Applied filters - Language: ${language || 'All'}, Domain: ${domain || 'All'}, Subdomain: ${subdomain || 'All'}`);
}
// Update the filter form submission handler with logging
document.getElementById('filterForm').addEventListener('submit', (e) => {
e.preventDefault();
const language = document.getElementById('language').value;
const domain = document.getElementById('domain').value;
const subdomain = document.getElementById('subdomain').value;
logFilterSelection(language, domain, subdomain);
if (language) {
loadNextRecording();
}
});
async function loadDomains() {
try {
const response = await fetch('/domains');
const data = await response.json();
const domainSelect = document.getElementById('domain');
domainSelect.innerHTML = '<option value="">All Domains</option>'; // Option to not filter by domain
if (data.status === 'success' && data.domains) {
// Add domain options
Object.entries(data.domains).forEach(([code, name]) => {
const option = document.createElement('option');
option.value = code;
option.textContent = `${name} (${code})`;
domainSelect.appendChild(option);
});
}
} catch (error) {
console.error('Error loading domains:', error);
}
}
async function loadSubdomains(domainCode) {
try {
const subdomainSelect = document.getElementById('subdomain');
if (!domainCode) {
subdomainSelect.innerHTML = '<option value="">All Subdomains</option>';
subdomainSelect.disabled = true;
return;
}
subdomainSelect.innerHTML = '<option value="">Loading...</option>';
subdomainSelect.disabled = true;
const response = await fetch(`/domains/${domainCode}/subdomains`);
const data = await response.json();
subdomainSelect.innerHTML = '<option value="">All Subdomains</option>'; // Option to not filter by subdomain
if (data.status === 'success' && data.subdomains) {
data.subdomains.forEach(subdomain => {
const option = document.createElement('option');
option.value = subdomain.mnemonic;
option.textContent = `${subdomain.name} (${subdomain.mnemonic})`;
subdomainSelect.appendChild(option);
});
subdomainSelect.disabled = false;
}
} catch (error) {
console.error('Error loading subdomains:', error);
const subdomainSelect = document.getElementById('subdomain');
subdomainSelect.innerHTML = '<option value="">Error loading</option>';
}
}
async function loadNextRecording() {
try {
const language = document.getElementById('language').value;
const domain = document.getElementById('domain').value;
const subdomain = document.getElementById('subdomain').value;
// Skip the API call if no language is selected yet
if (!language) return;
logFilterSelection(language, domain, subdomain);
// Build the URL with query parameters for domain and subdomain
let url = `/validation/api/next_recording?language=${language}`;
if (domain) url += `&domain=${domain}`;
if (subdomain) url += `&subdomain=${subdomain}`;
console.log(`Requesting recordings with URL: ${url}`);
// Show loading state
document.getElementById('recordingContent').innerHTML = `
<div class="text-center p-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3">Loading recording...</p>
</div>
`;
const response = await fetch(url);
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to load recording');
}
if (data.status === 'no_recordings') {
let message = 'No recordings available for validation';
if (domain) message += ` in domain "${domain}"`;
if (subdomain) message += ` and subdomain "${subdomain}"`;
document.getElementById('noRecordingMessage').innerHTML = `
<i class="fas fa-check-circle"></i>
<h5>No recordings to validate</h5>
<p>${message}</p>
`;
document.getElementById('noRecordingMessage').style.display = 'block';
document.getElementById('recordingContent').style.display = 'none';
currentRecording = null;
return;
}
currentRecording = data.recording;
if (!currentRecording || !currentRecording.audio_path) {
throw new Error('Invalid recording data received');
}
document.getElementById('noRecordingMessage').style.display = 'none';
document.getElementById('recordingContent').style.display = 'block';
// Reset the content
document.getElementById('recordingContent').innerHTML = `
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="speaker-info-container">
<span class="badge bg-primary" id="speakerInfo"></span>
<span class="badge bg-info" id="genderInfo"></span>
<span class="badge bg-secondary" id="ageInfo"></span>
<span class="badge bg-light text-dark" id="stateInfo"></span>
<span class="badge bg-accent" id="accentInfo"></span>
<span class="badge bg-success" id="domainInfo"></span>
<span class="badge bg-warning text-dark" id="subdomainInfo"></span>
</div>
</div>
<div class="audio-player mb-4">
<audio controls class="w-100" preload="auto"
data-bs-toggle="tooltip" data-bs-placement="top"
title="Press 'Space' to Play/Pause"></audio>
</div>
<div class="transcript-box mb-4">
<div class="p-2 bg-light rounded" id="transcriptionText"></div>
</div>
<div class="validation-actions">
<button class="btn btn-success" onclick="verifyRecording(true)"
title="Accept Recording (A)">
<i class="fas fa-check me-2"></i>Accept
</button>
<button class="btn btn-danger" onclick="verifyRecording(false)"
title="Reject Recording (R)">
<i class="fas fa-times me-2"></i>Reject
</button>
</div>
`;
// Update UI with recording details
const audio = document.querySelector('#recordingContent audio');
audio.src = `/validation/api/audio/${currentRecording.audio_path}`;
document.getElementById('transcriptionText').textContent =
currentRecording.transcription_text || 'No transcription available';
document.getElementById('speakerInfo').textContent =
`${currentRecording.speaker_name || 'Unknown'}`;
document.getElementById('genderInfo').textContent =
currentRecording.gender || 'Gender: Unknown';
document.getElementById('ageInfo').textContent =
currentRecording.age_group || 'Age: Unknown';
document.getElementById('stateInfo').textContent =
currentRecording.state || 'State: Unknown';
document.getElementById('accentInfo').textContent =
currentRecording.accent || 'Accent: Unknown';
// Add domain and subdomain information
if (currentRecording.domain) {
document.getElementById('domainInfo').textContent = `Domain: ${currentRecording.domain}`;
document.getElementById('domainInfo').style.display = 'inline-block';
} else {
document.getElementById('domainInfo').style.display = 'none';
}
if (currentRecording.subdomain) {
document.getElementById('subdomainInfo').textContent = `Subdomain: ${currentRecording.subdomain}`;
document.getElementById('subdomainInfo').style.display = 'inline-block';
} else {
document.getElementById('subdomainInfo').style.display = 'none';
}
// Re-initialize tooltips
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl, {
trigger: 'hover'
})
})
} catch (error) {
console.error('Error loading next recording:', error);
document.getElementById('noRecordingMessage').style.display = 'block';
document.getElementById('noRecordingMessage').innerHTML = `
<i class="fas fa-exclamation-triangle text-warning"></i>
<h5>Error Loading Recording</h5>
<p>${error.message}</p>
`;
document.getElementById('recordingContent').style.display = 'none';
currentRecording = null;
}
}
async function verifyRecording(isAccepted) {
if (!currentRecording) return;
// Store references to all needed elements at the start
const actionButton = document.querySelector(isAccepted ? '.btn-success' : '.btn-danger');
const allButtons = document.querySelectorAll('.validation-actions button');
const originalText = actionButton.innerHTML;
try {
// Disable all buttons while processing
allButtons.forEach(btn => btn.disabled = true);
// Show loading state
const actionText = isAccepted ? 'Accepting' : 'Rejecting';
actionButton.innerHTML = `<i class="fas fa-spinner fa-spin me-2"></i>${actionText}...`;
const response = await fetch(`/validation/api/verify/${currentRecording.audio_path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ verify: isAccepted })
});
if (!response.ok) throw new Error('Verification failed');
// Load next recording
await loadNextRecording();
} catch (error) {
console.error('Error verifying recording:', error);
alert('Error verifying recording. Please try again.');
} finally {
// Re-enable all buttons and restore original text
allButtons.forEach(btn => btn.disabled = false);
actionButton.innerHTML = originalText;
}
}
// Add domain change handler
document.getElementById('domain').addEventListener('change', function() {
loadSubdomains(this.value);
});
// Initial load
document.addEventListener('DOMContentLoaded', function() {
const languageSelect = document.getElementById('language');
// Get user's default language
const userDataElem = document.getElementById('validatorUserData');
let userLanguage = '';
if (userDataElem) {
try {
const userData = JSON.parse(userDataElem.textContent);
userLanguage = userData.language;
} catch (e) {
console.error('Error parsing user data:', e);
}
}
// Set default language if available and trigger load
if (userLanguage && languageSelect) {
languageSelect.value = userLanguage;
// Only trigger if a language is selected
if (languageSelect.value) {
loadNextRecording();
}
}
// Load domains
loadDomains();
});
</script>
{% if session.get('user') %}
<script type="application/json" id="validatorUserData">
{
"language": "{{ session.user.language|default('') }}"
}
</script>
{% endif %}
</div>
</body>
</html>