| |
| var isRegenerating = false; |
|
|
| async function startUpload() { |
| console.log("Upload started..."); |
| const f = document.getElementById('fileIn').files[0]; |
| if(!f) return; |
|
|
| if (!db) { |
| try { |
| console.log("Waiting for DB..."); |
| await initDB(); |
| } catch(e) { |
| alert("خطا در بارگذاری دیتابیس. لطفا صفحه را رفرش کنید."); |
| return; |
| } |
| } |
|
|
| document.getElementById('homeScreen').style.display = 'none'; |
| document.getElementById('loader').style.display = 'flex'; |
| |
| |
| const titleEl = document.getElementById('loaderTitle'); |
| const msgEl = document.getElementById('queueStatusMsg'); |
| const progContainer = document.getElementById('uploadProgressContainer'); |
| const progBar = document.getElementById('uploadProgressBar'); |
|
|
| if (titleEl) titleEl.innerText = "در حال آپلود ویدیو..."; |
| if (msgEl) msgEl.innerText = "لطفاً این پنجره را نبندید ☘️"; |
| if (progContainer) progContainer.style.display = 'block'; |
| if (progBar) progBar.style.width = '0%'; |
| |
| const txCheck = db.transaction("projects", "readwrite"); |
| const storeCheck = txCheck.objectStore("projects"); |
| const reqAll = storeCheck.getAll(); |
| |
| reqAll.onsuccess = async (e) => { |
| let projects = e.target.result; |
| projects.sort((a, b) => a.id - b.id); |
| if (projects.length >= 4) { |
| const toDelete = projects[0]; |
| storeCheck.delete(toDelete.id); |
| } |
| |
| const defaultName = `پروژه ${projects.length >= 4 ? 4 : projects.length + 1}`; |
| const fd = new FormData(); fd.append('file', f); |
| |
| try { |
| |
| const uploadResponse = await new Promise((resolve, reject) => { |
| const xhr = new XMLHttpRequest(); |
| xhr.open('POST', '/api/upload', true); |
|
|
| xhr.upload.onprogress = (event) => { |
| if (event.lengthComputable) { |
| const percentComplete = Math.round((event.loaded / event.total) * 100); |
| if (progBar) progBar.style.width = percentComplete + '%'; |
| if (msgEl) msgEl.innerText = `در حال ارسال فایل: ${percentComplete}%`; |
| } |
| }; |
|
|
| xhr.onload = () => { |
| if (xhr.status >= 200 && xhr.status < 300) { |
| resolve(JSON.parse(xhr.responseText)); |
| } else { |
| reject(new Error("Server Error: " + xhr.status)); |
| } |
| }; |
|
|
| xhr.onerror = () => reject(new Error("Network Error occurred")); |
| xhr.send(fd); |
| }); |
|
|
| |
| if (progContainer) progContainer.style.display = 'none'; |
| if (titleEl) titleEl.innerText = "در حال تولید زیرنویس..."; |
| if (msgEl) msgEl.innerText = "هوش مصنوعی در حال پردازش صدا است..."; |
|
|
| const taskId = uploadResponse.task_id; |
| let isCompleted = false; |
| let finalData = null; |
|
|
| |
| while (!isCompleted) { |
| await new Promise(r => setTimeout(r, 2000)); |
| const statusRes = await fetch(`/api/upload-status/${taskId}`); |
| if (!statusRes.ok) throw new Error("Status check failed"); |
| const statusData = await statusRes.json(); |
|
|
| if (statusData.status === 'completed') { |
| isCompleted = true; |
| finalData = statusData.result; |
| } else if (statusData.status === 'failed') { |
| throw new Error(statusData.error || "خطا در پردازش سرور"); |
| } |
| } |
|
|
| const d = finalData; |
| |
| state.id = d.file_id; state.w = d.width; state.h = d.height; |
| state.st = { |
| f: 'pinar', fz: 45, col: '#ffffff', bg: '#000000', |
| type: 'solid', y: 150, x: 0, name: 'alpha_gradient', |
| radius: 24, paddingX: 5, paddingY: 15, |
| useActiveColor: true, |
| fadeUnread: false, |
| fadeSurrounding: false |
| }; |
|
|
| let rawSegs = d.segments.map((s, idx) => ({ |
| ...s, |
| id: s.id || `seg_${Date.now()}_${idx}_${Math.floor(Math.random() * 1000)}`, |
| isHidden: false |
| })); |
| rawSegs.sort((a, b) => a.start - b.start); |
| |
| for(let i = 0; i < rawSegs.length - 1; i++) { |
| const curr = rawSegs[i]; const next = rawSegs[i+1]; |
| if (curr.end > next.start) { curr.end = next.start; |
| if (curr.words) { |
| curr.words = curr.words.filter(w => w.start < curr.end); |
| if(curr.words.length > 0) { |
| let lastW = curr.words[curr.words.length - 1]; |
| if(lastW.end > curr.end) lastW.end = curr.end; |
| } |
| } |
| } |
| } |
|
|
| |
| rawSegs.forEach(seg => { |
| if (seg.words && seg.words.length > 0) { |
| seg.words.sort((a,b) => a.start - b.start); |
| seg.start = seg.words[0].start; |
| seg.end = seg.words[seg.words.length - 1].end; |
| } |
| }); |
|
|
| state.segs = rawSegs; |
| |
| const durSec = rawSegs.length > 0 ? rawSegs[rawSegs.length-1].end : 0; |
| let mm = Math.floor(durSec / 60); |
| let ss = Math.floor(durSec % 60); |
| const durStr = `${mm < 10 ? '0'+mm : mm}:${ss < 10 ? '0'+ss : ss}`; |
|
|
| const txSave = db.transaction("projects", "readwrite"); |
| const newProject = { |
| id: Date.now(), name: defaultName, dateStr: getPersianDate(), |
| lastModified: Date.now(), videoBlob: f, state: JSON.parse(JSON.stringify(state)), |
| duration: durStr, thumbnail: null |
| }; |
| |
| txSave.objectStore("projects").add(newProject); |
| txSave.oncomplete = () => { |
| openProject(newProject.id); |
| |
| setTimeout(() => { |
| if (typeof showAITooltip === 'function') { |
| showAITooltip(); |
| } |
| }, 1000); |
| }; |
|
|
| } catch(e) { |
| console.error(e); |
| alert("خطا در آپلود یا پردازش: " + e.message); loadHome(); |
| } finally { |
| document.getElementById('loader').style.display='none'; |
| document.getElementById('fileIn').value = ''; |
| document.getElementById('queueStatusMsg').innerText = ""; |
| } |
| }; |
| } |
|
|
| |
| async function regenerateProjectSubtitles() { |
| isRegenerating = true; |
|
|
| |
| const sheet = document.getElementById('sheet-regenerate'); |
| const stepStart = document.getElementById('regen-step-start'); |
| const stepLoad = document.getElementById('regen-step-loading'); |
| const stepSuccess = document.getElementById('regen-step-success'); |
|
|
| stepStart.style.display = 'none'; |
| stepLoad.style.display = 'flex'; |
| |
| document.getElementById('sheetOverlay').classList.add('show'); |
| sheet.classList.add('active'); |
|
|
| try { |
| |
| const videoBlob = await getVideoBlobFromDB(currentProjectId); |
| if (!videoBlob) throw new Error("فایل ویدیو در دیتابیس یافت نشد"); |
|
|
| |
| const fd = new FormData(); |
| fd.append('file', videoBlob); |
|
|
| |
| const r = await fetch('/api/upload', {method:'POST', body:fd}); |
| if (!r.ok) throw new Error("خطای سرور: " + r.status); |
| const uploadResponse = await r.json(); |
|
|
| const taskId = uploadResponse.task_id; |
| if (!taskId) throw new Error("مشکل در ارتباط با سرور"); |
|
|
| let isCompleted = false; |
| let finalData = null; |
|
|
| |
| while (!isCompleted) { |
| await new Promise(res => setTimeout(res, 2000)); |
| const statusRes = await fetch(`/api/upload-status/${taskId}`); |
| if (!statusRes.ok) throw new Error("خطا در بررسی وضعیت"); |
| const statusData = await statusRes.json(); |
|
|
| if (statusData.status === 'completed') { |
| isCompleted = true; |
| finalData = statusData.result; |
| } else if (statusData.status === 'failed') { |
| throw new Error(statusData.error || "خطا در پردازش سرور"); |
| } |
| } |
|
|
| const d = finalData; |
|
|
| |
| let rawSegs = d.segments.map((s, idx) => ({ |
| ...s, |
| id: s.id || `seg_${Date.now()}_${idx}_${Math.floor(Math.random() * 1000)}`, |
| isHidden: false |
| })); |
| rawSegs.sort((a, b) => a.start - b.start); |
|
|
| for(let i = 0; i < rawSegs.length - 1; i++) { |
| const curr = rawSegs[i]; const next = rawSegs[i+1]; |
| if (curr.end > next.start) { curr.end = next.start; |
| if (curr.words) { |
| curr.words = curr.words.filter(w => w.start < curr.end); |
| if(curr.words.length > 0) { |
| let lastW = curr.words[curr.words.length - 1]; |
| if(lastW.end > curr.end) lastW.end = curr.end; |
| } |
| } |
| } |
| } |
|
|
| |
| rawSegs.forEach(seg => { |
| if (seg.words && seg.words.length > 0) { |
| seg.words.sort((a,b) => a.start - b.start); |
| seg.start = seg.words[0].start; |
| seg.end = seg.words[seg.words.length - 1].end; |
| } |
| }); |
|
|
| state.id = d.file_id; |
| state.segs = rawSegs; |
|
|
| |
| renderSegList(); |
| upd(); |
| saveProjectToDB(); |
|
|
| |
| isRegenerating = false; |
|
|
| |
| stepLoad.style.display = 'none'; |
| stepSuccess.style.display = 'flex'; |
|
|
| setTimeout(() => { |
| |
| closeAllSheets(); |
| |
| |
| if (typeof showToast === 'function') { |
| showToast("زیرنویس مجدد تولید شد!", "fa-solid fa-circle-check"); |
| } |
|
|
| |
| setTimeout(() => { |
| stepSuccess.style.display = 'none'; |
| stepStart.style.display = 'flex'; |
| }, 400); |
| }, 1200); |
|
|
| } catch (e) { |
| console.error(e); |
| isRegenerating = false; |
| alert("خطا در بازتولید زیرنویس: " + e.message); |
| |
| stepLoad.style.display = 'none'; |
| stepStart.style.display = 'flex'; |
| } |
| } |
|
|
| async function generateAIStyle() { |
| const desc = document.getElementById('magicPrompt').value; |
| if (!desc) return; |
| const btn = document.querySelector('.btn-magic-action'); |
| const originalText = btn.innerHTML; |
| btn.innerHTML = '<div class="spinner" style="width:20px;height:20px;border-width:3px;"></div> در حال طراحی...'; |
| btn.disabled = true; |
|
|
| try { |
| const r = await fetch('/api/generate-style', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ description: desc }) |
| }); |
| if (!r.ok) throw new Error('Server returned an error'); |
| const styleData = await r.json(); |
|
|
| const currentStyleName = state.st.name; |
| const nonCustomizableStyles = ['plain_white', 'white_outline', 'auto_director', 'music_player', 'instagram_box', 'alpha_gradient', 'karaoke_static', 'simple_bar', 'dark_edges', 'falling_words']; |
|
|
| state.st.col = styleData.primaryColor; |
| state.st.bg = styleData.outlineColor; |
| state.st.f = styleData.font; |
| state.st.fz = parseInt(styleData.fontSize, 10) || 65; |
|
|
| if (nonCustomizableStyles.includes(currentStyleName)) { |
| setStylePreset('classic', document.querySelector('#customAccordion .style-card')); |
| setMode('solid'); |
| if (!document.getElementById('customAccordion').classList.contains('open')) { |
| toggleCustomAccordion(); |
| } |
| } else { |
| setMode(styleData.backType); |
| } |
|
|
| syncUIWithState(); |
| |
| document.querySelectorAll('.font-btn').forEach(btn => btn.classList.remove('ticked')); |
| const fontBtn = Array.from(document.querySelectorAll('.font-btn')).find(btn => btn.getAttribute('onclick').includes(`'${state.st.f}'`)); |
| if (fontBtn) fontBtn.classList.add('ticked'); |
|
|
| upd(); |
|
|
| toggleTool('style'); |
| showToast("طراحی هوشمند اعمال شد!", "fa-solid fa-wand-magic-sparkles"); |
|
|
| } catch (e) { |
| console.error(e); |
| alert('خطا در ارتباط با هوش مصنوعی. لطفا دوباره تلاش کنید.'); |
| } finally { |
| btn.innerHTML = originalText; |
| btn.disabled = false; |
| } |
| } |
|
|
|
|
| async function render() { |
| v.pause(); togglePlayIcon(false); |
| document.getElementById('loader').style.display='flex'; |
| const statusMsg = document.getElementById('queueStatusMsg'); |
| statusMsg.innerText = "در حال ارسال به صف..."; |
|
|
| const activeSegments = state.segs.filter(s => !s.isHidden); |
| |
| |
| const cleanSegments = activeSegments.map((s, idx) => ({ |
| id: s.id || `seg_${Date.now()}_${idx}_${Math.floor(Math.random() * 1000)}`, |
| start: s.start, |
| end: s.end, |
| text: s.text, |
| words: s.words ? s.words.map(w => ({ |
| word: w.word, |
| start: w.start, |
| end: w.end, |
| highlight: w.highlight || false, |
| color: w.color || null |
| })) : [] |
| })); |
|
|
| |
| let finalW = parseInt(state.w) || 0; |
| let finalH = parseInt(state.h) || 0; |
| |
| |
| if ((finalW === 0 || finalH === 0) && v) { |
| finalW = v.videoWidth || 1080; |
| finalH = v.videoHeight || 1920; |
| } |
|
|
| const pl = { |
| file_id: state.id, |
| segments: cleanSegments, |
| video_width: finalW, |
| video_height: finalH, |
| style: { |
| font: state.st.f, |
| fontSize: state.st.fz, |
| primaryColor: state.st.col, |
| outlineColor: state.st.bg, |
| backType: state.st.type, |
| marginV: state.st.y, |
| x: state.st.x || 0, |
| name: state.st.name, |
| radius: state.st.radius || 16, |
| paddingX: state.st.paddingX || 20, |
| paddingY: state.st.paddingY || 10, |
| useActiveColor: ((state.st.styleActiveWordToggles && state.st.styleActiveWordToggles[state.st.name]) !== false), |
| fadeUnread: state.st.fadeUnread !== false, |
| fadeSurrounding: state.st.fadeSurrounding || false, |
| typewriter: state.st.typewriter || false, |
| styleBgColors: state.st.styleBgColors || {}, |
| styleColors: state.st.styleColors || {}, |
| styleActiveColors: state.st.styleActiveColors || {} |
| } |
| }; |
| |
| try { |
| let r = await fetch('/api/enqueue-render', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(pl)}); |
| let d = await r.json(); |
| |
| if (d.error_code && d.error_code === 'VIDEO_NOT_FOUND') { |
| statusMsg.innerText = "ویدیو در سرور یافت نشد، در حال بارگذاری مجدد..."; |
| const videoBlob = await getVideoBlobFromDB(currentProjectId); |
| const fd = new FormData(); fd.append('file', videoBlob); fd.append('file_id', state.id); |
| const reuploadResponse = await fetch('/api/reupload', { method: 'POST', body: fd }); |
| if (!reuploadResponse.ok) throw new Error('خطا در بارگذاری مجدد ویدیو.'); |
| statusMsg.innerText = "بارگذاری مجدد موفق بود، ارسال دوباره به صف..."; |
| r = await fetch('/api/enqueue-render', {method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(pl)}); |
| d = await r.json(); |
| } |
|
|
| if (d.job_id) { pollStatus(d.job_id); } |
| else { throw new Error(d.error || "خطا لطفاً این پروژه را حذف و ویدیو را مجدداً آپلود کنید."); } |
|
|
| } catch(e) { |
| console.error(e); |
| alert('خطا: ' + e.message); document.getElementById('loader').style.display='none'; |
| } |
| } |
|
|
| function pollStatus(jobId) { |
| const statusMsg = document.getElementById('queueStatusMsg'); |
| const interval = setInterval(async () => { |
| try { |
| const r = await fetch(`/api/job-status/${jobId}`); |
| const d = await r.json(); |
| if (d.status === 'queued') statusMsg.innerHTML = `شما نفر <span style="color:#00e676; font-size:1.3em;">${d.queue_position}</span> در صف ساخت هستید...`; |
| else if (d.status === 'processing') statusMsg.innerText = "نوبت شماست! در حال ساخت ویدیو..."; |
| else if (d.status === 'completed') { clearInterval(interval); showFinalResult(d.url); } |
| else if (d.status === 'failed') { clearInterval(interval); alert("خطا در ساخت ویدیو: " + d.error); document.getElementById('loader').style.display='none'; } |
| } catch (e) { console.error("خطا در چک کردن وضعیت", e); } |
| }, 2500); |
| } |
|
|
| function showFinalResult(url) { |
| document.getElementById('loader').style.display='none'; |
| const resVid = document.getElementById('resultVideo'); |
| resVid.src = url + "?t=" + new Date().getTime(); |
| resVid.load(); |
| const fullUrl = new URL(url, window.location.origin).href; |
| const dlBtn = document.getElementById('downloadBtn'); |
| dlBtn.href = fullUrl; |
| dlBtn.onclick = function(e) { |
| e.preventDefault(); |
| window.parent.postMessage({ type: 'DOWNLOAD_REQUEST', url: fullUrl }, '*'); |
| }; |
| document.getElementById('resultScreen').style.display='flex'; |
| resVid.play(); |
| } |
|
|
| function closeResult() { document.getElementById('resultScreen').style.display='none'; const rv = document.getElementById('resultVideo'); rv.pause(); rv.src = ""; } |