Athspi commited on
Commit
a14dcb8
Β·
verified Β·
1 Parent(s): 5a6cf61

Update static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +372 -99
static/index.html CHANGED
@@ -547,16 +547,31 @@
547
  <body>
548
  <div class="container">
549
  <header>
550
- <div class="bubbles"></div>
 
 
551
  <h1>AI Audio Translator</h1>
552
  <p>Upload an audio file or record via microphone, select a target language, and get the transcription, translation, and translated audio!</p>
553
  </header>
554
 
555
  <div class="main-content">
556
  <div class="audio-input card">
557
- <h2>Audio Input</h2>
 
 
 
 
 
 
 
558
  <div class="dropzone" id="dropzone">
559
- <div class="icon">πŸ“</div>
 
 
 
 
 
 
560
  <p>Drag and drop your audio file here</p>
561
  <p>or</p>
562
  <label for="audioFileInput" class="custom-file-upload">
@@ -568,27 +583,51 @@
568
 
569
  <div class="record-option">
570
  <button id="recordBtn" class="record-btn">
571
- 🎀 Start Recording
 
 
 
 
572
  </button>
573
  </div>
574
 
575
  <div id="audioVisualization" class="audio-visualization hidden">
576
- <div class="visualizer-bars" id="visualizerBars"></div>
 
 
577
  </div>
578
  </div>
579
 
580
  <div class="language-selection card">
581
- <h2>Target Language</h2>
 
 
 
 
 
 
 
 
 
 
582
  <div class="language-dropdown">
583
  <select id="languageSelect">
584
  <option value="" disabled selected>Select language</option>
585
- <!-- Languages populated dynamically -->
586
  </select>
587
  </div>
588
 
589
  <button id="translateBtn" class="translate-btn" disabled>
590
  <span class="btn-content">
591
- 🌐 Translate Audio
 
 
 
 
 
 
 
 
592
  </span>
593
  </button>
594
  </div>
@@ -602,25 +641,42 @@
602
  <div class="results-container">
603
  <div class="result-card">
604
  <h3>Original Transcription</h3>
605
- <div id="originalText" class="result-content"></div>
 
 
606
  <audio id="originalAudio" class="audio-player" controls></audio>
607
  <div class="controls">
608
  <button id="copyOriginal" class="control-btn copy-btn">
609
- πŸ“‹ Copy Text
 
 
 
 
610
  </button>
611
  </div>
612
  </div>
613
 
614
  <div class="result-card">
615
  <h3>Translated Text</h3>
616
- <div id="translatedText" class="result-content"></div>
 
 
617
  <audio id="translatedAudio" class="audio-player" controls></audio>
618
  <div class="controls">
619
  <button id="copyTranslated" class="control-btn copy-btn">
620
- πŸ“‹ Copy Text
 
 
 
 
621
  </button>
622
  <button id="downloadTranslated" class="control-btn download-btn">
623
- ⬇️ Download
 
 
 
 
 
624
  </button>
625
  </div>
626
  </div>
@@ -633,107 +689,324 @@
633
 
634
  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.3.4/axios.min.js"></script>
635
  <script>
636
- let currentJob = {
637
- fileHash: null,
638
- chunksUploaded: 0,
639
- totalChunks: 0,
640
- progress: 0,
641
- state: null
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
642
  };
643
-
644
- // Save state to localStorage
645
- function saveState() {
646
- localStorage.setItem('translationJob', JSON.stringify(currentJob));
647
- document.cookie = `translationJob=${currentJob.fileHash}; max-age=${60*60*24}; path=/`;
648
- }
649
-
650
- // Load state on page load
651
- window.addEventListener('load', () => {
652
- const savedJob = localStorage.getItem('translationJob');
653
- const cookieJob = document.cookie.split('; ').find(row => row.startsWith('translationJob='));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
654
 
655
- if (cookieJob && savedJob) {
656
- currentJob = JSON.parse(savedJob);
657
- if (!currentJob.state) {
658
- showResumeDialog();
 
 
 
659
  }
660
  }
661
- });
662
 
