|
<!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/bootstrap@5.1.3/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">
|
|
|
|
<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>
|
|
|
|
|
|
<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>
|
|
|
|
<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">
|
|
|
|
<div class="collapse d-md-block" id="filterCollapse">
|
|
<form id="filterForm" class="row g-3 align-items-center flex-nowrap mobile-filter-form">
|
|
|
|
<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>
|
|
|
|
|
|
<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">
|
|
|
|
</select>
|
|
<i class="fas fa-chevron-down select-arrow"></i>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<div class="card border-0">
|
|
<div id="validateRecordings">
|
|
|
|
<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>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<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>
|
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
|
|
|
|
<script>
|
|
let currentRecording = null;
|
|
|
|
|
|
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'
|
|
})
|
|
})
|
|
});
|
|
|
|
|
|
document.addEventListener('keydown', function(event) {
|
|
if (!currentRecording) return;
|
|
|
|
|
|
if (event.target.tagName === 'INPUT' || event.target.tagName === 'TEXTAREA') return;
|
|
|
|
|
|
if (event.target.tagName === 'BUTTON') return;
|
|
|
|
const audio = document.querySelector('#recordingContent audio');
|
|
|
|
switch(event.key.toLowerCase()) {
|
|
case ' ':
|
|
event.preventDefault();
|
|
if (audio.paused) {
|
|
audio.play();
|
|
} else {
|
|
audio.pause();
|
|
}
|
|
break;
|
|
case 'a':
|
|
event.preventDefault();
|
|
verifyRecording(true);
|
|
break;
|
|
case 'r':
|
|
event.preventDefault();
|
|
verifyRecording(false);
|
|
break;
|
|
}
|
|
});
|
|
|
|
|
|
function logFilterSelection(language, domain, subdomain) {
|
|
console.log(`Applied filters - Language: ${language || 'All'}, Domain: ${domain || 'All'}, Subdomain: ${subdomain || 'All'}`);
|
|
}
|
|
|
|
|
|
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>';
|
|
|
|
if (data.status === 'success' && data.domains) {
|
|
|
|
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>';
|
|
|
|
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;
|
|
|
|
|
|
if (!language) return;
|
|
|
|
logFilterSelection(language, domain, 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}`);
|
|
|
|
|
|
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';
|
|
|
|
|
|
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>
|
|
`;
|
|
|
|
|
|
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';
|
|
|
|
|
|
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';
|
|
}
|
|
|
|
|
|
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;
|
|
|
|
|
|
const actionButton = document.querySelector(isAccepted ? '.btn-success' : '.btn-danger');
|
|
const allButtons = document.querySelectorAll('.validation-actions button');
|
|
const originalText = actionButton.innerHTML;
|
|
|
|
try {
|
|
|
|
allButtons.forEach(btn => btn.disabled = true);
|
|
|
|
|
|
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');
|
|
|
|
|
|
await loadNextRecording();
|
|
} catch (error) {
|
|
console.error('Error verifying recording:', error);
|
|
alert('Error verifying recording. Please try again.');
|
|
} finally {
|
|
|
|
allButtons.forEach(btn => btn.disabled = false);
|
|
actionButton.innerHTML = originalText;
|
|
}
|
|
}
|
|
|
|
|
|
document.getElementById('domain').addEventListener('change', function() {
|
|
loadSubdomains(this.value);
|
|
});
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const languageSelect = document.getElementById('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);
|
|
}
|
|
}
|
|
|
|
|
|
if (userLanguage && languageSelect) {
|
|
languageSelect.value = userLanguage;
|
|
|
|
if (languageSelect.value) {
|
|
loadNextRecording();
|
|
}
|
|
}
|
|
|
|
|
|
loadDomains();
|
|
});
|
|
</script>
|
|
|
|
{% if session.get('user') %}
|
|
<script type="application/json" id="validatorUserData">
|
|
{
|
|
"language": "{{ session.user.language|default('') }}"
|
|
}
|
|
</script>
|
|
{% endif %}
|
|
</div>
|
|
</body>
|
|
</html>
|
|
|