| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>LetsPhish Morphis</title> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" rel="stylesheet"> |
| <style> |
| :root { |
| --primary: #4f46e5; |
| --primary-hover: #4338ca; |
| --background: #f9fafb; |
| --card-bg: #ffffff; |
| --text: #1f2937; |
| --text-light: #6b7280; |
| --border: #e5e7eb; |
| --radius: 8px; |
| --shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); |
| } |
| * { margin: 0; padding: 0; box-sizing: border-box; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; } |
| body { background-color: var(--background); color: var(--text); padding: 0; margin: 0; min-height: 100vh; } |
| .navbar { background-color: var(--card-bg); padding: 1rem 2rem; box-shadow: var(--shadow); display: flex; align-items: center; } |
| .logo { font-size: 1.5rem; font-weight: 700; color: var(--primary); display: flex; align-items: center; gap: 0.5rem; } |
| .container { max-width: 1200px; margin: 2rem auto; padding: 0 1rem; } |
| .app-header { text-align: center; margin-bottom: 2rem; } |
| .app-title { font-size: 2rem; margin-bottom: 0.5rem; color: var(--text); } |
| .app-description { color: var(--text-light); max-width: 600px; margin: 0 auto; } |
| .card { background-color: var(--card-bg); border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; margin-bottom: 2rem; } |
| .card-header { padding: 1rem; border-bottom: 1px solid var(--border); font-weight: 600; } |
| .card-body { padding: 1rem; } |
| .flex { display: flex; } |
| .flex-col { flex-direction: column; } |
| .grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 2rem; } |
| @media (max-width: 768px) { .grid { grid-template-columns: 1fr; } } |
| .video-container { position: relative; width: 100%; aspect-ratio: 16/9; overflow: hidden; border-radius: var(--radius); background-color: #000; } |
| video, #output { position: absolute; width: 100%; height: 100%; object-fit: cover; transform: scaleX(-1); } |
| #video { z-index: 1; } |
| #output { z-index: 2; } |
| #canvas { display: none; } |
| .controls { margin-top: 1rem; display: flex; gap: 1rem; flex-wrap: wrap; } |
| .btn { padding: 0.5rem 1rem; border-radius: var(--radius); border: none; font-weight: 500; cursor: pointer; display: flex; align-items: center; gap: 0.5rem; transition: all 0.2s; } |
| .btn-primary { background-color: var(--primary); color: white; } |
| .btn-primary:hover { background-color: var(--primary-hover); } |
| .btn-outline { border: 1px solid var(--primary); color: var(--primary); background-color: transparent; } |
| .btn-outline:hover { background-color: var(--primary); color: white; } |
| .file-input-container { display: flex; flex-direction: column; gap: 1rem; } |
| .file-input-label { border: 2px dashed var(--border); border-radius: var(--radius); padding: 2rem; text-align: center; cursor: pointer; transition: all 0.2s; } |
| .file-input-label:hover { border-color: var(--primary); } |
| .file-input { display: none; } |
| .preview-container { width: 128px; height: 128px; border-radius: 50%; overflow: hidden; margin: 1rem auto; border: 4px solid var(--primary); } |
| .preview-image { width: 100%; height: 100%; object-fit: cover; } |
| .status-container { display: flex; align-items: center; gap: 0.5rem; color: var(--text-light); margin-top: 1rem; } |
| .status-dot { width: 10px; height: 10px; border-radius: 50%; background-color: #ccc; } |
| .status-dot.active { background-color: #10b981; } |
| .timer { margin-top: 1rem; font-size: 1.2rem; font-weight: bold; text-align: center; } |
| .footer { padding: 2rem; text-align: center; color: var(--text-light); border-top: 1px solid var(--border); margin-top: 3rem; } |
| .auth-error { background-color: #fef2f2; color: #dc2626; padding: 1rem; border-radius: var(--radius); margin-bottom: 2rem; text-align: center; } |
| </style> |
| </head> |
| <body> |
| <nav class="navbar"> |
| <div class="logo"> |
| <i class="fas fa-mask"></i> |
| <span>LetsPhish Morphis</span> |
| </div> |
| </nav> |
| <div class="container"> |
| <div id="authError" class="auth-error" style="display: none;"> |
| <i class="fas fa-exclamation-triangle"></i> |
| <span>Authentication failed. Please check your access key.</span> |
| </div> |
| <div class="app-header"> |
| <h1 class="app-title">Real-Time Face Swapping</h1> |
| <p class="app-description">Experience real-time face swapping powered by AI. Upload your own face or use the default one.</p> |
| </div> |
| <div class="grid"> |
| |
| <div class="flex flex-col" style="grid-column: span 2;"> |
| <div class="card"> |
| <div class="card-header"> |
| <i class="fas fa-camera"></i> Live Preview |
| </div> |
| <div class="card-body"> |
| <div class="video-container"> |
| <video id="video" autoplay></video> |
| <img id="output"> |
| </div> |
| <canvas id="canvas"></canvas> |
| <div class="controls"> |
| <button id="toggleBtn" class="btn btn-primary"> |
| <i class="fas fa-pause"></i> Pause Face Swap |
| </button> |
| <button id="screenshotBtn" class="btn btn-outline"> |
| <i class="fas fa-camera"></i> Take Screenshot |
| </button> |
| </div> |
| <div class="status-container"> |
| <div id="statusDot" class="status-dot active"></div> |
| <span id="statusText">Face swap active</span> |
| </div> |
| |
| <div id="timerDisplay" class="timer">Time remaining: 15:00</div> |
| </div> |
| </div> |
| </div> |
| |
| <div class="flex flex-col"> |
| <div class="card"> |
| <div class="card-header"> |
| <i class="fas fa-upload"></i> Upload Reference Face |
| </div> |
| <div class="card-body"> |
| <div class="preview-container"> |
| <img id="facePreview" class="preview-image" alt="Current face"> |
| </div> |
| <div class="file-input-container"> |
| <label for="faceUpload" class="file-input-label"> |
| <i class="fas fa-cloud-upload-alt fa-2x"></i> |
| <p>Drop your image here or click to browse</p> |
| <p class="text-light">JPG or PNG, max 5MB</p> |
| </label> |
| <input type="file" id="faceUpload" class="file-input" accept="image/*"> |
| </div> |
| <div class="controls"> |
| <button id="uploadBtn" class="btn btn-primary" disabled> |
| <i class="fas fa-check"></i> Use This Face |
| </button> |
| <button id="resetBtn" class="btn btn-outline"> |
| <i class="fas fa-undo"></i> Reset to Default |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| <footer class="footer"> |
| <p>Powered By LetsPhish Morphis</p> |
| </footer> |
| <script> |
| |
| const urlParams = new URLSearchParams(window.location.search); |
| const authKey = urlParams.get('key'); |
| |
| |
| if (!authKey) { |
| document.getElementById('authError').style.display = 'block'; |
| document.getElementById('authError').innerHTML = '<i class="fas fa-exclamation-triangle"></i> <span>No access key provided. Please add ?key=your_access_key to the URL.</span>'; |
| } |
| |
| |
| function addKeyToUrl(url) { |
| if (!authKey) return url; |
| const separator = url.includes('?') ? '&' : '?'; |
| return url + separator + 'key=' + encodeURIComponent(authKey); |
| } |
| |
| |
| function handleAuthError(error) { |
| console.error('Authentication error:', error); |
| document.getElementById('authError').style.display = 'block'; |
| document.getElementById('statusDot').classList.remove('active'); |
| document.getElementById('statusDot').style.backgroundColor = 'red'; |
| document.getElementById('statusText').textContent = 'Authentication failed'; |
| |
| |
| if (intervalId) clearInterval(intervalId); |
| if (timerIntervalId) clearInterval(timerIntervalId); |
| isProcessing = false; |
| toggleBtn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Auth Error'; |
| toggleBtn.disabled = true; |
| } |
| |
| const video = document.getElementById('video'); |
| const canvas = document.getElementById('canvas'); |
| const output = document.getElementById('output'); |
| const context = canvas.getContext('2d'); |
| const toggleBtn = document.getElementById('toggleBtn'); |
| const screenshotBtn = document.getElementById('screenshotBtn'); |
| const faceUpload = document.getElementById('faceUpload'); |
| const facePreview = document.getElementById('facePreview'); |
| const uploadBtn = document.getElementById('uploadBtn'); |
| const resetBtn = document.getElementById('resetBtn'); |
| const statusDot = document.getElementById('statusDot'); |
| const statusText = document.getElementById('statusText'); |
| const timerDisplay = document.getElementById('timerDisplay'); |
| let isProcessing = true; |
| let processingInterval = 100; |
| let intervalId = null; |
| let selectedFile = null; |
| let frameCounter = 0; |
| const skipFrames = 2; |
| let processingInProgress = false; |
| |
| let totalTime = 900; |
| let timerIntervalId = null; |
| |
| |
| if (authKey) { |
| facePreview.src = addKeyToUrl('/get_current_face'); |
| } |
| |
| |
| function updateTimerDisplay() { |
| let minutes = Math.floor(totalTime / 60); |
| let seconds = totalTime % 60; |
| timerDisplay.textContent = `Time remaining: ${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; |
| } |
| |
| |
| function startTimer() { |
| updateTimerDisplay(); |
| timerIntervalId = setInterval(() => { |
| totalTime--; |
| updateTimerDisplay(); |
| if (totalTime <= 0) { |
| clearInterval(timerIntervalId); |
| if (isProcessing) { |
| clearInterval(intervalId); |
| isProcessing = false; |
| toggleBtn.innerHTML = '<i class="fas fa-play"></i> Resume Face Swap'; |
| statusDot.classList.remove('active'); |
| statusText.textContent = 'Live preview turned off after 15 minutes'; |
| alert("Live preview has been turned off after 15 minutes."); |
| } |
| } |
| }, 1000); |
| } |
| |
| |
| toggleBtn.addEventListener('click', () => { |
| if (!authKey) { |
| handleAuthError('No authentication key'); |
| return; |
| } |
| |
| isProcessing = !isProcessing; |
| if (isProcessing) { |
| toggleBtn.innerHTML = '<i class="fas fa-pause"></i> Pause Face Swap'; |
| intervalId = setInterval(captureFrame, processingInterval); |
| statusDot.classList.add('active'); |
| statusText.textContent = 'Face swap active'; |
| |
| if (totalTime <= 0) { |
| totalTime = 900; |
| startTimer(); |
| } |
| } else { |
| toggleBtn.innerHTML = '<i class="fas fa-play"></i> Resume Face Swap'; |
| clearInterval(intervalId); |
| statusDot.classList.remove('active'); |
| statusText.textContent = 'Face swap paused'; |
| } |
| }); |
| |
| |
| screenshotBtn.addEventListener('click', () => { |
| const link = document.createElement('a'); |
| link.download = 'faceswap-screenshot.jpg'; |
| link.href = output.src; |
| link.click(); |
| }); |
| |
| |
| faceUpload.addEventListener('change', (e) => { |
| selectedFile = e.target.files[0]; |
| if (selectedFile) { |
| const reader = new FileReader(); |
| reader.onload = (event) => { |
| facePreview.src = event.target.result; |
| uploadBtn.disabled = false; |
| }; |
| reader.readAsDataURL(selectedFile); |
| } |
| }); |
| |
| |
| uploadBtn.addEventListener('click', () => { |
| if (!selectedFile || !authKey) return; |
| |
| const formData = new FormData(); |
| formData.append('face', selectedFile); |
| |
| fetch(addKeyToUrl('/upload_face'), { |
| method: 'POST', |
| body: formData |
| }) |
| .then(response => { |
| if (response.status === 401) { |
| throw new Error('Authentication failed'); |
| } |
| return response.json(); |
| }) |
| .then(data => { |
| if (data.success) { |
| alert('Face updated successfully!'); |
| facePreview.src = addKeyToUrl(`/get_current_face?t=${new Date().getTime()}`); |
| uploadBtn.disabled = true; |
| } else { |
| alert('Error: ' + data.message); |
| } |
| }) |
| .catch(error => { |
| if (error.message === 'Authentication failed') { |
| handleAuthError(error); |
| } else { |
| console.error('Error:', error); |
| alert('An error occurred while uploading the face.'); |
| } |
| }); |
| }); |
| |
| |
| resetBtn.addEventListener('click', () => { |
| if (!authKey) return; |
| |
| fetch(addKeyToUrl('/reset_face'), { method: 'POST' }) |
| .then(response => { |
| if (response.status === 401) { |
| throw new Error('Authentication failed'); |
| } |
| return response.json(); |
| }) |
| .then(data => { |
| if (data.success) { |
| alert('Reset to default face successfully!'); |
| facePreview.src = addKeyToUrl(`/get_current_face?t=${new Date().getTime()}`); |
| } else { |
| alert('Error: ' + data.message); |
| } |
| }) |
| .catch(error => { |
| if (error.message === 'Authentication failed') { |
| handleAuthError(error); |
| } else { |
| console.error('Error:', error); |
| alert('An error occurred while resetting the face.'); |
| } |
| }); |
| }); |
| |
| |
| navigator.mediaDevices.getUserMedia({ video: true }) |
| .then((stream) => { video.srcObject = stream; }) |
| .catch((err) => { |
| console.error("Error accessing webcam: " + err); |
| statusText.textContent = 'Error: Cannot access webcam'; |
| statusDot.classList.remove('active'); |
| statusDot.style.backgroundColor = 'red'; |
| }); |
| |
| |
| async function captureFrame() { |
| if (processingInProgress || !authKey) return; |
| frameCounter++; |
| if (frameCounter % skipFrames !== 0) return; |
| if (!video.videoWidth) return; |
| |
| canvas.width = video.videoWidth; |
| canvas.height = video.videoHeight; |
| |
| |
| context.save(); |
| context.scale(-1, 1); |
| context.drawImage(video, -canvas.width, 0, canvas.width, canvas.height); |
| context.restore(); |
| |
| |
| const dataURL = canvas.toDataURL('image/jpeg', 0.5); |
| |
| processingInProgress = true; |
| try { |
| const response = await fetch(addKeyToUrl('/process_frame'), { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ image: dataURL }) |
| }); |
| |
| if (response.status === 401) { |
| throw new Error('Authentication failed'); |
| } |
| |
| const data = await response.json(); |
| output.src = data.image; |
| } catch (error) { |
| if (error.message === 'Authentication failed') { |
| handleAuthError(error); |
| } else { |
| console.error('Error:', error); |
| } |
| } finally { |
| processingInProgress = false; |
| } |
| } |
| |
| |
| if (authKey) { |
| intervalId = setInterval(captureFrame, processingInterval); |
| startTimer(); |
| } else { |
| |
| toggleBtn.disabled = true; |
| uploadBtn.disabled = true; |
| resetBtn.disabled = true; |
| } |
| </script> |
| </body> |
| </html> |