if (document.querySelector(".upload-view").style.display !== "none") { const uploadArea = document.getElementById("uploadArea"); const fileInput = document.getElementById("fileInput"); const uploadBtn = document.getElementById("uploadBtn"); const fileInfo = document.getElementById("fileInfo"); const progressContainer = document.getElementById("progressContainer"); const progressFill = document.getElementById("progressFill"); const uploadProgressFill = document.getElementById("uploadProgressFill"); const progressText = document.getElementById("progressText"); const resultArea = document.getElementById("resultArea"); const transcriptPreview = document.getElementById("transcriptPreview"); let selectedFile = null; uploadArea.addEventListener("click", () => fileInput.click()); uploadArea.addEventListener("dragover", (e) => { e.preventDefault(); uploadArea.classList.add("dragover"); }); uploadArea.addEventListener("dragleave", () => uploadArea.classList.remove("dragover"), ); uploadArea.addEventListener("drop", (e) => { e.preventDefault(); uploadArea.classList.remove("dragover"); if (e.dataTransfer.files.length > 0) handleFileSelect(e.dataTransfer.files[0]); }); fileInput.addEventListener("change", (e) => { if (e.target.files.length > 0) handleFileSelect(e.target.files[0]); }); function handleFileSelect(file) { selectedFile = file; const sizeInMB = (file.size / (1024 * 1024)).toFixed(2); fileInfo.style.display = "block"; fileInfo.innerHTML = `Tệp đã chọn: ${file.name}
Kích thước: ${sizeInMB} MB`; uploadBtn.style.display = "inline-block"; } uploadBtn.addEventListener("click", async () => { if (!selectedFile) return; const language = document.querySelector( 'input[name="language"]:checked', ).value; const model = document.getElementById("modelSelect").value; uploadBtn.disabled = true; progressContainer.style.display = "block"; resultArea.style.display = "none"; try { await uploadFileInChunks(selectedFile, language, model); } catch (error) { alert("Tải lên thất bại: " + error.message); uploadBtn.disabled = false; progressContainer.style.display = "none"; } }); async function uploadFileInChunks(file, language, model) { const CHUNK_SIZE = 3 * 60 * 1000; // 3 minutes in milliseconds let jobId = null; // Create audio context to get duration const audioContext = new ( window.AudioContext || window.webkitAudioContext )(); const arrayBuffer = await file.arrayBuffer(); const audioBuffer = await audioContext.decodeAudioData(arrayBuffer); const duration = audioBuffer.duration * 1000; // Duration in milliseconds const totalChunks = Math.ceil(duration / CHUNK_SIZE); progressText.textContent = `Đang chuẩn bị tải lên ${totalChunks} phần...`; for (let i = 0; i < totalChunks; i++) { const start = i * CHUNK_SIZE; const end = Math.min(start + CHUNK_SIZE, duration); progressText.textContent = `Đang tải lên phần ${i + 1}/${totalChunks}...`; // Extract chunk from audio const chunkBlob = await extractAudioChunk( audioBuffer, start, end, file.type, ); const formData = new FormData(); formData.append("chunk", chunkBlob, `chunk_${i}.mp3`); formData.append("chunk_index", i); formData.append("total_chunks", totalChunks); formData.append("filename", file.name); formData.append("language", language); formData.append("model", model); if (jobId) { formData.append("job_id", jobId); } const response = await fetch("/upload-chunk", { method: "POST", body: formData, }); const data = await response.json(); if (!response.ok) { throw new Error(data.error || "Upload failed"); } jobId = data.job_id; // Update upload progress const uploadProgress = ((i + 1) / totalChunks) * 100; uploadProgressFill.style.width = uploadProgress + "%"; if (data.status === "processing") { progressText.textContent = "Tất cả các phần đã được tải lên! Đang bắt đầu phiên âm..."; startPolling(jobId); return; } } } async function extractAudioChunk( audioBuffer, startTime, endTime, mimeType, ) { const sampleRate = audioBuffer.sampleRate; const startSample = Math.floor((startTime * sampleRate) / 1000); const endSample = Math.floor((endTime * sampleRate) / 1000); const frameCount = endSample - startSample; // Create new audio buffer for chunk const chunkBuffer = new AudioContext().createBuffer( audioBuffer.numberOfChannels, frameCount, sampleRate, ); // Copy data for ( let channel = 0; channel < audioBuffer.numberOfChannels; channel++ ) { const channelData = audioBuffer.getChannelData(channel); const chunkData = chunkBuffer.getChannelData(channel); for (let i = 0; i < frameCount; i++) { chunkData[i] = channelData[startSample + i]; } } // Convert to WAV first, then let server handle conversion const wavBlob = audioBufferToWav(chunkBuffer); // If it's already an audio file, return as is, otherwise convert if (mimeType.includes("audio")) { return wavBlob; } else { // For video files, we'll send the audio portion return wavBlob; } } function audioBufferToWav(buffer) { const numChannels = buffer.numberOfChannels; const sampleRate = buffer.sampleRate; const format = 1; // PCM const bitDepth = 16; let resultBuffer; if (numChannels === 2) { resultBuffer = interleave( buffer.getChannelData(0), buffer.getChannelData(1), ); } else { resultBuffer = buffer.getChannelData(0); } const dataLength = resultBuffer.length * 2; const bufferLength = 44 + dataLength; const arrayBuffer = new ArrayBuffer(bufferLength); const view = new DataView(arrayBuffer); // Write WAV header writeString(view, 0, "RIFF"); view.setUint32(4, 36 + dataLength, true); writeString(view, 8, "WAVE"); writeString(view, 12, "fmt "); view.setUint32(16, 16, true); view.setUint16(20, format, true); view.setUint16(22, numChannels, true); view.setUint32(24, sampleRate, true); view.setUint32(28, sampleRate * 4, true); view.setUint16(32, numChannels * 2, true); view.setUint16(34, bitDepth, true); writeString(view, 36, "data"); view.setUint32(40, dataLength, true); // Write audio data const offset = 44; for (let i = 0; i < resultBuffer.length; i++) { const sample = Math.max(-1, Math.min(1, resultBuffer[i])); view.setInt16( offset + i * 2, sample < 0 ? sample * 0x8000 : sample * 0x7fff, true, ); } return new Blob([arrayBuffer], { type: "audio/wav" }); } function writeString(view, offset, string) { for (let i = 0; i < string.length; i++) { view.setUint8(offset + i, string.charCodeAt(i)); } } function interleave(left, right) { const length = left.length + right.length; const result = new Float32Array(length); let inputIndex = 0; for (let i = 0; i < length; i += 2) { result[i] = left[inputIndex]; result[i + 1] = right[inputIndex]; inputIndex++; } return result; } function startPolling(jobId) { const pollInterval = setInterval(async () => { try { const data = await (await fetch(`/status/${jobId}`)).json(); progressFill.style.width = data.progress + "%"; if (data.status === "processing") { progressText.textContent = data.current_chunk ? `Đang phiên âm đoạn ${data.current_chunk}...` : `Đang xử lý: ${data.progress}%`; } else if (data.status === "completed") { clearInterval(pollInterval); progressText.textContent = "Hoàn tất!"; showResult(jobId); } else if (data.status === "error") { clearInterval(pollInterval); progressText.textContent = "Lỗi: " + data.error; uploadBtn.disabled = false; } } catch (e) { console.error("Lỗi kiểm tra:", e); } }, 2000); } async function showResult(jobId) { try { const html = await (await fetch(`/view/${jobId}`)).text(); const doc = new DOMParser().parseFromString(html, "text/html"); const content = doc.querySelector(".transcript-content")?.textContent || "Đã tải bản phiên âm"; transcriptPreview.innerHTML = `
${content.substring(0, 300)}...
`; resultArea.style.display = "block"; uploadBtn.disabled = false; document.getElementById("viewFullBtn").onclick = () => window.open(`/view/${jobId}`, "_blank"); document.getElementById("downloadBtn").onclick = () => (window.location.href = `/transcript/${jobId}`); } catch (e) { console.error("Lỗi hiển thị:", e); } } }