undefined / index.html
Escapingmatrixtoday's picture
DEEPSEEK V3 — PATCH + MASTER FIX: Elite Transcript AI
cc90b15 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VerboseWhisper - Elite Transcript AI</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script>
<style>
.transcript-container {
scrollbar-width: thin;
scrollbar-color: #4f46e5 #e5e7eb;
}
.transcript-container::-webkit-scrollbar {
width: 8px;
}
.transcript-container::-webkit-scrollbar-track {
background: #e5e7eb;
}
.transcript-container::-webkit-scrollbar-thumb {
background-color: #4f46e5;
border-radius: 4px;
}
.word-timestamp {
transition: all 0.2s ease;
}
.segment:hover .word-timestamp {
opacity: 1;
transform: translateY(0);
}
.fullscreen-transcript {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
background: white;
padding: 2rem;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<!-- Header -->
<div class="text-center mb-12">
<h1 class="text-4xl font-bold text-indigo-600 mb-2">VerboseWhisper</h1>
<p class="text-xl text-gray-600">Elite AI-powered transcription for YouTube & TikTok</p>
</div>
<!-- Main Content -->
<div class="flex flex-col lg:flex-row gap-8">
<!-- Input Panel -->
<div class="w-full lg:w-1/3 bg-white rounded-xl shadow-md p-6 sticky top-4">
<div class="mb-6">
<label for="video-url" class="block text-sm font-medium text-gray-700 mb-2">Video URL</label>
<div class="flex">
<input
type="text"
id="video-url"
placeholder="Paste YouTube or TikTok URL here..."
class="flex-1 min-w-0 block w-full px-3 py-2 rounded-l-md border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
>
<button
id="transcribe-btn"
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-r-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<span>Transcribe</span>
<i data-feather="mic" class="ml-2"></i>
</button>
</div>
</div>
<div class="mb-6">
<label class="block text-sm font-medium text-gray-700 mb-2">Options</label>
<div class="space-y-3">
<div class="flex items-center">
<input id="autopunct" name="autopunct" type="checkbox" class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="autopunct" class="ml-2 block text-sm text-gray-700">Auto-punctuate</label>
</div>
<div class="flex items-center">
<input id="preserve-fillers" name="preserve-fillers" type="checkbox" checked class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="preserve-fillers" class="ml-2 block text-sm text-gray-700">Preserve fillers (um, ah)</label>
</div>
<div class="flex items-center">
<input id="word-timestamps" name="word-timestamps" type="checkbox" checked class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="word-timestamps" class="ml-2 block text-sm text-gray-700">Word-level timestamps</label>
</div>
</div>
</div>
<div class="bg-gray-100 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h3 class="text-sm font-medium text-gray-700">Status</h3>
<span id="status-indicator" class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-200 text-gray-800">
Idle
</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2.5">
<div id="progress-bar" class="bg-indigo-600 h-2.5 rounded-full" style="width: 0%"></div>
</div>
<p id="status-detail" class="mt-2 text-xs text-gray-600">Ready to transcribe</p>
</div>
</div>
<!-- Transcript Panel -->
<div id="transcript-container" class="w-full lg:w-2/3 bg-white rounded-xl shadow-md p-6 min-h-[70vh] max-h-[85vh] overflow-y-auto transcript-container relative">
<div id="loading-indicator" class="absolute inset-0 bg-white bg-opacity-80 z-10 flex items-center justify-center hidden">
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
</div>
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-medium text-gray-900">Transcript</h2>
<div class="flex space-x-2">
<button id="increase-font" class="p-1 rounded hover:bg-gray-100">
<i data-feather="plus" class="w-4 h-4 text-gray-600"></i>
</button>
<button id="decrease-font" class="p-1 rounded hover:bg-gray-100">
<i data-feather="minus" class="w-4 h-4 text-gray-600"></i>
</button>
<button id="toggle-wrap" class="p-1 rounded hover:bg-gray-100">
<i data-feather="align-left" class="w-4 h-4 text-gray-600"></i>
</button>
<button id="fullscreen-btn" class="p-1 rounded hover:bg-gray-100">
<i data-feather="maximize" class="w-4 h-4 text-gray-600"></i>
</button>
<button id="copy-all" class="p-1 rounded hover:bg-gray-100">
<i data-feather="copy" class="w-4 h-4 text-gray-600"></i>
</button>
<button id="export-btn" class="p-1 rounded hover:bg-gray-100">
<i data-feather="download" class="w-4 h-4 text-gray-600"></i>
</button>
</div>
</div>
<div id="transcript-content" class="font-mono text-sm">
<p class="text-gray-400 italic">Transcript will appear here...</p>
</div>
</div>
</div>
</div>
<!-- Export Modal -->
<div id="export-modal" class="fixed inset-0 bg-black bg-opacity-50 z-50 hidden">
<div class="flex items-center justify-center min-h-screen">
<div class="bg-white rounded-lg shadow-xl p-6 w-full max-w-md">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900">Export Transcript</h3>
<button id="close-export-modal" class="text-gray-400 hover:text-gray-500">
<i data-feather="x" class="w-5 h-5"></i>
</button>
</div>
<div class="space-y-2">
<button class="export-option w-full flex items-center justify-between px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50">
<span>Plain Text (.txt)</span>
<i data-feather="file-text" class="w-4 h-4"></i>
</button>
<button class="export-option w-full flex items-center justify-between px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50">
<span>SubRip Subtitles (.srt)</span>
<i data-feather="file-text" class="w-4 h-4"></i>
</button>
<button class="export-option w-full flex items-center justify-between px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50">
<span>WebVTT (.vtt)</span>
<i data-feather="file-text" class="w-4 h-4"></i>
</button>
<button class="export-option w-full flex items-center justify-between px-4 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 hover:bg-gray-50">
<span>Word Document (.docx)</span>
<i data-feather="file-text" class="w-4 h-4"></i>
</button>
</div>
</div>
</div>
</div>
<script>
feather.replace();
// Constants
const POLL_INTERVAL = 2000;
const MAX_RETRIES = 3;
const RETRY_DELAY = 1000;
// DOM Elements
const transcribeBtn = document.getElementById('transcribe-btn');
const loadingIndicator = document.getElementById('loading-indicator');
const videoUrlInput = document.getElementById('video-url');
const transciptContent = document.getElementById('transcript-content');
const transciptContainer = document.getElementById('transcript-container');
const statusIndicator = document.getElementById('status-indicator');
const statusDetail = document.getElementById('status-detail');
const progressBar = document.getElementById('progress-bar');
const fullscreenBtn = document.getElementById('fullscreen-btn');
const increaseFontBtn = document.getElementById('increase-font');
const decreaseFontBtn = document.getElementById('decrease-font');
const toggleWrapBtn = document.getElementById('toggle-wrap');
const copyAllBtn = document.getElementById('copy-all');
const exportBtn = document.getElementById('export-btn');
const exportModal = document.getElementById('export-modal');
const closeExportModal = document.getElementById('close-export-modal');
const exportOptions = document.querySelectorAll('.export-option');
// State
let isFullscreen = false;
let fontSize = 14;
let isWrapped = false;
let currentJobId = null;
let eventSource = null;
// Event Listeners
transcribeBtn.addEventListener('click', handleTranscribe);
fullscreenBtn.addEventListener('click', toggleFullscreen);
increaseFontBtn.addEventListener('click', () => adjustFontSize(1));
decreaseFontBtn.addEventListener('click', () => adjustFontSize(-1));
toggleWrapBtn.addEventListener('click', toggleTextWrap);
copyAllBtn.addEventListener('click', copyTranscript);
exportBtn.addEventListener('click', () => exportModal.classList.remove('hidden'));
closeExportModal.addEventListener('click', () => exportModal.classList.add('hidden'));
exportOptions.forEach(option => option.addEventListener('click', handleExport));
// Functions
// URL validation
function isValidVideoUrl(url) {
try {
const parsed = new URL(url);
const host = parsed.hostname;
const path = parsed.pathname;
// YouTube patterns
const ytPatterns = [
/youtube\.com\/watch\?v=/,
/youtu\.be\//,
/youtube\.com\/shorts\//,
/youtube\.com\/live\//
];
// TikTok patterns
const tiktokPatterns = [
/tiktok\.com\/@.+\/video\//,
/tiktok\.com\/t\/\w+/,
/vm\.tiktok\.com\/\w+/,
/vt\.tiktok\.com\/\w+/
];
return ytPatterns.some(p => p.test(url)) ||
tiktokPatterns.some(p => p.test(url));
} catch {
return false;
}
}
function handleTranscribe() {
const url = videoUrlInput.value.trim();
if (!url) {
showError("Please enter a YouTube or TikTok URL");
return;
}
if (!isValidVideoUrl(url)) {
showError("Please enter a valid YouTube or TikTok URL");
return;
}
// Disable button during processing
transcribeBtn.disabled = true;
transcribeBtn.innerHTML = '<span>Processing</span><i data-feather="loader" class="ml-2 animate-spin"></i>';
feather.replace();
// Reset transcript
transciptContent.innerHTML = '<p class="text-gray-400 italic">Processing transcription...</p>';
loadingIndicator.classList.remove('hidden');
// Show status
updateStatus('queued', 'Waiting in queue...', 0);
// Make API call
fetch('/transcribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: url,
options: {
autopunct: document.getElementById('autopunct').checked,
preserve_fillers: document.getElementById('preserve-fillers').checked,
word_timestamps: document.getElementById('word-timestamps').checked,
chunk_sec: 60
}
})
})
.then(response => {
if (response.status === 202) {
return response.json().then(data => {
currentJobId = data.job_id;
startPolling(data.job_id);
});
} else if (response.status === 200) {
return response.json().then(data => {
updateTranscript(data);
loadingIndicator.classList.add('hidden');
transcribeBtn.disabled = false;
transcribeBtn.innerHTML = '<span>Transcribe</span><i data-feather="mic" class="ml-2"></i>';
feather.replace();
});
} else {
throw new Error('Failed to start transcription');
}
})
.catch(error => {
showError("Failed to start transcription: " + error.message);
loadingIndicator.classList.add('hidden');
transcribeBtn.disabled = false;
transcribeBtn.innerHTML = '<span>Transcribe</span><i data-feather="mic" class="ml-2"></i>';
feather.replace();
});
// In real implementation:
// fetch('/transcribe', {
// method: 'POST',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify({
// url: url,
// options: {
// autopunct: document.getElementById('autopunct').checked,
// preserve_fillers: document.getElementById('preserve-fillers').checked,
// word_timestamps: document.getElementById('word-timestamps').checked
// }
// })
// })
// .then(response => response.json())
// .then(data => {
// currentJobId = data.job_id;
// startPollingOrSSE(data.job_id);
// })
// .catch(error => {
// showError("Failed to start transcription: " + error.message);
// transcribeBtn.disabled = false;
// transcribeBtn.innerHTML = '<span>Transcribe</span><i data-feather="mic" class="ml-2"></i>';
// feather.replace();
// });
}
function startPolling(jobId) {
let retryCount = 0;
const poll = () => {
fetch(`/transcribe/${jobId}`)
.then(response => {
if (!response.ok) throw new Error('Polling failed');
return response.json();
})
.then(data => {
if (data.status === 'complete') {
updateTranscript(data.result);
loadingIndicator.classList.add('hidden');
transcribeBtn.disabled = false;
transcribeBtn.innerHTML = '<span>Transcribe</span><i data-feather="mic" class="ml-2"></i>';
feather.replace();
} else if (data.status === 'failed') {
showError("Transcription failed: " + (data.error || 'Unknown error'));
loadingIndicator.classList.add('hidden');
transcribeBtn.disabled = false;
transcribeBtn.innerHTML = '<span>Transcribe</span><i data-feather="mic" class="ml-2"></i>';
feather.replace();
} else {
// Update progress
updateStatus(data.status, data.progress?.stage || 'Processing', data.progress?.percent || 0);
// Update partial results if available
if (data.partial_results) {
updateTranscript({
segments: data.partial_results,
is_partial: true
});
}
// Continue polling
setTimeout(poll, POLL_INTERVAL);
}
})
.catch(error => {
if (retryCount < MAX_RETRIES) {
retryCount++;
setTimeout(poll, RETRY_DELAY * retryCount);
} else {
showError("Failed to get transcription status: " + error.message);
loadingIndicator.classList.add('hidden');
transcribeBtn.disabled = false;
transcribeBtn.innerHTML = '<span>Transcribe</span><i data-feather="mic" class="ml-2"></i>';
feather.replace();
}
});
};
poll();
}
function updateStatus(status, detail, percent) {
statusDetail.textContent = detail;
progressBar.style.width = percent + '%';
let bgColor = 'bg-gray-200';
let textColor = 'text-gray-800';
switch(status) {
case 'queued':
bgColor = 'bg-yellow-100';
textColor = 'text-yellow-800';
break;
case 'processing':
bgColor = 'bg-blue-100';
textColor = 'text-blue-800';
break;
case 'complete':
bgColor = 'bg-green-100';
textColor = 'text-green-800';
break;
case 'failed':
bgColor = 'bg-red-100';
textColor = 'text-red-800';
break;
}
statusIndicator.className = `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${bgColor} ${textColor}`;
statusIndicator.textContent = status.charAt(0).toUpperCase() + status.slice(1);
}
function updateTranscript(data) {
if (!data.segments || data.segments.length === 0) return;
let html = '';
data.segments.forEach(segment => {
const confidence = segment.words?.[0]?.conf || 1.0;
const confidencePercent = Math.round(confidence * 100);
const confidenceColor = confidence > 0.9 ? 'text-green-600' :
confidence > 0.7 ? 'text-yellow-600' : 'text-red-600';
html += `
<div class="segment mb-4 pb-2 border-b border-gray-100">
<div class="flex justify-between items-start">
<span class="text-xs font-mono text-gray-500">
${formatTime(segment.start)}${formatTime(segment.end)}
</span>
<span class="text-xs ${confidenceColor}">
${confidencePercent}% conf
</span>
</div>
<p class="mt-1 text-gray-800 ${isWrapped ? 'whitespace-pre-wrap' : 'whitespace-pre'}">
${segment.text}
</p>
${segment.words ? `
<div class="mt-1 flex flex-wrap gap-1">
${segment.words.map(word => `
<span class="word-timestamp relative group">
<span class="text-gray-700 hover:text-indigo-600 cursor-pointer">
${word.w}
</span>
<span class="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-1 px-2 py-1 text-xs text-white bg-gray-900 rounded opacity-0 group-hover:opacity-100 transition-opacity">
${formatTime(word.s)}s
</span>
</span>
`).join('')}
</div>
` : ''}
</div>
`;
});
if (data.is_partial) {
transciptContent.innerHTML += html;
} else {
transciptContent.innerHTML = html;
}
// Auto-scroll to bottom if new content is added
if (data.is_partial) {
transciptContainer.scrollTop = transciptContainer.scrollHeight;
}
}
// Debounce the transcribe button
transcribeBtn.addEventListener('click', debounce(handleTranscribe, 1000));
function debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
</script>
</body>
</html>