Initial commit: added frontend and backend
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"age_spot": {
|
||||
"raw_score": 98.26628875732422,
|
||||
"ui_score": 96,
|
||||
"output_mask_name": "age_spot_output.png"
|
||||
},
|
||||
"radiance": {
|
||||
"raw_score": 97.64312505722046,
|
||||
"ui_score": 96,
|
||||
"output_mask_name": "radiance_output.png"
|
||||
},
|
||||
"oiliness": {
|
||||
"raw_score": 83.94287872314453,
|
||||
"ui_score": 82,
|
||||
"output_mask_name": "oiliness_output.png"
|
||||
},
|
||||
"firmness": {
|
||||
"raw_score": 89.18848633766174,
|
||||
"ui_score": 85,
|
||||
"output_mask_name": "firmness_output.png"
|
||||
},
|
||||
"dark_circle_v2": {
|
||||
"raw_score": 55.23899793624878,
|
||||
"ui_score": 67,
|
||||
"output_mask_name": "dark_circle_v2_output.png"
|
||||
},
|
||||
"droopy_lower_eyelid": {
|
||||
"raw_score": 80.4837703704834,
|
||||
"ui_score": 81,
|
||||
"output_mask_name": "droopy_lower_eyelid_output.png"
|
||||
},
|
||||
"droopy_upper_eyelid": {
|
||||
"raw_score": 82.7034592628479,
|
||||
"ui_score": 82,
|
||||
"output_mask_name": "droopy_upper_eyelid_output.png"
|
||||
},
|
||||
"wrinkle": {
|
||||
"raw_score": 95.83742618560791,
|
||||
"ui_score": 91,
|
||||
"output_mask_name": "wrinkle_output.png"
|
||||
},
|
||||
"redness": {
|
||||
"raw_score": 100.0,
|
||||
"ui_score": 99,
|
||||
"output_mask_name": "redness_output.png"
|
||||
},
|
||||
"pore": {
|
||||
"raw_score": 99.96079206466675,
|
||||
"ui_score": 99,
|
||||
"output_mask_name": "pore_output.png"
|
||||
},
|
||||
"eye_bag": {
|
||||
"raw_score": 88.28426599502563,
|
||||
"ui_score": 84,
|
||||
"output_mask_name": "eye_bag_output.png"
|
||||
},
|
||||
"texture": {
|
||||
"raw_score": 97.28496074676514,
|
||||
"ui_score": 94,
|
||||
"output_mask_name": "texture_output.png"
|
||||
},
|
||||
"moisture": {
|
||||
"raw_score": 71.30312919616699,
|
||||
"ui_score": 78,
|
||||
"output_mask_name": "moisture_output.png"
|
||||
},
|
||||
"acne": {
|
||||
"raw_score": 100.0,
|
||||
"ui_score": 99,
|
||||
"output_mask_name": "acne_output.png"
|
||||
},
|
||||
"all": {
|
||||
"score": 88.07142857142857
|
||||
},
|
||||
"skin_age": 26
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
After Width: | Height: | Size: 398 KiB |
|
After Width: | Height: | Size: 408 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 765 B |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
@@ -0,0 +1,69 @@
|
||||
<!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>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsencrypt/3.3.2/jsencrypt.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.6s ease-out forwards;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
.image-preview {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.image-preview:hover {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.btn-hover {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.btn-hover:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-white font-sans">
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
[
|
||||
{
|
||||
"weights": [
|
||||
{
|
||||
"name": "dense0/conv0/filters",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
32
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.008194216092427571,
|
||||
"min": -0.9423348506291708
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense0/conv0/bias",
|
||||
"shape": [
|
||||
32
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.006839508168837603,
|
||||
"min": -0.8412595047670252
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense0/conv1/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
32,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.009194007106855804,
|
||||
"min": -1.2779669878529567
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense0/conv1/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
32,
|
||||
32
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.0036026100317637128,
|
||||
"min": -0.3170296827952067
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense0/conv1/bias",
|
||||
"shape": [
|
||||
32
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.000740380117706224,
|
||||
"min": -0.06367269012273527
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense0/conv2/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
32,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 1,
|
||||
"min": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense0/conv2/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
32,
|
||||
32
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 1,
|
||||
"min": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense0/conv2/bias",
|
||||
"shape": [
|
||||
32
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.0037702228508743585,
|
||||
"min": -0.6220867703942692
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense1/conv0/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
32,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.0033707996209462483,
|
||||
"min": -0.421349952618281
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense1/conv0/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
32,
|
||||
64
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.014611541991140328,
|
||||
"min": -1.8556658328748217
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense1/conv0/bias",
|
||||
"shape": [
|
||||
64
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.002832523046755323,
|
||||
"min": -0.30307996600281956
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense1/conv1/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
64,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.006593170586754294,
|
||||
"min": -0.6329443763284123
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense1/conv1/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
64,
|
||||
64
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.012215249211180444,
|
||||
"min": -1.6001976466646382
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense1/conv1/bias",
|
||||
"shape": [
|
||||
64
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.002384825547536214,
|
||||
"min": -0.3028728445370992
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense1/conv2/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
64,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.005859645441466687,
|
||||
"min": -0.7617539073906693
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense1/conv2/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
64,
|
||||
64
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.013121426806730382,
|
||||
"min": -1.7845140457153321
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense1/conv2/bias",
|
||||
"shape": [
|
||||
64
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.0032247188044529336,
|
||||
"min": -0.46435950784122243
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense2/conv0/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
64,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.002659512618008782,
|
||||
"min": -0.32977956463308894
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense2/conv0/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
64,
|
||||
128
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.015499923743453681,
|
||||
"min": -1.9839902391620712
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense2/conv0/bias",
|
||||
"shape": [
|
||||
128
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.0032450980999890497,
|
||||
"min": -0.522460794098237
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense2/conv1/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
128,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.005911862382701799,
|
||||
"min": -0.792189559282041
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense2/conv1/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
128,
|
||||
128
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.021025861478319356,
|
||||
"min": -2.2077154552235325
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense2/conv1/bias",
|
||||
"shape": [
|
||||
128
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.00349616945958605,
|
||||
"min": -0.46149436866535865
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense2/conv2/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
128,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.008104994250278847,
|
||||
"min": -1.013124281284856
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense2/conv2/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
128,
|
||||
128
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.029337059282789044,
|
||||
"min": -3.5791212325002633
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "dense2/conv2/bias",
|
||||
"shape": [
|
||||
128
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.0038808938334969913,
|
||||
"min": -0.4230174278511721
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fc/weights",
|
||||
"shape": [
|
||||
128,
|
||||
136
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.014016061670639936,
|
||||
"min": -1.8921683255363912
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fc/bias",
|
||||
"shape": [
|
||||
136
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.0029505149698724935,
|
||||
"min": 0.088760145008564
|
||||
}
|
||||
}
|
||||
],
|
||||
"paths": [
|
||||
"face_landmark_68_tiny_model-shard1"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,273 @@
|
||||
[
|
||||
{
|
||||
"weights": [
|
||||
{
|
||||
"name": "conv0/filters",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
3,
|
||||
16
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.009007044399485869,
|
||||
"min": -1.2069439495311063
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv0/bias",
|
||||
"shape": [
|
||||
16
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.005263455241334205,
|
||||
"min": -0.9211046672334858
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv1/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
16,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.004001977630690033,
|
||||
"min": -0.5042491814669441
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv1/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
16,
|
||||
32
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.013836609615999109,
|
||||
"min": -1.411334180831909
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv1/bias",
|
||||
"shape": [
|
||||
32
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.0015159862590771096,
|
||||
"min": -0.30926119685173037
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv2/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
32,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.002666276225856706,
|
||||
"min": -0.317286870876948
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv2/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
32,
|
||||
64
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.015265831292844286,
|
||||
"min": -1.6792414422128714
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv2/bias",
|
||||
"shape": [
|
||||
64
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.0020280554598453,
|
||||
"min": -0.37113414915168985
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv3/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
64,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.006100742489683862,
|
||||
"min": -0.8907084034938438
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv3/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
64,
|
||||
128
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.016276211832083907,
|
||||
"min": -2.0508026908425725
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv3/bias",
|
||||
"shape": [
|
||||
128
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.003394414279975143,
|
||||
"min": -0.7637432129944072
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv4/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
128,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.006716050119961009,
|
||||
"min": -0.8059260143953211
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv4/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
128,
|
||||
256
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.021875603993733724,
|
||||
"min": -2.8875797271728514
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv4/bias",
|
||||
"shape": [
|
||||
256
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.0041141652009066415,
|
||||
"min": -0.8187188749804216
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv5/depthwise_filter",
|
||||
"shape": [
|
||||
3,
|
||||
3,
|
||||
256,
|
||||
1
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.008423839597141042,
|
||||
"min": -0.9013508368940915
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv5/pointwise_filter",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
256,
|
||||
512
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.030007277283014035,
|
||||
"min": -3.8709387695088107
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv5/bias",
|
||||
"shape": [
|
||||
512
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.008402082966823203,
|
||||
"min": -1.4871686851277068
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv8/filters",
|
||||
"shape": [
|
||||
1,
|
||||
1,
|
||||
512,
|
||||
25
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.028336129469030042,
|
||||
"min": -4.675461362389957
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "conv8/bias",
|
||||
"shape": [
|
||||
25
|
||||
],
|
||||
"dtype": "float32",
|
||||
"quantization": {
|
||||
"dtype": "uint8",
|
||||
"scale": 0.002268134028303857,
|
||||
"min": -0.41053225912299807
|
||||
}
|
||||
}
|
||||
],
|
||||
"paths": [
|
||||
"tiny_face_detector_model-shard1"
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,493 @@
|
||||
<!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}×tamp=${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>
|
||||
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||