Files
limelite_faceanalyzer/frontend/build/results.html
T

494 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>skin Analysis</title>
<!-- Tailwind CSS for styling -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- JSZip for handling the mask zip file -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<!-- JSEncrypt for API authentication -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsencrypt/3.3.2/jsencrypt.min.js"></script>
<style>
/* Custom animations for a smooth user experience */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in {
animation: fadeIn 0.6s ease-out forwards;
}
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(147, 197, 253, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(147, 197, 253, 0); }
100% { box-shadow: 0 0 0 0 rgba(147, 197, 253, 0); }
}
.pulse {
animation: pulse 2s infinite;
}
</style>
</head>
<body class="bg-gradient-to-br from-blue-50 to-green-50 min-h-screen font-sans text-gray-800">
<div class="container mx-auto px-4 py-12">
<!-- Header Section -->
<header class="text-center mb-12 fade-in">
<h1 class="text-4xl md:text-5xl font-bold text-blue-800 mb-4">AI-Powered Skin Analysis</h1>
<p class="text-lg text-gray-600 max-w-2xl mx-auto">
Upload a photo or take one instantly to get your skin analysis powered by AI.
</p>
</header>
<!-- Main Content Area -->
<main class="max-w-4xl mx-auto">
<div id="appContainer" class="bg-white rounded-2xl shadow-xl overflow-hidden fade-in">
<div class="p-8 md:p-12">
<!-- Initial View: Upload/Camera Buttons -->
<div id="initialView" class="flex flex-col sm:flex-row gap-4 mb-8 justify-center">
<button id="uploadBtn" class="btn-hover flex-1 bg-blue-100 hover:bg-blue-200 text-blue-800 font-medium py-3 px-6 rounded-lg transition-all flex items-center justify-center gap-2">
<i class="fas fa-upload"></i> Upload Photo
</button>
<button id="cameraBtn" class="btn-hover flex-1 bg-green-100 hover:bg-green-200 text-green-800 font-medium py-3 px-6 rounded-lg transition-all flex items-center justify-center gap-2">
<i class="fas fa-camera"></i> Take Photo
</button>
</div>
<!-- File Input (Hidden) -->
<input type="file" id="fileInput" accept="image/*" class="hidden">
<!-- Camera Preview View -->
<div id="cameraView" class="hidden mb-8">
<div class="relative bg-gray-100 rounded-lg overflow-hidden h-64 md:h-96">
<video id="cameraPreview" class="w-full h-full object-cover"></video>
<button id="captureBtn" class="absolute bottom-4 left-1/2 transform -translate-x-1/2 bg-white rounded-full p-3 shadow-lg pulse">
<i class="fas fa-circle text-red-500 text-2xl"></i>
</button>
</div>
</div>
<!-- Image Preview View -->
<div id="previewView" class="hidden mb-8">
<h3 class="text-xl font-semibold text-gray-700 mb-4">Your Photo</h3>
<div class="image-preview bg-gray-100 rounded-lg overflow-hidden flex justify-center items-center h-64 md:h-96">
<img id="previewImage" class="w-full h-full object-contain" alt="Your skin photo" />
<canvas id="hiddenCanvas" class="hidden"></canvas>
</div>
<button id="retakeBtn" class="mt-4 text-blue-600 hover:text-blue-800 flex items-center gap-2">
<i class="fas fa-redo"></i> Retake Photo
</button>
</div>
<!-- Submit Button & Loading Indicator -->
<div id="submitContainer" class="hidden text-center mt-8">
<button id="submitBtn" class="btn-hover bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-8 rounded-lg transition-all transform hover:scale-105">
Analyze My skin <i class="fas fa-arrow-right ml-2"></i>
</button>
<p id="loadingIndicator" class="hidden mt-4 text-gray-500">Processing...</p>
<p id="errorMsg" class="hidden mt-4 text-red-600"></p>
</div>
</div>
</div>
<!-- Analysis Results Section (Initially Hidden) -->
<section id="resultsSection" class="hidden mt-16 max-w-5xl mx-auto fade-in">
<h2 class="text-2xl font-bold text-blue-800 mb-6">Detailed skin Scores</h2>
<div class="flex flex-col md:flex-row gap-6">
<!-- Image with Overlay -->
<div class="relative w-full md:w-1/2">
<img id="resultImage" src="#" alt="Uploaded skin" class="w-full rounded-lg border shadow"/>
<img id="maskImage" src="#" alt="Analysis mask" class="absolute top-0 left-0 w-full h-full object-contain opacity-70 hidden"/>
</div>
<!-- Score List -->
<div id="scoreList" class="flex-1 overflow-y-auto max-h-[500px] bg-white rounded-lg shadow p-4 space-y-3">
<!-- Scores will be dynamically inserted here -->
</div>
</div>
<div class="text-center mt-8">
<button id="newAnalysisBtn" class="btn-hover bg-green-600 hover:bg-green-700 text-white font-medium py-3 px-8 rounded-lg transition-all transform hover:scale-105">
Start New Analysis <i class="fas fa-plus ml-2"></i>
</button>
</div>
</section>
</main>
</div>
<script>
// All JavaScript logic is contained here.
document.addEventListener('DOMContentLoaded', function() {
// ================== DOM Elements ==================
const uploadBtn = document.getElementById('uploadBtn');
const cameraBtn = document.getElementById('cameraBtn');
const fileInput = document.getElementById('fileInput');
const initialView = document.getElementById('initialView');
const cameraView = document.getElementById('cameraView');
const cameraPreview = document.getElementById('cameraPreview');
const captureBtn = document.getElementById('captureBtn');
const previewView = document.getElementById('previewView');
const previewImage = document.getElementById('previewImage');
const hiddenCanvas = document.getElementById('hiddenCanvas');
const retakeBtn = document.getElementById('retakeBtn');
const submitContainer = document.getElementById('submitContainer');
const submitBtn = document.getElementById('submitBtn');
const loadingIndicator = document.getElementById('loadingIndicator');
const errorMsg = document.getElementById('errorMsg');
const resultsSection = document.getElementById('resultsSection');
const resultImage = document.getElementById('resultImage');
const maskImage = document.getElementById('maskImage');
const scoreList = document.getElementById('scoreList');
const newAnalysisBtn = document.getElementById('newAnalysisBtn');
// ================== State & Configuration ==================
let currentFile = null;
let cameraStream = null;
const clientId = 'YTiFKSfOADhKE6HOS7wPbcrxeXA1LUyF';
const publicKey = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCGdjQUhil+XNmLtjLcsAAl+tV2bg0mEZAmkr7rg4Yn13wWoM4NY/uJk1ao0sN9ZfHmp7E+53fZYwPeD4wPss7q97h+MguhtDxdJMWarlora11adFpRN+VLEYCzfOPHOR2odrQdhQ5t9DtT0KyNJ4B2vcBzVwVslMk+AoSt5AOrUwIDAQAB
-----END PUBLIC KEY-----`;
// ================== Helper Functions ==================
function setView(viewId) {
const views = ['initialView', 'cameraView', 'previewView', 'resultsSection', 'submitContainer'];
views.forEach(id => {
const element = document.getElementById(id);
if (id === viewId || (viewId === 'previewView' && id === 'submitContainer') || (viewId === 'resultsSection' && id === 'submitContainer')) {
element.classList.remove('hidden');
} else {
element.classList.add('hidden');
}
});
if (viewId === 'resultsSection') {
document.getElementById('appContainer').classList.add('hidden');
resultsSection.classList.remove('hidden');
} else {
document.getElementById('appContainer').classList.remove('hidden');
resultsSection.classList.add('hidden');
}
}
// --- API & Core Logic Functions ---
async function authenticate() {
const plain = `client_id=${clientId}&timestamp=${Date.now()}`;
const encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);
const id_token = encrypt.encrypt(plain);
if (!id_token) throw new Error('Encryption failed');
const response = await fetch('https://yce-api-01.perfectcorp.com/s2s/v1.0/client/auth', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: clientId, id_token })
});
if (!response.ok) {
const error = await response.json();
throw new Error(`Authentication failed: ${error.error || response.statusText}`);
}
const data = await response.json();
return data.result.access_token;
}
async function createUploadSlot(token, fileName, fileSize, contentType = 'image/jpeg') {
const response = await fetch('https://yce-api-01.perfectcorp.com/s2s/v1.1/file/skin-analysis', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
files: [{
content_type: contentType,
file_name: fileName,
file_size: fileSize
}]
})
});
if (!response.ok) {
const errText = await response.text();
throw new Error('Failed to create file slot: ' + errText);
}
const data = await response.json();
return data.result.files[0];
}
async function uploadToS3(uploadRequest, file) {
const response = await fetch(uploadRequest.url, {
method: uploadRequest.method,
headers: uploadRequest.headers,
body: file
});
if (!response.ok) {
throw new Error(`S3 upload failed with status ${response.status}`);
}
return response;
}
async function analyzeskin(token, fileId) {
const payload = {
request_id: 0,
payload: {
file_sets: {
src_ids: [fileId.trim()]
},
actions: [{
id: 0,
params: {},
dst_actions: ["wrinkle", "pore", "texture", "acne"]
}]
}
};
const response = await fetch('https://yce-api-01.perfectcorp.com/s2s/v1.0/task/skin-analysis', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorDetails = await response.json();
throw new Error(`Analysis failed: ${errorDetails.error || response.statusText}`);
}
const result = await response.json();
if (!result.result || !result.result.task_id) {
throw new Error("No task_id returned from API");
}
return result.result.task_id;
}
async function getAnalysisResult(token, taskId) {
const response = await fetch(
`https://yce-api-01.perfectcorp.com/s2s/v1.0/task/skin-analysis?task_id=${encodeURIComponent(taskId)}`,
{
method: 'GET',
headers: { 'Authorization': `Bearer ${token}` }
}
);
if (!response.ok) {
const error = await response.json();
throw new Error(`Failed to get results: ${error.error || response.statusText}`);
}
return await response.json();
}
async function pollForAnalysisResult(token, taskId) {
while (true) {
try {
const result = await getAnalysisResult(token, taskId);
const status = result.result.status;
const pollingInterval = result.result.polling_interval || 2000;
if (status === 'success') {
if (result.result.results && result.result.results.length > 0) {
return result;
}
throw new Error('Empty results array');
}
if (status === 'error') {
const errorMessage = result.result.error_message || 'No detailed error message provided';
throw new Error(`Analysis error: ${errorMessage}`);
}
await new Promise(resolve => setTimeout(resolve, pollingInterval));
} catch (error) {
throw error;
}
}
}
async function resizeImageFile(file, maxWidth = 2048, maxHeight = 2048, minShortSide = 1080) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
let { width, height } = img;
let scaleRatio = 1;
if (width > maxWidth || height > maxHeight) {
const widthRatio = maxWidth / width;
const heightRatio = maxHeight / height;
scaleRatio = Math.min(widthRatio, heightRatio);
}
const shortSide = Math.min(width, height);
if (shortSide * scaleRatio < minShortSide) {
const upscaleRatio = minShortSide / shortSide;
scaleRatio = Math.max(upscaleRatio, scaleRatio);
}
const targetWidth = Math.round(width * scaleRatio);
const targetHeight = Math.round(height * scaleRatio);
const canvas = document.createElement('canvas');
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
canvas.toBlob(
(blob) => {
if (!blob) return reject(new Error('Canvas is empty'));
const resizedFile = new File([blob], file.name.replace(/\.\w+$/, '.jpg'), { type: 'image/jpeg' });
resolve(resizedFile);
},
'image/jpeg',
0.9
);
};
img.onerror = () => reject(new Error('Failed to load image'));
const reader = new FileReader();
reader.onload = e => {
img.src = e.target.result;
};
reader.onerror = () => reject(new Error('File reading error'));
reader.readAsDataURL(file);
});
}
// ================== Event Listeners ==================
uploadBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (file) {
currentFile = file;
previewImage.src = URL.createObjectURL(file);
setView('previewView');
}
});
cameraBtn.addEventListener('click', async () => {
try {
cameraStream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'user' },
audio: false
});
cameraPreview.srcObject = cameraStream;
cameraPreview.play();
setView('cameraView');
} catch (err) {
errorMsg.textContent = "Could not access the camera. Please check permissions.";
errorMsg.classList.remove('hidden');
}
});
captureBtn.addEventListener('click', () => {
if (!cameraStream) return;
hiddenCanvas.width = cameraPreview.videoWidth;
hiddenCanvas.height = cameraPreview.videoHeight;
const ctx = hiddenCanvas.getContext('2d');
ctx.drawImage(cameraPreview, 0, 0, hiddenCanvas.width, hiddenCanvas.height);
hiddenCanvas.toBlob((blob) => {
if (blob) {
currentFile = new File([blob], "captured-image.jpeg", { type: "image/jpeg" });
previewImage.src = URL.createObjectURL(currentFile);
setView('previewView');
}
}, 'image/jpeg', 0.9);
cameraStream.getTracks().forEach(track => track.stop());
cameraStream = null;
});
retakeBtn.addEventListener('click', () => {
currentFile = null;
previewImage.src = '';
setView('initialView');
errorMsg.classList.add('hidden');
});
newAnalysisBtn.addEventListener('click', () => {
currentFile = null;
previewImage.src = '';
resultImage.src = '';
maskImage.src = '';
scoreList.innerHTML = '';
setView('initialView');
errorMsg.classList.add('hidden');
});
submitBtn.addEventListener('click', async () => {
if (!currentFile) {
errorMsg.textContent = 'Please select or capture a photo first.';
errorMsg.classList.remove('hidden');
return;
}
submitBtn.disabled = true;
submitBtn.textContent = 'Processing...';
loadingIndicator.classList.remove('hidden');
errorMsg.classList.add('hidden');
try {
const resizedFile = await resizeImageFile(currentFile);
const token = await authenticate();
const uploadSlot = await createUploadSlot(token, resizedFile.name, resizedFile.size);
await uploadToS3(uploadSlot.requests[0], resizedFile);
const cleanFileId = uploadSlot.file_id.replace(/^\/+/,'');
const taskId = await analyzeskin(token, cleanFileId);
const result = await pollForAnalysisResult(token, taskId);
const scores = result.result.results[0].scores;
const maskZipUrl = result.result.results[0].data[0].url;
const zipBlob = await fetch(maskZipUrl).then(r => r.blob());
const zip = await JSZip.loadAsync(zipBlob);
const maskMap = {};
for (const key in scores) {
const score = scores[key];
if (score.output_mask_name && zip.file(score.output_mask_name)) {
const fileData = await zip.file(score.output_mask_name).async("blob");
maskMap[score.output_mask_name] = URL.createObjectURL(fileData);
}
}
// Display results
resultImage.src = URL.createObjectURL(currentFile);
scoreList.innerHTML = '';
Object.keys(scores).filter(k => scores[k].ui_score !== undefined).forEach(metric => {
const score = scores[metric];
const card = document.createElement('div');
card.className = 'score-card p-4 rounded-lg cursor-pointer transition-all duration-200 bg-gray-50 hover:bg-gray-100';
card.innerHTML = `<span class="capitalize">${metric.replace(/_/g, ' ')}</span><span class="font-bold text-blue-700 ml-2">${score.ui_score}</span>`;
card.addEventListener('mouseenter', () => {
if (score.output_mask_name && maskMap[score.output_mask_name]) {
maskImage.src = maskMap[score.output_mask_name];
maskImage.classList.remove('hidden');
}
card.classList.add('bg-blue-100', 'border-l-4', 'border-blue-500');
});
card.addEventListener('mouseleave', () => {
maskImage.classList.add('hidden');
card.classList.remove('bg-blue-100', 'border-l-4', 'border-blue-500');
});
scoreList.appendChild(card);
});
setView('resultsSection');
} catch (e) {
console.error(e);
errorMsg.textContent = `Analysis failed: ${e.message}`;
errorMsg.classList.remove('hidden');
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Analyze My skin';
loadingIndicator.classList.add('hidden');
}
});
// Add animation to elements when they come into view
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('fade-in');
}
});
}, { threshold: 0.1 });
document.querySelectorAll('section, .feature-card').forEach(section => {
observer.observe(section);
});
});
</script>
</body>
</html>