Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Auto Video Editor</title> | |
| <link rel="icon" | |
| href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 512 512%22><path fill=%22%2320f3b4%22 d=%22M448 32H64C28.7 32 0 60.7 0 96v320c0 35.3 28.7 64 64 64h384c35.3 0 64-28.7 64-64V96c0-35.3-28.7-64-64-64zm-52.2 64l-32 64H271.8l32-64h92zm-144 0l-32 64H127.8l32-64h92zM64 96h19.8l-32 64H64V96zm384 320H64V192h384v224z%22/></svg>"> | |
| <link | |
| href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@300;500;800&family=Work+Sans:ital,wght@0,100..900;1,100..900&display=swap" | |
| rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"> | |
| <style> | |
| :root { | |
| --glass: rgba(255, 255, 255, 0.03); | |
| --glass-border: rgba(255, 255, 255, 0.12); | |
| --chlorophyll: #20f3b4; | |
| /* Greenish Blue / Teal */ | |
| --deep-forest: #071512; | |
| --prism-1: rgba(32, 243, 180, 0.15); | |
| --prism-2: rgba(4, 120, 87, 0.1); | |
| --prism-3: rgba(6, 182, 212, 0.1); | |
| --text-main: #e2e8f0; | |
| --text-dim: #94a3b8; | |
| --font-sans: 'Plus Jakarta Sans', sans-serif; | |
| --font-mono: 'Work Sans', monospace; | |
| --success-color: #00ff9d; | |
| --error-color: #ff4d4d; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| background: var(--deep-forest); | |
| color: var(--text-main); | |
| font-family: var(--font-sans); | |
| height: 100vh; | |
| overflow: hidden; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| background-image: | |
| radial-gradient(circle at 10% 20%, var(--prism-1) 0%, transparent 40%), | |
| radial-gradient(circle at 90% 80%, var(--prism-3) 0%, transparent 40%), | |
| url("https://www.transparenttextures.com/patterns/carbon-fibre.png"); | |
| background-attachment: fixed; | |
| } | |
| .grain-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 999; | |
| opacity: 0.05; | |
| } | |
| .main-container { | |
| width: 95vw; | |
| height: 90vh; | |
| display: grid; | |
| grid-template-columns: 380px 1fr 340px; | |
| gap: 20px; | |
| padding: 20px; | |
| z-index: 10; | |
| } | |
| .glass-panel { | |
| background: var(--glass); | |
| backdrop-filter: blur(24px) saturate(180%); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 24px; | |
| padding: 24px; | |
| position: relative; | |
| overflow: hidden; | |
| transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1); | |
| } | |
| .glass-panel:hover { | |
| border-color: var(--chlorophyll); | |
| box-shadow: 0 0 40px rgba(0, 243, 255, 0.05); | |
| } | |
| .glass-panel::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 2px; | |
| background: linear-gradient(90deg, transparent, var(--chlorophyll), transparent); | |
| opacity: 0.3; | |
| } | |
| .section-label { | |
| font-size: 0.65rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.2rem; | |
| color: var(--chlorophyll); | |
| margin-bottom: 20px; | |
| font-weight: 800; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .section-label span { | |
| width: 4px; | |
| height: 4px; | |
| background: var(--chlorophyll); | |
| border-radius: 50%; | |
| } | |
| .hub-toggle { | |
| display: flex; | |
| background: rgba(0, 0, 0, 0.2); | |
| border-radius: 12px; | |
| padding: 4px; | |
| margin-bottom: 20px; | |
| } | |
| .hub-toggle button { | |
| flex: 1; | |
| background: transparent; | |
| border: none; | |
| color: var(--text-dim); | |
| padding: 8px; | |
| font-size: 0.7rem; | |
| font-weight: 600; | |
| border-radius: 8px; | |
| transition: 0.3s; | |
| text-transform: uppercase; | |
| } | |
| .hub-toggle button.active { | |
| background: var(--glass-border); | |
| color: white; | |
| } | |
| .dropzone-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| } | |
| .dropzone { | |
| border: 1px dashed var(--glass-border); | |
| border-radius: 16px; | |
| height: 100px; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| transition: 0.3s; | |
| position: relative; | |
| cursor: pointer; | |
| } | |
| .dropzone:hover { | |
| background: rgba(0, 243, 255, 0.05); | |
| border-color: var(--chlorophyll); | |
| } | |
| .dropzone input[type="file"] { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| opacity: 0; | |
| cursor: pointer; | |
| } | |
| .file-info { | |
| font-size: 0.7rem; | |
| font-family: var(--font-mono); | |
| color: var(--text-dim); | |
| text-align: center; | |
| max-width: 80%; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .cloud-explorer { | |
| display: none; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .onedrive-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| padding: 10px; | |
| border-radius: 10px; | |
| background: rgba(255, 255, 255, 0.02); | |
| font-size: 0.8rem; | |
| cursor: pointer; | |
| transition: 0.2s; | |
| border: 1px solid transparent; | |
| } | |
| .onedrive-item:hover { | |
| background: rgba(0, 243, 255, 0.05); | |
| border-color: var(--glass-border); | |
| } | |
| .onedrive-item.selected { | |
| border-color: var(--chlorophyll); | |
| background: rgba(0, 243, 255, 0.1); | |
| } | |
| .creative-studio { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| .text-console { | |
| flex: 1; | |
| background: rgba(0, 0, 0, 0.1); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 16px; | |
| padding: 30px; | |
| color: var(--text-main); | |
| font-family: var(--font-sans); | |
| font-size: 1.2rem; | |
| font-weight: 300; | |
| resize: none; | |
| outline: none; | |
| line-height: 1.6; | |
| } | |
| .text-console:focus { | |
| border-color: var(--chlorophyll); | |
| background: rgba(0, 0, 0, 0.2); | |
| } | |
| .quick-presets { | |
| display: flex; | |
| gap: 10px; | |
| overflow-x: auto; | |
| padding-bottom: 10px; | |
| } | |
| .preset-chip { | |
| padding: 6px 14px; | |
| background: var(--glass-border); | |
| border-radius: 100px; | |
| font-size: 0.7rem; | |
| white-space: nowrap; | |
| cursor: pointer; | |
| transition: 0.2s; | |
| } | |
| .preset-chip:hover { | |
| background: var(--chlorophyll); | |
| color: var(--deep-forest); | |
| } | |
| .command-center { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 20px; | |
| } | |
| .control-group label { | |
| display: block; | |
| font-size: 0.6rem; | |
| color: var(--text-dim); | |
| margin-bottom: 6px; | |
| font-family: var(--font-mono); | |
| letter-spacing: 1px; | |
| } | |
| .control-input { | |
| width: 100%; | |
| background: rgba(0, 0, 0, 0.2); | |
| border: 1px solid var(--glass-border); | |
| padding: 10px; | |
| border-radius: 10px; | |
| color: white; | |
| font-family: var(--font-mono); | |
| font-size: 0.85rem; | |
| outline: none; | |
| } | |
| .control-input:focus { | |
| border-color: var(--chlorophyll); | |
| } | |
| .toggle-switch { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 10px; | |
| background: rgba(0, 0, 0, 0.2); | |
| border-radius: 10px; | |
| cursor: pointer; | |
| } | |
| .pulse-feed { | |
| height: 220px; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .thinking-logs { | |
| flex: 1; | |
| font-family: var(--font-mono); | |
| font-size: 0.65rem; | |
| color: var(--chlorophyll); | |
| overflow-y: auto; | |
| line-height: 1.6; | |
| opacity: 0.8; | |
| padding-right: 10px; | |
| } | |
| .stage-tracker { | |
| margin-top: 15px; | |
| display: flex; | |
| gap: 4px; | |
| } | |
| .stage-bar { | |
| flex: 1; | |
| height: 3px; | |
| background: var(--glass-border); | |
| border-radius: 2px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .stage-bar.complete { | |
| background: var(--chlorophyll); | |
| } | |
| .stage-bar.active::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| height: 100%; | |
| width: 50%; | |
| background: var(--chlorophyll); | |
| animation: slide 2s infinite linear; | |
| } | |
| .delivery-hub { | |
| position: fixed; | |
| top: 50%; | |
| left: 50%; | |
| transform: translate(-50%, -50%) scale(0.9); | |
| width: 700px; | |
| z-index: 1000; | |
| visibility: hidden; | |
| opacity: 0; | |
| transition: 0.5s cubic-bezier(0.19, 1, 0.22, 1); | |
| } | |
| .delivery-hub.active { | |
| visibility: visible; | |
| opacity: 1; | |
| transform: translate(-50%, -50%) scale(1); | |
| } | |
| #delivery-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.8); | |
| backdrop-filter: blur(8px); | |
| z-index: 999; | |
| display: none; | |
| } | |
| .video-preview { | |
| width: 100%; | |
| height: 300px; | |
| background: #000; | |
| border-radius: 16px; | |
| margin-bottom: 20px; | |
| position: relative; | |
| overflow: hidden; | |
| border: 1px solid var(--glass-border); | |
| } | |
| .video-preview video { | |
| width: 100%; | |
| height: 100%; | |
| object-fit: contain; | |
| } | |
| .status-badge { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 8px 16px; | |
| border-radius: 8px; | |
| font-size: 0.65rem; | |
| font-weight: 800; | |
| background: var(--chlorophyll); | |
| color: var(--deep-forest); | |
| text-transform: uppercase; | |
| } | |
| .action-button { | |
| background: var(--chlorophyll); | |
| color: var(--deep-forest); | |
| border: none; | |
| padding: 14px; | |
| border-radius: 12px; | |
| font-weight: 800; | |
| width: 100%; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| transition: 0.3s; | |
| cursor: pointer; | |
| font-family: var(--font-sans); | |
| } | |
| .action-button:hover:not(:disabled) { | |
| filter: brightness(1.1); | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 20px rgba(0, 243, 255, 0.2); | |
| } | |
| .action-button:disabled { | |
| background: var(--glass-border); | |
| color: var(--text-dim); | |
| cursor: not-allowed; | |
| } | |
| @keyframes slide { | |
| 0% { | |
| left: -100%; | |
| } | |
| 100% { | |
| left: 100%; | |
| } | |
| } | |
| ::-webkit-scrollbar { | |
| width: 4px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: transparent; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--glass-border); | |
| border-radius: 10px; | |
| } | |
| .prism-refraction { | |
| position: absolute; | |
| width: 100%; | |
| height: 100%; | |
| top: 0; | |
| left: 0; | |
| pointer-events: none; | |
| background: linear-gradient(135deg, rgba(255, 255, 255, 0.05) 0%, transparent 50%, rgba(0, 243, 255, 0.02) 100%); | |
| } | |
| #onedrive-search { | |
| margin-bottom: 10px; | |
| background: rgba(0, 0, 0, 0.3); | |
| border: 1px solid var(--glass-border); | |
| border-radius: 8px; | |
| padding: 8px; | |
| color: white; | |
| font-size: 0.75rem; | |
| width: 100%; | |
| } | |
| .modal-btn { | |
| text-decoration: none; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 8px; | |
| } | |
| #message-area { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 2000; | |
| } | |
| .message { | |
| padding: 12px 20px; | |
| border-radius: 12px; | |
| font-size: 0.8rem; | |
| backdrop-filter: blur(10px); | |
| margin-top: 10px; | |
| border-left: 4px solid; | |
| animation: slideIn 0.3s ease-out; | |
| max-width: 300px; | |
| } | |
| .message.success { | |
| background: rgba(0, 255, 157, 0.1); | |
| border-color: var(--success); | |
| color: var(--success); | |
| } | |
| .message.error { | |
| background: rgba(255, 77, 77, 0.1); | |
| border-color: var(--error); | |
| color: var(--error); | |
| } | |
| @keyframes slideIn { | |
| from { | |
| transform: translateX(100%); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateX(0); | |
| opacity: 1; | |
| } | |
| } | |
| #onedrive-instruction b { | |
| color: var(--chlorophyll); | |
| } | |
| #onedrive-instruction a { | |
| color: #fff; | |
| text-decoration: underline; | |
| font-weight: bold; | |
| } | |
| .custom-switch { | |
| position: relative; | |
| display: inline-block; | |
| width: 34px; | |
| height: 20px; | |
| } | |
| .custom-switch input { | |
| opacity: 0; | |
| width: 0; | |
| height: 0; | |
| } | |
| .slider { | |
| position: absolute; | |
| cursor: pointer; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(255, 255, 255, 0.1); | |
| transition: .4s; | |
| border-radius: 34px; | |
| } | |
| .slider:before { | |
| position: absolute; | |
| content: ""; | |
| height: 14px; | |
| width: 14px; | |
| left: 3px; | |
| bottom: 3px; | |
| background-color: var(--text-dim); | |
| transition: .4s; | |
| border-radius: 50%; | |
| } | |
| input:checked+.slider { | |
| background-color: var(--chlorophyll); | |
| } | |
| input:checked+.slider:before { | |
| transform: translateX(14px); | |
| background-color: var(--deep-forest); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <svg class="grain-overlay"> | |
| <filter id="noise"> | |
| <feTurbulence type="fractalNoise" baseFrequency="0.6" numOctaves="3" stitchTiles="stitch" /> | |
| <feColorMatrix type="saturate" values="0" /> | |
| </filter> | |
| <rect width="100%" height="100%" filter="url(#noise)" /> | |
| </svg> | |
| <form id="video-form" style="display: contents;"> | |
| <div class="main-container"> | |
| <!-- LEFT: Ingestion Hub --> | |
| <div class="glass-panel"> | |
| <div class="prism-refraction"></div> | |
| <h2 class="section-label"><span></span> Local Assets</h2> | |
| <div class="dropzone-container"> | |
| <div class="dropzone"> | |
| <input type="file" id="videos" name="videos[]" accept="video/*" multiple required | |
| onchange="updateFiles(this, 'videos-info')"> | |
| <i class="fas fa-video" style="opacity: 0.5;"></i> | |
| <p style="font-size: 0.7rem; color: var(--text-dim)">Source Videos [Required]</p> | |
| <div id="videos-info" class="file-info">Ready for Input</div> | |
| </div> | |
| <div class="dropzone" style="height: 70px;"> | |
| <input type="file" id="style_sample" name="style_sample" accept="video/*" | |
| onchange="updateFiles(this, 'style-info')"> | |
| <i class="fas fa-eye" style="opacity: 0.5; font-size: 0.8rem;"></i> | |
| <p style="font-size: 0.6rem; color: var(--text-dim)">Visual Reference [Optional]</p> | |
| <div id="style-info" class="file-info">Unlinked</div> | |
| </div> | |
| </div> | |
| <h2 class="section-label" style="margin-top: 25px;"><span></span> Clipchamp Template (OneDrive)</h2> | |
| <div id="hub-cloud" class="cloud-explorer" style="display: flex;"> | |
| <button type="button" id="onedrive-login-btn" class="action-button" | |
| style="background: rgba(0, 243, 255, 0.05); color: var(--chlorophyll); border: 1px solid var(--chlorophyll); margin-bottom: 10px;"> | |
| <i class="fab fa-microsoft"></i> Connect OneDrive | |
| </button> | |
| <div id="onedrive-instruction" | |
| style="font-size: 0.85rem; padding: 15px; border: 1px solid var(--glass-border); border-radius: 12px; margin-bottom: 10px; display: none; background: rgba(0,0,0,0.2);"> | |
| </div> | |
| <div id="onedrive-status" | |
| style="font-size: 0.7rem; color: var(--success-color); margin-bottom: 10px; display: none;"> | |
| <i class="fas fa-check-circle"></i> Linked to OneDrive | |
| </div> | |
| <div id="onedrive-project-selection" style="display: none;"> | |
| <input type="text" id="onedrive-search" placeholder="Search Project Folders..."> | |
| <div id="onedrive-project-list" style="max-height: 150px; overflow-y: auto;"> | |
| <!-- Projects --> | |
| </div> | |
| </div> | |
| <input type="hidden" id="onedrive_item_id" name="onedrive_item_id"> | |
| <input type="hidden" id="onedrive_access_token" name="onedrive_access_token"> | |
| </div> | |
| </div> | |
| <!-- CENTER: Creative Studio --> | |
| <div class="glass-panel creative-studio"> | |
| <div class="prism-refraction"></div> | |
| <h2 class="section-label"><span></span>AI Prompt</h2> | |
| <textarea id="style_desc" name="style_desc" class="text-console" required | |
| placeholder="Describe the desired cinematic flow...">{{ default_style_desc | escape }}</textarea> | |
| <div class="quick-presets"> | |
| <div class="preset-chip" | |
| onclick="setPreset('Kitchen environment, bright lighting, focus on culinary activity.')">Kitchen | |
| </div> | |
| <div class="preset-chip" | |
| onclick="setPreset('Home decor style, warm lighting, elegant camera pans.')">Home Decor | |
| </div> | |
| <div class="preset-chip" | |
| onclick="setPreset('Lifestyle product showcase, aesthetic setup, dynamic angles.')">Lifestyle | |
| Product</div> | |
| </div> | |
| </div> | |
| <!-- RIGHT: Command & Pulse --> | |
| <div style="display: flex; flex-direction: column; gap: 20px;"> | |
| <div class="glass-panel command-center"> | |
| <h2 class="section-label"><span></span> Command</h2> | |
| <div class="control-group"> | |
| <label>TARGET DURATION</label> | |
| <input type="number" id="duration" name="duration" class="control-input" value="30" min="1"> | |
| </div> | |
| <div class="control-group"> | |
| <label>LLM MODEL</label> | |
| <select id="model_name" name="model_name" class="control-input"> | |
| <option value="gemini-3-flash-preview">Initializing...</option> | |
| </select> | |
| </div> | |
| <div class="toggle-switch" onclick="document.getElementById('mute_audio').click()"> | |
| <span style="font-size: 0.65rem; font-weight: 600; font-family: var(--font-mono);">MUTE | |
| AUDIO</span> | |
| <label class="custom-switch" onclick="event.stopPropagation()"> | |
| <input type="checkbox" id="mute_audio" name="mute_audio" checked> | |
| <span class="slider"></span> | |
| </label> | |
| </div> | |
| <button type="submit" id="submit-button" class="action-button">Initiate Synthesis</button> | |
| </div> | |
| <div class="glass-panel pulse-feed"> | |
| <h2 class="section-label"><span></span> Live-Feed</h2> | |
| <div class="thinking-logs" id="thinking-header"> | |
| > Waiting for your input... | |
| </div> | |
| <div id="progress-area" style="display: none; margin-top: auto;"> | |
| <div id="progress-message" | |
| style="font-size: 0.65rem; color: var(--chlorophyll); font-family: var(--font-mono); margin-bottom: 5px;"> | |
| </div> | |
| <div class="stage-tracker"> | |
| <div class="stage-bar" id="stage-1"></div> | |
| <div class="stage-bar" id="stage-2"></div> | |
| <div class="stage-bar" id="stage-3"></div> | |
| <div class="stage-bar" id="stage-4"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </form> | |
| <!-- Results Modal --> | |
| <div id="delivery-overlay" onclick="closeHub()"></div> | |
| <div class="glass-panel delivery-hub" id="delivery-hub"> | |
| <h2 class="section-label" id="modal-title"><span></span> Delivery Hub</h2> | |
| <div class="video-preview"> | |
| <video id="modal-video" controls style="display: none;"></video> | |
| <div id="placeholder-preview" | |
| style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); text-align: center;"> | |
| <div style="color: var(--chlorophyll); font-size: 0.8rem; font-family: var(--font-mono);">SYNC COMPLETE | |
| </div> | |
| </div> | |
| </div> | |
| <div style="display: flex; gap: 15px; align-items: center;"> | |
| <div id="cloud-sync-badge" class="status-badge" style="display: none;"> | |
| <i class="fas fa-cloud-upload-alt"></i> CLOUD SYNC ACTIVE | |
| </div> | |
| <a id="modal-download-link" href="#" class="action-button" download | |
| style="display: flex; align-items: center; justify-content: center; gap: 10px;"> | |
| <i class="fas fa-download"></i> Download Asset | |
| </a> | |
| </div> | |
| </div> | |
| <div id="message-area"></div> | |
| <script> | |
| const form = document.getElementById('video-form'); | |
| const submitButton = document.getElementById('submit-button'); | |
| const progressArea = document.getElementById('progress-area'); | |
| const progressMessage = document.getElementById('progress-message'); | |
| const thinkingHeader = document.getElementById('thinking-header'); | |
| const messageArea = document.getElementById('message-area'); | |
| const modelSelect = document.getElementById('model_name'); | |
| const deliveryHub = document.getElementById('delivery-hub'); | |
| const deliveryOverlay = document.getElementById('delivery-overlay'); | |
| const modalTitle = document.getElementById('modal-title'); | |
| const modalVideo = document.getElementById('modal-video'); | |
| const modalDownloadLink = document.getElementById('modal-download-link'); | |
| const syncBadge = document.getElementById('cloud-sync-badge'); | |
| // Cookie Helpers | |
| function setCookie(name, value, days) { | |
| let expires = ""; | |
| if (days) { | |
| let date = new Date(); | |
| date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); | |
| expires = "; expires=" + date.toUTCString(); | |
| } | |
| document.cookie = name + "=" + (value || "") + expires + "; path=/"; | |
| } | |
| function getCookie(name) { | |
| let nameEQ = name + "="; | |
| let ca = document.cookie.split(';'); | |
| for (let i = 0; i < ca.length; i++) { | |
| let c = ca[i]; | |
| while (c.charAt(0) == ' ') c = c.substring(1, c.length); | |
| if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); | |
| } | |
| return null; | |
| } | |
| function eraseCookie(name) { | |
| document.cookie = name + '=; Max-Age=-99999999; path=/'; | |
| } | |
| let currentRequestId = null; | |
| let progressInterval = null; | |
| const POLLING_INTERVAL = 2000; | |
| function updateFiles(input, infoId) { | |
| const info = document.getElementById(infoId); | |
| if (input.files && input.files.length > 0) { | |
| const count = input.files.length; | |
| info.textContent = count > 1 ? `${count} Files Ready` : input.files[0].name; | |
| info.style.color = 'var(--chlorophyll)'; | |
| } else { | |
| info.textContent = 'None'; | |
| info.style.color = 'var(--text-dim)'; | |
| } | |
| } | |
| function setPreset(text) { | |
| document.getElementById('style_desc').value = text; | |
| } | |
| // --- Model Loader --- | |
| async function loadModels() { | |
| try { | |
| const response = await fetch('/api/models'); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| modelSelect.innerHTML = ''; | |
| data.models.forEach(model => { | |
| const option = document.createElement('option'); | |
| option.value = model.name; | |
| option.textContent = model.display_name || model.name; | |
| if (model.name.includes('gemini-3-flash')) option.selected = true; | |
| modelSelect.appendChild(option); | |
| }); | |
| } | |
| } catch (error) { | |
| modelSelect.innerHTML = '<option value="gemini-3-flash-preview">Gemini 3 Flash Preview</option>'; | |
| } | |
| } | |
| loadModels(); | |
| // --- Submission --- | |
| form.addEventListener('submit', async (e) => { | |
| e.preventDefault(); | |
| messageArea.innerHTML = ''; | |
| if (document.getElementById('videos').files.length === 0) { | |
| showMessage('error', 'Critical Input Missing: Source Videos'); | |
| return; | |
| } | |
| submitButton.disabled = true; | |
| submitButton.textContent = 'Synthesis Active'; | |
| thinkingHeader.innerHTML = "> PREPARING SEQUENCE...<br>> INITIATING HANDSHAKE..."; | |
| thinkingHeader.scrollTop = thinkingHeader.scrollHeight; | |
| const formData = new FormData(form); | |
| try { | |
| const res = await fetch('/generate', { method: 'POST', body: formData }); | |
| const result = await res.json(); | |
| if (result.status === 'processing_started') { | |
| currentRequestId = result.request_id; | |
| thinkingHeader.innerHTML += "<br>> Handshake established.<br>> Core systems active."; | |
| thinkingHeader.scrollTop = thinkingHeader.scrollHeight; | |
| progressArea.style.display = 'block'; | |
| if (progressInterval) clearInterval(progressInterval); | |
| progressInterval = setInterval(pollProgress, POLLING_INTERVAL); | |
| } else { | |
| throw new Error(result.message); | |
| } | |
| } catch (err) { | |
| thinkingHeader.innerHTML += `<br>><span style="color: var(--error-color);"> SYNTHESIS FAILED: ${err.message}</span>`; | |
| thinkingHeader.scrollTop = thinkingHeader.scrollHeight; | |
| submitButton.disabled = false; | |
| submitButton.textContent = 'Initiate Synthesis'; | |
| } | |
| }); | |
| async function pollProgress() { | |
| if (!currentRequestId) return; | |
| try { | |
| const res = await fetch(`/progress/${currentRequestId}`); | |
| const data = await res.json(); | |
| updatePulse(data); | |
| if (data.stage === 'COMPLETED') { | |
| clearInterval(progressInterval); | |
| completeHub(data.result); | |
| } else if (data.stage === 'FAILED') { | |
| clearInterval(progressInterval); | |
| showMessage('error', data.error); | |
| submitButton.disabled = false; | |
| submitButton.textContent = 'Initiate Synthesis'; | |
| } | |
| } catch (err) { console.error(err); } | |
| } | |
| function updatePulse(data) { | |
| progressMessage.textContent = data.message; | |
| if (data.thinking_header) { | |
| thinkingHeader.innerHTML += `<br>> ${data.thinking_header}`; | |
| thinkingHeader.scrollTop = thinkingHeader.scrollHeight; | |
| } | |
| const stages = ['INGESTING', 'ANALYZING', 'GENERATING', 'SAVING']; | |
| const currentIdx = stages.indexOf(data.stage); | |
| document.querySelectorAll('.stage-bar').forEach((bar, idx) => { | |
| bar.classList.remove('active', 'complete'); | |
| if (idx < currentIdx) bar.classList.add('complete'); | |
| if (idx === currentIdx) bar.classList.add('active'); | |
| }); | |
| } | |
| function completeHub(result) { | |
| submitButton.disabled = false; | |
| submitButton.textContent = 'Initiate Synthesis'; | |
| deliveryOverlay.style.display = 'block'; | |
| deliveryHub.classList.add('active'); | |
| if (result.clipchamp_url) { | |
| modalVideo.style.display = 'none'; | |
| document.getElementById('placeholder-preview').style.display = 'block'; | |
| modalDownloadLink.href = result.clipchamp_url; | |
| modalDownloadLink.download = result.project_name + '.clipchamp'; | |
| if (result.onedrive_uploaded) { | |
| syncBadge.style.display = 'inline-flex'; | |
| modalTitle.innerHTML = '<span></span> SYNC COMPLETE'; | |
| modalDownloadLink.innerHTML = '<i class="fas fa-cloud"></i> Open Cloud Edit'; | |
| } else { | |
| syncBadge.style.display = 'none'; | |
| modalTitle.innerHTML = '<span></span> PROJECT READY'; | |
| modalDownloadLink.innerHTML = '<i class="fas fa-download"></i> Download .clipchamp'; | |
| } | |
| } else { | |
| modalVideo.style.display = 'block'; | |
| document.getElementById('placeholder-preview').style.display = 'none'; | |
| modalVideo.src = result.video_url; | |
| modalDownloadLink.href = result.video_url; | |
| modalDownloadLink.innerHTML = '<i class="fas fa-download"></i> Download MP4'; | |
| syncBadge.style.display = 'none'; | |
| } | |
| } | |
| function closeHub() { | |
| deliveryHub.classList.remove('active'); | |
| deliveryOverlay.style.display = 'none'; | |
| modalVideo.pause(); | |
| } | |
| function showMessage(type, text) { | |
| const div = document.createElement('div'); | |
| div.className = `message ${type}`; | |
| div.innerHTML = text; | |
| messageArea.appendChild(div); | |
| setTimeout(() => div.remove(), 5000); | |
| } | |
| // --- OneDrive --- | |
| const btnOnedrive = document.getElementById('onedrive-login-btn'); | |
| const instructionOnedrive = document.getElementById('onedrive-instruction'); | |
| const statusOnedrive = document.getElementById('onedrive-status'); | |
| const projectSelectionOnedrive = document.getElementById('onedrive-project-selection'); | |
| const projectListOnedrive = document.getElementById('onedrive-project-list'); | |
| const searchOnedrive = document.getElementById('onedrive-search'); | |
| let onedriveProjects = []; | |
| btnOnedrive.addEventListener('click', async () => { | |
| btnOnedrive.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Linking...'; | |
| btnOnedrive.disabled = true; | |
| try { | |
| const res = await fetch('/api/onedrive/login_start', { method: 'POST' }); | |
| const data = await res.json(); | |
| if (data.status === 'success') { | |
| instructionOnedrive.style.display = 'block'; | |
| btnOnedrive.style.display = 'none'; | |
| instructionOnedrive.innerHTML = ` | |
| <div style="margin-bottom: 12px; color: var(--chlorophyll); font-weight: 800; letter-spacing: 1px;"><i class="fas fa-lock"></i> SIGN-IN REQUIRED</div> | |
| <div style="margin-bottom: 8px;">1. Visit <a href="https://www.microsoft.com/link" target="_blank" style="font-size: 0.95rem;">microsoft.com/link</a></div> | |
| <div>2. Enter code:<br> | |
| <b style="font-size: 1.6rem; letter-spacing: 3px; color: var(--chlorophyll); background: rgba(0,0,0,0.4); padding: 8px 16px; border-radius: 8px; display: inline-block; margin-top: 8px; user-select: text;">${data.user_code}</b> | |
| </div> | |
| `; | |
| const poll = setInterval(async () => { | |
| const sRes = await fetch(`/api/onedrive/login_status/${data.session_id}`); | |
| const sData = await sRes.json(); | |
| if (sData.status === 'success') { | |
| clearInterval(poll); | |
| document.getElementById('onedrive_access_token').value = sData.access_token; | |
| setCookie('velocity_onedrive_token', sData.access_token, 7); | |
| instructionOnedrive.style.display = 'none'; | |
| btnOnedrive.style.display = 'none'; | |
| statusOnedrive.style.display = 'block'; | |
| statusOnedrive.innerHTML = '<i class="fab fa-windows"></i> Linked to OneDrive'; | |
| statusOnedrive.style.fontSize = '0.9rem'; | |
| projectSelectionOnedrive.style.display = 'block'; | |
| fetchOneDriveProjects(sData.access_token); | |
| } | |
| }, 5000); | |
| } | |
| } catch (err) { | |
| btnOnedrive.disabled = false; | |
| btnOnedrive.innerHTML = '<i class="fab fa-windows"></i> Retry Link'; | |
| } | |
| }); | |
| async function fetchOneDriveProjects(token) { | |
| projectListOnedrive.innerHTML = '<div style="padding: 10px; font-size: 0.7rem; color: var(--text-dim);"><i class="fas fa-spinner fa-spin"></i> Fetching Cloud Projects...</div>'; | |
| try { | |
| const res = await fetch('/api/onedrive/projects', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ access_token: token }) | |
| }); | |
| const data = await res.json(); | |
| if (data.status === 'success') { | |
| onedriveProjects = data.projects; | |
| renderProjects(onedriveProjects); | |
| } else { | |
| // Token expired or invalid | |
| resetOneDriveLogin(); | |
| } | |
| } catch (e) { | |
| resetOneDriveLogin(); | |
| } | |
| } | |
| function resetOneDriveLogin() { | |
| eraseCookie('velocity_onedrive_token'); | |
| btnOnedrive.style.display = 'block'; | |
| btnOnedrive.innerHTML = '<i class="fab fa-windows"></i> Connect OneDrive'; | |
| btnOnedrive.disabled = false; | |
| statusOnedrive.style.display = 'none'; | |
| projectSelectionOnedrive.style.display = 'none'; | |
| projectListOnedrive.innerHTML = ''; | |
| } | |
| async function initOneDriveCache() { | |
| const cachedToken = getCookie('velocity_onedrive_token'); | |
| if (cachedToken) { | |
| btnOnedrive.style.display = 'none'; | |
| statusOnedrive.style.display = 'block'; | |
| statusOnedrive.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Restoring Cloud Link...'; | |
| document.getElementById('onedrive_access_token').value = cachedToken; | |
| projectSelectionOnedrive.style.display = 'block'; | |
| fetchOneDriveProjects(cachedToken).then(() => { | |
| statusOnedrive.innerHTML = '<i class="fab fa-windows"></i> Linked to OneDrive'; | |
| statusOnedrive.style.fontSize = '0.9rem'; | |
| }); | |
| } | |
| } | |
| initOneDriveCache(); | |
| function renderProjects(list) { | |
| projectListOnedrive.innerHTML = ''; | |
| list.forEach(p => { | |
| const div = document.createElement('div'); | |
| div.className = 'onedrive-item'; | |
| div.innerHTML = `<i class="fas fa-clapperboard"></i> <span>${p.name}</span>`; | |
| div.onclick = () => { | |
| document.querySelectorAll('.onedrive-item').forEach(e => e.classList.remove('selected')); | |
| div.classList.add('selected'); | |
| document.getElementById('onedrive_item_id').value = p.id; | |
| }; | |
| projectListOnedrive.appendChild(div); | |
| }); | |
| } | |
| searchOnedrive.oninput = (e) => { | |
| const v = e.target.value.toLowerCase(); | |
| renderProjects(onedriveProjects.filter(p => p.name.toLowerCase().includes(v))); | |
| }; | |
| // Tilt | |
| document.addEventListener('mousemove', (e) => { | |
| const x = (e.clientX / window.innerWidth - 0.5) * 4; | |
| const y = (e.clientY / window.innerHeight - 0.5) * 4; | |
| document.querySelectorAll('.glass-panel').forEach(p => { | |
| p.style.transform = `perspective(1000px) rotateX(${-y}deg) rotateY(${x}deg)`; | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |