| | <!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> |