Athspi commited on
Commit
4e7c8a7
·
verified ·
1 Parent(s): b33af77

Update static/index.html

Browse files
Files changed (1) hide show
  1. static/index.html +99 -372
static/index.html CHANGED
@@ -547,31 +547,16 @@
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,51 +568,27 @@
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,42 +602,25 @@
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,324 +633,107 @@
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>
 
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
 
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
  <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
 
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>