|
|
<!DOCTYPE html> |
|
|
<html lang="fa" dir="rtl"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
|
|
<title>AI Subtitle Monster</title> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@100;300;500;700;900&display=swap" rel="stylesheet"> |
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> |
|
|
<style> |
|
|
:root { |
|
|
--bg-dark: #0f172a; |
|
|
--bg-card: #1e293b; |
|
|
--primary: #8b5cf6; |
|
|
--primary-glow: rgba(139, 92, 246, 0.5); |
|
|
--accent: #f43f5e; |
|
|
--text-main: #f8fafc; |
|
|
--text-muted: #94a3b8; |
|
|
--border: #334155; |
|
|
--success: #10b981; |
|
|
} |
|
|
|
|
|
* { box-sizing: border-box; outline: none; -webkit-tap-highlight-color: transparent; } |
|
|
|
|
|
body { |
|
|
font-family: 'Vazirmatn', sans-serif; |
|
|
background-color: var(--bg-dark); |
|
|
color: var(--text-main); |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
overflow-x: hidden; |
|
|
min-height: 100vh; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
} |
|
|
|
|
|
|
|
|
.bg-mesh { |
|
|
position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; |
|
|
background: radial-gradient(circle at 50% 50%, #1e1b4b 0%, #0f172a 100%); |
|
|
overflow: hidden; |
|
|
} |
|
|
.blob { |
|
|
position: absolute; |
|
|
filter: blur(80px); |
|
|
opacity: 0.4; |
|
|
animation: float 10s infinite ease-in-out; |
|
|
} |
|
|
.blob-1 { top: -10%; left: -10%; width: 50vw; height: 50vw; background: var(--primary); } |
|
|
.blob-2 { bottom: -10%; right: -10%; width: 40vw; height: 40vw; background: var(--accent); animation-delay: -5s; } |
|
|
@keyframes float { 0%, 100% { transform: translate(0, 0); } 50% { transform: translate(30px, 50px); } } |
|
|
|
|
|
.app-header { |
|
|
padding: 15px 20px; |
|
|
background: rgba(30, 41, 59, 0.8); |
|
|
backdrop-filter: blur(10px); |
|
|
border-bottom: 1px solid var(--border); |
|
|
display: flex; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
z-index: 100; |
|
|
position: sticky; |
|
|
top: 0; |
|
|
} |
|
|
.brand { font-weight: 900; font-size: 1.4rem; background: linear-gradient(to right, var(--primary), var(--accent)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; } |
|
|
|
|
|
.app-body { |
|
|
flex: 1; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
padding: 20px; |
|
|
max-width: 800px; |
|
|
margin: 0 auto; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
|
|
|
.view { display: none; animation: fadeIn 0.4s ease; } |
|
|
.view.active { display: block; } |
|
|
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } |
|
|
|
|
|
.card { |
|
|
background: var(--bg-card); |
|
|
border: 1px solid var(--border); |
|
|
border-radius: 20px; |
|
|
padding: 25px; |
|
|
margin-bottom: 20px; |
|
|
box-shadow: 0 10px 30px -10px rgba(0,0,0,0.3); |
|
|
} |
|
|
|
|
|
.btn { |
|
|
width: 100%; |
|
|
padding: 16px; |
|
|
border-radius: 14px; |
|
|
border: none; |
|
|
font-weight: 800; |
|
|
font-size: 1.1rem; |
|
|
cursor: pointer; |
|
|
transition: 0.2s; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
gap: 10px; |
|
|
color: white; |
|
|
} |
|
|
.btn-primary { |
|
|
background: linear-gradient(135deg, var(--primary), #4338ca); |
|
|
box-shadow: 0 0 20px var(--primary-glow); |
|
|
} |
|
|
.btn-primary:active { transform: scale(0.98); } |
|
|
|
|
|
|
|
|
.upload-zone { |
|
|
border: 2px dashed var(--border); |
|
|
border-radius: 20px; |
|
|
height: 300px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
justify-content: center; |
|
|
align-items: center; |
|
|
cursor: pointer; |
|
|
transition: 0.3s; |
|
|
background: rgba(255,255,255,0.02); |
|
|
} |
|
|
.upload-zone:hover { border-color: var(--primary); background: rgba(99, 102, 241, 0.05); } |
|
|
.upload-icon { font-size: 4rem; margin-bottom: 20px; color: var(--text-muted); } |
|
|
|
|
|
|
|
|
.segment-container { |
|
|
max-height: 400px; |
|
|
overflow-y: auto; |
|
|
padding-right: 5px; |
|
|
margin-bottom: 25px; |
|
|
border: 1px solid var(--border); |
|
|
border-radius: 12px; |
|
|
padding: 10px; |
|
|
background: rgba(0,0,0,0.2); |
|
|
} |
|
|
|
|
|
.segment-row { |
|
|
background: rgba(255,255,255,0.03); |
|
|
border-radius: 12px; |
|
|
padding: 10px; |
|
|
margin-bottom: 10px; |
|
|
border-right: 3px solid transparent; |
|
|
} |
|
|
.segment-row:focus-within { border-right-color: var(--accent); background: rgba(255,255,255,0.06); } |
|
|
|
|
|
.seg-time { font-size: 0.75rem; color: var(--text-muted); margin-bottom: 5px; font-family: monospace; } |
|
|
|
|
|
.seg-input { |
|
|
width: 100%; |
|
|
background: rgba(0, 0, 0, 0.3); |
|
|
border: 1px solid var(--border); |
|
|
border-radius: 8px; |
|
|
color: #fff; |
|
|
font-size: 1.1rem; |
|
|
font-family: inherit; |
|
|
resize: vertical; |
|
|
padding: 10px; |
|
|
min-height: 60px; |
|
|
line-height: 1.5; |
|
|
} |
|
|
.seg-input:focus { border-color: var(--primary); background: rgba(0, 0, 0, 0.5); } |
|
|
|
|
|
.control-group { margin-bottom: 18px; } |
|
|
.control-label { display: flex; justify-content: space-between; font-size: 0.85rem; color: var(--text-muted); margin-bottom: 8px; } |
|
|
|
|
|
input[type="range"] { width: 100%; height: 6px; background: var(--border); border-radius: 5px; appearance: none; } |
|
|
input[type="range"]::-webkit-slider-thumb { appearance: none; width: 20px; height: 20px; background: var(--primary); border-radius: 50%; cursor: pointer; } |
|
|
input[type="color"] { width: 100%; height: 40px; border: none; border-radius: 8px; cursor: pointer; background: transparent; } |
|
|
|
|
|
.style-chips { display: flex; gap: 10px; overflow-x: auto; padding-bottom: 5px; } |
|
|
.chip { |
|
|
padding: 8px 16px; background: var(--border); border-radius: 20px; |
|
|
font-size: 0.85rem; cursor: pointer; white-space: nowrap; transition: 0.2s; |
|
|
} |
|
|
.chip.active { background: rgba(99, 102, 241, 0.2); color: var(--primary); border: 1px solid var(--primary); } |
|
|
|
|
|
.preview-box { |
|
|
position: relative; width: 100%; aspect-ratio: 16/9; |
|
|
background: #000; border-radius: 12px; margin-bottom: 20px; |
|
|
display: flex; align-items: center; justify-content: center; overflow: hidden; |
|
|
background-image: url('https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?q=80&w=1000&auto=format&fit=crop'); |
|
|
background-size: cover; |
|
|
} |
|
|
.preview-text { |
|
|
position: absolute; text-align: center; pointer-events: none; |
|
|
transition: 0.1s; line-height: 1.4; max-width: 80%; |
|
|
} |
|
|
|
|
|
#inlineResult { |
|
|
margin-top: 30px; |
|
|
border-top: 2px dashed var(--border); |
|
|
padding-top: 20px; |
|
|
text-align: center; |
|
|
display: none; |
|
|
animation: slideDown 0.5s ease; |
|
|
} |
|
|
@keyframes slideDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } } |
|
|
|
|
|
.result-video { width: 100%; border-radius: 12px; margin-top: 10px; box-shadow: 0 10px 30px rgba(0,0,0,0.3); } |
|
|
.dl-btn { |
|
|
display: inline-block; margin-top: 15px; padding: 15px 30px; |
|
|
background: var(--success); color: white; text-decoration: none; |
|
|
border-radius: 12px; font-weight: bold; font-size: 1.1rem; |
|
|
box-shadow: 0 5px 20px rgba(16, 185, 129, 0.3); |
|
|
} |
|
|
|
|
|
|
|
|
.loader-screen { |
|
|
position: fixed; top:0; left:0; width:100%; height:100%; |
|
|
background: rgba(15, 23, 42, 0.95); z-index: 1000; |
|
|
display: none; flex-direction: column; justify-content: center; align-items: center; |
|
|
} |
|
|
.loader-screen.flex { display: flex; } |
|
|
.spinner { width: 50px; height: 50px; border: 5px solid var(--border); border-top: 5px solid var(--primary); border-radius: 50%; animation: spin 1s linear infinite; margin-bottom: 20px; } |
|
|
@keyframes spin { 100% { transform: rotate(360deg); } } |
|
|
|
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
|
|
|
<div class="bg-mesh"><div class="blob blob-1"></div><div class="blob blob-2"></div></div> |
|
|
|
|
|
<div id="loader" class="loader-screen"> |
|
|
<div class="spinner"></div> |
|
|
<h3 id="loaderMsg">در حال پردازش...</h3> |
|
|
</div> |
|
|
|
|
|
<header class="app-header"> |
|
|
<div class="brand"><i class="fa-solid fa-wand-magic-sparkles"></i> زیرنویس (آلفا)</div> |
|
|
</header> |
|
|
|
|
|
<div class="app-body"> |
|
|
|
|
|
|
|
|
<div id="view-upload" class="view active"> |
|
|
<div style="max-width: 600px; margin: 40px auto;"> |
|
|
<div class="card"> |
|
|
<h2 style="text-align: center; color: var(--text-main);">آپلود ویدیو</h2> |
|
|
<div class="upload-zone" onclick="document.getElementById('fileIn').click()"> |
|
|
<i class="fa-solid fa-cloud-arrow-up upload-icon"></i> |
|
|
<h3>انتخاب فایل</h3> |
|
|
<input type="file" id="fileIn" hidden accept="video/*" onchange="handleUpload()"> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="view-editor" class="view"> |
|
|
|
|
|
<div class="card"> |
|
|
|
|
|
|
|
|
<h2 style="color: var(--primary); margin-top:0;">🎨 تنظیمات ظاهری</h2> |
|
|
|
|
|
<div class="preview-box"> |
|
|
<div id="livePreview" class="preview-text">پیشنمایش متن</div> |
|
|
</div> |
|
|
|
|
|
<div class="control-group"> |
|
|
<div class="control-label"><span>فونت</span></div> |
|
|
<div class="style-chips"> |
|
|
<div class="chip active" onclick="setFont('lalezar', this)">لاله زار (فا)</div> |
|
|
<div class="chip" onclick="setFont('vazir', this)">وزیر (فا)</div> |
|
|
<div class="chip" onclick="setFont('roboto', this)">Roboto (Eng)</div> |
|
|
<div class="chip" onclick="setFont('bangers', this)">Bangers (Eng)</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="control-group"> |
|
|
<div class="control-label"><span>رنگ متن</span></div> |
|
|
<input type="color" id="colorMain" value="#FFFFFF" oninput="updatePreview()"> |
|
|
</div> |
|
|
|
|
|
<div class="control-group"> |
|
|
<div class="control-label"><span>رنگ کادر</span></div> |
|
|
<input type="color" id="colorOutline" value="#000000" oninput="updatePreview()"> |
|
|
</div> |
|
|
|
|
|
<div class="control-group"> |
|
|
<div class="control-label"><span>نوع کادر</span></div> |
|
|
<div class="style-chips"> |
|
|
<div class="chip active" onclick="setStyle('solid', this)">پُر رنگ</div> |
|
|
<div class="chip" onclick="setStyle('transparent', this)">شیشهای</div> |
|
|
<div class="chip" onclick="setStyle('outline', this)">حاشیه</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="control-group"> |
|
|
<div class="control-label"><span>سایز متن</span> <span id="lblSize">100</span></div> |
|
|
<input type="range" id="rngSize" min="30" max="400" value="100" oninput="updatePreview()"> |
|
|
</div> |
|
|
|
|
|
<div class="control-group"> |
|
|
<div class="control-label"><span>موقعیت عمودی</span></div> |
|
|
<input type="range" id="rngPos" min="10" max="600" value="150" oninput="updatePreview()"> |
|
|
</div> |
|
|
|
|
|
<hr style="border-color: var(--border); margin: 30px 0;"> |
|
|
|
|
|
|
|
|
<h2 style="color: var(--text-main);">📝 ویرایش متنها</h2> |
|
|
<div id="segmentsList" class="segment-container"></div> |
|
|
|
|
|
|
|
|
<button class="btn btn-primary" onclick="startRender()"> |
|
|
<i class="fa-solid fa-bolt"></i> ساخت ویدیو نهایی |
|
|
</button> |
|
|
|
|
|
|
|
|
<div id="inlineResult"> |
|
|
<h3 style="color: var(--success); margin:0;">✅ ویدیو آماده شد!</h3> |
|
|
<video id="finalPlayer" controls class="result-video"></video> |
|
|
<a id="dlBtn" href="#" download class="dl-btn">دانلود ویدیو</a> |
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<script> |
|
|
let appState = { |
|
|
fileId: null, |
|
|
segments: [], |
|
|
style: { |
|
|
backType: 'solid', |
|
|
font: 'lalezar' |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const STORAGE_KEYS = ['colorMain', 'colorOutline', 'rngSize', 'rngPos']; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
STORAGE_KEYS.forEach(key => { |
|
|
const val = localStorage.getItem(key); |
|
|
if(val) { |
|
|
document.getElementById(key).value = val; |
|
|
if(key === 'rngSize') document.getElementById('lblSize').innerText = val; |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
STORAGE_KEYS.forEach(key => { |
|
|
document.getElementById(key).addEventListener('input', (e) => { |
|
|
localStorage.setItem(key, e.target.value); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
async function handleUpload() { |
|
|
const file = document.getElementById('fileIn').files[0]; |
|
|
if(!file) return; |
|
|
|
|
|
showLoader("در حال استخراج متن..."); |
|
|
|
|
|
const formData = new FormData(); |
|
|
formData.append("file", file); |
|
|
|
|
|
try { |
|
|
const res = await fetch("/api/analyze", { method: "POST", body: formData }); |
|
|
const data = await res.json(); |
|
|
|
|
|
if(data.error) throw new Error(data.error); |
|
|
|
|
|
appState.fileId = data.file_id; |
|
|
appState.segments = data.segments; |
|
|
|
|
|
renderSegments(); |
|
|
|
|
|
document.getElementById('view-upload').classList.remove('active'); |
|
|
document.getElementById('view-editor').classList.add('active'); |
|
|
updatePreview(); |
|
|
|
|
|
window.scrollTo(0, 0); |
|
|
|
|
|
} catch(e) { |
|
|
alert("Error: " + e.message); |
|
|
} finally { |
|
|
hideLoader(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function renderSegments() { |
|
|
const container = document.getElementById('segmentsList'); |
|
|
container.innerHTML = ""; |
|
|
|
|
|
appState.segments.forEach((seg, idx) => { |
|
|
const div = document.createElement('div'); |
|
|
div.className = 'segment-row'; |
|
|
div.innerHTML = ` |
|
|
<div class="seg-time">${formatTime(seg.start)} -> ${formatTime(seg.end)}</div> |
|
|
<textarea class="seg-input" rows="1" oninput="updateSegment(${idx}, this)">${seg.text}</textarea> |
|
|
`; |
|
|
container.appendChild(div); |
|
|
}); |
|
|
} |
|
|
|
|
|
function updateSegment(idx, el) { |
|
|
appState.segments[idx].text = el.value; |
|
|
document.getElementById('livePreview').innerText = el.value; |
|
|
el.style.height = 'auto'; |
|
|
el.style.height = (el.scrollHeight) + 'px'; |
|
|
} |
|
|
|
|
|
function formatTime(s) { |
|
|
const m = Math.floor(s / 60); |
|
|
const sec = Math.floor(s % 60); |
|
|
return `${m}:${sec.toString().padStart(2, '0')}`; |
|
|
} |
|
|
|
|
|
|
|
|
function setStyle(type, el) { |
|
|
appState.style.backType = type; |
|
|
el.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('active')); |
|
|
el.classList.add('active'); |
|
|
updatePreview(); |
|
|
} |
|
|
|
|
|
function setFont(font, el) { |
|
|
appState.style.font = font; |
|
|
el.parentElement.querySelectorAll('.chip').forEach(c => c.classList.remove('active')); |
|
|
el.classList.add('active'); |
|
|
updatePreview(); |
|
|
} |
|
|
|
|
|
function updatePreview() { |
|
|
const txt = document.getElementById('livePreview'); |
|
|
const size = document.getElementById('rngSize').value; |
|
|
const pos = document.getElementById('rngPos').value; |
|
|
const color = document.getElementById('colorMain').value; |
|
|
const outline = document.getElementById('colorOutline').value; |
|
|
|
|
|
let font = 'Vazirmatn'; |
|
|
if(appState.style.font === 'lalezar') font = 'Lalezar'; |
|
|
if(appState.style.font === 'roboto') font = 'Arial'; |
|
|
if(appState.style.font === 'bangers') font = 'Impact'; |
|
|
|
|
|
document.getElementById('lblSize').innerText = size; |
|
|
|
|
|
txt.style.fontFamily = font; |
|
|
txt.style.fontSize = (size / 5) + 'px'; |
|
|
txt.style.color = color; |
|
|
txt.style.bottom = (pos / 6) + 'px'; |
|
|
|
|
|
if(appState.style.backType === 'solid') { |
|
|
txt.style.backgroundColor = outline; |
|
|
txt.style.textShadow = 'none'; |
|
|
txt.style.padding = '2px 8px'; |
|
|
txt.style.borderRadius = '4px'; |
|
|
txt.style.webkitTextStroke = '0px'; |
|
|
} else if (appState.style.backType === 'transparent') { |
|
|
txt.style.backgroundColor = 'rgba(0,0,0,0.6)'; |
|
|
txt.style.textShadow = 'none'; |
|
|
txt.style.padding = '2px 8px'; |
|
|
txt.style.borderRadius = '4px'; |
|
|
txt.style.webkitTextStroke = '0px'; |
|
|
} else { |
|
|
txt.style.backgroundColor = 'transparent'; |
|
|
txt.style.webkitTextStroke = `1px ${outline}`; |
|
|
txt.style.textShadow = `0 0 2px ${outline}`; |
|
|
txt.style.padding = '0'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
async function startRender() { |
|
|
document.getElementById('inlineResult').style.display = 'none'; |
|
|
showLoader("در حال ساخت ویدیو..."); |
|
|
|
|
|
const payload = { |
|
|
file_id: appState.fileId, |
|
|
segments: appState.segments, |
|
|
style: { |
|
|
font: appState.style.font, |
|
|
fontSize: parseInt(document.getElementById('rngSize').value), |
|
|
primaryColor: document.getElementById('colorMain').value, |
|
|
outlineColor: document.getElementById('colorOutline').value, |
|
|
backType: appState.style.backType, |
|
|
outlineWidth: 2.0, |
|
|
marginV: parseInt(document.getElementById('rngPos').value), |
|
|
alignment: 2 |
|
|
} |
|
|
}; |
|
|
|
|
|
try { |
|
|
const res = await fetch("/api/render", { |
|
|
method: "POST", |
|
|
headers: { "Content-Type": "application/json" }, |
|
|
body: JSON.stringify(payload) |
|
|
}); |
|
|
const data = await res.json(); |
|
|
|
|
|
if(data.error) throw new Error(data.error); |
|
|
|
|
|
const resultBox = document.getElementById('inlineResult'); |
|
|
resultBox.style.display = 'block'; |
|
|
|
|
|
document.getElementById('finalPlayer').src = data.url + "?t=" + Date.now(); |
|
|
document.getElementById('dlBtn').href = data.url; |
|
|
|
|
|
resultBox.scrollIntoView({behavior: 'smooth'}); |
|
|
|
|
|
} catch(e) { |
|
|
alert("Render Error: " + e.message); |
|
|
} finally { |
|
|
hideLoader(); |
|
|
} |
|
|
} |
|
|
|
|
|
function showLoader(msg) { |
|
|
document.getElementById('loaderMsg').innerText = msg; |
|
|
document.getElementById('loader').classList.add('flex'); |
|
|
} |
|
|
function hideLoader() { |
|
|
document.getElementById('loader').classList.remove('flex'); |
|
|
} |
|
|
|
|
|
</script> |
|
|
</body> |
|
|
</html> |