Spaces:
Sleeping
Sleeping
| // تعامل با سرور FastAPI | |
| // متغیر وضعیت برای بازتولید زیرنویس | |
| 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'; | |
| document.getElementById('queueStatusMsg').innerText = "در حال آپلود و پردازش اولیه..."; | |
| 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 r = await fetch('/api/upload', {method:'POST', body:fd}); | |
| if (!r.ok) throw new Error("Server Error: " + r.status); | |
| const d = await r.json(); | |
| state.id = d.file_id; state.w = d.width; state.h = d.height; | |
| state.st = { f: 'vazir', fz: 50, col: '#A020F0', bg: '#000000', type: 'solid', y: 150, x: 0, name: 'karaoke_static', radius: 24, paddingX: 5, paddingY: 30 }; | |
| let rawSegs = d.segments.map(s => ({...s, 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; | |
| } | |
| } | |
| } | |
| } | |
| 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); }; | |
| } catch(e) { | |
| console.error(e); | |
| alert("خطا در آپلود یا پردازش: " + e); 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); | |
| const r = await fetch('/api/upload', {method:'POST', body:fd}); | |
| if (!r.ok) throw new Error("خطای سرور: " + r.status); | |
| const d = await r.json(); | |
| // 4. پردازش سگمنتهای جدید | |
| let rawSegs = d.segments.map(s => ({...s, 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; | |
| } | |
| } | |
| } | |
| } | |
| state.id = d.file_id; | |
| state.segs = rawSegs; | |
| // 5. ذخیره و رندر مجدد | |
| renderSegList(); | |
| upd(); | |
| saveProjectToDB(); | |
| // 6. اتمام موفقیت آمیز | |
| 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 = '<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']; | |
| 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 => ({ | |
| id: s.id, | |
| 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 | |
| } | |
| }; | |
| 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 = ""; } |