663
- async function uploadFile(file) {
664
- const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB chunks
665
- const fileHash = await calculateFileHash(file);
666
- const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
667
-
668
- currentJob = {
669
- fileHash,
670
- chunksUploaded: 0,
671
- totalChunks,
672
- progress: 0,
673
- state: 'uploading'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
674
  };
675
- saveState();
676
 
677
- for (let i = 0; i < totalChunks; i++) {
678
- const chunk = file.slice(i * CHUNK_SIZE, (i+1) * CHUNK_SIZE);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
  const formData = new FormData();
680
- formData.append('file', chunk);
681
- formData.append('chunkIndex', i);
682
- formData.append('totalChunks', totalChunks);
683
- formData.append('fileHash', fileHash);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
684
 
685
- await axios.post('/upload-chunk', formData);
686
-
687
- currentJob.chunksUploaded++;
688
- currentJob.progress = Math.round((currentJob.chunksUploaded / totalChunks) * 50);
689
- saveState();
690
- updateProgressBar();
691
- }
692
 
693
- currentJob.state = 'processing';
694
- saveState();
695
-
696
- const result = await axios.post('/process-file', {
697
- fileHash,
698
- language: document.getElementById('languageSelect').value
699
  });
700
-
701
- currentJob.state = 'complete';
702
- currentJob.progress = 100;
703
- currentJob.result = result.data;
704
- saveState();
705
-
706
- // Show results
707
- showResults(result.data);
708
- }
709
 
710
- async function calculateFileHash(file) {
711
- const buffer = await file.arrayBuffer();
712
- const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
713
- return Array.from(new Uint8Array(hashBuffer))
714
- .map(b => b.toString(16).padStart(2, '0')).join('');
715
- }
 
716
 
717
- function showResumeDialog() {
718
- if (confirm('Previous translation job found. Resume?')) {
719
- resumeJob();
720
- } else {
721
- clearJob();
722
- }
723
- }
 
724
 
725
- function clearJob() {
726
- localStorage.removeItem('translationJob');
727
- document.cookie = 'translationJob=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
728
- }
 
 
 
 
729
 
730
- // Add to existing translate button handler
731
- document.getElementById('translateBtn').addEventListener('click', async () => {
732
- const file = document.getElementById('audioFileInput').files[0];
733
- if (file) {
734
- await uploadFile(file);
735
- }
736
- });
737
  </script>
738
  </body>
739
  </html>
 
547
  <body>
548
  <div class="container">
549
  <header>
550
+ <div class="bubbles">
551
+ <!-- Bubbles generated by JS -->
552
+ </div>
553
  <h1>AI Audio Translator</h1>
554
  <p>Upload an audio file or record via microphone, select a target language, and get the transcription, translation, and translated audio!</p>
555
  </header>
556
 
557
  <div class="main-content">
558
  <div class="audio-input card">
559
+ <h2>
560
+ <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">
561
+ <path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3z"></path>
562
+ <path d="M19 10v2a7 7 0 0 1-14 0v-2"></path>
563
+ <line x1="12" y1="19" x2="12" y2="22"></line>
564
+ </svg>
565
+ Audio Input
566
+ </h2>
567
  <div class="dropzone" id="dropzone">
568
+ <div class="icon">
569
+ <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">
570
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
571
+ <polyline points="17 8 12 3 7 8"></polyline>
572
+ <line x1="12" y1="3" x2="12" y2="15"></line>
573
+ </svg>
574
+ </div>
575
  <p>Drag and drop your audio file here</p>
576
  <p>or</p>
577
  <label for="audioFileInput" class="custom-file-upload">
 
583
 
584
  <div class="record-option">
585
  <button id="recordBtn" class="record-btn">
586
+ <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">
587
+ <circle cx="12" cy="12" r="10"></circle>
588
+ <circle cx="12" cy="12" r="3"></circle>
589
+ </svg>
590
+ Start Recording
591
  </button>
592
  </div>
593
 
594
  <div id="audioVisualization" class="audio-visualization hidden">
595
+ <div class="visualizer-bars" id="visualizerBars">
596
+ <!-- Bars will be generated dynamically -->
597
+ </div>
598
  </div>
599
  </div>
600
 
601
  <div class="language-selection card">
602
+ <h2>
603
+ <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">
604
+ <path d="M5 8l6 6"></path>
605
+ <path d="M4 14l6-6 2-3"></path>
606
+ <path d="M2 5h12"></path>
607
+ <path d="M7 2h1"></path>
608
+ <path d="M22 22l-5-10-5 10"></path>
609
+ <path d="M14 18h6"></path>
610
+ </svg>
611
+ Target Language
612
+ </h2>
613
  <div class="language-dropdown">
614
  <select id="languageSelect">
615
  <option value="" disabled selected>Select language</option>
616
+ <!-- Languages will be populated dynamically -->
617
  </select>
618
  </div>
619
 
620
  <button id="translateBtn" class="translate-btn" disabled>
