// متغیر وضعیت برای بازتولید زیرنویس 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'; // تنظیمات UI برای نمایش درصد آپلود 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 { // استفاده از XMLHttpRequest برای دریافت درصد پیشرفت واقعی 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); }); // وقتی آپلود 100 درصد شد، پیام را تغییر میدهیم 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)); // وقفه 2 ثانیه ای 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); // 1 ثانیه تاخیر برای رندر شدن محیط ادیتور }; } catch(e) { console.error(e); alert("خطا در آپلود یا پردازش: " + e.message); loadHome(); } finally { document.getElementById('loader').style.display='none'; document.getElementById('fileIn').value = ''; document.getElementById('queueStatusMsg').innerText = ""; } }; } // تابع جدید: منطق بازتولید زیرنویس با استفاده از Blob ذخیره شده async function regenerateProjectSubtitles() { isRegenerating = true; // فعال کردن فلگ وضعیت // 1. تغییر وضعیت UI شیت به حالت لودینگ 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 { // 2. دریافت فایل ویدیو از دیتابیس const videoBlob = await getVideoBlobFromDB(currentProjectId); if (!videoBlob) throw new Error("فایل ویدیو در دیتابیس یافت نشد"); // 3. ارسال به سرور برای پردازش مجدد const fd = new FormData(); fd.append('file', videoBlob); // ارسال درخواست و دریافت task_id (مانند سیستم آپلود جدید) 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; // 4. حلقه بررسی وضعیت پردازش در پسزمینه while (!isCompleted) { await new Promise(res => setTimeout(res, 2000)); // وقفه 2 ثانیه ای 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; // 5. پردازش سگمنتهای جدید 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; // 6. ذخیره و رندر مجدد renderSegList(); upd(); saveProjectToDB(); // 7. اتمام موفقیت آمیز isRegenerating = false; // غیرفعال کردن فلگ // نمایش انیمیشن تیک داخل شیت برای لحظهای کوتاه stepLoad.style.display = 'none'; stepSuccess.style.display = 'flex'; setTimeout(() => { // بستن شیت closeAllSheets(); // نمایش Toast زیبا در بالا 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 = '
در حال طراحی...'; 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); // پاکسازی دادهها برای ارسال به سرور (رفع خطای 422) 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 = `شما نفر ${d.queue_position} در صف ساخت هستید...`; 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 = ""; }