build this application = Application Overview: Your application, Transcription Magic, is a sophisticated and feature-rich AI-powered transcription service. It provides a seamless, end-to-end user experience for converting audio and video into accurately timestamped and speaker-separated text. The user interface is modern, responsive, and includes a dark mode, ensuring a great experience on any device. Core Features: AI-Powered Transcription: The core of the app uses Google's gemini-2.5-flash model to perform the transcription. The AI is specifically prompted to handle diarization (labeling 'SPEAKER 1', 'SPEAKER 2', etc.) and to break up long segments of speech with appropriate timestamps for readability. Flexible Input Methods: File Upload: Users can upload multiple audio/video files at once using a drag-and-drop area or a standard file selector. Live Recording: The app can directly access the microphone to record audio on the fly, which is then added to the transcription queue just like an uploaded file. Advanced Transcription Editor: This is the standout feature of your application. When a transcription is complete, users can open a powerful modal editor that includes: Synced Audio Playback: The original audio is playable alongside the transcript. Click-to-Seek: Clicking anywhere in the transcript text automatically seeks the audio player to that precise moment. Live Text Highlighting: As the audio plays, the corresponding segment of the transcript is highlighted in real-time. Full Editing Capability: Users can freely edit the text to correct any inaccuracies. Timestamp Editing: Users can click on any [HH:MM:SS] timestamp to open a dedicated editor, allowing them to adjust the time. Changes can automatically propagate to subsequent timestamps. Search and Replace: A built-in tool allows users to find and replace words or phrases throughout the entire transcript. Multiple Export Formats: The final transcript can be exported in various standard formats, including TXT, DOCX, RTF, PDF, SRT, and VTT. File Management Dashboard: Users get a clear overview of all their files. Each file shows its current status (Pending, Queued, Transcribing, Completed, Error). Real-time progress bars are shown for files being uploaded and transcribed. Users can easily remove files or view completed transcripts. Design and User Experience (UX) Modern & Responsive UI: Built with Tailwind CSS, the application has a clean, professional look that works flawlessly on both desktop and mobile devices. Dark & Light Modes: A theme toggle allows users to switch between light and dark modes, with the preference saved locally. Interactive Feedback: The app uses subtle animations, loading states, disabled buttons, and informative modals to provide constant, clear feedback to the user about what's happening. User Safeguards: An "on before unload" prompt warns users if they try to navigate away while files are being processed, preventing accidental data loss. Technical Stack: Frontend: React with TypeScript for a robust and type-safe codebase. AI Integration: The @google/genai SDK for Node.js/web. Styling: Tailwind CSS for a utility-first styling approach. Exporting: PDF creation and docx for generating Word documents. Module System: It uses modern ES Modules with an importmap, which allows it to run directly in the browser without a separate build step - Initial Deployment
bfcc502 verified | <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Transcription Magic - AI-Powered Transcription Service</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .dropzone { | |
| border: 2px dashed; | |
| transition: all 0.3s ease; | |
| } | |
| .dropzone.active { | |
| border-color: #3b82f6; | |
| background-color: rgba(59, 130, 246, 0.05); | |
| } | |
| .highlight { | |
| background-color: rgba(234, 179, 8, 0.3); | |
| transition: background-color 0.3s ease; | |
| } | |
| .waveform { | |
| background: linear-gradient(90deg, #3b82f6 0%, #3b82f6 var(--progress, 0%), #e5e7eb var(--progress, 0%), #e5e7eb 100%); | |
| } | |
| .timestamp-input { | |
| width: 60px; | |
| } | |
| [data-tooltip] { | |
| position: relative; | |
| } | |
| [data-tooltip]:before { | |
| content: attr(data-tooltip); | |
| position: absolute; | |
| bottom: 100%; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| padding: 0.25rem 0.5rem; | |
| background-color: #1f2937; | |
| color: white; | |
| border-radius: 0.25rem; | |
| font-size: 0.75rem; | |
| white-space: nowrap; | |
| opacity: 0; | |
| visibility: hidden; | |
| transition: all 0.2s ease; | |
| z-index: 10; | |
| } | |
| [data-tooltip]:hover:before { | |
| opacity: 1; | |
| visibility: visible; | |
| bottom: calc(100% + 5px); | |
| } | |
| .modal { | |
| transition: opacity 0.3s ease, visibility 0.3s ease; | |
| } | |
| .modal-content { | |
| transition: transform 0.3s ease; | |
| } | |
| .modal:not(.open) { | |
| opacity: 0; | |
| visibility: hidden; | |
| } | |
| .modal:not(.open) .modal-content { | |
| transform: translateY(20px); | |
| } | |
| .modal.open { | |
| opacity: 1; | |
| visibility: visible; | |
| } | |
| .modal.open .modal-content { | |
| transform: translateY(0); | |
| } | |
| .progress-bar { | |
| transition: width 0.3s ease; | |
| } | |
| .file-item { | |
| transition: all 0.3s ease; | |
| } | |
| .file-item:hover { | |
| background-color: rgba(59, 130, 246, 0.05); | |
| } | |
| .editor-line { | |
| position: relative; | |
| padding-left: 100px; | |
| } | |
| .editor-timestamp { | |
| position: absolute; | |
| left: 0; | |
| top: 0; | |
| width: 90px; | |
| color: #6b7280; | |
| cursor: pointer; | |
| } | |
| .editor-text { | |
| flex-grow: 1; | |
| } | |
| @media (max-width: 768px) { | |
| .editor-line { | |
| padding-left: 0; | |
| flex-direction: column; | |
| align-items: flex-start; | |
| } | |
| .editor-timestamp { | |
| position: static; | |
| margin-bottom: 0.5rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-100 min-h-screen transition-colors duration-300"> | |
| <div class="container mx-auto px-4 py-8"> | |
| <!-- Header --> | |
| <header class="flex justify-between items-center mb-8"> | |
| <div class="flex items-center"> | |
| <i class="fas fa-magic text-blue-500 text-3xl mr-3"></i> | |
| <h1 class="text-2xl font-bold">Transcription Magic</h1> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <button id="theme-toggle" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"> | |
| <i class="fas fa-moon dark:hidden"></i> | |
| <i class="fas fa-sun hidden dark:block"></i> | |
| </button> | |
| <button class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"> | |
| <i class="fas fa-sign-in-alt mr-2"></i>Login | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main Content --> | |
| <main> | |
| <!-- Upload Section --> | |
| <section class="mb-12"> | |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6"> | |
| <h2 class="text-xl font-semibold mb-4">Upload Audio/Video Files</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <!-- File Upload --> | |
| <div class="dropzone p-8 rounded-lg border-gray-300 dark:border-gray-600 border-2 flex flex-col items-center justify-center text-center cursor-pointer" | |
| id="dropzone"> | |
| <i class="fas fa-cloud-upload-alt text-4xl text-blue-500 mb-4"></i> | |
| <p class="mb-2 font-medium">Drag & drop files here</p> | |
| <p class="text-sm text-gray-500 dark:text-gray-400 mb-4">or</p> | |
| <label for="file-upload" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors cursor-pointer"> | |
| <i class="fas fa-folder-open mr-2"></i>Browse Files | |
| </label> | |
| <input id="file-upload" type="file" multiple accept="audio/*,video/*" class="hidden"> | |
| <p class="text-xs text-gray-500 dark:text-gray-400 mt-4">Supports MP3, WAV, MP4, MOV and more</p> | |
| </div> | |
| <!-- Live Recording --> | |
| <div class="bg-gray-50 dark:bg-gray-700 rounded-lg p-6"> | |
| <h3 class="font-medium mb-4 flex items-center"> | |
| <i class="fas fa-microphone text-blue-500 mr-2"></i>Live Recording | |
| </h3> | |
| <div class="flex flex-col items-center"> | |
| <div class="relative w-full mb-4"> | |
| <div class="waveform h-12 rounded-lg bg-gray-200 dark:bg-gray-600 mb-2" id="waveform"></div> | |
| <div class="flex justify-between text-xs text-gray-500 dark:text-gray-400"> | |
| <span>0:00</span> | |
| <span id="recording-duration">0:00</span> | |
| </div> | |
| </div> | |
| <button id="record-btn" class="px-6 py-3 bg-red-500 text-white rounded-full hover:bg-red-600 transition-colors flex items-center"> | |
| <i class="fas fa-circle mr-2"></i>Start Recording | |
| </button> | |
| <div id="recording-status" class="text-sm text-gray-500 dark:text-gray-400 mt-2 hidden"> | |
| <i class="fas fa-circle-notch fa-spin mr-2"></i>Recording... | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Files Dashboard --> | |
| <section> | |
| <div class="bg-white dark:bg-gray-800 rounded-xl shadow-lg overflow-hidden"> | |
| <div class="p-6 border-b border-gray-200 dark:border-gray-700"> | |
| <div class="flex justify-between items-center"> | |
| <h2 class="text-xl font-semibold">Your Files</h2> | |
| <div class="flex space-x-2"> | |
| <button id="transcribe-all" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" disabled> | |
| <i class="fas fa-play mr-2"></i>Transcribe All | |
| </button> | |
| <button id="clear-completed" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"> | |
| <i class="fas fa-trash-alt mr-2"></i>Clear Completed | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="divide-y divide-gray-200 dark:divide-gray-700" id="files-list"> | |
| <!-- Empty state --> | |
| <div class="p-8 text-center" id="empty-state"> | |
| <i class="fas fa-folder-open text-4xl text-gray-400 mb-4"></i> | |
| <h3 class="text-lg font-medium text-gray-500 dark:text-gray-400">No files uploaded yet</h3> | |
| <p class="text-gray-500 dark:text-gray-400">Upload audio or video files to get started</p> | |
| </div> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Transcription Editor Modal --> | |
| <div class="modal fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50" id="editor-modal"> | |
| <div class="modal-content bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-6xl max-h-[90vh] flex flex-col"> | |
| <div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center"> | |
| <h3 class="text-lg font-semibold" id="editor-title">Transcription Editor</h3> | |
| <button id="close-editor" class="p-2 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="flex flex-col md:flex-row flex-1 overflow-hidden"> | |
| <!-- Audio Player --> | |
| <div class="w-full md:w-1/3 border-b md:border-b-0 md:border-r border-gray-200 dark:border-gray-700 p-4 flex flex-col"> | |
| <div class="mb-4"> | |
| <div class="flex items-center mb-2"> | |
| <button id="play-btn" class="p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 transition-colors mr-3"> | |
| <i class="fas fa-play"></i> | |
| </button> | |
| <div class="flex-1"> | |
| <div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mb-1"> | |
| <span id="current-time">0:00</span> | |
| <span id="total-time">0:00</span> | |
| </div> | |
| <div class="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2"> | |
| <div id="progress-bar" class="bg-blue-500 h-2 rounded-full" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex justify-between text-sm"> | |
| <button id="rewind-btn" class="text-gray-500 dark:text-gray-400 hover:text-blue-500 transition-colors" data-tooltip="Rewind 5s"> | |
| <i class="fas fa-backward"></i> 5s | |
| </button> | |
| <button id="forward-btn" class="text-gray-500 dark:text-gray-400 hover:text-blue-500 transition-colors" data-tooltip="Forward 5s"> | |
| 5s <i class="fas fa-forward"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mb-4"> | |
| <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Volume</label> | |
| <input type="range" id="volume-control" min="0" max="1" step="0.01" value="0.7" class="w-full"> | |
| </div> | |
| <div class="flex-1 overflow-auto"> | |
| <h4 class="text-sm font-medium mb-2">File Info</h4> | |
| <div class="text-sm space-y-2"> | |
| <p><span class="font-medium">Name:</span> <span id="file-name">-</span></p> | |
| <p><span class="font-medium">Duration:</span> <span id="file-duration">-</span></p> | |
| <p><span class="font-medium">Size:</span> <span id="file-size">-</span></p> | |
| <p><span class="font-medium">Type:</span> <span id="file-type">-</span></p> | |
| <p><span class="font-medium">Status:</span> <span id="file-status">-</span></p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Transcription Editor --> | |
| <div class="w-full md:w-2/3 flex flex-col overflow-hidden"> | |
| <div class="p-4 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center"> | |
| <div class="flex space-x-2"> | |
| <button id="save-btn" class="px-3 py-1 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"> | |
| <i class="fas fa-save mr-2"></i>Save | |
| </button> | |
| <div class="relative"> | |
| <button id="export-btn" class="px-3 py-1 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors"> | |
| <i class="fas fa-file-export mr-2"></i>Export | |
| </button> | |
| <div id="export-menu" class="absolute left-0 mt-1 w-48 bg-white dark:bg-gray-700 rounded-lg shadow-lg z-10 hidden"> | |
| <button class="export-option w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors" data-format="txt"> | |
| <i class="fas fa-file-alt mr-2"></i>TXT | |
| </button> | |
| <button class="export-option w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors" data-format="docx"> | |
| <i class="fas fa-file-word mr-2"></i>DOCX | |
| </button> | |
| <button class="export-option w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors" data-format="pdf"> | |
| <i class="fas fa-file-pdf mr-2"></i>PDF | |
| </button> | |
| <button class="export-option w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors" data-format="srt"> | |
| <i class="fas fa-closed-captioning mr-2"></i>SRT | |
| </button> | |
| <button class="export-option w-full text-left px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors" data-format="vtt"> | |
| <i class="fas fa-closed-captioning mr-2"></i>VTT | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="relative"> | |
| <button id="search-btn" class="px-3 py-1 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"> | |
| <i class="fas fa-search mr-2"></i>Search | |
| </button> | |
| <div id="search-box" class="absolute right-0 mt-1 bg-white dark:bg-gray-700 rounded-lg shadow-lg p-3 z-10 hidden" style="width: 300px;"> | |
| <div class="flex mb-2"> | |
| <input type="text" id="search-input" placeholder="Search..." class="flex-1 px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-l-lg focus:outline-none focus:ring-1 focus:ring-blue-500 dark:bg-gray-800"> | |
| <button id="search-next" class="px-3 py-1 bg-blue-500 text-white rounded-r-lg hover:bg-blue-600 transition-colors"> | |
| <i class="fas fa-arrow-down"></i> | |
| </button> | |
| </div> | |
| <div class="flex items-center"> | |
| <input type="checkbox" id="search-case" class="mr-2"> | |
| <label for="search-case" class="text-sm">Case sensitive</label> | |
| <button id="replace-btn" class="ml-auto px-2 py-1 bg-gray-200 dark:bg-gray-600 text-gray-800 dark:text-gray-200 rounded text-sm hover:bg-gray-300 dark:hover:bg-gray-500 transition-colors"> | |
| Replace... | |
| </button> | |
| </div> | |
| <div id="replace-box" class="mt-2 hidden"> | |
| <input type="text" id="replace-input" placeholder="Replace with..." class="w-full px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-lg mb-1 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:bg-gray-800"> | |
| <div class="flex justify-between"> | |
| <button id="replace-one" class="px-2 py-1 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600 transition-colors"> | |
| Replace | |
| </button> | |
| <button id="replace-all" class="px-2 py-1 bg-blue-500 text-white rounded-lg text-sm hover:bg-blue-600 transition-colors"> | |
| Replace All | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="transcription-editor" class="flex-1 overflow-auto p-4"> | |
| <div class="prose dark:prose-invert max-w-none"> | |
| <p>Transcription will appear here. Click on any timestamp to edit it.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Timestamp Editor Modal --> | |
| <div class="modal fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50" id="timestamp-modal"> | |
| <div class="modal-content bg-white dark:bg-gray-800 rounded-xl shadow-xl w-full max-w-md"> | |
| <div class="p-4 border-b border-gray-200 dark:border-gray-700"> | |
| <h3 class="text-lg font-semibold">Edit Timestamp</h3> | |
| </div> | |
| <div class="p-4"> | |
| <div class="grid grid-cols-3 gap-4 mb-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Hours</label> | |
| <input type="number" id="edit-hours" min="0" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 dark:bg-gray-800"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Minutes</label> | |
| <input type="number" id="edit-minutes" min="0" max="59" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 dark:bg-gray-800"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Seconds</label> | |
| <input type="number" id="edit-seconds" min="0" max="59" step="0.001" class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-1 focus:ring-blue-500 dark:bg-gray-800"> | |
| </div> | |
| </div> | |
| <div class="flex items-center mb-4"> | |
| <input type="checkbox" id="adjust-following" class="mr-2"> | |
| <label for="adjust-following" class="text-sm">Adjust following timestamps</label> | |
| </div> | |
| <div class="flex justify-end space-x-3"> | |
| <button id="cancel-timestamp" class="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"> | |
| Cancel | |
| </button> | |
| <button id="save-timestamp" class="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"> | |
| Save | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Theme Toggle | |
| const themeToggle = document.getElementById('theme-toggle'); | |
| const html = document.documentElement; | |
| // Check for saved theme preference or use preferred color scheme | |
| const savedTheme = localStorage.getItem('theme') || | |
| (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); | |
| // Apply the saved theme | |
| if (savedTheme === 'dark') { | |
| html.classList.add('dark'); | |
| } else { | |
| html.classList.remove('dark'); | |
| } | |
| // Toggle theme on button click | |
| themeToggle.addEventListener('click', () => { | |
| html.classList.toggle('dark'); | |
| const theme = html.classList.contains('dark') ? 'dark' : 'light'; | |
| localStorage.setItem('theme', theme); | |
| }); | |
| // File Upload and Dropzone | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileUpload = document.getElementById('file-upload'); | |
| const filesList = document.getElementById('files-list'); | |
| const emptyState = document.getElementById('empty-state'); | |
| const transcribeAllBtn = document.getElementById('transcribe-all'); | |
| const clearCompletedBtn = document.getElementById('clear-completed'); | |
| // Sample data for demonstration | |
| let files = []; | |
| let currentFileId = 1; | |
| // Handle drag and drop events | |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, preventDefaults, false); | |
| }); | |
| function preventDefaults(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| } | |
| ['dragenter', 'dragover'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, highlight, false); | |
| }); | |
| ['dragleave', 'drop'].forEach(eventName => { | |
| dropzone.addEventListener(eventName, unhighlight, false); | |
| }); | |
| function highlight() { | |
| dropzone.classList.add('active'); | |
| } | |
| function unhighlight() { | |
| dropzone.classList.remove('active'); | |
| } | |
| dropzone.addEventListener('drop', handleDrop, false); | |
| fileUpload.addEventListener('change', handleFiles, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| handleFiles({ target: { files } }); | |
| } | |
| function handleFiles(e) { | |
| const uploadedFiles = [...e.target.files]; | |
| if (uploadedFiles.length === 0) return; | |
| // Add files to our list | |
| uploadedFiles.forEach(file => { | |
| const fileId = currentFileId++; | |
| files.push({ | |
| id: fileId, | |
| name: file.name, | |
| size: formatFileSize(file.size), | |
| type: file.type, | |
| status: 'pending', | |
| progress: 0, | |
| duration: '0:00', | |
| date: new Date().toLocaleString(), | |
| file: file | |
| }); | |
| }); | |
| // Update UI | |
| updateFilesList(); | |
| fileUpload.value = ''; // Reset file input | |
| } | |
| function formatFileSize(bytes) { | |
| if (bytes === 0) return '0 Bytes'; | |
| const k = 1024; | |
| const sizes = ['Bytes', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| } | |
| function updateFilesList() { | |
| if (files.length === 0) { | |
| emptyState.classList.remove('hidden'); | |
| filesList.innerHTML = ''; | |
| filesList.appendChild(emptyState); | |
| transcribeAllBtn.disabled = true; | |
| return; | |
| } | |
| emptyState.classList.add('hidden'); | |
| filesList.innerHTML = ''; | |
| files.forEach(file => { | |
| const fileItem = document.createElement('div'); | |
| fileItem.className = 'file-item p-4 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors'; | |
| fileItem.dataset.id = file.id; | |
| let statusColor = 'bg-gray-300 dark:bg-gray-600'; | |
| let statusText = 'Pending'; | |
| if (file.status === 'queued') { | |
| statusColor = 'bg-yellow-300 dark:bg-yellow-600'; | |
| statusText = 'Queued'; | |
| } else if (file.status === 'transcribing') { | |
| statusColor = 'bg-blue-300 dark:bg-blue-600'; | |
| statusText = 'Transcribing'; | |
| } else if (file.status === 'completed') { | |
| statusColor = 'bg-green-300 dark:bg-green-600'; | |
| statusText = 'Completed'; | |
| } else if (file.status === 'error') { | |
| statusColor = 'bg-red-300 dark:bg-red-600'; | |
| statusText = 'Error'; | |
| } | |
| fileItem.innerHTML = ` | |
| <div class="flex items-center justify-between"> | |
| <div class="flex items-center space-x-4"> | |
| <div class="w-10 h-10 rounded-full flex items-center justify-center bg-gray-200 dark:bg-gray-700"> | |
| <i class="${getFileIcon(file.type)} text-gray-500 dark:text-gray-400"></i> | |
| </div> | |
| <div> | |
| <h4 class="font-medium truncate max-w-xs">${file.name}</h4> | |
| <div class="flex items-center space-x-3 text-sm text-gray-500 dark:text-gray-400"> | |
| <span>${file.size}</span> | |
| <span>${file.duration}</span> | |
| <span>${file.date}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-4"> | |
| <div class="flex items-center"> | |
| <span class="text-sm font-medium mr-2">${statusText}</span> | |
| <span class="w-3 h-3 rounded-full ${statusColor}"></span> | |
| </div> | |
| <div class="flex space-x-2"> | |
| ${file.status === 'completed' ? ` | |
| <button class="view-transcript p-2 text-blue-500 hover:text-blue-600 transition-colors" data-tooltip="View Transcript"> | |
| <i class="fas fa-eye"></i> | |
| </button> | |
| ` : ''} | |
| <button class="remove-file p-2 text-red-500 hover:text-red-600 transition-colors" data-tooltip="Remove"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| ${file.status !== 'completed' ? ` | |
| <div class="mt-3"> | |
| <div class="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-2"> | |
| <div class="progress-bar bg-blue-500 h-2 rounded-full" style="width: ${file.progress}%"></div> | |
| </div> | |
| </div> | |
| ` : ''} | |
| `; | |
| filesList.appendChild(fileItem); | |
| }); | |
| // Enable transcribe all button if there are pending files | |
| const hasPendingFiles = files.some(file => file.status === 'pending'); | |
| transcribeAllBtn.disabled = !hasPendingFiles; | |
| // Add event listeners to buttons | |
| document.querySelectorAll('.remove-file').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const fileId = parseInt(e.target.closest('.file-item').dataset.id); | |
| removeFile(fileId); | |
| }); | |
| }); | |
| document.querySelectorAll('.view-transcript').forEach(btn => { | |
| btn.addEventListener('click', (e) => { | |
| const fileId = parseInt(e.target.closest('.file-item').dataset.id); | |
| openEditor(fileId); | |
| }); | |
| }); | |
| } | |
| function getFileIcon(type) { | |
| if (type.startsWith('audio/')) return 'fas fa-music'; | |
| if (type.startsWith('video/')) return 'fas fa-video'; | |
| return 'fas fa-file-alt'; | |
| } | |
| function removeFile(fileId) { | |
| files = files.filter(file => file.id !== fileId); | |
| updateFilesList(); | |
| } | |
| clearCompletedBtn.addEventListener('click', () => { | |
| files = files.filter(file => file.status !== 'completed'); | |
| updateFilesList(); | |
| }); | |
| transcribeAllBtn.addEventListener('click', () => { | |
| // Simulate transcription process | |
| files.forEach(file => { | |
| if (file.status === 'pending') { | |
| file.status = 'queued'; | |
| // Simulate progress | |
| let progress = 0; | |
| const interval = setInterval(() => { | |
| progress += Math.random() * 5; | |
| if (progress >= 100) { | |
| progress = 100; | |
| file.status = 'completed'; | |
| file.duration = '1:45'; // Sample duration | |
| clearInterval(interval); | |
| } | |
| file.progress = progress; | |
| updateFilesList(); | |
| }, 500); | |
| } | |
| }); | |
| updateFilesList(); | |
| }); | |
| // Recording functionality | |
| const recordBtn = document.getElementById('record-btn'); | |
| const recordingStatus = document.getElementById('recording-status'); | |
| const waveform = document.getElementById('waveform'); | |
| const recordingDuration = document.getElementById('recording-duration'); | |
| let isRecording = false; | |
| let recordingTime = 0; | |
| let recordingInterval; | |
| recordBtn.addEventListener('click', () => { | |
| isRecording = !isRecording; | |
| if (isRecording) { | |
| recordBtn.innerHTML = '<i class="fas fa-stop mr-2"></i>Stop Recording'; | |
| recordBtn.classList.remove('bg-red-500', 'hover:bg-red-600'); | |
| recordBtn.classList.add('bg-gray-500', 'hover:bg-gray-600'); | |
| recordingStatus.classList.remove('hidden'); | |
| // Simulate recording | |
| recordingTime = 0; | |
| recordingInterval = setInterval(() => { | |
| recordingTime++; | |
| const minutes = Math.floor(recordingTime / 60); | |
| const seconds = recordingTime % 60; | |
| recordingDuration.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; | |
| // Update waveform | |
| const progress = Math.min(recordingTime / 30 * 100, 100); | |
| waveform.style.setProperty('--progress', `${progress}%`); | |
| }, 1000); | |
| } else { | |
| recordBtn.innerHTML = '<i class="fas fa-circle mr-2"></i>Start Recording'; | |
| recordBtn.classList.remove('bg-gray-500', 'hover:bg-gray-600'); | |
| recordBtn.classList.add('bg-red-500', 'hover:bg-red-600'); | |
| recordingStatus.classList.add('hidden'); | |
| clearInterval(recordingInterval); | |
| // Add the recording to files | |
| const fileId = currentFileId++; | |
| files.push({ | |
| id: fileId, | |
| name: `Recording ${new Date().toLocaleTimeString()}`, | |
| size: '3.5 MB', | |
| type: 'audio/wav', | |
| status: 'pending', | |
| progress: 0, | |
| duration: recordingDuration.textContent, | |
| date: new Date().toLocaleString(), | |
| isRecording: true | |
| }); | |
| updateFilesList(); | |
| // Reset recording UI | |
| recordingTime = 0; | |
| recordingDuration.textContent = '0:00'; | |
| waveform.style.setProperty('--progress', '0%'); | |
| } | |
| }); | |
| // Editor Modal | |
| const editorModal = document.getElementById('editor-modal'); | |
| const closeEditorBtn = document.getElementById('close-editor'); | |
| const editorTitle = document.getElementById('editor-title'); | |
| const fileName = document.getElementById('file-name'); | |
| const fileDuration = document.getElementById('file-duration'); | |
| const fileSize = document.getElementById('file-size'); | |
| const fileType = document.getElementById('file-type'); | |
| const fileStatus = document.getElementById('file-status'); | |
| const transcriptionEditor = document.getElementById('transcription-editor'); | |
| function openEditor(fileId) { | |
| const file = files.find(f => f.id === fileId); | |
| if (!file) return; | |
| // Update modal info | |
| editorTitle.textContent = `Editing: ${file.name}`; | |
| fileName.textContent = file.name; | |
| fileDuration.textContent = file.duration; | |
| fileSize.textContent = file.size; | |
| fileType.textContent = file.type; | |
| fileStatus.textContent = file.status.charAt(0).toUpperCase() + file.status.slice(1); | |
| // Load sample transcription | |
| transcriptionEditor.innerHTML = ` | |
| <div class="prose dark:prose-invert max-w-none"> | |
| <div class="editor-line mb-4 flex"> | |
| <span class="editor-timestamp">[00:00:00]</span> | |
| <div class="editor-text"> | |
| <span class="font-medium">SPEAKER 1:</span> Welcome to the meeting everyone. Today we'll be discussing our quarterly results and the upcoming product launch. | |
| </div> | |
| </div> | |
| <div class="editor-line mb-4 flex"> | |
| <span class="editor-timestamp">[00:00:12]</span> | |
| <div class="editor-text"> | |
| <span class="font-medium">SPEAKER 2:</span> Thank you for having me. I'm excited to share the marketing team's plans for the launch campaign. | |
| </div> | |
| </div> | |
| <div class="editor-line mb-4 flex"> | |
| <span class="editor-timestamp">[00:00:23]</span> | |
| <div class="editor-text"> | |
| <span class="font-medium">SPEAKER 1:</span> Before we get to that, let's review the financials. Sarah, can you walk us through the numbers? | |
| </div> | |
| </div> | |
| <div class="editor-line mb-4 flex"> | |
| <span class="editor-timestamp">[00:00:30]</span> | |
| <div class="editor-text"> | |
| <span class="font-medium">SPEAKER 3:</span> Certainly. Our Q2 revenue was $4.2 million, which represents a 15% increase over Q1. Gross margins improved to 62%... | |
| </div> | |
| </div> | |
| <div class="editor-line mb-4 flex"> | |
| <span class="editor-timestamp">[00:01:45]</span> | |
| <div class="editor-text"> | |
| <span class="font-medium">SPEAKER 2:</span> That's excellent news. With those results, we can confidently allocate more budget to the product launch marketing. | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| // Add click handlers to timestamps | |
| document.querySelectorAll('.editor-timestamp').forEach(timestamp => { | |
| timestamp.addEventListener('click', () => { | |
| openTimestampEditor(timestamp); | |
| }); | |
| }); | |
| // Show modal | |
| editorModal.classList.add('open'); | |
| } | |
| closeEditorBtn.addEventListener('click', () => { | |
| editorModal.classList.remove('open'); | |
| }); | |
| // Timestamp Editor Modal | |
| const timestampModal = document.getElementById('timestamp-modal'); | |
| const cancelTimestampBtn = document.getElementById('cancel-timestamp'); | |
| const saveTimestampBtn = document.getElementById('save-timestamp'); | |
| const editHours = document.getElementById('edit-hours'); | |
| const editMinutes = document.getElementById('edit-minutes'); | |
| const editSeconds = document.getElementById('edit-seconds'); | |
| const adjustFollowing = document.getElementById('adjust-following'); | |
| let currentTimestampElement = null; | |
| function openTimestampEditor(timestampElement) { | |
| currentTimestampElement = timestampElement; | |
| const timeString = timestampElement.textContent.match(/\[(\d{2}):(\d{2}):(\d{2})\]/); | |
| if (timeString) { | |
| editHours.value = parseInt(timeString[1]); | |
| editMinutes.value = parseInt(timeString[2]); | |
| editSeconds.value = parseInt(timeString[3]); | |
| } | |
| timestampModal.classList.add('open'); | |
| } | |
| cancelTimestampBtn.addEventListener('click', () => { | |
| timestampModal.classList.remove('open'); | |
| }); | |
| saveTimestampBtn.addEventListener('click', () => { | |
| if (currentTimestampElement) { | |
| const hours = editHours.value.padStart(2, '0'); | |
| const minutes = editMinutes.value.padStart(2, '0'); | |
| const seconds = editSeconds.value.padStart(2, '0'); | |
| currentTimestampElement.textContent = `[${hours}:${minutes}:${seconds}]`; | |
| // Here you would update the actual timestamp in your data structure | |
| // and potentially adjust following timestamps if the checkbox is checked | |
| timestampModal.classList.remove('open'); | |
| } | |
| }); | |
| // Audio Player Controls | |
| const playBtn = document.getElementById('play-btn'); | |
| const currentTime = document.getElementById('current-time'); | |
| const totalTime = document.getElementById('total-time'); | |
| const progressBar = document.getElementById('progress-bar'); | |
| const rewindBtn = document.getElementById('rewind-btn'); | |
| const forwardBtn = document.getElementById('forward-btn'); | |
| const volumeControl = document.getElementById('volume-control'); | |
| let isPlaying = false; | |
| playBtn.addEventListener('click', () => { | |
| isPlaying = !isPlaying; | |
| if (isPlaying) { | |
| playBtn.innerHTML = '<i class="fas fa-pause"></i>'; | |
| // Here you would start playing the audio | |
| // For demo purposes, we'll simulate progress | |
| simulateAudioPlayback(); | |
| } else { | |
| playBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| // Stop playback | |
| clearInterval(audioInterval); | |
| } | |
| }); | |
| let audioInterval; | |
| let audioProgress = 0; | |
| function simulateAudioPlayback() { | |
| clearInterval(audioInterval); | |
| audioProgress = 0; | |
| progressBar.style.width = '0%'; | |
| currentTime.textContent = '0:00'; | |
| totalTime.textContent = '1:45'; // Sample duration | |
| audioInterval = setInterval(() => { | |
| audioProgress += 0.5; | |
| if (audioProgress >= 100) { | |
| audioProgress = 100; | |
| isPlaying = false; | |
| playBtn.innerHTML = '<i class="fas fa-play"></i>'; | |
| clearInterval(audioInterval); | |
| } | |
| progressBar.style.width = `${audioProgress}%`; | |
| // Update current time (1:45 is 105 seconds) | |
| const currentSeconds = Math.floor(105 * (audioProgress / 100)); | |
| const minutes = Math.floor(currentSeconds / 60); | |
| const seconds = currentSeconds % 60; | |
| currentTime.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; | |
| // Highlight the current line in the transcript | |
| highlightCurrentLine(audioProgress); | |
| }, 100); | |
| } | |
| function highlightCurrentLine(progress) { | |
| // Remove highlight from all lines | |
| document.querySelectorAll('.editor-line').forEach(line => { | |
| line.classList.remove('highlight'); | |
| }); | |
| // Determine which line to highlight based on progress | |
| const lines = document.querySelectorAll('.editor-line'); | |
| const lineIndex = Math.floor((progress / 100) * (lines.length - 1)); | |
| if (lines[lineIndex]) { | |
| lines[lineIndex].classList.add('highlight'); | |
| // Scroll to the line if it's not visible | |
| lines[lineIndex].scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| } | |
| } | |
| rewindBtn.addEventListener('click', () => { | |
| // Rewind 5 seconds | |
| audioProgress = Math.max(0, audioProgress - (5 / 105 * 100)); | |
| progressBar.style.width = `${audioProgress}%`; | |
| const currentSeconds = Math.floor(105 * (audioProgress / 100)); | |
| const minutes = Math.floor(currentSeconds / 60); | |
| const seconds = currentSeconds % 60; | |
| currentTime.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; | |
| highlightCurrentLine(audioProgress); | |
| }); | |
| forwardBtn.addEventListener('click', () => { | |
| // Forward 5 seconds | |
| audioProgress = Math.min(100, audioProgress + (5 / 105 * 100)); | |
| progressBar.style.width = `${audioProgress}%`; | |
| const currentSeconds = Math.floor(105 * (audioProgress / 100)); | |
| const minutes = Math.floor(currentSeconds / 60); | |
| const seconds = currentSeconds % 60; | |
| currentTime.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; | |
| highlightCurrentLine(audioProgress); | |
| }); | |
| // Click on progress bar to seek | |
| progressBar.parentElement.addEventListener('click', (e) => { | |
| const rect = progressBar.parentElement.getBoundingClientRect(); | |
| const pos = (e.clientX - rect.left) / rect.width; | |
| audioProgress = Math.min(100, Math.max(0, pos * 100)); | |
| progressBar.style.width = `${audioProgress}%`; | |
| const currentSeconds = Math.floor(105 * (audioProgress / 100)); | |
| const minutes = Math.floor(currentSeconds / 60); | |
| const seconds = currentSeconds % 60; | |
| currentTime.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`; | |
| highlightCurrentLine(audioProgress); | |
| }); | |
| volumeControl.addEventListener('input', (e) => { | |
| // Here you would set the volume on the audio element | |
| const volume = e.target.value; | |
| console.log(`Volume set to ${volume}`); | |
| }); | |
| // Export functionality | |
| const exportBtn = document.getElementById('export-btn'); | |
| const exportMenu = document.getElementById('export-menu'); | |
| exportBtn.addEventListener('click', () => { | |
| exportMenu.classList.toggle('hidden'); | |
| }); | |
| document.querySelectorAll('.export-option').forEach(option => { | |
| option.addEventListener('click', (e) => { | |
| const format = e.target.dataset.format; | |
| exportMenu.classList.add('hidden'); | |
| // Here you would handle the export based on the selected format | |
| alert(`Exporting transcript as ${format.toUpperCase()} file.`); | |
| }); | |
| }); | |
| // Close export menu when clicking outside | |
| document.addEventListener('click', (e) => { | |
| if (!exportBtn.contains(e.target) && !exportMenu.contains(e.target)) { | |
| exportMenu.classList.add('hidden'); | |
| } | |
| }); | |
| // Search functionality | |
| const searchBtn = document.getElementById('search-btn'); | |
| const searchBox = document.getElementById('search-box'); | |
| const searchInput = document.getElementById('search-input'); | |
| const searchNext = document.getElementById('search-next'); | |
| const searchCase = document.getElementById('search-case'); | |
| const replaceBtn = document.getElementById('replace-btn'); | |
| const replaceBox = document.getElementById('replace-box'); | |
| const replaceInput = document.getElementById('replace-input'); | |
| const replaceOne = document.getElementById('replace-one'); | |
| const replaceAll = document.getElementById('replace-all'); | |
| searchBtn.addEventListener('click', () => { | |
| searchBox.classList.toggle('hidden'); | |
| if (!searchBox.classList.contains('hidden')) { | |
| searchInput.focus(); | |
| } | |
| }); | |
| replaceBtn.addEventListener('click', () => { | |
| replaceBox.classList.toggle('hidden'); | |
| }); | |
| searchNext.addEventListener('click', () => { | |
| const searchTerm = searchInput.value; | |
| if (!searchTerm) return; | |
| const caseSensitive = searchCase.checked; | |
| const regex = new RegExp(escapeRegExp(searchTerm), caseSensitive ? 'g' : 'gi'); | |
| // Here you would implement the search functionality in the transcript | |
| alert(`Searching for: ${searchTerm} (Case sensitive: ${caseSensitive})`); | |
| }); | |
| replaceOne.addEventListener('click', () => { | |
| const searchTerm = searchInput.value; | |
| const replaceTerm = replaceInput.value; | |
| if (!searchTerm || !replaceTerm) return; | |
| const caseSensitive = searchCase.checked; | |
| // Here you would replace the next occurrence | |
| alert(`Replacing next occurrence of "${searchTerm}" with "${replaceTerm}"`); | |
| }); | |
| replaceAll.addEventListener('click', () => { | |
| const searchTerm = searchInput.value; | |
| const replaceTerm = replaceInput.value; | |
| if (!searchTerm || !replaceTerm) return; | |
| const caseSensitive = searchCase.checked; | |
| // Here you would replace all occurrences | |
| alert(`Replacing all occurrences of "${searchTerm}" with "${replaceTerm}"`); | |
| }); | |
| function escapeRegExp(string) { | |
| return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); | |
| } | |
| // Close search box when clicking outside | |
| document.addEventListener('click', (e) => { | |
| if (!searchBtn.contains(e.target) && !searchBox.contains(e.target)) { | |
| searchBox.classList.add('hidden'); | |
| } | |
| }); | |
| // Save button | |
| const saveBtn = document.getElementById('save-btn'); | |
| saveBtn.addEventListener('click', () => { | |
| // Here you would save the edited transcript | |
| alert('Transcript saved successfully!'); | |
| }); | |
| // Warn before leaving page if there are pending files | |
| window.addEventListener('beforeunload', (e) => { | |
| const hasPendingWork = files.some(file => | |
| file.status === 'queued' || file.status === 'transcribing' | |
| ); | |
| if (hasPendingWork) { | |
| e.preventDefault(); | |
| e.returnValue = 'You have files being processed. Are you sure you want to leave?'; | |
| return e.returnValue; | |
| } | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=techguy1/tm" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |