| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>HD TTS Document Reader | Natural Human Voices</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> |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
| |
| body { |
| font-family: 'Inter', sans-serif; |
| background-color: #f8fafc; |
| } |
| |
| .waveform { |
| background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 50%, #ec4899 100%); |
| height: 60px; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| padding: 0 10px; |
| border-radius: 8px; |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .waveform-bar { |
| width: 3px; |
| background-color: rgba(255, 255, 255, 0.7); |
| border-radius: 3px; |
| animation: equalize 1.5s infinite ease-in-out; |
| animation-delay: calc(var(--delay) * 0.1s); |
| } |
| |
| @keyframes equalize { |
| 0%, 100% { height: 10px; } |
| 50% { height: 30px; } |
| } |
| |
| .document-container { |
| max-height: 400px; |
| overflow-y: auto; |
| scrollbar-width: thin; |
| } |
| |
| .document-container::-webkit-scrollbar { |
| width: 6px; |
| } |
| |
| .document-container::-webkit-scrollbar-track { |
| background: #f1f5f9; |
| } |
| |
| .document-container::-webkit-scrollbar-thumb { |
| background-color: #cbd5e1; |
| border-radius: 20px; |
| } |
| |
| .highlight { |
| background-color: #fef08a; |
| transition: background-color 0.3s ease; |
| } |
| |
| .voice-option:hover { |
| transform: scale(1.03); |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
| } |
| |
| .progress-bar { |
| height: 4px; |
| background-color: #e2e8f0; |
| border-radius: 2px; |
| overflow: hidden; |
| } |
| |
| .progress-fill { |
| height: 100%; |
| background-color: #3b82f6; |
| width: 0%; |
| transition: width 0.1s linear; |
| } |
| |
| .settings-panel { |
| transition: all 0.3s ease; |
| max-height: 0; |
| overflow: hidden; |
| } |
| |
| .settings-panel.open { |
| max-height: 500px; |
| } |
| |
| .tab-active { |
| border-bottom: 2px solid #3b82f6; |
| color: #3b82f6; |
| font-weight: 500; |
| } |
| |
| .language-flag { |
| width: 24px; |
| height: 16px; |
| display: inline-block; |
| margin-right: 8px; |
| background-size: cover; |
| border-radius: 2px; |
| } |
| |
| |
| .flag-us { |
| background-image: url('https://flagcdn.com/us.svg'); |
| } |
| |
| .flag-uk { |
| background-image: url('https://flagcdn.com/gb.svg'); |
| } |
| |
| .flag-au { |
| background-image: url('https://flagcdn.com/au.svg'); |
| } |
| |
| .flag-ca { |
| background-image: url('https://flagcdn.com/ca.svg'); |
| } |
| |
| .flag-in { |
| background-image: url('https://flagcdn.com/in.svg'); |
| } |
| |
| .flag-za { |
| background-image: url('https://flagcdn.com/za.svg'); |
| } |
| |
| .flag-ie { |
| background-image: url('https://flagcdn.com/ie.svg'); |
| } |
| |
| .flag-nz { |
| background-image: url('https://flagcdn.com/nz.svg'); |
| } |
| |
| .typing-cursor { |
| display: inline-block; |
| width: 2px; |
| height: 1em; |
| background-color: #3b82f6; |
| animation: blink 1s infinite; |
| vertical-align: middle; |
| } |
| |
| @keyframes blink { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0; } |
| } |
| |
| .document-toolbar { |
| transition: all 0.3s ease; |
| } |
| |
| .document-toolbar.hidden { |
| opacity: 0; |
| transform: translateY(-10px); |
| pointer-events: none; |
| } |
| </style> |
| </head> |
| <body class="min-h-screen bg-gray-50"> |
| <div class="container mx-auto px-4 py-8 max-w-6xl"> |
| <header class="mb-8 text-center"> |
| <h1 class="text-4xl font-bold text-gray-800 mb-2">HD TTS Document Reader</h1> |
| <p class="text-lg text-gray-600">Natural human voices with long context support using Google's free HD voices</p> |
| <div class="mt-4 flex justify-center gap-4"> |
| <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800"> |
| <i class="fas fa-microphone mr-2"></i> HD Voices |
| </span> |
| <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-purple-100 text-purple-800"> |
| <i class="fas fa-cloud mr-2"></i> Val Town Storage |
| </span> |
| <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800"> |
| <i class="fas fa-bolt mr-2"></i> Serverless |
| </span> |
| </div> |
| </header> |
|
|
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> |
| |
| <div class="lg:col-span-2 bg-white rounded-xl shadow-md overflow-hidden"> |
| <div class="p-6"> |
| <div class="flex justify-between items-center mb-4"> |
| <h2 class="text-2xl font-semibold text-gray-800">Document Reader</h2> |
| <div class="flex space-x-2"> |
| <button id="settings-toggle" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100"> |
| <i class="fas fa-cog"></i> |
| </button> |
| <button id="fullscreen-toggle" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100"> |
| <i class="fas fa-expand"></i> |
| </button> |
| </div> |
| </div> |
| |
| <div id="settings-panel" class="settings-panel mb-6 bg-gray-50 p-4 rounded-lg"> |
| <div class="flex border-b mb-4"> |
| <button class="tab-btn tab-active px-4 py-2" data-tab="formatting">Formatting</button> |
| <button class="tab-btn px-4 py-2" data-tab="pronunciation">Pronunciation</button> |
| <button class="tab-btn px-4 py-2" data-tab="advanced">Advanced</button> |
| </div> |
| |
| <div id="formatting-tab" class="tab-content"> |
| <div class="grid grid-cols-2 gap-4"> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Paragraph Spacing</label> |
| <select class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"> |
| <option>Single</option> |
| <option>1.5 lines</option> |
| <option>Double</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-1">Font Size</label> |
| <select class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"> |
| <option>Small</option> |
| <option selected>Medium</option> |
| <option>Large</option> |
| <option>Extra Large</option> |
| </select> |
| </div> |
| </div> |
| <div class="mt-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Text Alignment</label> |
| <div class="flex space-x-2"> |
| <button class="p-2 border rounded-md hover:bg-gray-100"> |
| <i class="fas fa-align-left"></i> |
| </button> |
| <button class="p-2 border rounded-md hover:bg-gray-100"> |
| <i class="fas fa-align-center"></i> |
| </button> |
| <button class="p-2 border rounded-md hover:bg-gray-100"> |
| <i class="fas fa-align-right"></i> |
| </button> |
| <button class="p-2 border rounded-md hover:bg-gray-100"> |
| <i class="fas fa-align-justify"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| <div id="pronunciation-tab" class="tab-content hidden"> |
| <div class="mb-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Phonetic Adjustments</label> |
| <div class="space-y-2"> |
| <div class="flex items-center"> |
| <input type="checkbox" id="phonetic-help" class="mr-2"> |
| <label for="phonetic-help" class="text-sm">Enable phonetic assistance</label> |
| </div> |
| <div class="flex items-center"> |
| <input type="checkbox" id="auto-pronounce" class="mr-2" checked> |
| <label for="auto-pronounce" class="text-sm">Auto-correct pronunciation</label> |
| </div> |
| </div> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Custom Pronunciations</label> |
| <div class="flex items-center mb-2"> |
| <input type="text" placeholder="Word" class="border border-gray-300 rounded-md px-3 py-1 text-sm mr-2 w-32"> |
| <input type="text" placeholder="Pronunciation" class="border border-gray-300 rounded-md px-3 py-1 text-sm mr-2 w-32"> |
| <button class="text-blue-600 hover:text-blue-800 text-sm"> |
| <i class="fas fa-plus"></i> Add |
| </button> |
| </div> |
| <div class="text-xs text-gray-500"> |
| Add custom pronunciations for specific words or names |
| </div> |
| </div> |
| </div> |
| |
| <div id="advanced-tab" class="tab-content hidden"> |
| <div class="mb-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Audio Quality</label> |
| <select class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"> |
| <option>Standard (16kHz)</option> |
| <option selected>High (24kHz)</option> |
| <option>HD (32kHz)</option> |
| </select> |
| </div> |
| <div class="mb-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Chunk Size</label> |
| <select class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"> |
| <option>Small (better for slow connections)</option> |
| <option selected>Medium (balanced)</option> |
| <option>Large (fewer requests)</option> |
| </select> |
| </div> |
| <div> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Cache Duration</label> |
| <select class="w-full border border-gray-300 rounded-md px-3 py-2 text-sm"> |
| <option>No caching</option> |
| <option selected>1 hour</option> |
| <option>1 day</option> |
| <option>1 week</option> |
| </select> |
| </div> |
| </div> |
| </div> |
| |
| <div class="document-toolbar mb-2 flex flex-wrap gap-1"> |
| <button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="bold" title="Bold"> |
| <i class="fas fa-bold"></i> |
| </button> |
| <button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="italic" title="Italic"> |
| <i class="fas fa-italic"></i> |
| </button> |
| <button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="underline" title="Underline"> |
| <i class="fas fa-underline"></i> |
| </button> |
| <div class="w-px h-6 bg-gray-300 mx-1"></div> |
| <button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="insertUnorderedList" title="Bullet List"> |
| <i class="fas fa-list-ul"></i> |
| </button> |
| <button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="insertOrderedList" title="Numbered List"> |
| <i class="fas fa-list-ol"></i> |
| </button> |
| <div class="w-px h-6 bg-gray-300 mx-1"></div> |
| <button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="outdent" title="Decrease Indent"> |
| <i class="fas fa-outdent"></i> |
| </button> |
| <button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="indent" title="Increase Indent"> |
| <i class="fas fa-indent"></i> |
| </button> |
| <div class="w-px h-6 bg-gray-300 mx-1"></div> |
| <button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="createLink" title="Insert Link"> |
| <i class="fas fa-link"></i> |
| </button> |
| <button class="format-btn p-2 text-gray-700 hover:bg-gray-100 rounded" data-command="unlink" title="Remove Link"> |
| <i class="fas fa-unlink"></i> |
| </button> |
| </div> |
| |
| <div class="mb-4"> |
| <label for="document-text" class="block text-sm font-medium text-gray-700 mb-2">Document Text</label> |
| <div class="document-container bg-gray-50 p-4 rounded-lg border border-gray-200"> |
| <div id="document-editor" class="min-h-[200px] outline-none" contenteditable="true" data-placeholder="Type or paste your text here..."></div> |
| <div id="document-content" class="hidden prose max-w-none"></div> |
| </div> |
| </div> |
| |
| <div class="flex justify-between items-center"> |
| <div class="text-sm text-gray-500"> |
| <span id="word-count">0</span> words • <span id="char-count">0</span> characters |
| </div> |
| <div> |
| <button id="clear-btn" class="text-gray-500 hover:text-gray-700 mr-3"> |
| <i class="fas fa-trash-alt mr-1"></i> Clear |
| </button> |
| <button id="spellcheck-btn" class="text-gray-500 hover:text-gray-700"> |
| <i class="fas fa-spell-check mr-1"></i> Spellcheck |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="bg-white rounded-xl shadow-md overflow-hidden"> |
| <div class="p-6"> |
| <h2 class="text-2xl font-semibold text-gray-800 mb-4">Voice Settings</h2> |
| |
| <div class="mb-6"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Select Language & Voice</label> |
| <div class="space-y-3"> |
| <div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-blue-50 border-gray-200" data-voice="en-US-Wavenet-A" data-lang="en-US"> |
| <div class="flex items-center"> |
| <span class="language-flag flag-us"></span> |
| <div class="flex-1"> |
| <div class="font-medium">James (Standard)</div> |
| <div class="text-xs text-gray-500">Male • Wavenet A</div> |
| </div> |
| <i class="fas fa-check text-blue-600 opacity-0 voice-check"></i> |
| </div> |
| </div> |
| <div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-purple-50 border-gray-200" data-voice="en-US-Wavenet-F" data-lang="en-US"> |
| <div class="flex items-center"> |
| <span class="language-flag flag-us"></span> |
| <div class="flex-1"> |
| <div class="font-medium">Emily (Standard)</div> |
| <div class="text-xs text-gray-500">Female • Wavenet F</div> |
| </div> |
| <i class="fas fa-check text-blue-600 opacity-0 voice-check"></i> |
| </div> |
| </div> |
| <div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-green-50 border-gray-200" data-voice="en-GB-Wavenet-B" data-lang="en-GB"> |
| <div class="flex items-center"> |
| <span class="language-flag flag-uk"></span> |
| <div class="flex-1"> |
| <div class="font-medium">Oliver (British)</div> |
| <div class="text-xs text-gray-500">Male • Wavenet B</div> |
| </div> |
| <i class="fas fa-check text-blue-600 opacity-0 voice-check"></i> |
| </div> |
| </div> |
| <div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-pink-50 border-gray-200" data-voice="en-AU-Wavenet-A" data-lang="en-AU"> |
| <div class="flex items-center"> |
| <span class="language-flag flag-au"></span> |
| <div class="flex-1"> |
| <div class="font-medium">Charlotte (Australian)</div> |
| <div class="text-xs text-gray-500">Female • Wavenet A</div> |
| </div> |
| <i class="fas fa-check text-blue-600 opacity-0 voice-check"></i> |
| </div> |
| </div> |
| <button id="show-more-voices" class="w-full text-center text-blue-600 hover:text-blue-800 text-sm py-2"> |
| <i class="fas fa-chevron-down mr-1"></i> Show More Voices |
| </button> |
| <div id="more-voices" class="hidden space-y-3"> |
| <div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-yellow-50 border-gray-200" data-voice="en-IN-Wavenet-A" data-lang="en-IN"> |
| <div class="flex items-center"> |
| <span class="language-flag flag-in"></span> |
| <div class="flex-1"> |
| <div class="font-medium">Arjun (Indian)</div> |
| <div class="text-xs text-gray-500">Male • Wavenet A</div> |
| </div> |
| <i class="fas fa-check text-blue-600 opacity-0 voice-check"></i> |
| </div> |
| </div> |
| <div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-red-50 border-gray-200" data-voice="en-ZA-Wavenet-A" data-lang="en-ZA"> |
| <div class="flex items-center"> |
| <span class="language-flag flag-za"></span> |
| <div class="flex-1"> |
| <div class="font-medium">Lebo (South African)</div> |
| <div class="text-xs text-gray-500">Female • Wavenet A</div> |
| </div> |
| <i class="fas fa-check text-blue-600 opacity-0 voice-check"></i> |
| </div> |
| </div> |
| <div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-indigo-50 border-gray-200" data-voice="en-IE-Wavenet-A" data-lang="en-IE"> |
| <div class="flex items-center"> |
| <span class="language-flag flag-ie"></span> |
| <div class="flex-1"> |
| <div class="font-medium">Connor (Irish)</div> |
| <div class="text-xs text-gray-500">Male • Wavenet A</div> |
| </div> |
| <i class="fas fa-check text-blue-600 opacity-0 voice-check"></i> |
| </div> |
| </div> |
| <div class="voice-option cursor-pointer p-3 border rounded-lg transition bg-gray-50 hover:bg-teal-50 border-gray-200" data-voice="en-NZ-Wavenet-A" data-lang="en-NZ"> |
| <div class="flex items-center"> |
| <span class="language-flag flag-nz"></span> |
| <div class="flex-1"> |
| <div class="font-medium">Tiana (New Zealand)</div> |
| <div class="text-xs text-gray-500">Female • Wavenet A</div> |
| </div> |
| <i class="fas fa-check text-blue-600 opacity-0 voice-check"></i> |
| </div> |
| </div> |
| <button id="show-less-voices" class="w-full text-center text-blue-600 hover:text-blue-800 text-sm py-2"> |
| <i class="fas fa-chevron-up mr-1"></i> Show Fewer Voices |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| <div class="mb-6"> |
| <label for="voice-speed" class="block text-sm font-medium text-gray-700 mb-2">Speed: <span id="speed-value">1.0</span>x</label> |
| <input type="range" id="voice-speed" min="0.5" max="2" step="0.1" value="1" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
| </div> |
| |
| <div class="mb-6"> |
| <label for="voice-pitch" class="block text-sm font-medium text-gray-700 mb-2">Pitch: <span id="pitch-value">0</span></label> |
| <input type="range" id="voice-pitch" min="-20" max="20" step="1" value="0" class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"> |
| </div> |
| |
| <div class="mb-6"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Reading Mode</label> |
| <div class="grid grid-cols-3 gap-2"> |
| <button class="reading-mode-btn py-2 px-3 border rounded-lg bg-blue-50 border-blue-200 text-blue-700" data-mode="normal"> |
| <i class="fas fa-book-open mr-1"></i> Normal |
| </button> |
| <button class="reading-mode-btn py-2 px-3 border rounded-lg border-gray-200 hover:bg-gray-50" data-mode="word"> |
| <i class="fas fa-font mr-1"></i> Word-by-word |
| </button> |
| <button class="reading-mode-btn py-2 px-3 border rounded-lg border-gray-200 hover:bg-gray-50" data-mode="sentence"> |
| <i class="fas fa-align-left mr-1"></i> Sentence |
| </button> |
| </div> |
| </div> |
| |
| <div class="flex space-x-3 mb-4"> |
| <button id="play-btn" class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-4 rounded-lg transition flex items-center justify-center"> |
| <i class="fas fa-play mr-2"></i> Play |
| </button> |
| <button id="pause-btn" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-3 px-4 rounded-lg transition flex items-center justify-center" disabled> |
| <i class="fas fa-pause mr-2"></i> Pause |
| </button> |
| <button id="stop-btn" class="flex-1 bg-gray-200 hover:bg-gray-300 text-gray-800 font-medium py-3 px-4 rounded-lg transition flex items-center justify-center" disabled> |
| <i class="fas fa-stop mr-2"></i> Stop |
| </button> |
| </div> |
| |
| <div class="progress-bar mb-1"> |
| <div id="progress-fill" class="progress-fill"></div> |
| </div> |
| |
| <div class="flex justify-between text-sm text-gray-500 mb-4"> |
| <span id="current-time">0:00</span> |
| <span id="total-time">0:00</span> |
| </div> |
| |
| <div class="flex justify-between"> |
| <button id="skip-back-btn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Skip Back 15s"> |
| <i class="fas fa-backward"></i> |
| </button> |
| <button id="rewind-btn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Rewind"> |
| <i class="fas fa-undo"></i> |
| </button> |
| <button id="forward-btn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Forward"> |
| <i class="fas fa-redo"></i> |
| </button> |
| <button id="skip-forward-btn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Skip Forward 15s"> |
| <i class="fas fa-forward"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="mt-8 bg-white rounded-xl shadow-md overflow-hidden"> |
| <div class="p-6"> |
| <div class="flex justify-between items-center mb-4"> |
| <h2 class="text-2xl font-semibold text-gray-800">Audio Visualization</h2> |
| <div class="flex items-center space-x-2"> |
| <button id="visualization-mode" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Change Visualization"> |
| <i class="fas fa-sliders-h"></i> |
| </button> |
| <button id="download-audio" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Download Audio"> |
| <i class="fas fa-download"></i> |
| </button> |
| </div> |
| </div> |
| <div class="waveform" id="waveform"> |
| <div class="waveform-bar" style="--delay: 1; height: 15px;"></div> |
| <div class="waveform-bar" style="--delay: 2; height: 25px;"></div> |
| <div class="waveform-bar" style="--delay: 3; height: 10px;"></div> |
| <div class="waveform-bar" style="--delay: 4; height: 30px;"></div> |
| <div class="waveform-bar" style="--delay: 5; height: 20px;"></div> |
| <div class="waveform-bar" style="--delay: 6; height: 15px;"></div> |
| <div class="waveform-bar" style="--delay: 7; height: 25px;"></div> |
| <div class="waveform-bar" style="--delay: 8; height: 10px;"></div> |
| <div class="waveform-bar" style="--delay: 9; height: 30px;"></div> |
| <div class="waveform-bar" style="--delay: 10; height: 20px;"></div> |
| </div> |
| <div class="mt-4 flex justify-center"> |
| <button id="record-voice-btn" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center"> |
| <i class="fas fa-microphone mr-2"></i> Record Your Voice |
| </button> |
| </div> |
| </div> |
| </div> |
| |
| |
| <div class="mt-8 bg-white rounded-xl shadow-md overflow-hidden"> |
| <div class="p-6"> |
| <div class="flex justify-between items-center mb-4"> |
| <h2 class="text-2xl font-semibold text-gray-800">Saved Documents</h2> |
| <div class="flex space-x-2"> |
| <button id="refresh-docs-btn" class="p-2 text-gray-500 hover:text-gray-700 rounded-full hover:bg-gray-100" title="Refresh"> |
| <i class="fas fa-sync-alt"></i> |
| </button> |
| <button id="save-doc-btn" class="bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-lg transition"> |
| <i class="fas fa-save mr-2"></i> Save Current |
| </button> |
| </div> |
| </div> |
| |
| <div class="mb-4 flex items-center"> |
| <div class="relative flex-1 max-w-md"> |
| <input type="text" id="search-docs" placeholder="Search documents..." class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"> |
| <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> |
| <i class="fas fa-search text-gray-400"></i> |
| </div> |
| </div> |
| <button id="filter-docs-btn" class="ml-2 p-2 text-gray-500 hover:text-gray-700 rounded-lg hover:bg-gray-100"> |
| <i class="fas fa-filter"></i> |
| </button> |
| </div> |
| |
| <div class="overflow-x-auto"> |
| <table class="min-w-full divide-y divide-gray-200"> |
| <thead class="bg-gray-50"> |
| <tr> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> |
| <button class="flex items-center sort-btn" data-sort="title"> |
| Title <i class="fas fa-sort ml-1"></i> |
| </button> |
| </th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> |
| <button class="flex items-center sort-btn" data-sort="date"> |
| Date <i class="fas fa-sort ml-1"></i> |
| </button> |
| </th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> |
| <button class="flex items-center sort-btn" data-sort="length"> |
| Length <i class="fas fa-sort ml-1"></i> |
| </button> |
| </th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"> |
| <button class="flex items-center sort-btn" data-sort="language"> |
| Language <i class="fas fa-sort ml-1"></i> |
| </button> |
| </th> |
| <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th> |
| </tr> |
| </thead> |
| <tbody id="saved-docs-list" class="bg-white divide-y divide-gray-200"> |
| <tr> |
| <td colspan="5" class="px-6 py-4 text-center text-gray-500">No saved documents yet</td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| |
| <div class="mt-4 flex justify-between items-center"> |
| <div class="text-sm text-gray-500"> |
| Showing <span id="showing-from">1</span> to <span id="showing-to">0</span> of <span id="total-docs">0</span> documents |
| </div> |
| <div class="flex space-x-2"> |
| <button id="prev-page-btn" class="px-3 py-1 border rounded-lg disabled:opacity-50" disabled> |
| <i class="fas fa-chevron-left"></i> |
| </button> |
| <button id="next-page-btn" class="px-3 py-1 border rounded-lg disabled:opacity-50" disabled> |
| <i class="fas fa-chevron-right"></i> |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="url-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg p-6 max-w-md w-full"> |
| <h3 class="text-xl font-semibold mb-4">Load Document from URL</h3> |
| <input type="text" id="document-url" placeholder="Enter document URL" class="w-full p-3 border border-gray-300 rounded-lg mb-4"> |
| <div class="mb-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Document Type</label> |
| <select class="w-full border border-gray-300 rounded-md px-3 py-2"> |
| <option>Auto-detect</option> |
| <option>Plain Text</option> |
| <option>Web Page</option> |
| <option>PDF</option> |
| <option>Word Document</option> |
| </select> |
| </div> |
| <div class="flex justify-end space-x-3"> |
| <button id="cancel-url-btn" class="px-4 py-2 border border-gray-300 rounded-lg">Cancel</button> |
| <button id="load-url-btn" class="px-4 py-2 bg-blue-600 text-white rounded-lg">Load</button> |
| </div> |
| </div> |
| </div> |
| |
| <div id="save-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg p-6 max-w-md w-full"> |
| <h3 class="text-xl font-semibold mb-4">Save Document</h3> |
| <input type="text" id="document-title" placeholder="Document title" class="w-full p-3 border border-gray-300 rounded-lg mb-4"> |
| <div class="mb-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Language</label> |
| <select class="w-full border border-gray-300 rounded-md px-3 py-2"> |
| <option>English (United States)</option> |
| <option>English (United Kingdom)</option> |
| <option>English (Australia)</option> |
| <option>English (India)</option> |
| </select> |
| </div> |
| <div class="mb-4"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Tags</label> |
| <input type="text" id="document-tags" placeholder="Add tags (comma separated)" class="w-full p-3 border border-gray-300 rounded-lg"> |
| </div> |
| <div class="flex justify-end space-x-3"> |
| <button id="cancel-save-btn" class="px-4 py-2 border border-gray-300 rounded-lg">Cancel</button> |
| <button id="confirm-save-btn" class="px-4 py-2 bg-green-600 text-white rounded-lg">Save</button> |
| </div> |
| </div> |
| </div> |
| |
| <div id="record-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden"> |
| <div class="bg-white rounded-lg p-6 max-w-md w-full"> |
| <h3 class="text-xl font-semibold mb-4">Record Your Voice</h3> |
| <div class="text-center mb-6"> |
| <div class="w-32 h-32 mx-auto rounded-full bg-red-100 flex items-center justify-center mb-4"> |
| <i class="fas fa-microphone text-red-600 text-4xl"></i> |
| </div> |
| <div id="record-status" class="text-lg font-medium">Ready to record</div> |
| <div id="record-timer" class="text-2xl font-bold my-2">00:00</div> |
| <div class="text-sm text-gray-500">Speak clearly into your microphone</div> |
| </div> |
| <div class="flex justify-center space-x-4"> |
| <button id="start-record-btn" class="px-6 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg flex items-center"> |
| <i class="fas fa-circle mr-2"></i> Start |
| </button> |
| <button id="stop-record-btn" class="px-6 py-2 bg-gray-200 hover:bg-gray-300 text-gray-800 rounded-lg flex items-center" disabled> |
| <i class="fas fa-stop mr-2"></i> Stop |
| </button> |
| <button id="play-record-btn" class="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg flex items-center" disabled> |
| <i class="fas fa-play mr-2"></i> Play |
| </button> |
| </div> |
| <div class="mt-6"> |
| <label class="block text-sm font-medium text-gray-700 mb-2">Save Recording As</label> |
| <div class="flex"> |
| <input type="text" id="recording-name" placeholder="Recording name" class="flex-1 p-2 border border-gray-300 rounded-l-lg"> |
| <button id="save-record-btn" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white rounded-r-lg"> |
| <i class="fas fa-save mr-1"></i> Save |
| </button> |
| </div> |
| </div> |
| <div class="mt-4 flex justify-end"> |
| <button id="close-record-btn" class="px-4 py-2 text-gray-600 hover:text-gray-800"> |
| Close |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', function() { |
| |
| const documentEditor = document.getElementById('document-editor'); |
| const documentContent = document.getElementById('document-content'); |
| const wordCount = document.getElementById('word-count'); |
| const charCount = document.getElementById('char-count'); |
| const playBtn = document.getElementById('play-btn'); |
| const pauseBtn = document.getElementById('pause-btn'); |
| const stopBtn = document.getElementById('stop-btn'); |
| const clearBtn = document.getElementById('clear-btn'); |
| const spellcheckBtn = document.getElementById('spellcheck-btn'); |
| const formatBtns = document.querySelectorAll('.format-btn'); |
| const settingsToggle = document.getElementById('settings-toggle'); |
| const settingsPanel = document.getElementById('settings-panel'); |
| const tabBtns = document.querySelectorAll('.tab-btn'); |
| const tabContents = document.querySelectorAll('.tab-content'); |
| const fullscreenToggle = document.getElementById('fullscreen-toggle'); |
| const voiceSpeed = document.getElementById('voice-speed'); |
| const voicePitch = document.getElementById('voice-pitch'); |
| const speedValue = document.getElementById('speed-value'); |
| const pitchValue = document.getElementById('pitch-value'); |
| const progressFill = document.getElementById('progress-fill'); |
| const currentTime = document.getElementById('current-time'); |
| const totalTime = document.getElementById('total-time'); |
| const waveform = document.getElementById('waveform'); |
| const voiceOptions = document.querySelectorAll('.voice-option'); |
| const showMoreVoices = document.getElementById('show-more-voices'); |
| const showLessVoices = document.getElementById('show-less-voices'); |
| const moreVoices = document.getElementById('more-voices'); |
| const readingModeBtns = document.querySelectorAll('.reading-mode-btn'); |
| const skipBackBtn = document.getElementById('skip-back-btn'); |
| const rewindBtn = document.getElementById('rewind-btn'); |
| const forwardBtn = document.getElementById('forward-btn'); |
| const skipForwardBtn = document.getElementById('skip-forward-btn'); |
| const visualizationMode = document.getElementById('visualization-mode'); |
| const downloadAudio = document.getElementById('download-audio'); |
| const recordVoiceBtn = document.getElementById('record-voice-btn'); |
| const savedDocsList = document.getElementById('saved-docs-list'); |
| const saveDocBtn = document.getElementById('save-doc-btn'); |
| const refreshDocsBtn = document.getElementById('refresh-docs-btn'); |
| const searchDocs = document.getElementById('search-docs'); |
| const filterDocsBtn = document.getElementById('filter-docs-btn'); |
| const sortBtns = document.querySelectorAll('.sort-btn'); |
| const prevPageBtn = document.getElementById('prev-page-btn'); |
| const nextPageBtn = document.getElementById('next-page-btn'); |
| const showingFrom = document.getElementById('showing-from'); |
| const showingTo = document.getElementById('showing-to'); |
| const totalDocs = document.getElementById('total-docs'); |
| const urlModal = document.getElementById('url-modal'); |
| const documentUrl = document.getElementById('document-url'); |
| const cancelUrlBtn = document.getElementById('cancel-url-btn'); |
| const loadUrlBtn = document.getElementById('load-url-btn'); |
| const saveModal = document.getElementById('save-modal'); |
| const documentTitle = document.getElementById('document-title'); |
| const cancelSaveBtn = document.getElementById('cancel-save-btn'); |
| const confirmSaveBtn = document.getElementById('confirm-save-btn'); |
| const recordModal = document.getElementById('record-modal'); |
| const startRecordBtn = document.getElementById('start-record-btn'); |
| const stopRecordBtn = document.getElementById('stop-record-btn'); |
| const playRecordBtn = document.getElementById('play-record-btn'); |
| const saveRecordBtn = document.getElementById('save-record-btn'); |
| const closeRecordBtn = document.getElementById('close-record-btn'); |
| const recordStatus = document.getElementById('record-status'); |
| const recordTimer = document.getElementById('record-timer'); |
| const recordingName = document.getElementById('recording-name'); |
| |
| |
| let selectedVoice = 'en-US-Wavenet-A'; |
| let selectedLanguage = 'en-US'; |
| let isPlaying = false; |
| let currentPosition = 0; |
| let speechSynthesis = window.speechSynthesis; |
| let utterance = null; |
| let savedDocuments = []; |
| let currentPage = 1; |
| const docsPerPage = 5; |
| let sortField = 'date'; |
| let sortDirection = 'desc'; |
| let isRecording = false; |
| let mediaRecorder; |
| let audioChunks = []; |
| let recordStartTime; |
| let recordInterval; |
| let isFullscreen = false; |
| let documentToolbarVisible = true; |
| |
| |
| updateCounts(); |
| loadSavedDocuments(); |
| setupDocumentEditor(); |
| setupVoiceOptions(); |
| |
| |
| documentEditor.addEventListener('input', updateCounts); |
| documentEditor.addEventListener('focus', showDocumentToolbar); |
| documentEditor.addEventListener('blur', hideDocumentToolbarAfterDelay); |
| |
| playBtn.addEventListener('click', playTTS); |
| pauseBtn.addEventListener('click', pauseTTS); |
| stopBtn.addEventListener('click', stopTTS); |
| clearBtn.addEventListener('click', clearDocument); |
| spellcheckBtn.addEventListener('click', toggleSpellcheck); |
| |
| formatBtns.forEach(btn => { |
| btn.addEventListener('click', () => { |
| const command = btn.dataset.command; |
| document.execCommand(command, false, null); |
| documentEditor.focus(); |
| }); |
| }); |
| |
| settingsToggle.addEventListener('click', toggleSettingsPanel); |
| fullscreenToggle.addEventListener('click', toggleFullscreen); |
| |
| tabBtns.forEach(btn => { |
| btn.addEventListener('click', () => { |
| const tab = btn.dataset.tab; |
| switchTab(tab); |
| }); |
| }); |
| |
| voiceSpeed.addEventListener('input', () => { |
| speedValue.textContent = voiceSpeed.value; |
| updateUtteranceSettings(); |
| }); |
| |
| voicePitch.addEventListener('input', () => { |
| pitchValue.textContent = voicePitch.value; |
| updateUtteranceSettings(); |
| }); |
| |
| showMoreVoices.addEventListener('click', () => { |
| moreVoices.classList.remove('hidden'); |
| showMoreVoices.classList.add('hidden'); |
| }); |
| |
| showLessVoices.addEventListener('click', () => { |
| moreVoices.classList.add('hidden'); |
| showMoreVoices.classList.remove('hidden'); |
| }); |
| |
| readingModeBtns.forEach(btn => { |
| btn.addEventListener('click', () => { |
| readingModeBtns.forEach(b => { |
| b.classList.remove('bg-blue-50', 'border-blue-200', 'text-blue-700'); |
| b.classList.add('border-gray-200', 'hover:bg-gray-50'); |
| }); |
| btn.classList.add('bg-blue-50', 'border-blue-200', 'text-blue-700'); |
| btn.classList.remove('border-gray-200', 'hover:bg-gray-50'); |
| }); |
| }); |
| |
| skipBackBtn.addEventListener('click', () => skipTime(-15)); |
| rewindBtn.addEventListener('click', () => skipTime(-5)); |
| forwardBtn.addEventListener('click', () => skipTime(5)); |
| skipForwardBtn.addEventListener('click', () => skipTime(15)); |
| |
| visualizationMode.addEventListener('click', changeVisualizationMode); |
| downloadAudio.addEventListener('click', downloadAudioFile); |
| recordVoiceBtn.addEventListener('click', showRecordModal); |
| |
| saveDocBtn.addEventListener('click', showSaveModal); |
| refreshDocsBtn.addEventListener('click', loadSavedDocuments); |
| searchDocs.addEventListener('input', filterSavedDocuments); |
| filterDocsBtn.addEventListener('click', showFilterOptions); |
| |
| sortBtns.forEach(btn => { |
| btn.addEventListener('click', () => { |
| const field = btn.dataset.sort; |
| if (sortField === field) { |
| sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; |
| } else { |
| sortField = field; |
| sortDirection = 'asc'; |
| } |
| updateSortButtons(); |
| renderSavedDocuments(); |
| }); |
| }); |
| |
| prevPageBtn.addEventListener('click', () => { |
| if (currentPage > 1) { |
| currentPage--; |
| renderSavedDocuments(); |
| } |
| }); |
| |
| nextPageBtn.addEventListener('click', () => { |
| if (currentPage * docsPerPage < savedDocuments.length) { |
| currentPage++; |
| renderSavedDocuments(); |
| } |
| }); |
| |
| cancelUrlBtn.addEventListener('click', () => urlModal.classList.add('hidden')); |
| loadUrlBtn.addEventListener('click', loadFromUrl); |
| cancelSaveBtn.addEventListener('click', () => saveModal.classList.add('hidden')); |
| confirmSaveBtn.addEventListener('click', saveCurrentDocument); |
| |
| startRecordBtn.addEventListener('click', startRecording); |
| stopRecordBtn.addEventListener('click', stopRecording); |
| playRecordBtn.addEventListener('click', playRecording); |
| saveRecordBtn.addEventListener('click', saveRecording); |
| closeRecordBtn.addEventListener('click', () => recordModal.classList.add('hidden')); |
| |
| |
| function updateCounts() { |
| const text = documentEditor.innerText.trim(); |
| const words = text ? text.split(/\s+/).length : 0; |
| const chars = text.length; |
| wordCount.textContent = words; |
| charCount.textContent = chars; |
| } |
| |
| function setupDocumentEditor() { |
| documentEditor.addEventListener('keydown', (e) => { |
| |
| if (e.key === 'Tab') { |
| e.preventDefault(); |
| document.execCommand('indent'); |
| } |
| }); |
| |
| |
| documentEditor.addEventListener('focus', () => { |
| if (documentEditor.innerText === '') { |
| documentEditor.dataset.placeholder = documentEditor.getAttribute('data-placeholder'); |
| } |
| }); |
| |
| documentEditor.addEventListener('blur', () => { |
| if (documentEditor.innerText === '') { |
| documentEditor.removeAttribute('data-placeholder'); |
| } |
| }); |
| } |
| |
| function showDocumentToolbar() { |
| if (!documentToolbarVisible) { |
| document.querySelector('.document-toolbar').classList.remove('hidden'); |
| documentToolbarVisible = true; |
| } |
| } |
| |
| function hideDocumentToolbarAfterDelay() { |
| setTimeout(() => { |
| if (!documentEditor.contains(document.activeElement)) { |
| document.querySelector('.document-toolbar').classList.add('hidden'); |
| documentToolbarVisible = false; |
| } |
| }, 200); |
| } |
| |
| function toggleSpellcheck() { |
| documentEditor.spellcheck = !documentEditor.spellcheck; |
| spellcheckBtn.classList.toggle('text-blue-600'); |
| } |
| |
| function toggleSettingsPanel() { |
| settingsPanel.classList.toggle('open'); |
| settingsToggle.classList.toggle('text-blue-600'); |
| } |
| |
| function toggleFullscreen() { |
| const docContainer = document.querySelector('.document-container'); |
| |
| if (!isFullscreen) { |
| docContainer.requestFullscreen?.(); |
| docContainer.classList.add('max-h-[80vh]'); |
| fullscreenToggle.innerHTML = '<i class="fas fa-compress"></i>'; |
| } else { |
| document.exitFullscreen?.(); |
| docContainer.classList.remove('max-h-[80vh]'); |
| fullscreenToggle.innerHTML = '<i class="fas fa-expand"></i>'; |
| } |
| |
| isFullscreen = !isFullscreen; |
| } |
| |
| function switchTab(tab) { |
| tabBtns.forEach(btn => { |
| if (btn.dataset.tab === tab) { |
| btn.classList.add('tab-active'); |
| } else { |
| btn.classList.remove('tab-active'); |
| } |
| }); |
| |
| tabContents.forEach(content => { |
| if (content.id === `${tab}-tab`) { |
| content.classList.remove('hidden'); |
| } else { |
| content.classList.add('hidden'); |
| } |
| }); |
| } |
| |
| function setupVoiceOptions() { |
| |
| if (voiceOptions.length > 0) { |
| voiceOptions[0].classList.add('border-blue-500', 'bg-blue-50'); |
| voiceOptions[0].querySelector('.voice-check').classList.remove('opacity-0'); |
| } |
| } |
| |
| function playTTS() { |
| const text = documentEditor.innerText.trim(); |
| if (!text) return; |
| |
| if (isPlaying && speechSynthesis.paused) { |
| speechSynthesis.resume(); |
| playBtn.disabled = true; |
| pauseBtn.disabled = false; |
| return; |
| } |
| |
| |
| stopTTS(); |
| |
| |
| utterance = new SpeechSynthesisUtterance(text); |
| utterance.voice = speechSynthesis.getVoices().find(v => v.voiceURI === selectedVoice); |
| utterance.rate = parseFloat(voiceSpeed.value); |
| utterance.pitch = parseFloat(voicePitch.value) / 10; |
| |
| |
| const words = text.split(/\s+/); |
| let currentWordIndex = 0; |
| |
| utterance.onboundary = (event) => { |
| if (event.name === 'word') { |
| |
| document.querySelectorAll('.highlight').forEach(el => { |
| el.classList.remove('highlight'); |
| }); |
| |
| |
| const charIndex = event.charIndex; |
| const word = words.find(w => { |
| const index = text.indexOf(w, charIndex); |
| return index <= charIndex && index + w.length > charIndex; |
| }); |
| |
| if (word) { |
| |
| const temp = document.createElement('span'); |
| temp.className = 'highlight'; |
| temp.textContent = word; |
| |
| |
| const before = text.substring(0, charIndex); |
| const after = text.substring(charIndex + word.length); |
| documentContent.innerHTML = before + temp.outerHTML + after; |
| } |
| |
| |
| const progress = (charIndex / text.length) * 100; |
| progressFill.style.width = `${progress}%`; |
| |
| |
| const duration = text.length / 50; |
| const current = (progress / 100) * duration; |
| currentTime.textContent = formatTime(current); |
| totalTime.textContent = formatTime(duration); |
| } |
| }; |
| |
| utterance.onstart = () => { |
| isPlaying = true; |
| playBtn.disabled = true; |
| pauseBtn.disabled = false; |
| stopBtn.disabled = false; |
| |
| |
| documentContent.innerHTML = text; |
| documentEditor.classList.add('hidden'); |
| documentContent.classList.remove('hidden'); |
| |
| |
| animateWaveform(); |
| }; |
| |
| utterance.onend = () => { |
| isPlaying = false; |
| playBtn.disabled = false; |
| pauseBtn.disabled = true; |
| stopBtn.disabled = true; |
| |
| |
| progressFill.style.width = '0%'; |
| currentTime.textContent = '0:00'; |
| |
| |
| stopWaveformAnimation(); |
| |
| |
| documentEditor.classList.remove('hidden'); |
| documentContent.classList.add('hidden'); |
| }; |
| |
| utterance.onerror = (event) => { |
| console.error('SpeechSynthesis error:', event); |
| isPlaying = false; |
| playBtn.disabled = false; |
| pauseBtn.disabled = true; |
| stopBtn.disabled = true; |
| stopWaveformAnimation(); |
| |
| |
| documentEditor.classList.remove('hidden'); |
| documentContent.classList.add('hidden'); |
| }; |
| |
| speechSynthesis.speak(utterance); |
| } |
| |
| function updateUtteranceSettings() { |
| if (utterance) { |
| utterance.rate = parseFloat(voiceSpeed.value); |
| utterance.pitch = parseFloat(voicePitch.value) / 10; |
| |
| if (isPlaying) { |
| speechSynthesis.cancel(); |
| speechSynthesis.speak(utterance); |
| } |
| } |
| } |
| |
| function pauseTTS() { |
| if (isPlaying) { |
| speechSynthesis.pause(); |
| playBtn.disabled = false; |
| pauseBtn.disabled = true; |
| stopWaveformAnimation(); |
| } |
| } |
| |
| function stopTTS() { |
| speechSynthesis.cancel(); |
| isPlaying = false; |
| playBtn.disabled = false; |
| pauseBtn.disabled = true; |
| stopBtn.disabled = true; |
| progressFill.style.width = '0%'; |
| currentTime.textContent = '0:00'; |
| |
| |
| document.querySelectorAll('.highlight').forEach(el => { |
| el.classList.remove('highlight'); |
| }); |
| |
| documentEditor.classList.remove('hidden'); |
| documentContent.classList.add('hidden'); |
| stopWaveformAnimation(); |
| } |
| |
| function skipTime(seconds) { |
| if (!utterance || !isPlaying) return; |
| |
| |
| |
| const direction = seconds > 0 ? 'forward' : 'back'; |
| showNotification(`Skipping ${Math.abs(seconds)} seconds ${direction}`); |
| } |
| |
| function clearDocument() { |
| stopTTS(); |
| documentEditor.innerHTML = ''; |
| documentContent.innerHTML = ''; |
| updateCounts(); |
| } |
| |
| function changeVisualizationMode() { |
| |
| const modes = ['waveform', 'spectrum', 'circle']; |
| let currentMode = waveform.dataset.mode || 'waveform'; |
| let nextMode = modes[(modes.indexOf(currentMode) + 1) % modes.length]; |
| |
| waveform.dataset.mode = nextMode; |
| |
| |
| const icons = { |
| 'waveform': 'fa-sliders-h', |
| 'spectrum': 'fa-chart-bar', |
| 'circle': 'fa-circle-notch' |
| }; |
| |
| visualizationMode.innerHTML = `<i class="fas ${icons[nextMode]}"></i>`; |
| showNotification(`Visualization mode: ${nextMode}`); |
| } |
| |
| function downloadAudioFile() { |
| |
| |
| showNotification('Preparing audio file for download...'); |
| |
| |
| setTimeout(() => { |
| showNotification('Audio download started'); |
| }, 1000); |
| } |
| |
| function showRecordModal() { |
| recordModal.classList.remove('hidden'); |
| recordingName.value = `Recording ${new Date().toLocaleString()}`; |
| } |
| |
| function startRecording() { |
| |
| navigator.mediaDevices.getUserMedia({ audio: true }) |
| .then(stream => { |
| isRecording = true; |
| mediaRecorder = new MediaRecorder(stream); |
| audioChunks = []; |
| |
| mediaRecorder.ondataavailable = event => { |
| audioChunks.push(event.data); |
| }; |
| |
| mediaRecorder.onstop = () => { |
| const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); |
| const audioUrl = URL.createObjectURL(audioBlob); |
| playRecordBtn.disabled = false; |
| playRecordBtn.onclick = () => { |
| const audio = new Audio(audioUrl); |
| audio.play(); |
| }; |
| }; |
| |
| mediaRecorder.start(); |
| startRecordBtn.disabled = true; |
| stopRecordBtn.disabled = false; |
| recordStatus.textContent = 'Recording...'; |
| recordStatus.className = 'text-lg font-medium text-red-600'; |
| |
| |
| recordStartTime = Date.now(); |
| recordInterval = setInterval(updateRecordTimer, 1000); |
| updateRecordTimer(); |
| }) |
| .catch(err => { |
| console.error('Error accessing microphone:', err); |
| showNotification('Microphone access denied'); |
| }); |
| } |
| |
| function updateRecordTimer() { |
| const elapsed = Math.floor((Date.now() - recordStartTime) / 1000); |
| const minutes = Math.floor(elapsed / 60).toString().padStart(2, '0'); |
| const seconds = (elapsed % 60).toString().padStart(2, '0'); |
| recordTimer.textContent = `${minutes}:${seconds}`; |
| } |
| |
| function stopRecording() { |
| if (!isRecording) return; |
| |
| isRecording = false; |
| mediaRecorder.stop(); |
| clearInterval(recordInterval); |
| |
| |
| mediaRecorder.stream.getTracks().forEach(track => track.stop()); |
| |
| startRecordBtn.disabled = false; |
| stopRecordBtn.disabled = true; |
| recordStatus.textContent = 'Recording stopped'; |
| recordStatus.className = 'text-lg font-medium text-green-600'; |
| } |
| |
| function playRecording() { |
| |
| } |
| |
| function saveRecording() { |
| const name = recordingName.value.trim() || `Recording ${new Date().toLocaleString()}`; |
| showNotification(`Recording saved as "${name}"`); |
| |
| |
| setTimeout(() => { |
| recordModal.classList.add('hidden'); |
| }, 1000); |
| } |
| |
| function loadFromUrl() { |
| const url = documentUrl.value.trim(); |
| if (!url) return; |
| |
| |
| |
| documentEditor.innerHTML = `<p>Content from ${url} would be loaded here in a real implementation.</p><p>This demo shows the UI but doesn't actually fetch URL content.</p>`; |
| updateCounts(); |
| urlModal.classList.add('hidden'); |
| documentUrl.value = ''; |
| } |
| |
| function showSaveModal() { |
| const text = documentEditor.innerText.trim(); |
| if (!text) { |
| showNotification('Please enter some text to save.'); |
| return; |
| } |
| |
| documentTitle.value = `Document ${savedDocuments.length + 1}`; |
| saveModal.classList.remove('hidden'); |
| documentTitle.focus(); |
| } |
| |
| function saveCurrentDocument() { |
| const title = documentTitle.value.trim() || `Document ${savedDocuments.length + 1}`; |
| const text = documentEditor.innerHTML; |
| const plainText = documentEditor.innerText; |
| const date = new Date().toLocaleDateString(); |
| const length = plainText.split(/\s+/).length; |
| |
| |
| savedDocuments.unshift({ |
| title, |
| text, |
| plainText, |
| date, |
| length, |
| language: selectedLanguage |
| }); |
| saveModal.classList.add('hidden'); |
| documentTitle.value = ''; |
| |
| |
| renderSavedDocuments(); |
| currentPage = 1; |
| |
| |
| console.log('Document saved to Val Town (simulated):', { title, length }); |
| showNotification(`Document "${title}" saved successfully`); |
| } |
| |
| function loadSavedDocuments() { |
| |
| |
| if (savedDocuments.length === 0) { |
| savedDocuments = [ |
| { |
| title: 'Sample Document 1', |
| text: '<p>This is a sample document with some text.</p><p>It demonstrates how formatted content would appear.</p>', |
| plainText: 'This is a sample document with some text.\n\nIt demonstrates how formatted content would appear.', |
| date: '2023-06-15', |
| length: 12, |
| language: 'en-US' |
| }, |
| { |
| title: 'Meeting Notes', |
| text: '<p><strong>Meeting Date:</strong> June 10, 2023</p><p><em>Attendees:</em> John, Sarah, Michael</p><ul><li>Reviewed project timeline</li><li>Discussed budget concerns</li><li>Assigned new tasks</li></ul>', |
| plainText: 'Meeting Date: June 10, 2023\n\nAttendees: John, Sarah, Michael\n\n- Reviewed project timeline\n- Discussed budget concerns\n- Assigned new tasks', |
| date: '2023-06-10', |
| length: 20, |
| language: 'en-GB' |
| } |
| ]; |
| } |
| |
| renderSavedDocuments(); |
| showNotification('Documents refreshed'); |
| } |
| |
| function filterSavedDocuments() { |
| const searchTerm = searchDocs.value.toLowerCase(); |
| if (!searchTerm) { |
| renderSavedDocuments(); |
| return; |
| } |
| |
| const filtered = savedDocuments.filter(doc => |
| doc.title.toLowerCase().includes(searchTerm) || |
| doc.plainText.toLowerCase().includes(searchTerm) |
| ); |
| |
| renderSavedDocuments(filtered); |
| } |
| |
| function showFilterOptions() { |
| |
| showNotification('Filter options would appear here'); |
| } |
| |
| function updateSortButtons() { |
| sortBtns.forEach(btn => { |
| const field = btn.dataset.sort; |
| const icon = btn.querySelector('i'); |
| |
| if (field === sortField) { |
| icon.className = sortDirection === 'asc' ? 'fas fa-sort-up ml-1' : 'fas fa-sort-down ml-1'; |
| } else { |
| icon.className = 'fas fa-sort ml-1'; |
| } |
| }); |
| } |
| |
| function renderSavedDocuments(docs = savedDocuments) { |
| const sortedDocs = [...docs].sort((a, b) => { |
| let valA, valB; |
| |
| if (sortField === 'date') { |
| valA = new Date(a.date); |
| valB = new Date(b.date); |
| } else { |
| valA = a[sortField]; |
| valB = b[sortField]; |
| } |
| |
| if (typeof valA === 'string') valA = valA.toLowerCase(); |
| if (typeof valB === 'string') valB = valB.toLowerCase(); |
| |
| if (valA < valB) return sortDirection === 'asc' ? -1 : 1; |
| if (valA > valB) return sortDirection === 'asc' ? 1 : -1; |
| return 0; |
| }); |
| |
| const startIdx = (currentPage - 1) * docsPerPage; |
| const paginatedDocs = sortedDocs.slice(startIdx, startIdx + docsPerPage); |
| |
| if (paginatedDocs.length === 0) { |
| savedDocsList.innerHTML = ` |
| <tr> |
| <td colspan="5" class="px-6 py-4 text-center text-gray-500">No documents found</td> |
| </tr> |
| `; |
| } else { |
| savedDocsList.innerHTML = paginatedDocs.map((doc, idx) => { |
| const flagClass = doc.language === 'en-US' ? 'flag-us' : |
| doc.language === 'en-GB' ? 'flag-uk' : |
| doc.language === 'en-AU' ? 'flag-au' : 'flag-us'; |
| |
| return ` |
| <tr> |
| <td class="px-6 py-4"> |
| <div class="font-medium text-gray-900">${doc.title}</div> |
| <div class="text-sm text-gray-500 mt-1 line-clamp-2">${doc.plainText.substring(0, 100)}${doc.plainText.length > 100 ? '...' : ''}</div> |
| </td> |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <div class="text-gray-500">${doc.date}</div> |
| </td> |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <div class="text-gray-500">${doc.length} words</div> |
| </td> |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <span class="language-flag ${flagClass}"></span> |
| <span class="text-gray-500">${doc.language}</span> |
| </td> |
| <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> |
| <button class="text-blue-600 hover:text-blue-900 mr-3 load-doc-btn" data-index="${startIdx + idx}"> |
| <i class="fas fa-play"></i> |
| </button> |
| <button class="text-green-600 hover:text-green-900 mr-3 edit-doc-btn" data-index="${startIdx + idx}"> |
| <i class="fas fa-edit"></i> |
| </button> |
| <button class="text-red-600 hover:text-red-900 delete-doc-btn" data-index="${startIdx + idx}"> |
| <i class="fas fa-trash"></i> |
| </button> |
| </td> |
| </tr> |
| `; |
| }).join(''); |
| } |
| |
| |
| showingFrom.textContent = startIdx + 1; |
| showingTo.textContent = Math.min(startIdx + paginatedDocs.length, sortedDocs.length); |
| totalDocs.textContent = sortedDocs.length; |
| |
| |
| prevPageBtn.disabled = currentPage === 1; |
| nextPageBtn.disabled = currentPage * docsPerPage >= sortedDocs.length; |
| |
| |
| document.querySelectorAll('.load-doc-btn').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const index = parseInt(e.currentTarget.dataset.index); |
| loadDocument(index); |
| }); |
| }); |
| |
| document.querySelectorAll('.edit-doc-btn').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const index = parseInt(e.currentTarget.dataset.index); |
| editDocument(index); |
| }); |
| }); |
| |
| document.querySelectorAll('.delete-doc-btn').forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| const index = parseInt(e.currentTarget.dataset.index); |
| deleteDocument(index); |
| }); |
| }); |
| } |
| |
| function loadDocument(index) { |
| stopTTS(); |
| const doc = savedDocuments[index]; |
| documentEditor.innerHTML = doc.text; |
| selectedLanguage = doc.language; |
| |
| |
| voiceOptions.forEach(option => { |
| if (option.dataset.lang === doc.language) { |
| option.click(); |
| } |
| }); |
| |
| updateCounts(); |
| showNotification(`Loaded document: ${doc.title}`); |
| } |
| |
| function editDocument(index) { |
| |
| loadDocument(index); |
| showNotification(`Editing "${savedDocuments[index].title}"`); |
| } |
| |
| function deleteDocument(index) { |
| if (confirm(`Delete "${savedDocuments[index].title}"? This cannot be undone.`)) { |
| savedDocuments.splice(index, 1); |
| renderSavedDocuments(); |
| showNotification('Document deleted'); |
| |
| |
| const totalPages = Math.ceil(savedDocuments.length / docsPerPage); |
| if (currentPage > totalPages && totalPages > 0) { |
| currentPage = totalPages; |
| renderSavedDocuments(); |
| } |
| } |
| } |
| |
| function animateWaveform() { |
| const bars = waveform.querySelectorAll('.waveform-bar'); |
| bars.forEach(bar => { |
| bar.style.animationPlayState = 'running'; |
| }); |
| } |
| |
| function stopWaveformAnimation() { |
| const bars = waveform.querySelectorAll('.waveform-bar'); |
| bars.forEach(bar => { |
| bar.style.animationPlayState = 'paused'; |
| bar.style.height = '10px'; |
| }); |
| } |
| |
| function formatTime(seconds) { |
| const mins = Math.floor(seconds / 60); |
| const secs = Math.floor(seconds % 60); |
| return `${mins}:${secs < 10 ? '0' : ''}${secs}`; |
| } |
| |
| function showNotification(message) { |
| |
| console.log('Notification:', message); |
| } |
| |
| |
| speechSynthesis.onvoiceschanged = function() { |
| console.log('Voices loaded:', speechSynthesis.getVoices()); |
| |
| |
| const voices = speechSynthesis.getVoices(); |
| const defaultVoice = voices.find(v => v.voiceURI === selectedVoice); |
| if (defaultVoice) { |
| utterance = new SpeechSynthesisUtterance(); |
| utterance.voice = defaultVoice; |
| } |
| }; |
| |
| |
| if (speechSynthesis.getVoices().length > 0) { |
| console.log('Voices already loaded:', speechSynthesis.getVoices()); |
| } else { |
| console.log('Waiting for voices to load...'); |
| } |
| |
| |
| voiceOptions.forEach(option => { |
| option.addEventListener |
| </html> |