494 lines
22 KiB
HTML
494 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Audio Transcription Editor & Reader</title>
|
|
|
|
<!-- External CSS Libraries -->
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
|
|
|
|
<!-- Google Fonts -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Lora:ital,wght@0,400..700;1,400..700&family=Poppins:wght@500;700&display=swap" rel="stylesheet">
|
|
|
|
<!-- Quill Rich Text Editor -->
|
|
<link href="https://cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
|
|
|
<!-- Custom CSS -->
|
|
<link rel="stylesheet" href="/static/css/style.css">
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Loading Overlay -->
|
|
<div class="loading-overlay" id="loader">
|
|
<div class="spinner-border text-primary mb-3" role="status"></div>
|
|
<h4 id="loadingText" class="fw-bold text-muted">Thinking...</h4>
|
|
<p class="text-muted small" id="loadingSubtext">Generating audio or aligning text...</p>
|
|
</div>
|
|
|
|
<!-- Floating Action Buttons -->
|
|
<div class="floating-controls" id="floatingControls">
|
|
<button class="floating-btn chapter-btn" onclick="insertChapterMarker()" title="Add Chapter Marker">
|
|
<i class="bi bi-bookmark-star"></i>
|
|
<span class="tooltip-text">Add Chapter</span>
|
|
</button>
|
|
<button class="floating-btn section-btn" onclick="insertSectionMarker()" title="Add Section Marker">
|
|
<i class="bi bi-file-earmark-text"></i>
|
|
<span class="tooltip-text">Add Section</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- TTS Text Edit Modal -->
|
|
<div class="modal fade" id="ttsEditModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-light">
|
|
<h5 class="modal-title fw-bold"><i class="bi bi-soundwave me-2"></i>Edit Text for TTS Generation</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="text-muted small mb-2">
|
|
<i class="bi bi-info-circle-fill me-1"></i>
|
|
This text will be used to <strong>generate audio</strong>. The original text in the editor will still be used for the Reader and Export.
|
|
</p>
|
|
<textarea id="ttsTextInput" class="form-control" rows="10" style="font-family: 'Lora', serif; font-size: 1.1em;"></textarea>
|
|
<input type="hidden" id="currentMarkerId">
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="button" class="btn btn-primary fw-bold" onclick="saveTTSText()">Save TTS Text</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Library Modal -->
|
|
<div class="modal fade" id="libraryModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header bg-light">
|
|
<h5 class="modal-title fw-bold"><i class="bi bi-archive me-2"></i>My Library</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<!-- Database Stats -->
|
|
<div class="db-stats mb-4" id="dbStats">
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="statUploads">0</div>
|
|
<div class="stat-label">Uploads</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="statGenerations">0</div>
|
|
<div class="stat-label">Generations</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="statProjects">0</div>
|
|
<div class="stat-label">Projects</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="statSections">0</div>
|
|
<div class="stat-label">Sections</div>
|
|
</div>
|
|
<div class="stat-item">
|
|
<div class="stat-value" id="statDbSize">0 MB</div>
|
|
<div class="stat-label">Database Size</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Library Tabs -->
|
|
<ul class="nav nav-pills mb-3" id="libraryTabs">
|
|
<li class="nav-item">
|
|
<button class="nav-link active" data-bs-toggle="pill" data-bs-target="#uploadsTab">Uploads</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#generationsTab">Generations</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#projectsTab">Projects</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Library Tab Content -->
|
|
<div class="tab-content">
|
|
<div class="tab-pane fade show active" id="uploadsTab">
|
|
<div id="uploadsList"></div>
|
|
</div>
|
|
<div class="tab-pane fade" id="generationsTab">
|
|
<div id="generationsList"></div>
|
|
</div>
|
|
<div class="tab-pane fade" id="projectsTab">
|
|
<div id="projectsList"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Container -->
|
|
<div class="main-container">
|
|
|
|
<!-- App Header with User Menu -->
|
|
<div class="app-header">
|
|
<div><h1 class="m-0" style="font-size: 24px;"><i class="bi bi-soundwave me-2"></i>Audio Editor & Reader</h1></div>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<button class="btn btn-light fw-bold" onclick="openLibrary()">
|
|
<i class="bi bi-archive me-2"></i>Library
|
|
</button>
|
|
|
|
<!-- User Menu Dropdown -->
|
|
<div class="dropdown">
|
|
<button class="btn btn-light fw-bold dropdown-toggle" type="button" data-bs-toggle="dropdown" id="userMenuBtn">
|
|
<i class="bi bi-person-circle me-2"></i><span id="currentUsername">User</span>
|
|
</button>
|
|
<ul class="dropdown-menu dropdown-menu-end">
|
|
<li class="px-3 py-2 text-muted small" id="userInfoHeader">
|
|
<i class="bi bi-info-circle me-1"></i>Logged in as <strong id="userInfoName">User</strong>
|
|
</li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li id="adminLinkItem" style="display: none;">
|
|
<a class="dropdown-item" href="/admin">
|
|
<i class="bi bi-shield-lock me-2"></i>Admin Dashboard
|
|
</a>
|
|
</li>
|
|
<li id="adminDivider" style="display: none;"><hr class="dropdown-divider"></li>
|
|
<li>
|
|
<a class="dropdown-item text-danger" href="#" onclick="logout(); return false;">
|
|
<i class="bi bi-box-arrow-right me-2"></i>Logout
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<span class="version-badge">Kokoro AI Edition</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Input Card with Tabs -->
|
|
<div class="input-card">
|
|
<!-- Tab Navigation -->
|
|
<ul class="nav nav-tabs px-4 pt-3" id="inputTabs" role="tablist">
|
|
<li class="nav-item">
|
|
<button class="nav-link active" id="upload-tab" data-bs-toggle="tab" data-bs-target="#upload-panel" type="button">
|
|
<i class="bi bi-upload me-2"></i>Upload File
|
|
</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link" id="write-tab" data-bs-toggle="tab" data-bs-target="#write-panel" type="button">
|
|
<i class="bi bi-pen me-2"></i>Write & Generate
|
|
</button>
|
|
</li>
|
|
<li class="nav-item">
|
|
<button class="nav-link" id="bulk-tab" data-bs-toggle="tab" data-bs-target="#bulk-panel" type="button">
|
|
<i class="bi bi-layers-half me-2"></i>Bulk Audio Processing
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="tab-content">
|
|
|
|
<!-- Upload Tab Panel -->
|
|
<div class="tab-pane fade show active p-5" id="upload-panel">
|
|
<form id="uploadForm" class="row g-3 align-items-end">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-bold small text-uppercase text-muted">Audio File</label>
|
|
<input class="form-control form-control-lg" type="file" id="audioFile" accept=".mp3, .wav">
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-bold small text-uppercase text-muted">Story Text</label>
|
|
<input class="form-control form-control-lg" type="file" id="txtFile" accept=".txt">
|
|
</div>
|
|
<div class="col-12 mt-4">
|
|
<button type="submit" class="btn btn-dark w-100 py-3 fw-bold">Upload & Align</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Write & Generate Tab Panel -->
|
|
<div class="tab-pane fade" id="write-panel">
|
|
<div id="quill-editor"></div>
|
|
<div class="editor-actions">
|
|
<div class="d-flex gap-3 align-items-center">
|
|
<div>
|
|
<label class="small fw-bold text-muted d-block mb-1">VOICE</label>
|
|
<select class="form-select form-select-sm" id="voiceSelect" style="width: 220px;">
|
|
<option value="af_heart" selected>Heart (Fem)</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-primary fw-bold px-4 py-2" onclick="generateAudio()">
|
|
<i class="bi bi-magic me-2"></i>Generate Audio
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bulk Audio Processing Tab Panel -->
|
|
<div class="tab-pane fade" id="bulk-panel">
|
|
<div class="notion-editor-wrapper">
|
|
<div id="bulk-editor" contenteditable="true" spellcheck="false">
|
|
<p><br></p>
|
|
</div>
|
|
</div>
|
|
<div class="editor-actions">
|
|
<div class="d-flex align-items-center gap-3 w-100 justify-content-between">
|
|
<!-- Left side: Project name and Save button -->
|
|
<div class="d-flex align-items-center gap-3">
|
|
<div class="input-group" style="max-width: 300px;">
|
|
<span class="input-group-text bg-white fw-bold">Project Name</span>
|
|
<input type="text" id="bulkProjectName" class="form-control" placeholder="Book-1" value="Book-1">
|
|
</div>
|
|
<button class="btn save-project-btn" onclick="saveProject()" title="Save project without generating audio">
|
|
<i class="bi bi-save me-2"></i>Save Project
|
|
</button>
|
|
</div>
|
|
<!-- Right side: Export button -->
|
|
<button class="btn btn-dark fw-bold px-4 py-2" onclick="exportEverything()">
|
|
<i class="bi bi-box-arrow-up-right me-2"></i>Export Everything
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<!-- Help text for bulk editor -->
|
|
<div class="px-4 pb-3">
|
|
<small class="text-muted">
|
|
<i class="bi bi-info-circle me-1"></i>
|
|
<strong>Tips:</strong> Use floating buttons (right side) to add Chapter/Section markers.
|
|
Each section can have an image (drag & drop, browse, or paste from clipboard).
|
|
Click <strong>Save Project</strong> to save without generating audio, or <strong>Export Everything</strong> to save and download as ZIP.
|
|
</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Editor Section (Timeline & Reader) -->
|
|
<div id="editorSection" class="d-none animate-fade-in">
|
|
|
|
<!-- Playlist Navigator -->
|
|
<div class="card mb-4 border-0 shadow-sm bg-light" id="playlistNavigator" style="display: none;">
|
|
<div class="card-body py-2">
|
|
<div class="d-flex align-items-center justify-content-between flex-wrap gap-3">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span class="badge bg-dark fw-bold" style="letter-spacing: 1px;">PLAYLIST</span>
|
|
<div class="btn-group">
|
|
<button class="btn btn-sm btn-white border" title="Previous" onclick="playPrevTrack()"><i class="bi bi-skip-backward-fill"></i></button>
|
|
<button class="btn btn-sm btn-white border" title="Next" onclick="playNextTrack()"><i class="bi bi-skip-forward-fill"></i></button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex-grow-1" style="max-width: 500px;">
|
|
<select class="form-select form-select-sm fw-bold text-dark" id="trackSelect" onchange="loadTrackFromPlaylist(this.value)">
|
|
<option>No tracks generated yet...</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="small text-muted fw-bold" id="trackInfo"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audio Control Panel -->
|
|
<div class="control-panel">
|
|
<!-- Playback Controls -->
|
|
<div class="d-flex flex-wrap gap-3 align-items-center">
|
|
<div class="btn-group">
|
|
<button class="btn btn-light border fw-bold" onclick="togglePlayPause()">
|
|
<i class="bi bi-play-fill text-primary" id="playIcon"></i> <span id="playText">Play</span>
|
|
</button>
|
|
<button class="btn btn-light border" onclick="stopAudio()"><i class="bi bi-stop-fill"></i></button>
|
|
</div>
|
|
|
|
<!-- Pill Insert/Delete -->
|
|
<div class="btn-group">
|
|
<button class="btn btn-outline-primary fw-bold" onclick="insertPillAtPlayhead()"><i class="bi bi-plus-lg me-1"></i>Insert</button>
|
|
<button class="btn btn-outline-danger fw-bold" id="deleteBtn" onclick="deleteSelectedPill()" disabled><i class="bi bi-trash me-1"></i>Delete</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Single Export Controls -->
|
|
<div class="d-flex align-items-center gap-2 border-start ps-3" id="singleExportGroup">
|
|
<div class="input-group input-group-sm">
|
|
<span class="input-group-text bg-white fw-bold">Filename</span>
|
|
<input type="text" id="exportFilename" class="form-control" placeholder="1.1_task-1" value="1.1_task-1" style="max-width: 150px;">
|
|
<button class="btn btn-dark fw-bold" onclick="exportSingle()">
|
|
<i class="bi bi-box-arrow-up-right me-2"></i>Export
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Speed & Zoom Controls -->
|
|
<div class="d-flex align-items-center gap-4">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span class="text-muted small fw-bold">SPEED</span>
|
|
<input type="range" class="form-range" id="speedSlider" min="0.5" max="2.0" step="0.1" value="1.0" style="width: 80px;">
|
|
<span id="speedDisplay" class="badge bg-secondary">1.0x</span>
|
|
</div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<span class="text-muted small fw-bold">ZOOM</span>
|
|
<input type="range" class="form-range" id="zoomSlider" min="50" max="500" value="100" style="width: 100px;">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Timeline/Waveform Editor -->
|
|
<div class="timeline-wrapper" id="timelineWrapper">
|
|
<div class="timeline-content" id="timelineContent">
|
|
<div id="custom-playhead"></div>
|
|
<div id="timeline-ruler"></div>
|
|
<div class="audio-track-container">
|
|
<div class="track-label">Audio</div>
|
|
<div id="waveform"></div>
|
|
</div>
|
|
<div class="transcription-track-container" id="transcriptionTrack">
|
|
<div class="track-label">Transcript</div>
|
|
<div id="transcription-content"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Interactive Reader -->
|
|
<div class="reader-section">
|
|
<div class="reader-header">
|
|
<div class="d-flex align-items-center gap-3">
|
|
<span>Interactive Reader</span>
|
|
<span id="syncStatus" class="badge bg-secondary fs-6">Not Loaded</span>
|
|
</div>
|
|
<div class="form-check form-switch fs-6">
|
|
<input class="form-check-input" type="checkbox" id="mismatchToggle" onchange="toggleMismatches()">
|
|
<label class="form-check-label fw-bold text-muted" for="mismatchToggle">Show Mismatches</label>
|
|
</div>
|
|
</div>
|
|
<article class="story-text-container" id="readerContent"></article>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- External JS Libraries -->
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://unpkg.com/wavesurfer.js@7/dist/wavesurfer.min.js"></script>
|
|
<script src="https://unpkg.com/wavesurfer.js@7/dist/plugins/timeline.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/quill@1.3.6/dist/quill.min.js"></script>
|
|
|
|
<!-- Application JS Modules -->
|
|
<script src="/static/js/editor.js"></script>
|
|
<script src="/static/js/timeline.js"></script>
|
|
<script src="/static/js/interactive-reader.js"></script>
|
|
<script src="/static/js/app.js"></script>
|
|
|
|
<!-- User Authentication & Tab Change Handling -->
|
|
<script>
|
|
// ==========================================
|
|
// USER AUTHENTICATION
|
|
// ==========================================
|
|
|
|
/**
|
|
* Load current user info and update UI
|
|
*/
|
|
async function loadCurrentUser() {
|
|
try {
|
|
const res = await fetch('/api/me');
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
const username = data.user.username;
|
|
|
|
// Update username displays
|
|
document.getElementById('currentUsername').textContent = username;
|
|
document.getElementById('userInfoName').textContent = username;
|
|
|
|
// Show admin link if user is admin
|
|
if (data.user.is_admin) {
|
|
document.getElementById('adminLinkItem').style.display = 'block';
|
|
document.getElementById('adminDivider').style.display = 'block';
|
|
}
|
|
|
|
console.log(`👤 Logged in as: ${username} ${data.user.is_admin ? '(Admin)' : ''}`);
|
|
} else if (res.status === 401) {
|
|
// Not authenticated, redirect to login
|
|
console.log('🔒 Not authenticated, redirecting to login...');
|
|
window.location.href = '/login';
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load user info:', e);
|
|
// On error, redirect to login as a safety measure
|
|
window.location.href = '/login';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Logout and redirect to login page
|
|
*/
|
|
async function logout() {
|
|
try {
|
|
await fetch('/api/logout', { method: 'POST' });
|
|
console.log('👋 Logged out successfully');
|
|
} catch (e) {
|
|
console.error('Logout error:', e);
|
|
}
|
|
window.location.href = '/login';
|
|
}
|
|
|
|
// ==========================================
|
|
// TAB CHANGE EVENT HANDLING
|
|
// ==========================================
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Load current user info
|
|
loadCurrentUser();
|
|
|
|
// Handle tab switching for floating controls
|
|
const tabElements = document.querySelectorAll('#inputTabs button[data-bs-toggle="tab"]');
|
|
|
|
tabElements.forEach(tab => {
|
|
tab.addEventListener('shown.bs.tab', function(event) {
|
|
const targetId = event.target.getAttribute('data-bs-target');
|
|
|
|
if (targetId === '#bulk-panel') {
|
|
// Show floating controls for bulk processing
|
|
toggleFloatingControls(true);
|
|
// Re-initialize image handlers
|
|
setTimeout(initializeImageHandlers, 100);
|
|
} else {
|
|
// Hide floating controls for other tabs
|
|
toggleFloatingControls(false);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Initial state - hide floating controls since Upload tab is active by default
|
|
toggleFloatingControls(false);
|
|
});
|
|
|
|
// ==========================================
|
|
// SESSION CHECK (Periodic)
|
|
// ==========================================
|
|
|
|
/**
|
|
* Periodically check if session is still valid
|
|
*/
|
|
function startSessionCheck() {
|
|
// Check session every 5 minutes
|
|
setInterval(async () => {
|
|
try {
|
|
const res = await fetch('/api/me');
|
|
if (res.status === 401) {
|
|
console.log('🔒 Session expired, redirecting to login...');
|
|
window.location.href = '/login';
|
|
}
|
|
} catch (e) {
|
|
// Network error - don't redirect, might be temporary
|
|
console.warn('Session check failed:', e);
|
|
}
|
|
}, 5 * 60 * 1000); // 5 minutes
|
|
}
|
|
|
|
// Start session checking
|
|
startSessionCheck();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|