| <!DOCTYPE html> |
| <html lang="ro"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> |
| <title>VideoGen Pro · Generator 9:16</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300;400;500;600;700;800;900&display=swap" rel="stylesheet"> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet"> |
| <style> |
| :root { |
| --bg: #02040a; |
| --border: rgba(100, 200, 255, 0.14); |
| --text: #e8edf5; |
| --muted: #9aa3c7; |
| --accent: #64c8ff; |
| --gold: #f5d742; |
| --radius: 22px; |
| --shadow: 0 16px 48px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.04); |
| --bg-main: #02040a; |
| --bg-card: #0b0f19; |
| --bg-input: #1a1f2e; |
| --border-color: #2a3344; |
| --accent-color: #10b981; |
| --text-main: #e8edf5; |
| --text-secondary: #9aa3c7; |
| } |
| * { box-sizing: border-box; margin: 0; padding: 0; } |
| body { |
| background: var(--bg-main); |
| color: var(--text-main); |
| font-family: 'Inter', sans-serif; |
| min-height: 100vh; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| padding: 20px; |
| } |
| .bg { |
| position: fixed; inset: 0; z-index: -2; |
| background: |
| radial-gradient(ellipse at 15% 20%, rgba(100,200,255,0.08), transparent 50%), |
| radial-gradient(ellipse at 85% 10%, rgba(255,107,203,0.07), transparent 45%), |
| radial-gradient(ellipse at 50% 90%, rgba(155,93,229,0.06), transparent 50%), |
| #02040a; |
| } |
| .grid-overlay { |
| position: fixed; inset: 0; z-index: -1; pointer-events: none; |
| background-image: |
| linear-gradient(rgba(100,200,255,0.03) 1px, transparent 1px), |
| linear-gradient(90deg, rgba(100,200,255,0.03) 1px, transparent 1px); |
| background-size: 48px 48px; |
| mask-image: radial-gradient(ellipse at center, black 20%, transparent 75%); |
| } |
| .header { |
| position: sticky; top: 0; z-index: 1000; |
| background: rgba(2, 4, 10, 0.85); |
| backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); |
| border-bottom: 1px solid var(--border); |
| padding: 0 24px; height: 76px; |
| display: flex; align-items: center; justify-content: space-between; |
| margin: 0 auto 20px; width: 100%; max-width: 1400px; |
| } |
| .brand { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.1; } |
| .brand-name { |
| font-size: 1.6rem; font-weight: 800; letter-spacing: 1.5px; |
| color: var(--text); display: flex; align-items: baseline; gap: 6px; |
| } |
| .brand-name span { |
| background: linear-gradient(135deg, #10b981, #64c8ff); |
| -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; |
| } |
| .brand-sub { |
| font-size: 0.55rem; font-weight: 400; letter-spacing: 0.8px; |
| text-transform: uppercase; opacity: 0.85; margin-top: -2px; |
| } |
| .nav-links a { |
| font-size: 0.8rem; font-weight: 500; padding: 8px 18px; border-radius: 40px; |
| border: 1px solid var(--border); color: rgba(255,255,255,0.35); |
| transition: all 0.3s; background: rgba(255,255,255,0.02); text-decoration: none; |
| display: inline-flex; align-items: center; gap: 6px; |
| } |
| .nav-links a:hover { color: #e8edf5; border-color: var(--accent); background: rgba(100,200,255,0.06); } |
| @media (max-width: 768px) { |
| .header { padding: 0 14px; height: 64px; } |
| .brand-name { font-size: 1.25rem; } |
| } |
| .footer { |
| border-top: 1px solid var(--border); |
| text-align: center; padding: 28px 16px 36px; |
| color: #6a6f8a; font-size: 0.7rem; |
| width: 100%; max-width: 1400px; margin: 40px auto 0; |
| } |
| .footer strong { color: var(--muted); } |
| .studio-container { |
| background: var(--bg-card); |
| border: 1px solid var(--border-color); |
| border-radius: var(--radius); |
| width: 100%; max-width: 1140px; |
| display: grid; |
| grid-template-columns: 1fr 380px; |
| gap: 28px; padding: 28px; |
| box-shadow: var(--shadow); |
| } |
| @media (max-width: 860px) { |
| .studio-container { grid-template-columns: 1fr; gap: 22px; padding: 18px; } |
| } |
| .control-panel { display: flex; flex-direction: column; gap: 15px; } |
| .panel-header h1 { font-size: 26px; font-weight: 800; letter-spacing: -0.3px; } |
| .panel-header h1 span { |
| background: linear-gradient(135deg, #10b981, #64c8ff); |
| -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; |
| } |
| .panel-header p { font-size: 13.5px; color: var(--text-secondary); margin-top: 3px; } |
| |
| .mode-switch { |
| display: flex; background: var(--bg-input); border-radius: 40px; |
| padding: 4px; border: 1px solid var(--border-color); gap: 4px; |
| } |
| .mode-btn { |
| flex: 1; padding: 9px 14px; border-radius: 36px; border: none; |
| background: transparent; color: var(--text-secondary); font-size: 13px; |
| font-weight: 600; cursor: pointer; transition: 0.25s; font-family: Inter, sans-serif; |
| display: flex; align-items: center; justify-content: center; gap: 7px; |
| } |
| .mode-btn.active { |
| background: linear-gradient(105deg, #059669, #10b981); |
| color: #02040a; box-shadow: 0 2px 12px rgba(16,185,129,0.3); |
| } |
| .mode-btn:not(.active):hover { color: var(--text-main); background: rgba(255,255,255,0.04); } |
| |
| .info-box { |
| background: rgba(100,200,255,0.06); |
| border: 1px solid rgba(100,200,255,0.18); |
| border-radius: 12px; padding: 11px 13px; |
| font-size: 12px; color: var(--muted); line-height: 1.5; |
| } |
| .info-box strong { color: #64c8ff; } |
| .info-box code { font-size: 11px; background: rgba(0,0,0,0.3); padding: 1px 5px; border-radius: 4px; } |
| |
| .input-group { display: flex; flex-direction: column; gap: 7px; } |
| label { |
| font-size: 11px; font-weight: 600; text-transform: uppercase; |
| letter-spacing: 1px; color: var(--accent-color); |
| display: flex; align-items: center; gap: 6px; |
| } |
| textarea { |
| background: var(--bg-input); border: 1px solid var(--border-color); |
| border-radius: 10px; color: var(--text-main); padding: 12px 14px; |
| font-size: 13.5px; line-height: 1.55; resize: vertical; |
| min-height: 90px; outline: none; font-family: Inter, sans-serif; |
| transition: border-color 0.25s; |
| } |
| textarea:focus { border-color: var(--accent-color); } |
| textarea.sm { min-height: 54px; } |
| |
| .upload-zone { |
| border: 2px dashed var(--border-color); |
| border-radius: 12px; padding: 22px 16px; |
| text-align: center; cursor: pointer; transition: 0.25s; |
| background: rgba(255,255,255,0.02); position: relative; |
| } |
| .upload-zone:hover, .upload-zone.dragover { |
| border-color: var(--accent-color); |
| background: rgba(16,185,129,0.06); |
| } |
| .upload-zone.has-image { border-style: solid; padding: 0; overflow: hidden; } |
| .upload-zone input[type="file"] { display: none; } |
| .upload-placeholder { color: var(--text-secondary); font-size: 13px; line-height: 1.5; } |
| .upload-placeholder i { font-size: 28px; display: block; margin-bottom: 8px; color: var(--border-color); } |
| .upload-preview { |
| width: 100%; max-height: 200px; object-fit: contain; display: none; |
| background: #000; |
| } |
| .upload-zone.has-image .upload-preview { display: block; } |
| .upload-zone.has-image .upload-placeholder { display: none; } |
| .upload-clear { |
| position: absolute; top: 8px; right: 8px; |
| background: rgba(0,0,0,0.7); border: none; color: #fff; |
| width: 28px; height: 28px; border-radius: 50%; cursor: pointer; |
| display: none; align-items: center; justify-content: center; font-size: 12px; |
| z-index: 2; |
| } |
| .upload-zone.has-image .upload-clear { display: flex; } |
| |
| .row { |
| display: flex; flex-wrap: wrap; gap: 10px; align-items: center; |
| } |
| .row label { min-width: 72px; font-size: 11px; } |
| .row input, .row select { |
| flex: 1; min-width: 130px; |
| background: var(--bg-input); border: 1px solid var(--border-color); |
| border-radius: 40px; padding: 8px 14px; |
| color: var(--text-main); font-size: 13px; outline: none; |
| font-family: Inter, sans-serif; |
| } |
| .row input:focus, .row select:focus { border-color: var(--accent-color); } |
| .row select option { background: var(--bg-card); } |
| |
| .examples { display: flex; flex-wrap: wrap; gap: 6px; } |
| .example-chip { |
| font-size: 11px; padding: 5px 11px; border-radius: 20px; |
| background: rgba(255,255,255,0.04); border: 1px solid var(--border-color); |
| color: var(--text-secondary); cursor: pointer; transition: 0.2s; |
| } |
| .example-chip:hover { |
| background: rgba(16,185,129,0.12); border-color: var(--accent-color); color: var(--accent-color); |
| } |
| |
| .btn { |
| display: inline-flex; align-items: center; justify-content: center; |
| gap: 7px; padding: 11px 20px; border-radius: 40px; |
| font-weight: 600; font-size: 13.5px; border: none; |
| cursor: pointer; transition: 0.25s; font-family: Inter, sans-serif; |
| } |
| .btn-primary { |
| background: linear-gradient(105deg, #059669, #10b981); |
| color: #02040a; box-shadow: 0 4px 18px rgba(16,185,129,0.25); |
| } |
| .btn-primary:hover:not(:disabled) { |
| transform: translateY(-2px); box-shadow: 0 8px 28px rgba(16,185,129,0.35); |
| } |
| .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } |
| .btn-secondary { |
| background: rgba(255,255,255,0.06); color: var(--text-secondary); |
| border: 1px solid var(--border-color); |
| } |
| .btn-secondary:hover { background: rgba(255,255,255,0.1); } |
| .btn-test { |
| background: rgba(245,215,66,0.12); color: #f5d742; |
| border: 1px solid rgba(245,215,66,0.2); |
| } |
| .btn-test:hover { background: rgba(245,215,66,0.2); } |
| .btn-proxy { |
| background: rgba(100,200,255,0.10); color: #64c8ff; |
| border: 1px solid rgba(100,200,255,0.2); padding: 6px 13px; font-size: 12px; |
| white-space: nowrap; |
| } |
| .btn-proxy.active { background: rgba(100,200,255,0.25); border-color: #64c8ff; } |
| .btn-proxy.proxy-error { border-color: #ef4444; color: #ef4444; background: rgba(239,68,68,0.1); } |
| .action-row { display: flex; flex-wrap: wrap; gap: 9px; } |
| .action-row .btn { flex: 1; min-width: 95px; } |
| |
| .status-box { display: flex; flex-direction: column; gap: 6px; } |
| .progress-bar-container { |
| background: var(--bg-input); border: 1px solid var(--border-color); |
| border-radius: 4px; height: 9px; overflow: hidden; display: none; |
| } |
| .progress-bar-container.show { display: block; } |
| .progress-bar-fill { |
| background: linear-gradient(90deg, #059669, #10b981); |
| height: 100%; width: 0%; transition: width 0.45s ease; |
| } |
| .status-text { |
| font-size: 12.5px; font-weight: 500; color: var(--text-secondary); |
| min-height: 20px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; |
| } |
| .status-text .timer { color: var(--accent-color); font-weight: 600; font-variant-numeric: tabular-nums; } |
| .status-text.error { color: #ef4444; } |
| .status-text.success { color: var(--accent-color); } |
| .error-detail { |
| font-size: 11.5px; color: #ef4444; word-break: break-word; display: none; |
| white-space: pre-wrap; background: rgba(239,68,68,0.08); padding: 9px 11px; |
| border-radius: 8px; border: 1px solid rgba(239,68,68,0.2); |
| } |
| .warning-box { |
| background: rgba(245,215,66,0.08); border: 1px solid rgba(245,215,66,0.2); |
| border-radius: 10px; padding: 9px 13px; font-size: 12.5px; |
| color: #f5d742; display: none; |
| } |
| .warning-box.show { display: block; } |
| .proxy-row { |
| display: flex; flex-wrap: wrap; gap: 8px; align-items: center; |
| } |
| .proxy-row input { |
| flex: 1; min-width: 130px; |
| background: var(--bg-input); border: 1px solid var(--border-color); |
| border-radius: 40px; padding: 6px 13px; color: var(--text-main); |
| font-size: 12px; outline: none; font-family: Inter, sans-serif; |
| } |
| .proxy-row .proxy-select { |
| flex: 0 0 auto; min-width: 110px; |
| background: var(--bg-input); border: 1px solid var(--border-color); |
| border-radius: 40px; padding: 6px 12px; color: var(--text-main); |
| font-size: 11px; outline: none; font-family: Inter, sans-serif; |
| } |
| |
| .preview-panel { display: flex; flex-direction: column; align-items: center; gap: 11px; } |
| .preview-panel > label { |
| font-size: 11px; letter-spacing: 1px; text-transform: uppercase; |
| color: var(--accent-color); align-self: flex-start; |
| } |
| .viewport-9-16 { |
| background: #02040a; border: 2px dashed var(--border-color); |
| border-radius: 12px; width: 100%; aspect-ratio: 9 / 16; |
| max-height: 560px; display: flex; justify-content: center; |
| align-items: center; position: relative; overflow: hidden; |
| transition: border-color 0.35s; |
| } |
| .viewport-9-16.active { border-style: solid; border-color: var(--accent-color); } |
| .placeholder-text { |
| color: var(--text-secondary); font-size: 13.5px; text-align: center; |
| padding: 18px; pointer-events: none; line-height: 1.55; |
| } |
| .placeholder-text i { font-size: 30px; display: block; margin-bottom: 9px; color: var(--border-color); } |
| video { width: 100%; height: 100%; object-fit: cover; display: none; } |
| .download-btn { |
| display: none; background: transparent; border: 1px solid var(--accent-color); |
| color: var(--accent-color); padding: 8px 18px; border-radius: 40px; |
| font-size: 13px; font-weight: 600; cursor: pointer; |
| text-decoration: none; transition: 0.25s; font-family: Inter, sans-serif; |
| } |
| .download-btn:hover { background: var(--accent-color); color: #02040a; } |
| .hint { font-size: 11px; color: var(--text-secondary); text-align: center; } |
| .hidden { display: none !important; } |
| |
| @media (max-width: 480px) { |
| .action-row .btn { min-width: 100%; } |
| .row { flex-direction: column; align-items: stretch; } |
| .row input, .row select { min-width: unset; } |
| .proxy-row .proxy-select { flex: 1; min-width: 100%; } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="bg"></div> |
| <div class="grid-overlay"></div> |
|
|
| <header class="header"> |
| <div class="brand"> |
| <div class="brand-name">One<span>Studio</span></div> |
| <div class="brand-sub">Hub · Creative Tools</div> |
| </div> |
| <nav class="nav-links"> |
| <a href="index.html" title="Acasă"><i class="fas fa-home"></i></a> |
| </nav> |
| </header> |
|
|
| <div class="studio-container"> |
| <div class="control-panel"> |
| <div class="panel-header"> |
| <h1>Video<span>Gen</span> Pro</h1> |
| <p>Text-to-Video & Image-to-Video · 9:16 · Hugging Face</p> |
| </div> |
|
|
| |
| <div class="mode-switch"> |
| <button class="mode-btn active" id="modeT2V" data-mode="t2v"> |
| <i class="fas fa-font"></i> Text → Video |
| </button> |
| <button class="mode-btn" id="modeI2V" data-mode="i2v"> |
| <i class="fas fa-image"></i> Image → Video |
| </button> |
| </div> |
|
|
| <div class="info-box" id="infoBox"> |
| <strong>Modele suportate (serverless):</strong><br> |
| <strong>T2V:</strong> damo-vilab/text-to-video-ms-1.7b | |
| <strong>I2V:</strong> stabilityai/stable-video-diffusion-img2vid-xt · ali-vilab/i2vgen-xl<br> |
| <span style="color:#64c8ff;font-size:11px;">💡 Dacă ai eroare CORS, activează Proxy-ul de mai jos.</span> |
| </div> |
|
|
| |
| <div class="input-group hidden" id="imageGroup"> |
| <label><i class="fas fa-image"></i> Imagine de start</label> |
| <div class="upload-zone" id="uploadZone"> |
| <input type="file" id="imageInput" accept="image/png,image/jpeg,image/webp"> |
| <div class="upload-placeholder"> |
| <i class="fas fa-cloud-upload-alt"></i> |
| Click sau drag & drop o imagine<br> |
| <span style="font-size:11px;opacity:0.7">PNG / JPG / WEBP · max ~5 MB</span> |
| </div> |
| <img class="upload-preview" id="uploadPreview" alt="preview"> |
| <button class="upload-clear" id="uploadClear" title="Șterge"><i class="fas fa-times"></i></button> |
| </div> |
| </div> |
|
|
| <div class="input-group"> |
| <label for="prompt"><i class="fas fa-pen"></i> Prompt cinematic (engleză)</label> |
| <textarea id="prompt" placeholder="Ex: A majestic dragon flying over a futuristic city...">A majestic dragon flying over a futuristic neon city at night, cinematic lighting, vertical 9:16, highly detailed</textarea> |
| <div class="examples" id="examplePrompts"> |
| <span class="example-chip" data-prompt="A cyberpunk girl walking through rainy Tokyo streets at night, neon reflections, vertical 9:16, cinematic">Cyberpunk Tokyo</span> |
| <span class="example-chip" data-prompt="A majestic eagle soaring over snowy mountains at sunrise, dramatic clouds, vertical 9:16, epic">Eagle Mountains</span> |
| <span class="example-chip" data-prompt="Astronaut floating near a glowing nebula, slow camera push, vertical 9:16, photorealistic">Space</span> |
| <span class="example-chip" data-prompt="A cat wearing sunglasses surfing a giant wave, sunny day, playful, vertical 9:16">Cat Surfing</span> |
| </div> |
| </div> |
|
|
| <div class="input-group"> |
| <label for="negativePrompt"><i class="fas fa-ban"></i> Negative prompt</label> |
| <textarea id="negativePrompt" class="sm">blurry, low quality, distorted faces, watermark, text, logo, oversaturated, ugly</textarea> |
| </div> |
|
|
| <div class="row"> |
| <label><i class="fas fa-key"></i> API Key</label> |
| <input type="password" id="hfApiKey" placeholder="hf_xxxxxxxx" value="" autocomplete="off" /> |
| </div> |
|
|
| <div class="row"> |
| <label><i class="fas fa-cube"></i> Model</label> |
| <select id="modelSelect"> |
| <optgroup label="Text → Video" id="t2vModels"> |
| <option value="damo-vilab/text-to-video-ms-1.7b" selected>damo-vilab/text-to-video-ms-1.7b</option> |
| </optgroup> |
| <optgroup label="Image → Video" id="i2vModels" disabled> |
| <option value="stabilityai/stable-video-diffusion-img2vid-xt" selected>Stable Video Diffusion XT</option> |
| <option value="ali-vilab/i2vgen-xl">I2VGen-XL</option> |
| </optgroup> |
| </select> |
| </div> |
|
|
| <div class="row"> |
| <label><i class="fas fa-film"></i> Cadre</label> |
| <select id="numFrames"> |
| <option value="16">16 (~2s)</option> |
| <option value="24" selected>24 (~3s)</option> |
| <option value="25">25</option> |
| <option value="32">32 (~4s)</option> |
| <option value="48">48 (~6s)</option> |
| </select> |
| </div> |
|
|
| <div class="row"> |
| <label><i class="fas fa-dice"></i> Seed</label> |
| <input type="number" id="seedInput" placeholder="aleatoriu" min="0" max="999999999" /> |
| </div> |
|
|
| <div class="warning-box" id="corsWarning"> |
| <i class="fas fa-exclamation-triangle"></i> |
| <strong>file:// detectat!</strong> Proxy-ul CORS a fost activat automat. |
| </div> |
|
|
| <div class="proxy-row"> |
| <label style="font-size:11px;color:var(--text-secondary);"><i class="fas fa-shield-alt"></i> Proxy</label> |
| <select class="proxy-select" id="proxySelect"> |
| <option value="https://corsproxy.io/?url=">corsproxy.io</option> |
| <option value="https://api.allorigins.win/raw?url=" selected>allorigins.win</option> |
| <option value="https://cors-anywhere.herokuapp.com/">cors-anywhere</option> |
| <option value="https://proxy.cors.sh/">cors.sh</option> |
| </select> |
| <input type="text" id="proxyInput" value="https://api.allorigins.win/raw?url=" /> |
| <button class="btn btn-proxy active" id="proxyToggleBtn"><i class="fas fa-toggle-on"></i> Activ</button> |
| </div> |
|
|
| <div class="action-row"> |
| <button class="btn btn-primary" id="generateBtn"><i class="fas fa-magic"></i> Generează</button> |
| <button class="btn btn-test" id="testApiBtn"><i class="fas fa-vial"></i> Test</button> |
| <button class="btn btn-secondary" id="copyBtn"><i class="fas fa-copy"></i> Copiază</button> |
| <button class="btn btn-secondary" id="clearBtn"><i class="fas fa-undo-alt"></i> Reset</button> |
| </div> |
|
|
| <div class="status-box"> |
| <div class="progress-bar-container" id="progressContainer"> |
| <div class="progress-bar-fill" id="progressBar"></div> |
| </div> |
| <div class="status-text" id="statusText"> |
| <span>Alege modul, scrie promptul și apasă Generează.</span> |
| <span class="timer" id="timerDisplay"></span> |
| </div> |
| <div class="error-detail" id="errorDetail"></div> |
| </div> |
| </div> |
|
|
| <div class="preview-panel"> |
| <label><i class="fas fa-eye"></i> Previzualizare 9:16</label> |
| <div id="viewport" class="viewport-9-16"> |
| <div id="placeholder" class="placeholder-text"> |
| <i class="fas fa-film"></i> |
| Niciun video randat.<br>Introdu un prompt pentru a începe. |
| </div> |
| <video id="videoResult" controls loop playsinline></video> |
| </div> |
| <a id="downloadBtn" class="download-btn" download="videogen_9_16.mp4"> |
| <i class="fas fa-download"></i> Descarcă MP4 |
| </a> |
| <div class="hint"><i class="fas fa-info-circle"></i> Proxy activat implicit pentru file://.</div> |
| </div> |
| </div> |
|
|
| <footer class="footer"> |
| <p>© 2026 <strong>OneStudio Hub</strong> · Update 07.08.2026 · T2V + I2V</p> |
| </footer> |
|
|
| <script> |
| (function () { |
| 'use strict'; |
| |
| |
| const promptInput = document.getElementById('prompt'); |
| const negativeInput = document.getElementById('negativePrompt'); |
| const hfKeyInput = document.getElementById('hfApiKey'); |
| const modelSelect = document.getElementById('modelSelect'); |
| const numFramesSelect = document.getElementById('numFrames'); |
| const seedInput = document.getElementById('seedInput'); |
| const proxyInput = document.getElementById('proxyInput'); |
| const proxySelect = document.getElementById('proxySelect'); |
| const proxyToggleBtn = document.getElementById('proxyToggleBtn'); |
| const corsWarning = document.getElementById('corsWarning'); |
| const generateBtn = document.getElementById('generateBtn'); |
| const testApiBtn = document.getElementById('testApiBtn'); |
| const copyBtn = document.getElementById('copyBtn'); |
| const clearBtn = document.getElementById('clearBtn'); |
| const statusText = document.getElementById('statusText'); |
| const errorDetail = document.getElementById('errorDetail'); |
| const progressContainer = document.getElementById('progressContainer'); |
| const progressBar = document.getElementById('progressBar'); |
| const viewport = document.getElementById('viewport'); |
| const placeholder = document.getElementById('placeholder'); |
| const videoResult = document.getElementById('videoResult'); |
| const downloadBtn = document.getElementById('downloadBtn'); |
| const imageGroup = document.getElementById('imageGroup'); |
| const uploadZone = document.getElementById('uploadZone'); |
| const imageInput = document.getElementById('imageInput'); |
| const uploadPreview = document.getElementById('uploadPreview'); |
| const uploadClear = document.getElementById('uploadClear'); |
| const modeT2V = document.getElementById('modeT2V'); |
| const modeI2V = document.getElementById('modeI2V'); |
| const t2vModels = document.getElementById('t2vModels'); |
| const i2vModels = document.getElementById('i2vModels'); |
| |
| |
| let mode = 't2v'; |
| let isGenerating = false; |
| let currentBlobUrl = null; |
| let useProxy = true; |
| let timerInterval = null; |
| let startTime = 0; |
| let selectedImageFile = null; |
| let selectedImageBase64 = null; |
| |
| |
| const savedKey = localStorage.getItem('hf_api_key'); |
| if (savedKey) hfKeyInput.value = savedKey; |
| |
| hfKeyInput.addEventListener('change', () => { |
| const val = hfKeyInput.value.trim(); |
| if (val) localStorage.setItem('hf_api_key', val); |
| else localStorage.removeItem('hf_api_key'); |
| }); |
| |
| |
| proxySelect.addEventListener('change', () => { |
| proxyInput.value = proxySelect.value; |
| }); |
| proxyInput.addEventListener('input', () => { |
| |
| const options = proxySelect.options; |
| let matched = false; |
| for (let opt of options) { |
| if (opt.value === proxyInput.value) { |
| proxySelect.value = opt.value; |
| matched = true; |
| break; |
| } |
| } |
| if (!matched) { |
| |
| const opt = document.createElement('option'); |
| opt.value = proxyInput.value; |
| opt.textContent = 'personalizat'; |
| proxySelect.appendChild(opt); |
| proxySelect.value = proxyInput.value; |
| } |
| }); |
| |
| |
| function setMode(newMode) { |
| mode = newMode; |
| modeT2V.classList.toggle('active', mode === 't2v'); |
| modeI2V.classList.toggle('active', mode === 'i2v'); |
| imageGroup.classList.toggle('hidden', mode !== 'i2v'); |
| |
| t2vModels.disabled = mode !== 't2v'; |
| i2vModels.disabled = mode !== 'i2v'; |
| |
| if (mode === 't2v') { |
| modelSelect.value = 'damo-vilab/text-to-video-ms-1.7b'; |
| } else { |
| modelSelect.value = 'stabilityai/stable-video-diffusion-img2vid-xt'; |
| } |
| setStatus(mode === 't2v' ? 'Mod Text → Video activ.' : 'Mod Image → Video activ. Încarcă o imagine.'); |
| } |
| modeT2V.addEventListener('click', () => setMode('t2v')); |
| modeI2V.addEventListener('click', () => setMode('i2v')); |
| |
| |
| uploadZone.addEventListener('click', (e) => { |
| if (e.target === uploadClear || uploadClear.contains(e.target)) return; |
| imageInput.click(); |
| }); |
| uploadClear.addEventListener('click', (e) => { |
| e.stopPropagation(); |
| clearImage(); |
| }); |
| |
| imageInput.addEventListener('change', () => { |
| if (imageInput.files?.[0]) handleImageFile(imageInput.files[0]); |
| }); |
| |
| ['dragenter', 'dragover'].forEach(ev => { |
| uploadZone.addEventListener(ev, (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); }); |
| }); |
| ['dragleave', 'drop'].forEach(ev => { |
| uploadZone.addEventListener(ev, (e) => { e.preventDefault(); uploadZone.classList.remove('dragover'); }); |
| }); |
| uploadZone.addEventListener('drop', (e) => { |
| const file = e.dataTransfer?.files?.[0]; |
| if (file && file.type.startsWith('image/')) handleImageFile(file); |
| }); |
| |
| function handleImageFile(file) { |
| if (file.size > 6 * 1024 * 1024) { |
| setStatus('Imaginea e prea mare (max ~5-6 MB).', 'error'); |
| return; |
| } |
| selectedImageFile = file; |
| const reader = new FileReader(); |
| reader.onload = (e) => { |
| selectedImageBase64 = e.target.result; |
| uploadPreview.src = selectedImageBase64; |
| uploadZone.classList.add('has-image'); |
| setStatus('Imagine încărcată: ' + file.name); |
| }; |
| reader.readAsDataURL(file); |
| } |
| |
| function clearImage() { |
| selectedImageFile = null; |
| selectedImageBase64 = null; |
| uploadPreview.src = ''; |
| imageInput.value = ''; |
| uploadZone.classList.remove('has-image'); |
| setStatus('Imagine ștearsă.'); |
| } |
| |
| |
| if (window.location.protocol === 'file:') { |
| corsWarning.classList.add('show'); |
| useProxy = true; |
| proxyToggleBtn.innerHTML = '<i class="fas fa-toggle-on"></i> Activ'; |
| proxyToggleBtn.classList.add('active'); |
| } |
| |
| |
| document.querySelectorAll('.example-chip').forEach(chip => { |
| chip.addEventListener('click', () => { |
| promptInput.value = chip.dataset.prompt; |
| setStatus('Prompt exemplu încărcat.'); |
| }); |
| }); |
| |
| |
| function updateProxyUI() { |
| proxyToggleBtn.innerHTML = useProxy ? '<i class="fas fa-toggle-on"></i> Activ' : '<i class="fas fa-toggle-off"></i> Inactiv'; |
| proxyToggleBtn.classList.toggle('active', useProxy); |
| if (useProxy) corsWarning.classList.remove('show'); |
| setStatus(useProxy ? 'Proxy CORS activat.' : 'Proxy CORS dezactivat.'); |
| } |
| |
| proxyToggleBtn.addEventListener('click', () => { |
| useProxy = !useProxy; |
| updateProxyUI(); |
| }); |
| |
| |
| copyBtn.addEventListener('click', () => { |
| const text = promptInput.value.trim(); |
| if (!text) { setStatus('Prompt gol!', 'error'); return; } |
| copyToClipboard(text, 'Prompt copiat!'); |
| }); |
| clearBtn.addEventListener('click', () => { |
| promptInput.value = ''; |
| negativeInput.value = 'blurry, low quality, distorted faces, watermark, text, logo, oversaturated, ugly'; |
| seedInput.value = ''; |
| clearImage(); |
| setStatus('Resetat.'); |
| }); |
| |
| function copyToClipboard(text, msg) { |
| if (navigator.clipboard?.writeText) { |
| navigator.clipboard.writeText(text).then(() => setStatus(msg)).catch(() => fallbackCopy(text, msg)); |
| } else fallbackCopy(text, msg); |
| } |
| function fallbackCopy(text, msg) { |
| const ta = document.createElement('textarea'); |
| ta.value = text; ta.style.cssText = 'position:fixed;opacity:0'; |
| document.body.appendChild(ta); ta.select(); |
| try { document.execCommand('copy'); setStatus(msg); } catch (e) { setStatus('Nu s-a putut copia.', 'error'); } |
| document.body.removeChild(ta); |
| } |
| |
| |
| function setStatus(msg, type = 'info', detail = '') { |
| statusText.innerHTML = `<span>${msg}</span><span class="timer" id="timerDisplay"></span>`; |
| statusText.className = 'status-text'; |
| if (type === 'error') statusText.classList.add('error'); |
| if (type === 'success') statusText.classList.add('success'); |
| errorDetail.textContent = detail || ''; |
| errorDetail.style.display = detail ? 'block' : 'none'; |
| } |
| function setProgress(pct) { |
| progressContainer.classList.add('show'); |
| progressBar.style.width = Math.min(100, pct) + '%'; |
| } |
| function hideProgress() { |
| progressContainer.classList.remove('show'); |
| progressBar.style.width = '0%'; |
| stopTimer(); |
| } |
| function startTimer() { |
| startTime = performance.now(); |
| const td = document.getElementById('timerDisplay'); |
| if (td) td.textContent = '⏳ 0.0s'; |
| if (timerInterval) clearInterval(timerInterval); |
| timerInterval = setInterval(() => { |
| const el = document.getElementById('timerDisplay'); |
| if (el) el.textContent = `⏳ ${((performance.now() - startTime) / 1000).toFixed(1)}s`; |
| }, 100); |
| } |
| function stopTimer() { |
| if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } |
| } |
| |
| |
| function buildApiUrl(model) { |
| const base = `https://api-inference.huggingface.co/models/${model}`; |
| if (useProxy) { |
| let proxy = (proxyInput.value.trim() || 'https://api.allorigins.win/raw?url='); |
| |
| if (!proxy.includes('url=')) { |
| proxy = proxy.endsWith('/') ? proxy + '?url=' : proxy + (proxy.includes('?') ? '&url=' : '?url='); |
| } |
| return proxy + encodeURIComponent(base); |
| } |
| return base; |
| } |
| |
| |
| function dataURLtoBlob(dataurl) { |
| const arr = dataurl.split(','); |
| const mime = arr[0].match(/:(.*?);/)[1]; |
| const bstr = atob(arr[1]); |
| let n = bstr.length; |
| const u8arr = new Uint8Array(n); |
| while (n--) u8arr[n] = bstr.charCodeAt(n); |
| return new Blob([u8arr], { type: mime }); |
| } |
| |
| |
| async function generateVideo() { |
| if (isGenerating) return; |
| |
| const prompt = promptInput.value.trim(); |
| if (!prompt) { |
| setStatus('Scrie un prompt!', 'error'); |
| return; |
| } |
| if (mode === 'i2v' && !selectedImageBase64) { |
| setStatus('Încarcă o imagine pentru Image → Video!', 'error'); |
| return; |
| } |
| |
| const apiKey = hfKeyInput.value.trim(); |
| if (!apiKey) { |
| setStatus('Introdu cheia Hugging Face API! (hf_...)', 'error'); |
| return; |
| } |
| if (!apiKey.startsWith('hf_')) { |
| setStatus('Cheia trebuie să înceapă cu "hf_".', 'error'); |
| return; |
| } |
| |
| isGenerating = true; |
| generateBtn.disabled = true; |
| testApiBtn.disabled = true; |
| setProgress(5); |
| setStatus(mode === 'i2v' ? 'Se trimite imaginea + prompt...' : 'Se trimite cererea...'); |
| startTimer(); |
| |
| videoResult.style.display = 'none'; |
| placeholder.style.display = 'block'; |
| downloadBtn.style.display = 'none'; |
| viewport.classList.remove('active'); |
| if (currentBlobUrl) { URL.revokeObjectURL(currentBlobUrl); currentBlobUrl = null; } |
| errorDetail.style.display = 'none'; |
| |
| const model = modelSelect.value; |
| const url = buildApiUrl(model); |
| const numFrames = parseInt(numFramesSelect.value, 10) || 24; |
| const seed = seedInput.value ? parseInt(seedInput.value, 10) : Math.floor(Math.random() * 1e9); |
| const negative = negativeInput.value.trim(); |
| const finalPrompt = `${prompt}, vertical 9:16 aspect ratio, portrait orientation, cinematic`; |
| |
| |
| const progressSteps = [ |
| { t: 800, p: 15, m: 'Modelul se încarcă...' }, |
| { t: 2500, p: 32, m: 'Se procesează...' }, |
| { t: 5000, p: 52, m: 'Se randează cadrele...' }, |
| { t: 9000, p: 70, m: 'Rendere finală...' }, |
| { t: 14000, p: 84, m: 'Aproape gata...' } |
| ]; |
| const timeouts = progressSteps.map(s => |
| setTimeout(() => { if (isGenerating) { setProgress(s.p); setStatus(s.m); } }, s.t) |
| ); |
| |
| try { |
| const startReq = performance.now(); |
| let response; |
| |
| if (mode === 'i2v') { |
| const isSVD = model.includes('stable-video-diffusion'); |
| |
| if (isSVD) { |
| const blobImg = dataURLtoBlob(selectedImageBase64); |
| response = await fetch(url, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${apiKey}`, |
| 'Content-Type': 'application/octet-stream', |
| 'x-wait-for-model': 'true' |
| }, |
| body: blobImg |
| }); |
| } else { |
| const body = { |
| inputs: selectedImageBase64, |
| parameters: { |
| prompt: finalPrompt, |
| negative_prompt: negative || undefined, |
| num_frames: numFrames, |
| guidance_scale: 7.5, |
| seed: seed |
| } |
| }; |
| response = await fetch(url, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${apiKey}`, |
| 'Content-Type': 'application/json', |
| 'x-wait-for-model': 'true' |
| }, |
| body: JSON.stringify(body) |
| }); |
| } |
| } else { |
| const body = { |
| inputs: finalPrompt, |
| parameters: { |
| num_frames: numFrames, |
| guidance_scale: 7.5, |
| negative_prompt: negative || undefined, |
| seed: seed |
| } |
| }; |
| response = await fetch(url, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${apiKey}`, |
| 'Content-Type': 'application/json', |
| 'x-wait-for-model': 'true' |
| }, |
| body: JSON.stringify(body) |
| }); |
| } |
| |
| timeouts.forEach(clearTimeout); |
| const elapsed = ((performance.now() - startReq) / 1000).toFixed(1); |
| |
| if (!response.ok) { |
| let errMsg = '', detail = ''; |
| try { |
| const errData = await response.json(); |
| errMsg = errData.error || errData.message || JSON.stringify(errData); |
| detail = JSON.stringify(errData, null, 2); |
| } catch (_) { |
| errMsg = response.statusText || 'Eroare necunoscută'; |
| } |
| |
| if (response.status === 401 || response.status === 403) { |
| setStatus('❌ Cheie invalidă sau fără acces.', 'error', |
| 'Verifică token-ul pe https://huggingface.co/settings/tokens (trebuie acces Inference API).'); |
| } else if (response.status === 503) { |
| setStatus('⏳ Modelul se încarcă (cold start). Reîncearcă în 20-40s.', 'error', |
| 'A doua încercare e de obicei mai rapidă.'); |
| } else if (response.status === 429) { |
| setStatus('⏳ Rate limit. Așteaptă 1-2 minute.', 'error'); |
| } else if (response.status === 410) { |
| setStatus('❌ Endpoint dezafectat. Încearcă alt model.', 'error'); |
| } else { |
| setStatus('❌ Eroare API: ' + errMsg, 'error', detail || 'Status: ' + response.status); |
| } |
| return; |
| } |
| |
| const contentType = response.headers.get('content-type') || ''; |
| let blob; |
| |
| if (contentType.includes('application/json')) { |
| const data = await response.json(); |
| if (data.error) { |
| setStatus('❌ ' + data.error, 'error', JSON.stringify(data, null, 2)); |
| return; |
| } |
| const videoUrl = data.video || data.output || data.url || (Array.isArray(data) && data[0]); |
| if (typeof videoUrl === 'string' && videoUrl.startsWith('http')) { |
| setStatus('Se descarcă video-ul...'); |
| const vidResp = await fetch(videoUrl); |
| blob = await vidResp.blob(); |
| } else { |
| setStatus('❌ Răspuns JSON fără video.', 'error', JSON.stringify(data, null, 2)); |
| return; |
| } |
| } else { |
| blob = await response.blob(); |
| } |
| |
| if (!blob || blob.size < 800) { |
| setStatus('❌ Răspuns gol sau prea mic.', 'error'); |
| return; |
| } |
| |
| currentBlobUrl = URL.createObjectURL(blob); |
| setProgress(100); |
| setStatus(`✅ Video generat! (${elapsed}s · seed ${seed})`, 'success'); |
| |
| placeholder.style.display = 'none'; |
| videoResult.src = currentBlobUrl; |
| videoResult.style.display = 'block'; |
| videoResult.load(); |
| videoResult.play().catch(() => {}); |
| viewport.classList.add('active'); |
| |
| const sizeMB = (blob.size / (1024 * 1024)).toFixed(1); |
| downloadBtn.href = currentBlobUrl; |
| downloadBtn.download = `videogen_${mode}_${Date.now()}.mp4`; |
| downloadBtn.style.display = 'inline-block'; |
| downloadBtn.innerHTML = `<i class="fas fa-download"></i> Descarcă MP4 (${sizeMB} MB)`; |
| |
| } catch (err) { |
| console.error(err); |
| const msg = err.message || 'Conexiune eșuată'; |
| if (/fetch|network|Failed to fetch|CORS/i.test(msg)) { |
| setStatus('❌ Eroare de rețea / CORS.', 'error', |
| `Proxy-ul este ${useProxy ? 'ACTIV' : 'INACTIV'}.\n` + |
| `Proxy URL: ${proxyInput.value}\n\n` + |
| `Soluții:\n` + |
| `1. Activează Proxy-ul (butonul de mai sus)\n` + |
| `2. Încearcă alt proxy din dropdown\n` + |
| `3. Rulează cu Live Server (VS Code) sau python -m http.server\n` + |
| `4. Dacă ești online, folosește https://corsproxy.io/ sau https://api.allorigins.win/`); |
| } else { |
| setStatus('❌ Eroare: ' + msg, 'error'); |
| } |
| } finally { |
| isGenerating = false; |
| generateBtn.disabled = false; |
| testApiBtn.disabled = false; |
| hideProgress(); |
| } |
| } |
| |
| |
| async function testApi() { |
| const apiKey = hfKeyInput.value.trim(); |
| if (!apiKey) { setStatus('Introdu cheia API!', 'error'); return; } |
| if (!apiKey.startsWith('hf_')) { setStatus('Cheia trebuie să înceapă cu "hf_".', 'error'); return; } |
| |
| setStatus('Testare conexiune...'); |
| const model = modelSelect.value; |
| const url = buildApiUrl(model); |
| |
| try { |
| const resp = await fetch(url, { |
| method: 'POST', |
| headers: { |
| 'Authorization': `Bearer ${apiKey}`, |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ inputs: 'test connection' }) |
| }); |
| |
| if (resp.status === 200 || resp.status === 503) { |
| setStatus('✅ Conexiune OK (cheia e acceptată).', 'success'); |
| } else if (resp.status === 401 || resp.status === 403) { |
| setStatus('❌ Cheie invalidă sau fără acces.', 'error'); |
| } else if (resp.status === 410) { |
| setStatus('❌ Endpoint dezafectat. Schimbă modelul.', 'error'); |
| } else if (resp.status === 404) { |
| setStatus('❌ Modelul nu există sau nu e accesibil.', 'error'); |
| } else { |
| setStatus(`⚠️ Status ${resp.status}. Poate fi model cold.`, 'info'); |
| } |
| } catch (err) { |
| const msg = err.message || 'Conexiune eșuată'; |
| if (/fetch|network|Failed to fetch|CORS/i.test(msg)) { |
| setStatus('❌ Eroare de rețea / CORS la test.', 'error', |
| `Proxy activ: ${useProxy ? 'DA' : 'NU'}\n` + |
| `Proxy URL: ${proxyInput.value}\n\n` + |
| `Activează Proxy-ul sau rulează cu Live Server.`); |
| } else { |
| setStatus('❌ Eroare: ' + msg, 'error'); |
| } |
| } |
| } |
| |
| |
| generateBtn.addEventListener('click', generateVideo); |
| testApiBtn.addEventListener('click', testApi); |
| document.addEventListener('keydown', (e) => { |
| if (e.target?.tagName === 'INPUT' || e.target?.tagName === 'TEXTAREA') return; |
| if ((e.key === 'g' || e.key === 'G') && !isGenerating) generateBtn.click(); |
| }); |
| |
| |
| setStatus('Proxy activat implicit. Dacă ai erori, încearcă alt proxy din dropdown.'); |
| console.log('🎬 VideoGen Pro v3.2 · Proxy activat · Modele suportate'); |
| })(); |
| </script> |
| </body> |
| </html> |