621
  <span class="btn-content">
622
+ <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">
623
+ <path d="M5 8l6 6"></path>
624
+ <path d="M4 14l6-6 2-3"></path>
625
+ <path d="M2 5h12"></path>
626
+ <path d="M7 2h1"></path>
627
+ <path d="M22 22l-5-10-5 10"></path>
628
+ <path d="M14 18h6"></path>
629
+ </svg>
630
+ Translate Audio
631
  </span>
632
  </button>
633
  </div>
 
641
  <div class="results-container">
642
  <div class="result-card">
643
  <h3>Original Transcription</h3>
644
+ <div id="originalText" class="result-content">
645
+ <!-- Transcription will appear here -->
646
+ </div>
647
  <audio id="originalAudio" class="audio-player" controls></audio>
648
  <div class="controls">
649
  <button id="copyOriginal" class="control-btn copy-btn">
650
+ <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">
651
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
652
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
653
+ </svg>
654
+ Copy Text
655
  </button>
656
  </div>
657
  </div>
658
 
659
  <div class="result-card">
660
  <h3>Translated Text</h3>
661
+ <div id="translatedText" class="result-content">
662
+ <!-- Translation will appear here -->
663
+ </div>
664
  <audio id="translatedAudio" class="audio-player" controls></audio>
665
  <div class="controls">
666
  <button id="copyTranslated" class="control-btn copy-btn">
667
+ <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">
668
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
669
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
670
+ </svg>
671
+ Copy Text
672
  </button>
673
  <button id="downloadTranslated" class="control-btn download-btn">
674
+ <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">
675
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
676
+ <polyline points="7 10 12 15 17 10"></polyline>
677
+ <line x1="12" y1="15" x2="12" y2="3"></line>
678
+ </svg>
679
+ Download
680
  </button>
681
  </div>
682
  </div>
 
689
 
690
  <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/1.3.4/axios.min.js"></script>
691
  <script>
692
+ // Create bubble animation in header
693
+ const createBubbles = () => {
694
+ const bubblesContainer = document.querySelector('.bubbles');
695
+ const bubbleCount = 10;
696
+
697
+ for (let i = 0; i < bubbleCount; i++) {
698
+ const bubble = document.createElement('div');
699
+ bubble.classList.add('bubble');
700
+
701
+ // Random sizes
702
+ const size = Math.random() * 60 + 20;
703
+ bubble.style.width = `${size}px`;
704
+ bubble.style.height = `${size}px`;
705
+
706
+ // Random positions
707
+ bubble.style.left = `${Math.random() * 100}%`;
708
+ bubble.style.top = `${Math.random() * 100}%`;
709
+
710
+ // Random animation delay and duration
711
+ const animationDuration = Math.random() * 10 + 5;
712
+ const animationDelay = Math.random() * 5;
713
+
714
+ bubble.style.animationDuration = `${animationDuration}s`;
715
+ bubble.style.animationDelay = `${animationDelay}s`;
716
+
717
+ bubblesContainer.appendChild(bubble);
718
+ }
719
  };
720
+
721
+ // DOM Elements
722
+ const dropzone = document.getElementById('dropzone');
723
+ const fileInput = document.getElementById('audioFileInput');
724
+ const fileInfo = document.getElementById('fileInfo');
725
+ const recordBtn = document.getElementById('recordBtn');
726
+ const translateBtn = document.getElementById('translateBtn');
727
+ const languageSelect = document.getElementById('languageSelect');
728
+ const loadingSection = document.getElementById('loadingSection');
729
+ const outputContainer = document.getElementById('outputContainer');
730
+ const originalText = document.getElementById('originalText');
731
+ const translatedText = document.getElementById('translatedText');
732
+ const originalAudio = document.getElementById('originalAudio');
733
+ const translatedAudio = document.getElementById('translatedAudio');
734
+ const copyOriginal = document.getElementById('copyOriginal');
735
+ const copyTranslated = document.getElementById('copyTranslated');
736
+ const downloadTranslated = document.getElementById('downloadTranslated');
737
+ const toast = document.getElementById('toast');
738
+ const audioVisualization = document.getElementById('audioVisualization');
739
+ const visualizerBars = document.getElementById('visualizerBars');
740
+
741
+ // Variables
742
+ let audioFile = null;
743
+ let mediaRecorder = null;
744
+ let audioChunks = [];
745
+ let isRecording = false;
746
+ let audioContext = null;
747
+ let analyser = null;
748
+ let visualizationInterval = null;
749
+
750
+ // Create visualizer bars
751
+ const createVisualizerBars = () => {
752
+ visualizerBars.innerHTML = '';
753
+ const barCount = 50;
754
+
755
+ for (let i = 0; i < barCount; i++) {
756
+ const bar = document.createElement('div');
757
+ bar.classList.add('visualizer-bar');
758
+ visualizerBars.appendChild(bar);
759
+ }
760
+ };
761
+
762
+ // Initialize audio context
763
+ const initAudioContext = () => {
764
+ if (!audioContext) {
765
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
766
+ analyser = audioContext.createAnalyser();
767
+ analyser.fftSize = 256;
768
+ }
769
+ };
770
+
771
+ // Update visualization
772
+ const updateVisualization = (dataArray) => {
773
+ const bars = visualizerBars.querySelectorAll('.visualizer-bar');
774
+ const bufferLength = analyser.frequencyBinCount;
775
+
776
+ analyser.getByteFrequencyData(dataArray);
777
+
778
+ for (let i = 0; i < bars.length; i++) {
779
+ const index = Math.floor(i * (bufferLength / bars.length));
780
+ const value = dataArray[index] / 255;
781
+ const height = value * 100;
782
+ bars[i].style.height = `${height}%`;
783
+ }
784
+ };
785
+
786
+ // Start visualization
787
+ const startVisualization = (stream) => {
788
+ initAudioContext();
789
+
790
+ const source = audioContext.createMediaStreamSource(stream);
791
+ source.connect(analyser);
792
+
793
+ const dataArray = new Uint8Array(analyser.frequencyBinCount);
794
+
795
+ createVisualizerBars();
796
+ audioVisualization.classList.remove('hidden');
797
+
798
+ visualizationInterval = setInterval(() => {
799
+ updateVisualization(dataArray);
800
+ }, 50);
801
+ };
802
+
803
+ // Stop visualization
804
+ const stopVisualization = () => {
805
+ if (visualizationInterval) {
806
+ clearInterval(visualizationInterval);
807
+ visualizationInterval = null;
808
+ }
809
+
810
+ audioVisualization.classList.add('hidden');
811
+ };
812
+
813
+ // Initialize
814
+ const init = () => {
815
+ createBubbles();
816
+
817
+ // Drag and drop functionality
818
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
819
+ dropzone.addEventListener(eventName, preventDefaults, false);
820
+ });
821
+
822
+ function preventDefaults(e) {
823
+ e.preventDefault();
824
+ e.stopPropagation();
825
+ }
826
+
827
+ ['dragenter', 'dragover'].forEach(eventName => {
828
+ dropzone.addEventListener(eventName, highlight, false);
829
+ });
830
+
831
+ ['dragleave', 'drop'].forEach(eventName => {
832
+ dropzone.addEventListener(eventName, unhighlight, false);
833
+ });
834
+
835
+ function highlight() {
836
+ dropzone.classList.add('dragover');
837
+ }
838
+
839
+ function unhighlight() {
840
+ dropzone.classList.remove('dragover');
841
+ }
842
 
843
+ dropzone.addEventListener('drop', handleDrop, false);
844
+
845
+ function handleDrop(e) {
846
+ const dt = e.dataTransfer;
847
+ const files = dt.files;
848
+ if (files.length > 0) {
849
+ handleFile(files[0]);
850
  }
851
  }
 
852
 
853
+ // File input change handler
854
+ fileInput.addEventListener('change', (e) => {
855
+ if (e.target.files.length > 0) {
856
+ handleFile(e.target.files[0]);
857
+ }
858
+ });
859
+
860
+ // Handle file selection
861
+ const handleFile = (file) => {
862
+ if (file.type.startsWith('audio/')) {
863
+ audioFile = file;
864
+ fileInfo.textContent = `Selected file: ${file.name}`;
865
+ fileInfo.classList.remove('hidden');
866
+ translateBtn.disabled = false;
867
+ } else {
868
+ alert('Please upload a valid audio file.');
869
+ }
870
+ };
871
+
872
+ // Record button click handler
873
+ recordBtn.addEventListener('click', () => {
874
+ if (!isRecording) {
875
+ startRecording();
876
+ } else {
877
+ stopRecording();
878
+ }
879
+ });
880
+
881
+ // Start recording
882
+ const startRecording = async () => {
883
+ try {
884
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
885
+ mediaRecorder = new MediaRecorder(stream);
886
+ mediaRecorder.start();
887
+ isRecording = true;
888
+ recordBtn.classList.add('recording');
889
+ recordBtn.innerHTML = `
890
+ <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">
891
+ <circle cx="12" cy="12" r="10"></circle>
892
+ <rect x="6" y="6" width="12" height="12" rx="2"></rect>
893
+ </svg>
894
+ Stop Recording
895
+ `;
896
+ startVisualization(stream);
897
+
898
+ mediaRecorder.ondataavailable = (e) => {
899
+ audioChunks.push(e.data);
900
+ };
901
+
902
+ mediaRecorder.onstop = () => {
903
+ const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
904
+ audioFile = new File([audioBlob], 'recording.wav', { type: 'audio/wav' });
905
+ fileInfo.textContent = `Recording saved: ${audioFile.name}`;
906
+ fileInfo.classList.remove('hidden');
907
+ translateBtn.disabled = false;
908
+ stopVisualization();
909
+ };
910
+ } catch (err) {
911
+ console.error('Error starting recording:', err);
912
+ alert('Error starting recording. Please ensure you have a microphone connected and permissions granted.');
913
+ }
914
  };
 
