Spaces:
Paused
Paused
| <html lang="ar" dir="rtl"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI Video Swarm</title> | |
| <style> | |
| :root { | |
| --bg: #0a0a0f; --bg-card: rgba(15,15,25,0.95); | |
| --primary: #00ffaa; --primary-dim: rgba(0,255,170,0.1); | |
| --secondary: #00aaff; --accent: #ff00aa; | |
| --text: #e0e0e0; --text-dim: #888; --text-dark: #444; | |
| --border: rgba(0,255,170,0.15); --radius: 14px; | |
| --shadow: 0 8px 32px rgba(0,0,0,0.4); | |
| --glow: 0 0 30px rgba(0,255,170,0.08); | |
| } | |
| * { margin:0; padding:0; box-sizing:border-box; } | |
| body { | |
| background: var(--bg); color: var(--text); | |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| min-height: 100vh; line-height: 1.6; | |
| } | |
| .bg-grid { | |
| position: fixed; inset: 0; | |
| background-image: linear-gradient(rgba(0,255,170,0.02) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(0,255,170,0.02) 1px, transparent 1px); | |
| background-size: 60px 60px; pointer-events: none; z-index: 0; | |
| } | |
| .glow-orb { | |
| position: fixed; width: 500px; height: 500px; | |
| border-radius: 50%; pointer-events: none; z-index: 0; | |
| background: radial-gradient(circle, rgba(0,255,170,0.05) 0%, transparent 70%); | |
| top: -100px; right: -100px; | |
| } | |
| /* Header */ | |
| header { | |
| position: sticky; top: 0; z-index: 100; | |
| background: rgba(10,10,15,0.9); backdrop-filter: blur(20px); | |
| border-bottom: 1px solid var(--border); padding: 12px 24px; | |
| display: flex; align-items: center; justify-content: space-between; | |
| } | |
| .brand { display: flex; align-items: center; gap: 12px; } | |
| .brand-icon { | |
| width: 42px; height: 42px; border-radius: 12px; | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 20px; box-shadow: 0 0 20px rgba(0,255,170,0.2); | |
| } | |
| .brand h1 { | |
| font-size: 1.3rem; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| } | |
| .status { | |
| display: flex; align-items: center; gap: 8px; | |
| font-size: 0.85rem; color: var(--text-dim); | |
| } | |
| .status-dot { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: var(--primary); box-shadow: 0 0 8px var(--primary); | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} } | |
| /* Layout */ | |
| .main { position: relative; z-index: 1; max-width: 1400px; margin: 0 auto; padding: 24px; } | |
| .grid { display: grid; grid-template-columns: 1fr 380px; gap: 24px; } | |
| @media (max-width: 1100px) { .grid { grid-template-columns: 1fr; } } | |
| /* Cards */ | |
| .card { | |
| background: var(--bg-card); border: 1px solid var(--border); | |
| border-radius: var(--radius); padding: 24px; | |
| box-shadow: var(--shadow); backdrop-filter: blur(10px); | |
| } | |
| .card-title { | |
| font-size: 1.05rem; font-weight: 600; margin-bottom: 16px; | |
| display: flex; align-items: center; gap: 8px; | |
| color: var(--primary); | |
| } | |
| /* Form Elements */ | |
| label { display: block; font-size: 0.85rem; color: var(--text-dim); margin-bottom: 6px; } | |
| textarea, input[type="text"], input[type="number"], select { | |
| width: 100%; padding: 12px 16px; | |
| background: rgba(255,255,255,0.03); border: 1px solid var(--border); | |
| border-radius: 10px; color: var(--text); font-size: 0.95rem; | |
| outline: none; transition: all 0.3s; font-family: inherit; | |
| } | |
| textarea:focus, input:focus, select:focus { | |
| border-color: var(--primary); | |
| box-shadow: 0 0 15px rgba(0,255,170,0.08); | |
| background: rgba(255,255,255,0.05); | |
| } | |
| textarea { min-height: 100px; resize: vertical; } | |
| .btn { | |
| padding: 12px 24px; border: none; border-radius: 10px; | |
| font-size: 0.95rem; font-weight: 600; cursor: pointer; | |
| transition: all 0.3s; display: inline-flex; align-items: center; gap: 8px; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| color: #0a0a0f; | |
| } | |
| .btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 25px rgba(0,255,170,0.3); } | |
| .btn-secondary { | |
| background: rgba(255,255,255,0.05); color: var(--text); | |
| border: 1px solid var(--border); | |
| } | |
| .btn-secondary:hover { background: rgba(255,255,255,0.08); border-color: var(--primary); } | |
| /* Upload Zone */ | |
| .upload-zone { | |
| border: 2px dashed var(--border); border-radius: var(--radius); | |
| padding: 40px 20px; text-align: center; cursor: pointer; | |
| transition: all 0.3s; position: relative; overflow: hidden; | |
| } | |
| .upload-zone:hover, .upload-zone.dragover { | |
| border-color: var(--primary); background: var(--primary-dim); | |
| } | |
| .upload-zone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; } | |
| .upload-preview { max-width: 100%; max-height: 200px; border-radius: 8px; margin-top: 12px; } | |
| /* Selectors */ | |
| .selector-group { display: flex; gap: 8px; flex-wrap: wrap; } | |
| .selector-btn { | |
| padding: 8px 16px; border: 1px solid var(--border); | |
| background: rgba(255,255,255,0.03); color: var(--text-dim); | |
| border-radius: 8px; cursor: pointer; transition: all 0.3s; | |
| font-size: 0.85rem; | |
| } | |
| .selector-btn:hover { border-color: var(--primary); color: var(--primary); } | |
| .selector-btn.active { | |
| background: linear-gradient(135deg, var(--primary), var(--secondary)); | |
| color: #0a0a0f; border-color: transparent; font-weight: 600; | |
| } | |
| /* Toggle */ | |
| .toggle-row { display: flex; align-items: center; justify-content: space-between; padding: 10px 0; } | |
| .toggle-switch { | |
| width: 44px; height: 24px; border-radius: 12px; | |
| background: rgba(255,255,255,0.1); position: relative; | |
| cursor: pointer; transition: all 0.3s; | |
| } | |
| .toggle-switch::after { | |
| content: ''; position: absolute; width: 20px; height: 20px; | |
| border-radius: 50%; background: #fff; top: 2px; right: 2px; | |
| transition: all 0.3s; | |
| } | |
| .toggle-switch.on { background: var(--primary); } | |
| .toggle-switch.on::after { right: 22px; } | |
| /* Range */ | |
| input[type="range"] { | |
| -webkit-appearance: none; width: 100%; height: 6px; | |
| border-radius: 3px; background: rgba(255,255,255,0.1); outline: none; | |
| } | |
| input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; width: 18px; height: 18px; | |
| border-radius: 50%; background: var(--primary); | |
| cursor: pointer; box-shadow: 0 0 10px rgba(0,255,170,0.3); | |
| } | |
| .range-val { font-size: 0.8rem; color: var(--primary); margin-right: 8px; } | |
| /* Progress */ | |
| .progress-container { margin-top: 16px; } | |
| .progress-bar { | |
| height: 8px; border-radius: 4px; | |
| background: rgba(255,255,255,0.05); overflow: hidden; | |
| } | |
| .progress-fill { | |
| height: 100%; border-radius: 4px; | |
| background: linear-gradient(90deg, var(--primary), var(--secondary)); | |
| transition: width 0.5s ease; width: 0%; | |
| box-shadow: 0 0 10px rgba(0,255,170,0.3); | |
| } | |
| .progress-info { | |
| display: flex; justify-content: space-between; | |
| font-size: 0.8rem; color: var(--text-dim); margin-top: 8px; | |
| } | |
| .stage-list { margin-top: 12px; } | |
| .stage-item { | |
| display: flex; align-items: center; gap: 8px; | |
| padding: 6px 0; font-size: 0.85rem; color: var(--text-dim); | |
| transition: all 0.3s; | |
| } | |
| .stage-item.done { color: var(--primary); } | |
| .stage-item.active { color: var(--secondary); font-weight: 600; } | |
| .stage-dot { | |
| width: 8px; height: 8px; border-radius: 50%; | |
| background: rgba(255,255,255,0.1); flex-shrink: 0; | |
| } | |
| .stage-item.done .stage-dot { background: var(--primary); box-shadow: 0 0 6px var(--primary); } | |
| .stage-item.active .stage-dot { background: var(--secondary); animation: pulse 1s infinite; } | |
| /* Gallery */ | |
| .gallery-grid { | |
| display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | |
| gap: 16px; margin-top: 16px; | |
| } | |
| .gallery-item { | |
| background: var(--bg-card); border: 1px solid var(--border); | |
| border-radius: var(--radius); overflow: hidden; | |
| transition: all 0.3s; position: relative; | |
| } | |
| .gallery-item:hover { transform: translateY(-4px); box-shadow: var(--glow); border-color: var(--primary); } | |
| .gallery-item video { width: 100%; height: 160px; object-fit: cover; display: block; } | |
| .gallery-meta { | |
| padding: 12px; display: flex; justify-content: space-between; | |
| font-size: 0.8rem; color: var(--text-dim); | |
| } | |
| .gallery-meta span { display: flex; align-items: center; gap: 4px; } | |
| /* Collapsible */ | |
| .collapsible-header { | |
| display: flex; align-items: center; justify-content: space-between; | |
| cursor: pointer; padding: 12px 0; border-bottom: 1px solid var(--border); | |
| margin-bottom: 12px; | |
| } | |
| .collapsible-header h3 { font-size: 1rem; color: var(--text); } | |
| .chevron { transition: transform 0.3s; font-size: 0.8rem; } | |
| .collapsible-header.open .chevron { transform: rotate(180deg); } | |
| .collapsible-body { display: none; } | |
| .collapsible-body.open { display: block; } | |
| /* Connection badge */ | |
| .conn-badge { | |
| padding: 4px 12px; border-radius: 20px; font-size: 0.75rem; | |
| background: rgba(0,255,170,0.1); color: var(--primary); | |
| border: 1px solid rgba(0,255,170,0.2); | |
| } | |
| .conn-badge.disconnected { background: rgba(255,0,0,0.1); color: #ff4444; border-color: rgba(255,0,0,0.2); } | |
| /* Toast */ | |
| .toast-container { position: fixed; top: 80px; left: 24px; z-index: 200; display: flex; flex-direction: column; gap: 8px; } | |
| .toast { | |
| padding: 12px 20px; border-radius: 10px; | |
| background: var(--bg-card); border: 1px solid var(--border); | |
| box-shadow: var(--shadow); font-size: 0.9rem; | |
| animation: slideIn 0.3s ease; max-width: 320px; | |
| } | |
| .toast.success { border-color: var(--primary); } | |
| .toast.error { border-color: #ff4444; color: #ff8888; } | |
| @keyframes slideIn { from { transform: translateX(-100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } | |
| /* Loading */ | |
| .spinner { | |
| display: inline-block; width: 16px; height: 16px; | |
| border: 2px solid rgba(0,0,0,0.3); border-top-color: #0a0a0f; | |
| border-radius: 50%; animation: spin 0.8s linear infinite; | |
| } | |
| @keyframes spin { to { transform: rotate(360deg); } } | |
| /* Scrollbar */ | |
| ::-webkit-scrollbar { width: 8px; } | |
| ::-webkit-scrollbar-track { background: var(--bg); } | |
| ::-webkit-scrollbar-thumb { background: rgba(0,255,170,0.2); border-radius: 4px; } | |
| ::-webkit-scrollbar-thumb:hover { background: rgba(0,255,170,0.4); } | |
| </style> | |
| <base target="_blank"> | |
| </head> | |
| <body> | |
| <div class="bg-grid"></div> | |
| <div class="glow-orb"></div> | |
| <header> | |
| <div class="brand"> | |
| <div class="brand-icon">๐ฌ</div> | |
| <h1>AI Video Swarm</h1> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:16px;"> | |
| <div class="status"> | |
| <div class="status-dot" id="connDot"></div> | |
| <span id="connText">ู ุชุตู</span> | |
| </div> | |
| <a href="/logs" class="btn btn-secondary" style="padding:8px 16px;font-size:0.8rem;">๐ ุงูุณุฌูุงุช</a> | |
| <button class="btn btn-secondary" style="padding:8px 16px;font-size:0.8rem;" onclick="logout()">ุฎุฑูุฌ</button> | |
| </div> | |
| </header> | |
| <div class="main"> | |
| <div class="grid"> | |
| <!-- LEFT COLUMN --> | |
| <div class="left-col"> | |
| <!-- Prompt Section --> | |
| <div class="card" style="margin-bottom:20px;"> | |
| <div class="card-title">๐ ุงููุตู (Prompt)</div> | |
| <textarea id="promptInput" placeholder="ุตู ุงูู ุดูุฏ ุงูุฐู ุชุฑูุฏ ุชูููุฏู... ูุฏุนู ุงูุนุฑุจูุฉ ูุงูุฅูุฌููุฒูุฉ"></textarea> | |
| <div style="display:flex;justify-content:space-between;align-items:center;margin-top:12px;"> | |
| <span style="font-size:0.8rem;color:var(--text-dim);" id="charCount">0 ุญุฑู</span> | |
| <button class="btn btn-secondary" onclick="enhancePrompt()"> | |
| โจ ุชุญุณูู ุชููุงุฆู | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Image Upload --> | |
| <div class="card" style="margin-bottom:20px;"> | |
| <div class="card-title">๐ผ๏ธ ุงูุตูุฑุฉ ุงูู ุฑุฌุนูุฉ</div> | |
| <div class="upload-zone" id="uploadZone" onclick="document.getElementById('fileInput').click()"> | |
| <input type="file" id="fileInput" accept="image/*" onchange="handleFile(event)"> | |
| <div id="uploadPlaceholder"> | |
| <div style="font-size:2rem;margin-bottom:8px;">๐ค</div> | |
| <div>ุงุณุญุจ ุตูุฑุฉ ููุง ุฃู ุงููุฑ ููุงุฎุชูุงุฑ</div> | |
| <div style="font-size:0.8rem;color:var(--text-dim);margin-top:4px;">PNG, JPG, WEBP</div> | |
| </div> | |
| <img id="uploadPreview" class="upload-preview" style="display:none;"> | |
| </div> | |
| <div class="toggle-row" style="margin-top:12px;"> | |
| <span>ุชูููุฏ ุตูุฑุฉ ุชููุงุฆูุงู ุนุจุฑ FLUX</span> | |
| <div class="toggle-switch" id="toggleGenImage" onclick="toggleSwitch(this)"></div> | |
| </div> | |
| </div> | |
| <!-- Quality & Duration --> | |
| <div class="card" style="margin-bottom:20px;"> | |
| <div class="card-title">โ๏ธ ุงูุฅุนุฏุงุฏุงุช ุงูุฃุณุงุณูุฉ</div> | |
| <label style="margin-top:8px;">ุงูุฌูุฏุฉ</label> | |
| <div class="selector-group" id="qualityGroup"> | |
| <button class="selector-btn active" data-val="1K">1K (1024ร576)</button> | |
| <button class="selector-btn" data-val="2K">2K (1920ร1080)</button> | |
| <button class="selector-btn" data-val="4K">4K (3840ร2160)</button> | |
| </div> | |
| <label style="margin-top:16px;">ุงูู ุฏุฉ</label> | |
| <div class="selector-group" id="durationGroup"> | |
| <button class="selector-btn active" data-val="5">5 ุซูุงูู</button> | |
| <button class="selector-btn" data-val="10">10 ุซูุงูู</button> | |
| <button class="selector-btn" data-val="15">15 ุซุงููุฉ</button> | |
| <button class="selector-btn" data-val="30">30 ุซุงููุฉ</button> | |
| </div> | |
| <label style="margin-top:16px;">ูุณุจุฉ ุงูุนุฑุถ</label> | |
| <div class="selector-group" id="aspectGroup"> | |
| <button class="selector-btn" data-val="1:1">1:1 ู ุฑุจุน</button> | |
| <button class="selector-btn" data-val="9:16">9:16 ุฑููุฒ</button> | |
| <button class="selector-btn active" data-val="16:9">16:9 ูุงุณุน</button> | |
| <button class="selector-btn" data-val="4:3">4:3 ููุงุณู</button> | |
| <button class="selector-btn" data-val="21:9">21:9 ุณููู ุงุฆู</button> | |
| </div> | |
| </div> | |
| <!-- Advanced Settings --> | |
| <div class="card" style="margin-bottom:20px;"> | |
| <div class="collapsible-header" onclick="toggleCollapse(this)"> | |
| <h3>๐ง ุฅุนุฏุงุฏุงุช ู ุชูุฏู ุฉ</h3> | |
| <span class="chevron">โผ</span> | |
| </div> | |
| <div class="collapsible-body" id="advBody"> | |
| <label>FPS</label> | |
| <div class="selector-group" id="fpsGroup"> | |
| <button class="selector-btn active" data-val="24">24</button> | |
| <button class="selector-btn" data-val="30">30</button> | |
| <button class="selector-btn" data-val="48">48</button> | |
| <button class="selector-btn" data-val="60">60</button> | |
| </div> | |
| <div style="margin-top:16px;"> | |
| <label>CFG Scale <span class="range-val" id="cfgVal">7.0</span></label> | |
| <input type="range" id="cfgRange" min="1" max="15" step="0.5" value="7" oninput="document.getElementById('cfgVal').textContent=this.value"> | |
| </div> | |
| <div style="margin-top:16px;"> | |
| <label>ุฎุทูุงุช ุงูุงุณุชุฏูุงู <span class="range-val" id="stepsVal">30</span></label> | |
| <input type="range" id="stepsRange" min="1" max="100" step="1" value="30" oninput="document.getElementById('stepsVal').textContent=this.value"> | |
| </div> | |
| <div style="margin-top:16px;"> | |
| <label>ููุฉ ุงูุญุฑูุฉ <span class="range-val" id="motionVal">5</span></label> | |
| <input type="range" id="motionRange" min="1" max="10" step="1" value="5" oninput="document.getElementById('motionVal').textContent=this.value"> | |
| </div> | |
| <div style="margin-top:16px;"> | |
| <label>ุงูSeed</label> | |
| <div style="display:flex;gap:8px;"> | |
| <input type="text" id="seedInput" placeholder="ุนุดูุงุฆู" style="flex:1;"> | |
| <button class="btn btn-secondary" onclick="randomSeed()" style="padding:8px 12px;">๐ฒ</button> | |
| </div> | |
| </div> | |
| <div class="toggle-row"><span>ุงุณุชุนุงุฏุฉ ุงููุฌู</span><div class="toggle-switch on" onclick="toggleSwitch(this)"></div></div> | |
| <div class="toggle-row"><span>ุชูุนูู ุงูุฅุทุงุฑุงุช</span><div class="toggle-switch on" onclick="toggleSwitch(this)"></div></div> | |
| <div class="toggle-row"><span>ุชูุจูุฑ ุงูููุฏูู</span><div class="toggle-switch on" onclick="toggleSwitch(this)"></div></div> | |
| <div class="toggle-row"><span>ุชูููุฏ ุตูุช</span><div class="toggle-switch" onclick="toggleSwitch(this)"></div></div> | |
| </div> | |
| </div> | |
| <!-- Action Buttons --> | |
| <div style="display:flex;gap:12px;margin-bottom:20px;"> | |
| <button class="btn btn-secondary" style="flex:1;" onclick="saveSettings()"> | |
| ๐พ ุญูุธ ุงูุฅุนุฏุงุฏุงุช | |
| </button> | |
| <button class="btn btn-primary" style="flex:2;" id="genBtn" onclick="generateVideo()"> | |
| ๐ฌ ุชูููุฏ ุงูููุฏูู | |
| </button> | |
| </div> | |
| </div> | |
| <!-- RIGHT COLUMN --> | |
| <div class="right-col"> | |
| <!-- Progress Panel --> | |
| <div class="card" style="margin-bottom:20px;"> | |
| <div class="card-title">๐ ุญุงูุฉ ุงูุชูููุฏ</div> | |
| <div id="progressPanel"> | |
| <div style="text-align:center;color:var(--text-dim);padding:20px;"> | |
| <div style="font-size:2rem;margin-bottom:8px;">โณ</div> | |
| <div>ูุง ููุฌุฏ ู ูู ุฉ ูุดุทุฉ</div> | |
| <div style="font-size:0.8rem;margin-top:4px;">ุงุถุบุท "ุชูููุฏ ุงูููุฏูู" ููุจุฏุก</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Gallery --> | |
| <div class="card"> | |
| <div class="card-title">๐๏ธ ุงูู ุนุฑุถ</div> | |
| <div id="galleryContainer"> | |
| <div style="text-align:center;color:var(--text-dim);padding:20px;"> | |
| <div style="font-size:2rem;margin-bottom:8px;">๐ฌ</div> | |
| <div>ูุง ุชูุฌุฏ ููุฏูููุงุช ุจุนุฏ</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="toast-container" id="toastContainer"></div> | |
| <script> | |
| // โโ STATE โโ | |
| let ws = null; | |
| let currentJob = null; | |
| let uploadedImage = null; | |
| let settings = {}; | |
| // โโ INIT โโ | |
| document.addEventListener('DOMContentLoaded', () => { | |
| loadSettings(); | |
| initSelectors(); | |
| connectWS(); | |
| loadGallery(); | |
| // Drag & drop | |
| const zone = document.getElementById('uploadZone'); | |
| zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('dragover'); }); | |
| zone.addEventListener('dragleave', () => zone.classList.remove('dragover')); | |
| zone.addEventListener('drop', e => { | |
| e.preventDefault(); zone.classList.remove('dragover'); | |
| if (e.dataTransfer.files.length) handleFile({ target: { files: e.dataTransfer.files } }); | |
| }); | |
| // Char counter | |
| document.getElementById('promptInput').addEventListener('input', e => { | |
| document.getElementById('charCount').textContent = e.target.value.length + ' ุญุฑู'; | |
| }); | |
| }); | |
| // โโ SELECTORS โโ | |
| function initSelectors() { | |
| document.querySelectorAll('.selector-group').forEach(group => { | |
| group.addEventListener('click', e => { | |
| if (!e.target.classList.contains('selector-btn')) return; | |
| group.querySelectorAll('.selector-btn').forEach(b => b.classList.remove('active')); | |
| e.target.classList.add('active'); | |
| }); | |
| }); | |
| } | |
| function getSelectorVal(groupId) { | |
| const active = document.querySelector(`#${groupId} .selector-btn.active`); | |
| return active ? active.dataset.val : null; | |
| } | |
| // โโ TOGGLE โโ | |
| function toggleSwitch(el) { el.classList.toggle('on'); } | |
| function isToggleOn(el) { return el.classList.contains('on'); } | |
| // โโ COLLAPSE โโ | |
| function toggleCollapse(header) { | |
| header.classList.toggle('open'); | |
| document.getElementById('advBody').classList.toggle('open'); | |
| } | |
| // โโ FILE UPLOAD โโ | |
| function handleFile(e) { | |
| const file = e.target.files[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = ev => { | |
| uploadedImage = ev.target.result; | |
| document.getElementById('uploadPreview').src = uploadedImage; | |
| document.getElementById('uploadPreview').style.display = 'block'; | |
| document.getElementById('uploadPlaceholder').style.display = 'none'; | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| // โโ RANDOM SEED โโ | |
| function randomSeed() { | |
| document.getElementById('seedInput').value = Math.floor(Math.random() * 999999999); | |
| } | |
| // โโ ENHANCE PROMPT โโ | |
| async function enhancePrompt() { | |
| const prompt = document.getElementById('promptInput').value.trim(); | |
| if (!prompt) return showToast('ุฃุฏุฎู ูุตูุงู ุฃููุงู', 'error'); | |
| showToast('ุฌุงุฑู ุชุญุณูู ุงููุตู...', 'success'); | |
| // In production: call /api/enhance endpoint | |
| setTimeout(() => { | |
| document.getElementById('promptInput').value = | |
| prompt + 'ุ ุฅุถุงุกุฉ ุณููู ุงุฆูุฉุ ุญุฑูุฉ ุณูุณุฉุ ุชูุงุตูู ุฏูููุฉุ ุฌูุฏุฉ ุนุงููุฉุ 8Kุ ุฅุถุงุกุฉ ุฏุฑุงู ูุฉ'; | |
| showToast('ุชู ุชุญุณูู ุงููุตู!', 'success'); | |
| }, 1500); | |
| } | |
| // โโ SAVE SETTINGS โโ | |
| function saveSettings() { | |
| settings = collectSettings(); | |
| localStorage.setItem('aiVideoSettings', JSON.stringify(settings)); | |
| fetch('/api/settings', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() }, | |
| body: JSON.stringify(settings) | |
| }).catch(() => {}); | |
| showToast('ุชู ุญูุธ ุงูุฅุนุฏุงุฏุงุช!', 'success'); | |
| } | |
| function loadSettings() { | |
| const saved = localStorage.getItem('aiVideoSettings'); | |
| if (saved) settings = JSON.parse(saved); | |
| } | |
| function collectSettings() { | |
| return { | |
| quality: getSelectorVal('qualityGroup'), | |
| duration: parseInt(getSelectorVal('durationGroup')), | |
| aspect: getSelectorVal('aspectGroup'), | |
| fps: parseInt(getSelectorVal('fpsGroup')), | |
| cfg: parseFloat(document.getElementById('cfgRange').value), | |
| steps: parseInt(document.getElementById('stepsRange').value), | |
| motion: parseInt(document.getElementById('motionRange').value), | |
| seed: document.getElementById('seedInput').value || null, | |
| faceRestore: isToggleOn(document.querySelectorAll('.toggle-switch')[1]), | |
| frameInterp: isToggleOn(document.querySelectorAll('.toggle-switch')[2]), | |
| upscaler: isToggleOn(document.querySelectorAll('.toggle-switch')[3]), | |
| audio: isToggleOn(document.querySelectorAll('.toggle-switch')[4]), | |
| }; | |
| } | |
| // โโ GENERATE โโ | |
| async function generateVideo() { | |
| const prompt = document.getElementById('promptInput').value.trim(); | |
| if (!prompt) return showToast('ุฃุฏุฎู ูุตูุงู ููู ุดูุฏ', 'error'); | |
| const btn = document.getElementById('genBtn'); | |
| btn.disabled = true; | |
| btn.innerHTML = '<span class="spinner"></span> ุฌุงุฑู ุงูุชูููุฏ...'; | |
| const params = { | |
| prompt, | |
| image: uploadedImage, | |
| generateImage: isToggleOn(document.getElementById('toggleGenImage')), | |
| ...collectSettings() | |
| }; | |
| try { | |
| const res = await fetch('/api/generate', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() }, | |
| body: JSON.stringify(params) | |
| }); | |
| const data = await res.json(); | |
| if (res.ok) { | |
| currentJob = data.job_id; | |
| showProgressPanel(data.job_id); | |
| showToast('ุชู ุจุฏุก ุงูู ูู ุฉ #' + data.job_id, 'success'); | |
| } else { | |
| showToast(data.detail || 'ุฎุทุฃ ูู ุงูุจุฏุก', 'error'); | |
| } | |
| } catch (e) { | |
| showToast('ุฎุทุฃ ูู ุงูุงุชุตุงู', 'error'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.innerHTML = '๐ฌ ุชูููุฏ ุงูููุฏูู'; | |
| } | |
| } | |
| // โโ PROGRESS UI โโ | |
| const STAGES = [ | |
| { key: 'prompt', label: 'ุชุญุณูู ุงููุตู', icon: 'โจ' }, | |
| { key: 'image', label: 'ุชูููุฏ ุงูุฅุทุงุฑ ุงูุฑุฆูุณู', icon: '๐ผ๏ธ' }, | |
| { key: 'video', label: 'ุชูููุฏ ุงูููุฏูู (Wan 2.6)', icon: '๐ฌ' }, | |
| { key: 'restore', label: 'ุงุณุชุนุงุฏุฉ ุงููุฌู', icon: '๐ค' }, | |
| { key: 'interpolate', label: 'ุชูุนูู ุงูุฅุทุงุฑุงุช', icon: '๐' }, | |
| { key: 'upscale', label: 'ุชูุจูุฑ ุงูุฏูุฉ', icon: '๐' }, | |
| { key: 'encode', label: 'ุชุฑู ูุฒ ุงูููุงุฆู', icon: '๐พ' }, | |
| ]; | |
| function showProgressPanel(jobId) { | |
| const panel = document.getElementById('progressPanel'); | |
| panel.innerHTML = ` | |
| <div class="progress-container"> | |
| <div style="display:flex;justify-content:space-between;margin-bottom:8px;"> | |
| <span style="font-size:0.9rem;font-weight:600;">ุงูู ูู ุฉ #${jobId}</span> | |
| <span style="font-size:0.8rem;color:var(--text-dim);" id="jobStatus">ููุฏ ุงูุงูุชุธุงุฑ</span> | |
| </div> | |
| <div class="progress-bar"><div class="progress-fill" id="progFill"></div></div> | |
| <div class="progress-info"> | |
| <span id="progPercent">0%</span> | |
| <span id="progTime">--</span> | |
| </div> | |
| <div class="stage-list" id="stageList"> | |
| ${STAGES.map((s,i) => ` | |
| <div class="stage-item" id="stage-${i}" data-key="${s.key}"> | |
| <div class="stage-dot"></div> | |
| <span>${s.icon} ${s.label}</span> | |
| </div> | |
| `).join('')} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function updateProgress(data) { | |
| if (!currentJob || data.job_id !== currentJob) return; | |
| const pct = data.progress || 0; | |
| document.getElementById('progFill').style.width = pct + '%'; | |
| document.getElementById('progPercent').textContent = pct + '%'; | |
| document.getElementById('jobStatus').textContent = data.stage || 'ุฌุงุฑู ุงูุนู ู...'; | |
| // Update stages | |
| const stageIdx = STAGES.findIndex(s => s.key === data.stage_key); | |
| STAGES.forEach((s, i) => { | |
| const el = document.getElementById('stage-' + i); | |
| if (!el) return; | |
| el.classList.remove('done', 'active'); | |
| if (i < stageIdx) el.classList.add('done'); | |
| else if (i === stageIdx) el.classList.add('active'); | |
| }); | |
| if (data.status === 'completed') { | |
| document.getElementById('jobStatus').textContent = 'โ ุชู ุงูุงูุชูุงุก'; | |
| showToast('ุชู ุชูููุฏ ุงูููุฏูู ุจูุฌุงุญ!', 'success'); | |
| loadGallery(); | |
| currentJob = null; | |
| } else if (data.status === 'failed') { | |
| document.getElementById('jobStatus').textContent = 'โ ูุดู'; | |
| showToast('ูุดู ุงูุชูููุฏ: ' + (data.error || ''), 'error'); | |
| currentJob = null; | |
| } | |
| } | |
| // โโ WEBSOCKET โโ | |
| function connectWS() { | |
| const token = getToken(); | |
| if (!token) return; | |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| ws = new WebSocket(`${protocol}//${window.location.host}/ws/progress`); | |
| ws.onopen = () => { | |
| document.getElementById('connDot').style.background = 'var(--primary)'; | |
| document.getElementById('connText').textContent = 'ู ุชุตู'; | |
| }; | |
| ws.onmessage = e => { | |
| try { updateProgress(JSON.parse(e.data)); } catch(err) {} | |
| }; | |
| ws.onclose = () => { | |
| document.getElementById('connDot').style.background = '#ff4444'; | |
| document.getElementById('connText').textContent = 'ุบูุฑ ู ุชุตู'; | |
| setTimeout(connectWS, 3000); | |
| }; | |
| ws.onerror = () => ws.close(); | |
| } | |
| // โโ GALLERY โโ | |
| async function loadGallery() { | |
| try { | |
| const res = await fetch('/api/videos', { headers: { 'Authorization': 'Bearer ' + getToken() } }); | |
| const videos = await res.json(); | |
| const container = document.getElementById('galleryContainer'); | |
| if (!videos.length) { | |
| container.innerHTML = ` | |
| <div style="text-align:center;color:var(--text-dim);padding:20px;"> | |
| <div style="font-size:2rem;margin-bottom:8px;">๐ฌ</div> | |
| <div>ูุง ุชูุฌุฏ ููุฏูููุงุช ุจุนุฏ</div> | |
| </div>`; | |
| return; | |
| } | |
| container.innerHTML = '<div class="gallery-grid">' + videos.map(v => { | |
| const name = v.key.split('/').pop(); | |
| return ` | |
| <div class="gallery-item"> | |
| <video src="${v.url}" muted loop onmouseenter="this.play()" onmouseleave="this.pause()"></video> | |
| <div class="gallery-meta"> | |
| <span>๐น ${name}</span> | |
| <span>โฌ๏ธ <a href="${v.url}" download style="color:var(--primary);text-decoration:none;">ุชุญู ูู</a></span> | |
| </div> | |
| </div> | |
| `; | |
| }).join('') + '</div>'; | |
| } catch(e) {} | |
| } | |
| // โโ UTILS โโ | |
| function getToken() { | |
| const match = document.cookie.match(/access_token=([^;]+)/); | |
| return match ? match[1] : ''; | |
| } | |
| function showToast(msg, type='success') { | |
| const container = document.getElementById('toastContainer'); | |
| const toast = document.createElement('div'); | |
| toast.className = 'toast ' + type; | |
| toast.textContent = msg; | |
| container.appendChild(toast); | |
| setTimeout(() => toast.remove(), 4000); | |
| } | |
| function logout() { | |
| document.cookie = 'access_token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT'; | |
| window.location.href = '/'; | |
| } | |
| // Check auth | |
| (async function() { | |
| const token = getToken(); | |
| if (!token) return window.location.href = '/'; | |
| try { | |
| const res = await fetch('/api/auth/verify', { headers: { 'Authorization': 'Bearer ' + token } }); | |
| if (!res.ok) window.location.href = '/'; | |
| } catch(e) { window.location.href = '/'; } | |
| })(); | |
| </script> | |
| </body> | |
| </html> |