Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Audio Translator</title> | |
| <style> | |
| :root { | |
| --primary-color: #6366f1; | |
| --primary-light: #818cf8; | |
| --primary-dark: #4f46e5; | |
| --secondary-color: #f472b6; | |
| --accent-color: #34d399; | |
| --dark-color: #1f2937; | |
| --light-color: #f9fafb; | |
| --gray-color: #9ca3af; | |
| --danger-color: #ef4444; | |
| --success-color: #10b981; | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| body { | |
| background-color: var(--light-color); | |
| color: var(--dark-color); | |
| min-height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 2rem; | |
| overflow-x: hidden; | |
| } | |
| .container { | |
| width: 100%; | |
| max-width: 1000px; | |
| background-color: white; | |
| border-radius: 16px; | |
| box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1); | |
| overflow: hidden; | |
| position: relative; | |
| } | |
| header { | |
| background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); | |
| color: white; | |
| padding: 2rem; | |
| text-align: center; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| header h1 { | |
| font-size: 2.5rem; | |
| margin-bottom: 0.5rem; | |
| font-weight: 700; | |
| animation: fadeInDown 0.8s ease-out; | |
| } | |
| header p { | |
| font-size: 1.1rem; | |
| opacity: 0.9; | |
| max-width: 600px; | |
| margin: 0 auto; | |
| animation: fadeInUp 0.8s ease-out 0.2s both; | |
| } | |
| .bubbles { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| top: 0; | |
| left: 0; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| .bubble { | |
| position: absolute; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.1); | |
| animation: bubble 15s linear infinite; | |
| } | |
| @keyframes bubble { | |
| 0% { | |
| transform: translateY(0) rotate(0deg); | |
| opacity: 1; | |
| border-radius: 0; | |
| } | |
| 100% { | |
| transform: translateY(-1000px) rotate(720deg); | |
| opacity: 0; | |
| border-radius: 50%; | |
| } | |
| } | |
| .main-content { | |
| padding: 2rem; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2rem; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| .card { | |
| background-color: white; | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); | |
| transition: all 0.3s ease; | |
| } | |
| .card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1); | |
| } | |
| .card h2 { | |
| color: var(--primary-dark); | |
| margin-bottom: 1rem; | |
| display: flex; | |
| align-items: center; | |
| gap: 0.5rem; | |
| } | |
| .audio-input { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .dropzone { | |
| border: 2px dashed var(--gray-color); | |
| border-radius: 8px; | |
| padding: 2rem; | |
| text-align: center; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| background-color: rgba(99, 102, 241, 0.05); | |
| } | |
| .dropzone:hover, .dropzone.dragover { | |
| border-color: var(--primary-color); | |
| background-color: rgba(99, 102, 241, 0.1); | |
| } | |
| .dropzone p { | |
| color: var(--gray-color); | |
| margin: 0.5rem 0; | |
| } | |
| .dropzone .icon { | |
| font-size: 2.5rem; | |
| color: var(--primary-light); | |
| margin-bottom: 0.5rem; | |
| } | |
| .custom-file-upload { | |
| display: inline-block; | |
| font-weight: 500; | |
| cursor: pointer; | |
| background-color: var(--primary-color); | |
| color: white; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 6px; | |
| transition: all 0.2s ease; | |
| border: none; | |
| font-size: 1rem; | |
| } | |
| .custom-file-upload:hover { | |
| background-color: var(--primary-dark); | |
| transform: translateY(-2px); | |
| } | |
| #audioFileInput { | |
| display: none; | |
| } | |
| .record-option { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-top: 1rem; | |
| gap: 1rem; | |
| } | |
| .record-btn { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| background-color: white; | |
| border: 1px solid var(--primary-color); | |
| color: var(--primary-color); | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| font-weight: 500; | |
| font-size: 1rem; | |
| } | |
| .record-btn:hover { | |
| background-color: rgba(99, 102, 241, 0.1); | |
| } | |
| .record-btn.recording { | |
| background-color: var(--danger-color); | |
| color: white; | |
| border-color: var(--danger-color); | |
| animation: pulse 1.5s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { | |
| box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4); | |
| } | |
| 70% { | |
| box-shadow: 0 0 0 10px rgba(239, 68, 68, 0); | |
| } | |
| 100% { | |
| box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); | |
| } | |
| } | |
| .language-selection { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .language-dropdown { | |
| position: relative; | |
| } | |
| .language-dropdown select { | |
| width: 100%; | |
| padding: 0.75rem 1rem; | |
| border-radius: 6px; | |
| border: 1px solid var(--gray-color); | |
| background-color: white; | |
| font-size: 1rem; | |
| cursor: pointer; | |
| appearance: none; | |
| outline: none; | |
| transition: all 0.2s ease; | |
| } | |
| .language-dropdown select:focus { | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); | |
| } | |
| .language-dropdown::after { | |
| content: '▾'; | |
| position: absolute; | |
| right: 1rem; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--gray-color); | |
| pointer-events: none; | |
| } | |
| .results-container { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); | |
| gap: 1.5rem; | |
| } | |
| .result-card { | |
| background-color: white; | |
| border-radius: 12px; | |
| padding: 1.5rem; | |
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); | |
| transition: all 0.3s ease; | |
| height: 100%; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .result-card h3 { | |
| color: var(--primary-dark); | |
| margin-bottom: 1rem; | |
| padding-bottom: 0.5rem; | |
| border-bottom: 1px solid rgba(0, 0, 0, 0.1); | |
| } | |
| .result-content { | |
| flex: 1; | |
| overflow-y: auto; | |
| max-height: 200px; | |
| padding-right: 0.5rem; | |
| margin-bottom: 1rem; | |
| line-height: 1.6; | |
| } | |
| .result-content::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| .result-content::-webkit-scrollbar-track { | |
| background: rgba(0, 0, 0, 0.05); | |
| border-radius: 10px; | |
| } | |
| .result-content::-webkit-scrollbar-thumb { | |
| background: var(--primary-light); | |
| border-radius: 10px; | |
| } | |
| .audio-player { | |
| width: 100%; | |
| height: 40px; | |
| outline: none; | |
| } | |
| .translate-btn { | |
| display: block; | |
| width: 100%; | |
| padding: 1rem; | |
| background: linear-gradient(135deg, var(--primary-color), var(--primary-dark)); | |
| color: white; | |
| border: none; | |
| border-radius: 8px; | |
| font-size: 1.1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| margin-top: 1rem; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .translate-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 15px rgba(79, 70, 229, 0.3); | |
| } | |
| .translate-btn:active { | |
| transform: translateY(0); | |
| } | |
| .translate-btn .btn-content { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| position: relative; | |
| z-index: 2; | |
| } | |
| .translate-btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(135deg, var(--primary-dark), var(--secondary-color)); | |
| transition: all 0.5s ease; | |
| z-index: 1; | |
| } | |
| .translate-btn:hover::before { | |
| left: 0; | |
| } | |
| .loading { | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| flex-direction: column; | |
| gap: 1rem; | |
| padding: 2rem; | |
| text-align: center; | |
| } | |
| .spinner { | |
| width: 60px; | |
| height: 60px; | |
| border: 5px solid rgba(99, 102, 241, 0.2); | |
| border-radius: 50%; | |
| border-top-color: var(--primary-color); | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| .output-container { | |
| display: none; | |
| animation: fadeIn 0.5s ease-out; | |
| } | |
| .audio-visualization { | |
| height: 60px; | |
| background-color: rgba(99, 102, 241, 0.1); | |
| border-radius: 8px; | |
| overflow: hidden; | |
| margin: 1rem 0; | |
| position: relative; | |
| } | |
| .visualizer-bars { | |
| display: flex; | |
| align-items: flex-end; | |
| height: 100%; | |
| padding: 0 5px; | |
| gap: 2px; | |
| } | |
| .visualizer-bar { | |
| flex: 1; | |
| background: linear-gradient(to top, var(--primary-color), var(--primary-light)); | |
| max-width: 4px; | |
| border-radius: 2px; | |
| height: 0%; | |
| transition: height 0.05s ease; | |
| } | |
| .controls { | |
| display: flex; | |
| gap: 0.5rem; | |
| margin-top: 1rem; | |
| } | |
| .control-btn { | |
| flex: 1; | |
| padding: 0.75rem; | |
| border: none; | |
| border-radius: 6px; | |
| font-weight: 500; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 0.5rem; | |
| } | |
| .download-btn { | |
| background-color: var(--success-color); | |
| color: white; | |
| } | |
| .download-btn:hover { | |
| background-color: #0b9e6e; | |
| } | |
| .copy-btn { | |
| background-color: var(--accent-color); | |
| color: white; | |
| } | |
| .copy-btn:hover { | |
| background-color: #2bbb89; | |
| } | |
| .toast { | |
| position: fixed; | |
| bottom: 2rem; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(100px); | |
| background-color: var(--dark-color); | |
| color: white; | |
| padding: 0.75rem 1.5rem; | |
| border-radius: 6px; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2); | |
| z-index: 1000; | |
| opacity: 0; | |
| transition: all 0.3s ease; | |
| } | |
| .toast.show { | |
| transform: translateX(-50%) translateY(0); | |
| opacity: 1; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes fadeInDown { | |
| from { | |
| opacity: 0; | |
| transform: translateY(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .main-content { | |
| padding: 1.5rem; | |
| } | |
| .results-container { | |
| grid-template-columns: 1fr; | |
| } | |
| header h1 { | |
| font-size: 2rem; | |
| } | |
| header p { | |
| font-size: 1rem; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <div class="bubbles"> | |
| <!-- Bubbles generated by JS --> | |
| </div> | |
| <h1>AI Audio Translator</h1> | |
| <p>Upload an audio file or record via microphone, select a target language, and get the transcription, translation, and translated audio!</p> | |
| </header> | |
| <div class="main-content"> | |
| <div class="audio-input card"> | |
| <h2> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"></path> | |
| <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path> | |
| <line x1="12" y1="19" x2="12" y2="22"></line> | |
| </svg> | |
| Audio Input | |
| </h2> | |
| <div class="dropzone" id="dropzone"> | |
| <div class="icon"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="17 8 12 3 7 8"></polyline> | |
| <line x1="12" y1="3" x2="12" y2="15"></line> | |
| </svg> | |
| </div> | |
| <p>Drag and drop your audio file here</p> | |
| <p>or</p> | |
| <label for="audioFileInput" class="custom-file-upload"> | |
| Choose File | |
| </label> | |
| <input type="file" id="audioFileInput" accept="audio/*"> | |
| <p id="fileInfo" class="hidden"></p> | |
| </div> | |
| <div class="record-option"> | |
| <button id="recordBtn" class="record-btn"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <circle cx="12" cy="12" r="3"></circle> | |
| </svg> | |
| Start Recording | |
| </button> | |
| </div> | |
| <div id="audioVisualization" class="audio-visualization hidden"> | |
| <div class="visualizer-bars" id="visualizerBars"> | |
| <!-- Bars will be generated dynamically --> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="language-selection card"> | |
| <h2> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M5 8l6 6"></path> | |
| <path d="M4 14l6-6 2-3"></path> | |
| <path d="M2 5h12"></path> | |
| <path d="M7 2h1"></path> | |
| <path d="M22 22l-5-10-5 10"></path> | |
| <path d="M14 18h6"></path> | |
| </svg> | |
| Target Language | |
| </h2> | |
| <div class="language-dropdown"> | |
| <select id="languageSelect"> | |
| <option value="" disabled selected>Select language</option> | |
| <!-- Languages will be populated dynamically --> | |
| </select> | |
| </div> | |
| <button id="translateBtn" class="translate-btn" disabled> | |
| <span class="btn-content"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M5 8l6 6"></path> | |
| <path d="M4 14l6-6 2-3"></path> | |
| <path d="M2 5h12"></path> | |
| <path d="M7 2h1"></path> | |
| <path d="M22 22l-5-10-5 10"></path> | |
| <path d="M14 18h6"></path> | |
| </svg> | |
| Translate Audio | |
| </span> | |
| </button> | |
| </div> | |
| <div id="loadingSection" class="loading"> | |
| <div class="spinner"></div> | |
| <p>Processing your audio... This may take a moment.</p> | |
| </div> | |
| <div id="outputContainer" class="output-container"> | |
| <div class="results-container"> | |
| <div class="result-card"> | |
| <h3>Original Transcription</h3> | |
| <div id="originalText" class="result-content"> | |
| <!-- Transcription will appear here --> | |
| </div> | |
| <audio id="originalAudio" class="audio-player" controls></audio> | |
| <div class="controls"> | |
| <button id="copyOriginal" class="control-btn copy-btn"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | |
| <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | |
| </svg> | |
| Copy Text | |
| </button> | |
| </div> | |
| </div> | |
| <div class="result-card"> | |
| <h3>Translated Text</h3> | |
| <div id="translatedText" class="result-content"> | |
| <!-- Translation will appear here --> | |
| </div> | |
| <audio id="translatedAudio" class="audio-player" controls></audio> | |
| <div class="controls"> | |
| <button id="copyTranslated" class="control-btn copy-btn"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> | |
| <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> | |
| </svg> | |
| Copy Text | |
| </button> | |
| <button id="downloadTranslated" class="control-btn download-btn"> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path> | |
| <polyline points="7 10 12 15 17 10"></polyline> | |
| <line x1="12" y1="15" x2="12" y2="3"></line> | |
| </svg> | |
| Download | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="toast" class="toast">Copied to clipboard!</div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.3.4/axios.min.js"></script> | |
| <script> | |
| // Create bubble animation in header | |
| const createBubbles = () => { | |
| const bubblesContainer = document.querySelector('.bubbles'); | |
| const bubbleCount = 10; | |
| for (let i = 0; i < bubbleCount; i++) { | |
| const bubble = document.createElement('div'); | |
| bubble.classList.add('bubble'); | |
| // Random sizes | |
| const size = Math.random() * 60 + 20; | |
| bubble.style.width = `${size}px`; | |
| bubble.style.height = `${size}px`; | |
| // Random positions | |
| bubble.style.left = `${Math.random() * 100}%`; | |
| bubble.style.top = `${Math.random() * 100}%`; | |
| // Random animation delay and duration | |
| const animationDuration = Math.random() * 10 + 5; | |
| const animationDelay = Math.random() * 5; | |
| bubble.style.animationDuration = `${animationDuration}s`; | |
| bubble.style.animationDelay = `${animationDelay}s`; | |
| bubblesContainer.appendChild(bubble); | |
| } | |
| }; | |
| // DOM Elements | |
| const dropzone = document.getElementById('dropzone'); | |
| const fileInput = document.getElementById('audioFileInput'); | |
| const fileInfo = document.getElementById('fileInfo'); | |
| const recordBtn = document.getElementById('recordBtn'); | |
| const translateBtn = document.getElementById('translateBtn'); | |
| const languageSelect = document.getElementById('languageSelect'); | |
| const loadingSection = document.getElementById('loadingSection'); | |
| const outputContainer = document.getElementById('outputContainer'); | |
| const originalText = document.getElementById('originalText'); | |
| const translatedText = document.getElementById('translatedText'); | |
| const originalAudio = document.getElementById('originalAudio'); | |
| const translatedAudio = document.getElementById('translatedAudio'); | |
| const copyOriginal = document.getElementById('copyOriginal'); | |
| const copyTranslated = document.getElementById('copyTranslated'); | |
| const downloadTranslated = document.getElementById('downloadTranslated'); | |
| const toast = document.getElementById('toast'); | |
| const audioVisualization = document.getElementById('audioVisualization'); | |
| const visualizerBars = document.getElementById('visualizerBars'); | |
| // Variables | |
| let audioFile = null; | |
| let mediaRecorder = null; | |
| let audioChunks = []; | |
| let isRecording = false; | |
| let audioContext = null; | |
| let analyser = null; | |
| let visualizationInterval = null; | |
| // Create visualizer bars | |
| const createVisualizerBars = () => { | |
| visualizerBars.innerHTML = ''; | |
| const barCount = 50; | |
| for (let i = 0; i < barCount; i++) { | |
| const bar = document.createElement('div'); | |
| bar.classList.add('visualizer-bar'); | |
| visualizerBars.appendChild(bar); | |
| } | |
| }; | |
| // Initialize audio context | |
| const initAudioContext = () => { | |
| if (!audioContext) { | |
| audioContext = new (window.AudioContext || window.webkitAudioContext)(); | |
| analyser = audioContext.createAnalyser(); | |
| analyser.fftSize = 256; | |
| } | |
| }; | |
| // Update visualization | |
| const updateVisualization = (dataArray) => { | |
| const bars = visualizerBars.querySelectorAll('.visualizer-bar'); | |
| const bufferLength = analyser.frequencyBinCount; | |
| analyser.getByteFrequencyData(dataArray); | |
| for (let i = 0; i < bars.length; i++) { | |
| const index = Math.floor(i * (bufferLength / bars.length)); | |
| const value = dataArray[index] / 255; | |
| const height = value * 100; | |
| bars[i].style.height = `${height}%`; | |
| } | |
| }; | |
| // Start visualization | |
| const startVisualization = (stream) => { | |
| initAudioContext(); | |
| const source = audioContext.createMediaStreamSource(stream); | |
| source.connect(analyser); | |
| const dataArray = new Uint8Array(analyser.frequencyBinCount); | |
| createVisualizerBars(); | |
| audioVisualization.classList.remove('hidden'); | |
| visualizationInterval = setInterval(() => { | |
| updateVisualization(dataArray); | |
| }, 50); | |
| }; | |
| // Stop visualization | |
| const stopVisualization = () => { | |
| if (visualizationInterval) { | |
| clearInterval(visualizationInterval); | |
| visualizationInterval = null; | |
| } | |
| audioVisualization.classList.add('hidden'); | |
| }; | |
| // Initialize | |
| const init = () => { | |
| createBubbles(); | |
| // Drag and drop functionality | |
| ['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('dragover'); | |
| } | |
| function unhighlight() { | |
| dropzone.classList.remove('dragover'); | |
| } | |
| dropzone.addEventListener('drop', handleDrop, false); | |
| function handleDrop(e) { | |
| const dt = e.dataTransfer; | |
| const files = dt.files; | |
| if (files.length > 0) { | |
| handleFile(files[0]); | |
| } | |
| } | |
| // File input change handler | |
| fileInput.addEventListener('change', (e) => { | |
| if (e.target.files.length > 0) { | |
| handleFile(e.target.files[0]); | |
| } | |
| }); | |
| // Handle file selection | |
| const handleFile = (file) => { | |
| if (file.type.startsWith('audio/')) { | |
| audioFile = file; | |
| fileInfo.textContent = `Selected file: ${file.name}`; | |
| fileInfo.classList.remove('hidden'); | |
| translateBtn.disabled = false; | |
| } else { | |
| alert('Please upload a valid audio file.'); | |
| } | |
| }; | |
| // Record button click handler | |
| recordBtn.addEventListener('click', () => { | |
| if (!isRecording) { | |
| startRecording(); | |
| } else { | |
| stopRecording(); | |
| } | |
| }); | |
| // Start recording | |
| const startRecording = async () => { | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| mediaRecorder = new MediaRecorder(stream); | |
| mediaRecorder.start(); | |
| isRecording = true; | |
| recordBtn.classList.add('recording'); | |
| recordBtn.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <rect x="6" y="6" width="12" height="12" rx="2"></rect> | |
| </svg> | |
| Stop Recording | |
| `; | |
| startVisualization(stream); | |
| mediaRecorder.ondataavailable = (e) => { | |
| audioChunks.push(e.data); | |
| }; | |
| mediaRecorder.onstop = () => { | |
| const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); | |
| audioFile = new File([audioBlob], 'recording.wav', { type: 'audio/wav' }); | |
| fileInfo.textContent = `Recording saved: ${audioFile.name}`; | |
| fileInfo.classList.remove('hidden'); | |
| translateBtn.disabled = false; | |
| stopVisualization(); | |
| }; | |
| } catch (err) { | |
| console.error('Error starting recording:', err); | |
| alert('Error starting recording. Please ensure you have a microphone connected and permissions granted.'); | |
| } | |
| }; | |
| // Stop recording | |
| const stopRecording = () => { | |
| mediaRecorder.stop(); | |
| isRecording = false; | |
| recordBtn.classList.remove('recording'); | |
| recordBtn.innerHTML = ` | |
| <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> | |
| <circle cx="12" cy="12" r="10"></circle> | |
| <circle cx="12" cy="12" r="3"></circle> | |
| </svg> | |
| Start Recording | |
| `; | |
| audioChunks = []; | |
| }; | |
| // Translate button click handler | |
| translateBtn.addEventListener('click', async () => { | |
| if (!audioFile || !languageSelect.value) { | |
| alert('Please select an audio file and a target language.'); | |
| return; | |
| } | |
| loadingSection.style.display = 'flex'; | |
| outputContainer.style.display = 'none'; | |
| const formData = new FormData(); | |
| formData.append('audio', audioFile); | |
| formData.append('language', languageSelect.value); | |
| try { | |
| const response = await axios.post('/translate', formData); | |
| if (response.data.error) { | |
| throw new Error(response.data.error); | |
| } | |
| const { transcription, translation, audio_url } = response.data; | |
| originalText.textContent = transcription; | |
| translatedText.textContent = translation; | |
| originalAudio.src = URL.createObjectURL(audioFile); | |
| translatedAudio.src = audio_url; | |
| loadingSection.style.display = 'none'; | |
| outputContainer.style.display = 'block'; | |
| } catch (err) { | |
| console.error('Error:', err); | |
| showToast(err.message || 'An error occurred'); | |
| loadingSection.style.display = 'none'; | |
| } | |
| }); | |
| // Copy original text to clipboard | |
| copyOriginal.addEventListener('click', () => { | |
| navigator.clipboard.writeText(originalText.textContent).then(() => { | |
| showToast('Original text copied to clipboard!'); | |
| }); | |
| }); | |
| // Copy translated text to clipboard | |
| copyTranslated.addEventListener('click', () => { | |
| navigator.clipboard.writeText(translatedText.textContent).then(() => { | |
| showToast('Translated text copied to clipboard!'); | |
| }); | |
| }); | |
| // Download translated audio | |
| downloadTranslated.addEventListener('click', () => { | |
| const link = document.createElement('a'); | |
| link.href = translatedAudio.src; | |
| link.download = `translated_audio.${translatedAudio.src.split('.').pop()}`; | |
| link.click(); | |
| }); | |
| // Show toast message | |
| const showToast = (message) => { | |
| toast.textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| }; | |
| // Load languages | |
| fetch('/languages') | |
| .then(response => response.json()) | |
| .then(languages => { | |
| languageSelect.innerHTML = '<option value="" disabled selected>Select language</option>' + | |
| languages.map(lang => `<option value="${lang}">${lang}</option>`).join(''); | |
| }); | |
| }; | |
| // Initialize the app | |
| init(); | |
| </script> | |
| </body> | |
| </html> |