915
 
916
+ // Stop recording
917
+ const stopRecording = () => {
918
+ mediaRecorder.stop();
919
+ isRecording = false;
920
+ recordBtn.classList.remove('recording');
921
+ recordBtn.innerHTML = `
922
+ <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">
923
+ <circle cx="12" cy="12" r="10"></circle>
924
+ <circle cx="12" cy="12" r="3"></circle>
925
+ </svg>
926
+ Start Recording
927
+ `;
928
+ audioChunks = [];
929
+ };
930
+
931
+ // Translate button click handler
932
+ translateBtn.addEventListener('click', async () => {
933
+ if (!audioFile || !languageSelect.value) {
934
+ alert('Please select an audio file and a target language.');
935
+ return;
936
+ }
937
+
938
+ loadingSection.style.display = 'flex';
939
+ outputContainer.style.display = 'none';
940
+
941
  const formData = new FormData();
942
+ formData.append('audio', audioFile);
943
+ formData.append('language', languageSelect.value);
944
+
945
+ try {
946
+ const response = await axios.post('/translate', formData);
947
+
948
+ if (response.data.error) {
949
+ throw new Error(response.data.error);
950
+ }
951
+
952
+ const { transcription, translation, audio_url } = response.data;
953
+
954
+ originalText.textContent = transcription;
955
+ translatedText.textContent = translation;
956
+ originalAudio.src = URL.createObjectURL(audioFile);
957
+ translatedAudio.src = audio_url;
958
+
959
+ loadingSection.style.display = 'none';
960
+ outputContainer.style.display = 'block';
961
+ } catch (err) {
962
+ console.error('Error:', err);
963
+ showToast(err.message || 'An error occurred');
964
+ loadingSection.style.display = 'none';
965
+ }
966
+ });
967
 
968
+ // Copy original text to clipboard
969
+ copyOriginal.addEventListener('click', () => {
970
+ navigator.clipboard.writeText(originalText.textContent).then(() => {
971
+ showToast('Original text copied to clipboard!');
972
+ });
973
+ });
 
974
 
975
+ // Copy translated text to clipboard
976
+ copyTranslated.addEventListener('click', () => {
977
+ navigator.clipboard.writeText(translatedText.textContent).then(() => {
978
+ showToast('Translated text copied to clipboard!');
979
+ });
 
980
  });
 
 
 
 
 
 
 
 
 
981
 
982
+ // Download translated audio
983
+ downloadTranslated.addEventListener('click', () => {
984
+ const link = document.createElement('a');
985
+ link.href = translatedAudio.src;
986
+ link.download = `translated_audio.${translatedAudio.src.split('.').pop()}`;
987
+ link.click();
988
+ });
989
 
990
+ // Show toast message
991
+ const showToast = (message) => {
992
+ toast.textContent = message;
993
+ toast.classList.add('show');
994
+ setTimeout(() => {
995
+ toast.classList.remove('show');
996
+ }, 3000);
997
+ };
998
 
999
+ // Load languages
1000
+ fetch('/languages')
1001
+ .then(response => response.json())
1002
+ .then(languages => {
1003
+ languageSelect.innerHTML = '<option value="" disabled selected>Select language</option>' +
1004
+ languages.map(lang => `<option value="${lang}">${lang}</option>`).join('');
1005
+ });
1006
+ };
1007
 
1008
+ // Initialize the app
1009
+ init();
 
 
 
 
 
1010
  </script>
1011
  </body>
1012
  </html>