Zirnavis / static /js /api.js
Opera8's picture
Update static/js/api.js
a61be79 verified
Raw
History Blame Contribute Delete
20.6 kB
// متغیر وضعیت برای بازتولید زیرنویس
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 = '<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);
// پاکسازی داده‌ها برای ارسال به سرور (رفع خطای 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 = `شما نفر <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 = ""; }