Files

1621 lines
60 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</title>
<style>
:root {
--primary-color: #2d3e50;
--accent-color: #3498db;
--secondary-accent: #2ecc71;
--text-color: #333;
--light-bg: #f5f5f5;
--border-color: #ddd;
--shadow-color: rgba(0, 0, 0, 0.1);
--transition-speed: 0.3s;
--hover-bg: #e5e5e5;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
background-color: #ebebeb;
color: var(--text-color);
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 100vh;
}
main {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem 0;
}
.container {
display: flex;
width: 90%;
max-width: 1200px;
height: 80vh;
background-color: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 24px var(--shadow-color);
}
.left-panel {
flex: 0.4;
padding: 2rem;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border-right: 1px solid var(--border-color);
}
.right-panel {
flex: 0.6;
padding: 2rem;
display: flex;
flex-direction: column;
position: relative;
}
.upload-files-block {
width: 100%;
border: 3px dashed var(--border-color);
border-radius: 12px;
background-color: white;
overflow: hidden;
margin-bottom: 0;
}
.upload-files-block.active {
border-color: var(--secondary-accent);
box-shadow: 0 6px 18px rgba(46, 204, 113, 0.25);
}
.upload-files-block.active .upload-icon {
color: var(--secondary-accent);
}
.upload-area {
width: 100%;
height: 250px;
border: none;
border-bottom: 1px solid var(--border-color);
border-radius: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all var(--transition-speed) cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
background-color: rgba(255, 255, 255, 0.6);
margin: 0; /* Remove any margin */
padding: 0; /* Remove any padding */
}
.upload-area:before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: radial-gradient(circle, rgba(229, 229, 229, 0.1) 0%, rgba(229, 229, 229, 0.2) 100%);
opacity: 0;
transition: opacity var(--transition-speed) ease;
z-index: 0;
}
.upload-area:hover:before {
opacity: 1;
}
.upload-area:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.upload-files-block.active .upload-area {
background-color: rgba(46, 204, 113, 0.05);
transform: scale(1.01);
}
.upload-area .upload-content {
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
text-align: center;
padding: 2.5rem 1.5rem 0.8rem 1.5rem;
gap: 0rem;
height: 100%;
width: 100%;
}
.upload-area .upload-content .icon-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.upload-area .upload-content .text-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.125rem;
}
.upload-area .upload-content p {
margin: 0;
font-size: 14px;
color: #999;
font-weight: normal;
}
.upload-area .upload-content small {
margin: 0;
font-size: 14px;
color: #999;
font-weight: normal;
}
.upload-icon {
font-size: 6rem;
color: var(--primary-color);
transition: transform 0.5s ease;
}
.upload-area:hover .upload-icon {
transform: translateY(-5px);
}
.files-queue {
width: 100%;
background-color: white;
display: flex;
flex-direction: column;
margin: 0; /* Remove any margin */
padding: 0; /* Remove any padding */
}
.files-container {
overflow-y: hidden;
min-height: 300px;
max-height: 300px;
margin: 0; /* Remove any margin */
padding: 0; /* Remove any padding */
display: flex;
flex-direction: column;
}
.files-container.has-files {
overflow-y: auto;
}
.no-files-message {
text-align: center;
color: #aaa;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.file-item {
padding: 0.75rem 1rem;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background-color 0.2s;
display: flex;
flex-direction: column;
gap: 0.25rem;
min-height: 64px;
box-sizing: border-box;
margin: 0; /* Remove any margin */
}
.file-item:hover {
background-color: #f8f9fa;
}
.file-item.active {
background-color: var(--accent-color);
color: white;
}
.file-item.active .file-status {
color: rgba(255, 255, 255, 0.8);
}
.file-item:last-child {
border-bottom: none;
}
.file-name {
font-weight: 500;
font-size: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-status {
font-size: 12px;
color: #666;
display: flex;
align-items: center;
gap: 0.5rem;
}
.file-status.processing {
color: var(--accent-color);
}
.file-status.completed {
color: var(--secondary-accent);
}
.file-status.error {
color: #e74c3c;
}
.file-status.queued {
color: #f39c12;
}
.status-icon {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
}
.status-icon.processing {
background-color: var(--accent-color);
animation: pulse 1.5s ease-in-out infinite;
}
.status-icon.completed {
background-color: var(--secondary-accent);
}
.status-icon.error {
background-color: #e74c3c;
}
.status-icon.queued {
background-color: #f39c12;
}
.clear-queue-btn-small {
width: 40px;
height: 36px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
align-self: flex-end;
margin-top: 12px;
}
.clear-queue-btn-small:hover:not(:disabled) {
background-color: #c0392b;
}
.clear-queue-btn-small:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.features h2 {
margin-bottom: 1rem;
font-size: 20px;
color: var(--primary-color);
}
.features ul {
padding-left: 1.5rem;
}
.features li {
margin-bottom: 0.8rem;
font-size: 16px;
}
.player-container {
width: 100%;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.file-info {
margin-bottom: 8px;
font-size: 14px;
color: #666;
}
.audio-player {
width: 100%;
outline: none;
border-radius: 8px;
background-color: #f8f9fa;
}
.audio-player::-webkit-media-controls-panel {
background-color: #f8f9fa;
}
.content-container {
flex: 1;
display: flex;
flex-direction: column;
position: relative;
min-height: 300px;
overflow: hidden;
margin-bottom: 16px;
}
.transcript-placeholder, .debug-placeholder {
color: #aaa;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
width: 80%;
}
.tabs-content {
flex: 1;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.tab-panel {
flex: 1;
display: none;
position: relative;
overflow-y: auto;
padding: 1.5rem;
background-color: #fff;
}
.tab-panel.active {
display: block;
}
.controls-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
}
.tabs-navigation {
display: flex;
}
.tab-button {
padding: 8px 16px;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
margin-right: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.tab-button:not(.active) {
background-color: #f1f1f1;
color: var(--text-color);
}
.tab-button:hover:not(.active) {
background-color: var(--hover-bg);
}
.transcript-placeholder {
color: #aaa;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
width: 80%;
}
.download-icons {
display: flex;
gap: 12px;
}
.download-button {
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
width: 40px;
height: 36px;
display: flex;
align-items: center;
justify-content: center;
transition: background-color 0.2s;
}
.download-button:hover {
background-color: #34495e;
}
.download-button.copied {
background-color: var(--secondary-accent);
transform: scale(1.1);
}
.download-button.hidden {
display: none;
}
#transcript-content {
min-height: 100%;
display: none;
}
#transcript-editor {
width: 100%;
height: 100%;
border: none;
outline: none;
resize: none;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 16px;
line-height: 1.5;
padding: 0;
margin: 0;
}
#file-input {
display: none;
}
.status {
margin-top: 1rem;
padding: 0.5rem;
border-radius: 4px;
text-align: center;
display: none;
}
.status.error {
background-color: #ffebee;
color: #c62828;
display: block;
}
.status.success {
background-color: #e8f5e9;
color: #2e7d32;
display: block;
}
.status.loading {
display: block;
color: var(--primary-color);
}
.debug-panel {
margin-top: 1rem;
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
}
.debug-header {
background-color: #f1f1f1;
padding: 0.5rem 1rem;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border-color);
}
.debug-header h3 {
margin: 0;
font-size: 16px;
}
.debug-content {
font-family: monospace;
font-size: 14px;
white-space: pre-wrap;
word-break: break-all;
}
.debug-item {
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px dashed #ddd;
}
.debug-item:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
.debug-label {
font-weight: bold;
color: #555;
margin-bottom: 0.25rem;
}
.debug-value {
margin-left: 1rem;
}
footer {
background-color: #d5d5d5;
color: #555;
text-align: center;
padding: 1rem;
width: 100%;
box-sizing: border-box;
}
.loading-spinner {
display: inline-block;
width: 80px;
height: 80px;
border: 10px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: var(--accent-color);
animation: spin 1s ease-in-out infinite;
margin-right: 10px;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.processing-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.9);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 10;
display: none;
}
.processing-text {
margin-top: 10px;
color: var(--primary-color);
font-weight: bold;
}
.timer {
margin-top: 5px;
color: var(--primary-color);
}
.upload-progress {
position: absolute;
bottom: 0;
left: 0;
width: 0%;
height: 4px;
background: linear-gradient(to right, var(--accent-color), var(--secondary-accent));
transition: width 0.3s ease-out;
border-radius: 0 2px 2px 0;
display: none;
}
@media (max-width: 768px) {
main {
padding: 1rem;
align-items: flex-start;
}
.container {
flex-direction: column;
width: 100%;
max-width: 100%;
margin: 0;
border-radius: 4px;
height: auto;
}
.left-panel, .right-panel {
flex: none;
width: 100%;
padding: 1.5rem;
box-sizing: border-box;
}
.upload-area {
height: 180px;
}
.upload-area .upload-content {
padding: 1.75rem 1rem 0.6rem 1rem;
}
.upload-icon {
font-size: 4.5rem;
}
.files-container {
min-height: 220px;
max-height: 220px;
}
.content-container {
min-height: 250px;
}
.controls-container {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.tabs-navigation {
width: 100%;
margin-bottom: 8px;
}
.download-icons {
width: 100%;
justify-content: flex-end;
}
}
</style>
</head>
<body>
<main>
<div class="container">
<div class="left-panel">
<div class="upload-files-block" id="upload-files-block">
<div class="upload-area" id="upload-area">
<div class="upload-progress" id="upload-progress"></div>
<div class="upload-content">
<div class="icon-container">
<div class="upload-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
</div>
</div>
<div class="text-container">
<p>Drag audio files here</p>
<small>Multiple files supported</small>
</div>
</div>
<input type="file" id="file-input" accept="audio/*,.ogg" multiple />
</div>
<div class="files-queue" id="files-queue">
<div class="files-container" id="files-container">
<div class="no-files-message" id="no-files-message">
No files uploaded yet
</div>
</div>
</div>
</div>
<button class="clear-queue-btn-small" id="clear-queue-btn" title="Clear Queue" disabled>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c-1 0 2 1 2 2v2"></path>
</svg>
</button>
</div>
<div class="right-panel">
<div class="player-container" id="player-container">
<div class="file-info" id="file-info">No file selected</div>
<audio id="audio-player" class="audio-player" controls disabled></audio>
</div>
<div class="content-container">
<div class="tabs-content">
<div id="transcript-tab" class="tab-panel active">
<div class="transcript-placeholder" id="transcript-placeholder">
Your transcription will appear here
</div>
<div id="transcript-content"></div>
<textarea id="transcript-editor"></textarea>
<div class="processing-overlay" id="processing-overlay">
<div class="loading-spinner"></div>
<div class="processing-text">Processing audio...</div>
<div class="timer" id="timer">00:00</div>
</div>
</div>
<div id="debug-tab" class="tab-panel">
<div class="debug-placeholder" id="debug-placeholder">
API requests and responses will appear here
</div>
<div class="debug-content" id="debug-info"></div>
</div>
</div>
<div class="controls-container">
<div class="tabs-navigation">
<button id="transcript-tab-btn" class="tab-button active">Transcription</button>
<button id="debug-tab-btn" class="tab-button">Debug</button>
</div>
<div class="download-icons">
<button class="download-button" id="copy-text" title="Copy to Clipboard">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</button>
<button class="download-button" id="download-text" title="Download as Text">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
<button class="download-button" id="download-all" title="Download All Files">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="5 10 9 14 13 10"></polyline>
<line x1="9" y1="14" x2="9" y2="3"></line>
<polyline points="11 10 15 14 19 10"></polyline>
<line x1="15" y1="14" x2="15" y2="3"></line>
</svg>
</button>
<button class="download-button" id="download-json" title="Download JSON">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</main>
<footer>
© Audio Transcription Tool 2025 (v0.5.2)
</footer>
<script>
// Configuration. The same as in nginx path
const proxyPath = '/tools/transcription';
document.addEventListener('DOMContentLoaded', function() {
const uploadArea = document.getElementById('upload-area');
const uploadFilesBlock = document.getElementById('upload-files-block');
const fileInput = document.getElementById('file-input');
const transcriptPlaceholder = document.getElementById('transcript-placeholder');
const transcriptContent = document.getElementById('transcript-content');
const transcriptEditor = document.getElementById('transcript-editor');
const audioPlayer = document.getElementById('audio-player');
const fileInfo = document.getElementById('file-info');
const statusEl = document.getElementById('status');
const debugInfo = document.getElementById('debug-info');
const processingOverlay = document.getElementById('processing-overlay');
const timerElement = document.getElementById('timer');
const uploadProgress = document.getElementById('upload-progress');
const copyTextBtn = document.getElementById('copy-text');
const downloadTextBtn = document.getElementById('download-text');
const downloadAllBtn = document.getElementById('download-all');
const downloadJsonBtn = document.getElementById('download-json');
const filesContainer = document.getElementById('files-container');
const noFilesMessage = document.getElementById('no-files-message');
const clearQueueBtn = document.getElementById('clear-queue-btn');
// Tab navigation
const transcriptTabBtn = document.getElementById('transcript-tab-btn');
const debugTabBtn = document.getElementById('debug-tab-btn');
const transcriptTab = document.getElementById('transcript-tab');
const debugTab = document.getElementById('debug-tab');
// Tab switching
transcriptTabBtn.addEventListener('click', () => {
activateTab(transcriptTabBtn, transcriptTab);
});
debugTabBtn.addEventListener('click', () => {
activateTab(debugTabBtn, debugTab);
});
function activateTab(button, panel) {
// Deactivate all tabs
[transcriptTabBtn, debugTabBtn].forEach(btn => btn.classList.remove('active'));
[transcriptTab, debugTab].forEach(tab => tab.classList.remove('active'));
// Activate the selected tab
button.classList.add('active');
panel.classList.add('active');
// Show/hide buttons based on active tab
updateButtonVisibility(button);
}
function updateButtonVisibility(activeButton) {
if (activeButton === transcriptTabBtn) {
// Show transcript-related buttons, hide JSON download
copyTextBtn.classList.remove('hidden');
downloadTextBtn.classList.remove('hidden');
downloadAllBtn.classList.remove('hidden');
downloadJsonBtn.classList.add('hidden');
} else if (activeButton === debugTabBtn) {
// Hide transcript buttons, show JSON download
copyTextBtn.classList.add('hidden');
downloadTextBtn.classList.add('hidden');
downloadAllBtn.classList.add('hidden');
downloadJsonBtn.classList.remove('hidden');
}
}
// File management system
let fileManager = {
files: new Map(), // fileId -> {file, name, status, transcription, json, audioUrl, debugInfo}
activeFileId: null,
processingQueue: [],
isProcessing: false
};
let timerInterval = null;
let startTime = 0;
function addDebugInfo(label, value, fileId = null) {
// Use current processing file if no fileId specified
const targetFileId = fileId || fileManager.activeFileId;
if (!targetFileId) return;
const fileData = fileManager.files.get(targetFileId);
if (!fileData) return;
// Initialize debug info if not exists
if (!fileData.debugInfo) {
fileData.debugInfo = [];
}
// Add debug entry
fileData.debugInfo.push({
label,
value,
timestamp: new Date().toLocaleTimeString()
});
// Update UI if this is the active file
if (targetFileId === fileManager.activeFileId) {
updateDebugUI(fileData.debugInfo);
}
// Switch to debug tab when error occurs
if (label === 'Fetch Error' || label === 'Error Response') {
activateTab(debugTabBtn, debugTab);
}
}
function updateDebugUI(debugInfoArray) {
debugInfo.innerHTML = '';
if (!debugInfoArray || debugInfoArray.length === 0) {
document.getElementById('debug-placeholder').style.display = 'block';
return;
}
document.getElementById('debug-placeholder').style.display = 'none';
debugInfoArray.forEach(entry => {
const debugItem = document.createElement('div');
debugItem.className = 'debug-item';
const debugLabel = document.createElement('div');
debugLabel.className = 'debug-label';
debugLabel.textContent = `${entry.label} (${entry.timestamp})`;
const debugValue = document.createElement('div');
debugValue.className = 'debug-value';
if (typeof entry.value === 'object') {
debugValue.textContent = JSON.stringify(entry.value, null, 2);
} else {
debugValue.textContent = entry.value;
}
debugItem.appendChild(debugLabel);
debugItem.appendChild(debugValue);
debugInfo.appendChild(debugItem);
});
// Auto-scroll debug panel
if (debugTab.scrollHeight > debugTab.clientHeight) {
debugTab.scrollTop = debugTab.scrollHeight;
}
}
function clearDebugInfo() {
// Only clear UI, keep file data
debugInfo.innerHTML = '';
document.getElementById('debug-placeholder').style.display = 'block';
}
// Handle drag and drop events on entire upload block
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadFilesBlock.addEventListener(eventName, preventDefaults, false);
document.body.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
// Highlight drop area when dragging over it
['dragenter', 'dragover'].forEach(eventName => {
uploadFilesBlock.addEventListener(eventName, highlight, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadFilesBlock.addEventListener(eventName, unhighlight, false);
});
function highlight() {
uploadFilesBlock.classList.add('active');
}
function unhighlight() {
uploadFilesBlock.classList.remove('active');
}
// Handle dropped files
uploadFilesBlock.addEventListener('drop', handleDrop, false);
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
if (files.length > 0) {
handleFiles(files);
}
}
// Handle file input change
uploadArea.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
if (fileInput.files.length > 0) {
handleFiles(fileInput.files);
}
});
function handleFiles(files) {
const audioFiles = Array.from(files).filter(file =>
file.type.startsWith('audio/') || file.name.toLowerCase().endsWith('.ogg')
);
if (audioFiles.length === 0) {
showStatus('Please select audio files.', 'error');
return;
}
// Add files to manager
audioFiles.forEach(file => {
const fileId = generateFileId();
const audioUrl = URL.createObjectURL(file);
fileManager.files.set(fileId, {
file: file,
name: file.name,
status: 'queued',
transcription: '',
json: null,
audioUrl: audioUrl,
debugInfo: []
});
fileManager.processingQueue.push(fileId);
addFileToUI(fileId);
});
updateFilesCount();
// Start processing queue
if (!fileManager.isProcessing) {
processNextFile();
}
}
function generateFileId() {
return 'file_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
function addFileToUI(fileId) {
const fileData = fileManager.files.get(fileId);
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.dataset.fileId = fileId;
fileItem.innerHTML = `
<div class="file-name" title="${fileData.name}">${fileData.name}</div>
<div class="file-status queued">
<span class="status-icon queued"></span>
<span class="status-text">Waiting for transcription</span>
</div>
`;
fileItem.addEventListener('click', () => selectFile(fileId));
noFilesMessage.style.display = 'none';
// Add new files to the TOP of the container (prepend instead of append)
if (filesContainer.firstChild && filesContainer.firstChild !== noFilesMessage) {
filesContainer.insertBefore(fileItem, filesContainer.firstChild);
} else {
filesContainer.appendChild(fileItem);
}
}
function updateFilesCount() {
const count = fileManager.files.size;
// Enable/disable clear button
clearQueueBtn.disabled = count === 0;
// Show/hide no files message and manage scroll
if (count === 0) {
noFilesMessage.style.display = 'flex';
filesContainer.classList.remove('has-files');
} else {
noFilesMessage.style.display = 'none';
filesContainer.classList.add('has-files');
}
}
function selectFile(fileId) {
const fileData = fileManager.files.get(fileId);
if (!fileData) return;
// Update active file
fileManager.activeFileId = fileId;
// Update UI selection
document.querySelectorAll('.file-item').forEach(item => {
item.classList.remove('active');
});
document.querySelector(`[data-file-id="${fileId}"]`).classList.add('active');
// Setup audio player
setupAudioPlayer(fileData.file, fileData.audioUrl);
// Load transcription if available
loadTranscription(fileData);
// Show processing overlay if this file is currently being processed
if (fileData.status === 'processing') {
processingOverlay.style.display = 'flex';
transcriptPlaceholder.style.display = 'none';
} else {
processingOverlay.style.display = 'none';
}
}
function loadTranscription(fileData) {
if (fileData.transcription) {
transcriptPlaceholder.style.display = 'none';
transcriptEditor.value = fileData.transcription;
} else {
transcriptEditor.value = '';
if (fileData.status !== 'processing') {
transcriptPlaceholder.style.display = 'block';
}
}
// Load debug info
updateDebugUI(fileData.debugInfo);
}
function processNextFile() {
if (fileManager.processingQueue.length === 0) {
fileManager.isProcessing = false;
return;
}
fileManager.isProcessing = true;
// Process files from the BEGINNING of the queue (oldest files first)
// This means files will be processed from bottom to top in the UI
const fileId = fileManager.processingQueue.shift();
const fileData = fileManager.files.get(fileId);
if (!fileData) {
processNextFile();
return;
}
// Update file status
updateFileStatus(fileId, 'processing', 'Processing...');
// Select this file if none is selected
if (!fileManager.activeFileId) {
selectFile(fileId);
}
// Start processing
uploadFile(fileData.file, fileId);
}
function setupAudioPlayer(file, audioUrl) {
// Set the source for the audio player
audioPlayer.src = audioUrl || URL.createObjectURL(file);
audioPlayer.disabled = false;
// Update file info
fileInfo.textContent = `${file.name} (${formatFileSize(file.size)})`;
}
function updateFileStatus(fileId, status, statusText) {
const fileData = fileManager.files.get(fileId);
if (!fileData) return;
fileData.status = status;
const fileItem = document.querySelector(`[data-file-id="${fileId}"]`);
if (!fileItem) return;
const statusIcon = fileItem.querySelector('.status-icon');
const statusTextEl = fileItem.querySelector('.status-text');
// Remove all status classes
statusIcon.className = 'status-icon';
statusIcon.classList.add(status);
statusTextEl.textContent = statusText;
const statusContainer = fileItem.querySelector('.file-status');
statusContainer.className = 'file-status';
statusContainer.classList.add(status);
// Update processing overlay if this is the active file
if (fileManager.activeFileId === fileId) {
if (status === 'processing') {
processingOverlay.style.display = 'flex';
transcriptPlaceholder.style.display = 'none';
} else if (status === 'completed' || status === 'error') {
processingOverlay.style.display = 'none';
if (!fileData.transcription) {
transcriptPlaceholder.style.display = 'block';
}
}
}
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
else if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB';
else return (bytes / 1048576).toFixed(1) + ' MB';
}
function startTimer() {
startTime = Date.now();
// Show processing overlay only if the processing file is the active file
if (fileManager.activeFileId && fileManager.processingQueue.length > 0) {
const processingFileId = fileManager.processingQueue[0] || Object.keys(fileManager.files).find(id => fileManager.files.get(id).status === 'processing');
if (fileManager.activeFileId === processingFileId) {
processingOverlay.style.display = 'flex';
// Hide placeholder text during processing
transcriptPlaceholder.style.display = 'none';
}
}
// Switch to transcript tab when processing starts
activateTab(transcriptTabBtn, transcriptTab);
clearInterval(timerInterval);
timerInterval = setInterval(updateTimer, 1000);
updateTimer(); // Initial call to show 00:00
}
function stopTimer() {
clearInterval(timerInterval);
processingOverlay.style.display = 'none';
// Show placeholder back only if there's no transcription for active file
const activeFile = fileManager.activeFileId ? fileManager.files.get(fileManager.activeFileId) : null;
if (!activeFile || !activeFile.transcription) {
transcriptPlaceholder.style.display = 'block';
}
}
function updateTimer() {
const elapsedTime = Math.floor((Date.now() - startTime) / 1000);
const minutes = Math.floor(elapsedTime / 60).toString().padStart(2, '0');
const seconds = (elapsedTime % 60).toString().padStart(2, '0');
timerElement.textContent = `${minutes}:${seconds}`;
}
function uploadFile(file, fileId) {
startTimer(fileId);
clearDebugInfo();
const formData = new FormData();
formData.append('file', file);
formData.append('model', 'whisper-1');
// Show upload progress indicator
uploadProgress.style.display = 'block';
// Dynamically determine API URL based on current path
let apiUrl;
if (window.location.pathname.includes(proxyPath)) {
// Access through nginx proxy
apiUrl = `${proxyPath}/v1/audio/transcriptions`;
} else {
// Direct access to the server
apiUrl = '/v1/audio/transcriptions';
}
addDebugInfo('Request URL', apiUrl, fileId);
addDebugInfo('File Name', file.name, fileId);
addDebugInfo('File Size', `${(file.size / 1024).toFixed(2)} KB`, fileId);
addDebugInfo('File Type', file.type || 'audio/ogg', fileId);
addDebugInfo('Request Time', new Date().toLocaleTimeString(), fileId);
// Simulate upload progress for better UX
let progressInterval = simulateUploadProgress();
fetch(apiUrl, {
method: 'POST',
body: formData
})
.then(response => {
clearInterval(progressInterval);
uploadProgress.style.width = '100%';
addDebugInfo('Response Status', `${response.status} ${response.statusText}`, fileId);
addDebugInfo('Response Headers', Object.fromEntries([...response.headers]), fileId);
if (!response.ok) {
return response.text().then(text => {
addDebugInfo('Error Response', text, fileId);
throw new Error(`Server responded with ${response.status}: ${text}`);
});
}
return response.json();
})
.then(data => {
stopTimer();
addDebugInfo('Response Data', data, fileId);
// Store results
const fileData = fileManager.files.get(fileId);
if (fileData) {
fileData.transcription = data.text || data;
fileData.json = data;
}
// Update status
updateFileStatus(fileId, 'completed', 'Completed');
// Hide upload progress after a short delay
setTimeout(() => {
uploadProgress.style.width = '0%';
uploadProgress.style.display = 'none';
}, 500);
// Show transcription if this is the active file
if (fileManager.activeFileId === fileId) {
showTranscription(data.text || data);
}
// Process next file in queue
processNextFile();
})
.catch(error => {
stopTimer();
console.error('Error:', error);
addDebugInfo('Fetch Error', error.message, fileId);
// Update status
updateFileStatus(fileId, 'error', 'Error');
// Hide upload progress
uploadProgress.style.width = '0%';
uploadProgress.style.display = 'none';
// Process next file in queue
processNextFile();
});
}
function simulateUploadProgress() {
let progress = 0;
uploadProgress.style.width = '0%';
return setInterval(() => {
// Simulate slower progress as it gets closer to 100%
const increment = (100 - progress) / 10;
progress += Math.min(increment, 5);
// Cap at 90% - the real completion will set to 100%
if (progress >= 90) {
progress = 90;
clearInterval(progressInterval);
}
uploadProgress.style.width = `${progress}%`;
}, 300);
}
function showTranscription(text) {
transcriptPlaceholder.style.display = 'none';
// Set text in editor directly (always in edit mode)
transcriptEditor.value = text;
// Make sure transcript tab is active
activateTab(transcriptTabBtn, transcriptTab);
}
// Update active file transcription when editor changes
transcriptEditor.addEventListener('input', () => {
if (fileManager.activeFileId) {
const fileData = fileManager.files.get(fileManager.activeFileId);
if (fileData) {
fileData.transcription = transcriptEditor.value;
}
}
});
function getCurrentFileData() {
if (!fileManager.activeFileId) return null;
return fileManager.files.get(fileManager.activeFileId);
}
function getCurrentFileName() {
const fileData = getCurrentFileData();
if (!fileData) return 'transcription';
return fileData.name.replace(/\.[^/.]+$/, ''); // Remove extension
}
function clearQueue() {
// Clear processing
fileManager.isProcessing = false;
fileManager.processingQueue = [];
// Clean up object URLs
fileManager.files.forEach(fileData => {
if (fileData.audioUrl) {
URL.revokeObjectURL(fileData.audioUrl);
}
});
// Clear all data
fileManager.files.clear();
fileManager.activeFileId = null;
// Clear UI
filesContainer.innerHTML = '';
updateFilesCount();
// Reset audio player
audioPlayer.src = '';
audioPlayer.disabled = true;
fileInfo.textContent = 'No file selected';
// Clear transcription and debug
transcriptEditor.value = '';
transcriptPlaceholder.style.display = 'block';
clearDebugInfo();
// Hide processing overlay
if (processingOverlay.style.display === 'flex') {
stopTimer();
}
}
function downloadAllFiles() {
const completedFiles = Array.from(fileManager.files.values()).filter(
fileData => fileData.status === 'completed' && fileData.transcription
);
if (completedFiles.length === 0) {
activateTab(transcriptTabBtn, transcriptTab);
transcriptPlaceholder.textContent = 'No completed transcriptions to download';
setTimeout(() => {
transcriptPlaceholder.textContent = 'Your transcription will appear here';
}, 3000);
return;
}
// Download each file with a small delay to avoid browser blocking
completedFiles.forEach((fileData, index) => {
setTimeout(() => {
const filename = fileData.name.replace(/\.[^/.]+$/, '') + '.txt';
downloadFile(filename, fileData.transcription, 'text/plain');
}, index * 100); // 100ms delay between downloads
});
}
// Event handlers for new buttons
clearQueueBtn.addEventListener('click', () => {
if (fileManager.files.size === 0) return;
if (confirm('Are you sure you want to clear all files and transcriptions?')) {
clearQueue();
}
});
downloadAllBtn.addEventListener('click', downloadAllFiles);
// Initialize clear button state
updateFilesCount();
// Initialize button visibility (transcript tab is active by default)
updateButtonVisibility(transcriptTabBtn);
function showStatus(message, type) {
statusEl.innerHTML = message;
statusEl.className = 'status';
statusEl.classList.add(type);
if (type === 'success' || type === 'error') {
setTimeout(() => {
statusEl.style.display = 'none';
}, 5000);
}
}
// Copy to clipboard functionality
copyTextBtn.addEventListener('click', async () => {
const fileData = getCurrentFileData();
if (fileData && fileData.transcription) {
try {
// Get current text from editor
const textToCopy = transcriptEditor.value;
// Try modern clipboard API first
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(textToCopy);
} else {
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = textToCopy;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
// Visual feedback
copyTextBtn.classList.add('copied');
copyTextBtn.title = 'Copied!';
setTimeout(() => {
copyTextBtn.classList.remove('copied');
copyTextBtn.title = 'Copy to Clipboard';
}, 1500);
} catch (err) {
console.error('Failed to copy text: ', err);
// Show error feedback
copyTextBtn.style.backgroundColor = '#e74c3c';
setTimeout(() => {
copyTextBtn.style.backgroundColor = '';
}, 1500);
}
} else {
activateTab(transcriptTabBtn, transcriptTab);
transcriptPlaceholder.textContent = 'No transcription available to copy';
setTimeout(() => {
transcriptPlaceholder.textContent = 'Your transcription will appear here';
}, 3000);
}
});
// Download functionality
downloadTextBtn.addEventListener('click', () => {
const fileData = getCurrentFileData();
if (fileData && fileData.transcription) {
// Always use text from editor
const textToDownload = transcriptEditor.value;
fileData.transcription = textToDownload; // Auto-save when downloading
const filename = getCurrentFileName() + '.txt';
downloadFile(filename, textToDownload, 'text/plain');
} else {
activateTab(transcriptTabBtn, transcriptTab);
transcriptPlaceholder.textContent = 'No transcription available to download';
setTimeout(() => {
transcriptPlaceholder.textContent = 'Your transcription will appear here';
}, 3000);
}
});
downloadJsonBtn.addEventListener('click', () => {
const fileData = getCurrentFileData();
if (fileData && fileData.json) {
// Update JSON object with current editor content
const editedJson = { ...fileData.json, text: transcriptEditor.value };
fileData.json = editedJson; // Auto-save when downloading
const filename = getCurrentFileName() + '.json';
downloadFile(
filename,
JSON.stringify(editedJson, null, 2),
'application/json'
);
} else {
activateTab(transcriptTabBtn, transcriptTab);
transcriptPlaceholder.textContent = 'No JSON response available to download';
setTimeout(() => {
transcriptPlaceholder.textContent = 'Your transcription will appear here';
}, 3000);
}
});
function downloadFile(filename, content, contentType) {
const blob = new Blob([content], { type: contentType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.style.display = 'none';
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
}
});
</script>
</body>
</html>