File size: 17,467 Bytes
36354ac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
import os
import json
import time
import hashlib
from datetime import datetime
from typing import Optional, Dict, Any

from google import genai
from google.genai.types import Part


# =========================
# CONFIGURATION
# =========================
API_KEY = "AIzaSyCOGp8swGLAyDxvLZAehgmq5nTFye-qgm8"
MODEL_COMBINED = "models/gemini-2.0-flash-exp"

_analysis_cache = {}
_usage_log = []


# =========================
# CLIENT / HELPERS
# =========================
def load_client():
    return genai.Client(api_key=API_KEY)


def get_image_hash(image_path: str) -> str:
    with open(image_path, "rb") as f:
        return hashlib.md5(f.read()).hexdigest()


def log_api_usage(tokens_used: int, cost: float, success: bool = True):
    _usage_log.append({
        "timestamp": datetime.now().isoformat(),
        "tokens": tokens_used,
        "cost": cost,
        "success": success
    })

    with open("api_usage.log", "a") as f:
        f.write(f"{datetime.now()},{tokens_used},{cost},{success}\n")


def retry_with_backoff(func, max_retries: int = 3, initial_delay: float = 2.0):
    delay = initial_delay
    for attempt in range(max_retries):
        try:
            return func()
        except Exception as e:
            error_msg = str(e).lower()

            retryable = any(k in error_msg for k in [
                "500", "503", "502", "504",
                "timeout", "overload", "unavailable",
                "internal error", "service unavailable"
            ])

            if retryable and attempt < max_retries - 1:
                wait = delay * (2 ** attempt)
                print(f"⚠️ Attempt {attempt+1}/{max_retries} failed: {e}")
                print(f"   Retrying in {wait:.1f}s...")
                time.sleep(wait)
            elif attempt == max_retries - 1:
                print(f"❌ All {max_retries} attempts failed: {e}")
                raise
            else:
                # Non-retryable error, raise immediately
                raise
    return None


# =========================
# MAIN GEMINI SKIN ANALYSIS
# =========================
def analyze_skin_complete(
    image_path: str,
    use_cache: bool = True,
    max_retries: int = 3
):

    # Cache key based on image hash
    cache_key = f"complete_v2_{get_image_hash(image_path)}"
    if use_cache and cache_key in _analysis_cache:
        print("✓ Using cached analysis results")
        return _analysis_cache[cache_key]

    def _call():
        client = load_client()

        # Read image bytes
        with open(image_path, "rb") as f:
            image_bytes = f.read()

        image_part = Part.from_bytes(data=image_bytes, mime_type="image/jpeg")

        # UPDATED FULL PROMPT FROM SCRIPT #2
        prompt = """
You are an advanced AI skin analysis system. Analyze the face in this image comprehensively.

Return STRICT JSON with ALL these fields (use exact field names):

{
  "hydration": {
    "texture": float (0.0-1.0, smoothness level),
    "radiance": float (0.0-1.0, natural glow),
    "flakiness": float (0.0-1.0, visible dry flakes - higher is worse),
    "oil_balance": float (0.0-1.0, healthy surface moisture),
    "fine_lines": float (0.0-1.0, dryness lines - higher is worse)
  },
  "pigmentation": {
    "dark_spots": float (0.0-1.0, severity of dark spots),
    "hyperpigmentation": float (0.0-1.0, overall hyperpigmentation),
    "under_eye_pigmentation": float (0.0-1.0, dark circles),
    "redness": float (0.0-1.0, skin redness),
    "melanin_unevenness": float (0.0-1.0, uneven melanin distribution),
    "uv_damage": float (0.0-1.0, visible UV damage),
    "overall_evenness": float (0.0-1.0, overall skin tone evenness)
  },
  "acne": {
    "active_acne": float (0.0-1.0, active breakouts),
    "comedones": float (0.0-1.0, blackheads/whiteheads),
    "cystic_acne": float (0.0-1.0, deep cystic acne),
    "inflammation": float (0.0-1.0, inflammatory response),
    "oiliness": float (0.0-1.0, excess sebum production),
    "scarring": float (0.0-1.0, acne scarring),
    "congestion": float (0.0-1.0, pore congestion)
  },
  "pores": {
    "visibility": float (0.0-1.0, how visible/prominent pores are),
    "size": float (0.0-1.0, average pore size - larger is worse),
    "enlarged_pores": float (0.0-1.0, percentage of enlarged pores),
    "clogged_pores": float (0.0-1.0, degree of pore clogging),
    "texture_roughness": float (0.0-1.0, roughness due to pores),
    "t_zone_prominence": float (0.0-1.0, pore visibility in T-zone),
    "cheek_prominence": float (0.0-1.0, pore visibility on cheeks)
  },
  "wrinkles": {
    "forehead_lines": float (0.0-1.0, horizontal forehead wrinkles),
    "frown_lines": float (0.0-1.0, glabellar lines between eyebrows),
    "crows_feet": float (0.0-1.0, eye corner wrinkles),
    "nasolabial_folds": float (0.0-1.0, nose-to-mouth lines),
    "marionette_lines": float (0.0-1.0, mouth-to-chin lines),
    "under_eye_wrinkles": float (0.0-1.0, fine lines under eyes),
    "lip_lines": float (0.0-1.0, perioral wrinkles around mouth),
    "neck_lines": float (0.0-1.0, horizontal neck wrinkles if visible),
    "overall_severity": float (0.0-1.0, overall wrinkle severity),
    "depth": float (0.0-1.0, average depth of wrinkles),
    "dynamic_wrinkles": float (0.0-1.0, expression-related wrinkles),
    "static_wrinkles": float (0.0-1.0, wrinkles at rest)
  },
  "age_analysis": {
    "fitzpatrick_type": integer (1-6, skin type based on melanin),
    "eye_age": integer (estimated age of eye area),
    "skin_age": integer (estimated overall skin age)
  }
}

DETAILED ANALYSIS GUIDELINES:

PORES:
- Assess pore visibility across different facial zones
- Consider pore size relative to skin type
- Note if pores appear stretched, enlarged, or clogged
- T-zone (forehead, nose, chin) typically has more prominent pores
- Cheeks may show different pore characteristics

WRINKLES:
- Distinguish between dynamic (expression) and static (at rest) wrinkles
- Forehead lines: horizontal lines across forehead
- Frown lines: vertical lines between eyebrows (11 lines)
- Crow's feet: radiating lines from outer eye corners
- Nasolabial folds: lines from nose to mouth corners
- Marionette lines: lines from mouth corners downward
- Assess depth (superficial vs deep wrinkles)
- Consider fine lines vs established wrinkles

CRITICAL RULES:
- Return ONLY raw JSON, no markdown formatting
- No explanations, no text outside JSON
- All float values must be between 0.0 and 1.0
- All integer values must be positive integers
- Base analysis ONLY on visible features in the image
- Do NOT guess or infer anything not visible
- Ensure all fields are present in the response
- If a feature is not visible or applicable, use 0.0
"""

        # --- API CALL WITH TIMING ---
        start_time = time.time()
        response = client.models.generate_content(
            model=MODEL_COMBINED,
            contents=[prompt, image_part],
            config={"temperature": 0, "top_p": 1, "top_k": 1}
        )
        elapsed = time.time() - start_time

        # Clean response text
        clean_text = response.text.strip()
        clean_text = clean_text.replace("```json", "").replace("```", "").strip()

        # Convert to dict
        result = json.loads(clean_text)

        # Estimate token usage
        estimated_tokens = len(prompt) / 4 + len(clean_text) / 4 + 1000
        cost = (estimated_tokens / 1_000_000) * 0.075

        log_api_usage(int(estimated_tokens), cost, success=True)

        print(f"✓ Analysis completed in {elapsed:.2f}s (est. cost: ${cost:.6f})")

        return result

    try:
        result = retry_with_backoff(_call, max_retries=max_retries)
    except Exception as e:
        print(f"❌ Final failure: {e}")
        log_api_usage(0, 0, success=False)
        return None

    if result and use_cache:
        _analysis_cache[cache_key] = result

    return result

# =========================
# SCORE FUNCTIONS
# =========================
def compute_hydration_score(h):
    if not h: return None
    try:
        return round(
            h["radiance"]*30 +
            (1-h["flakiness"])*25 +
            (1-h["fine_lines"])*20 +
            h["oil_balance"]*15 +
            h["texture"]*10,
            1
        )
    except:
        return None


def compute_pigmentation_score(p):
    if not p: return None
    try:
        return round(
            p["hyperpigmentation"]*30 +
            p["dark_spots"]*25 +
            p["melanin_unevenness"]*20 +
            p["under_eye_pigmentation"]*10 +
            p["uv_damage"]*10 +
            p["redness"]*5,
            1
        )
    except:
        return None


def compute_acne_score(a):
    if not a: return None
    try:
        return round(
            a["active_acne"]*40 +
            a["comedones"]*20 +
            a["inflammation"]*15 +
            a["cystic_acne"]*15 +
            a["scarring"]*10,
            1
        )
    except:
        return None


def compute_pores_score(p):
    if not p: return None
    try:
        return round(
            p["visibility"]*25 +
            p["size"]*25 +
            p["enlarged_pores"]*20 +
            p["clogged_pores"]*15 +
            p["texture_roughness"]*15,
            1
        )
    except:
        return None


def compute_wrinkles_score(w):
    if not w: return None
    try:
        return round(
            w["overall_severity"]*30 +
            w["depth"]*20 +
            w["forehead_lines"]*10 +
            w["crows_feet"]*10 +
            w["nasolabial_folds"]*10 +
            w["frown_lines"]*8 +
            w["static_wrinkles"]*7 +
            w["under_eye_wrinkles"]*5,
            1
        )
    except:
        return None


# =========================
# GRADES
# =========================
def grade_wrinkles(p):
    if p <= 5: return "Grade 1 (Absent or barely visible fine lines)"
    elif p <= 25: return "Grade 2 (Shallow wrinkles visible only with muscle movement)"
    elif p <= 50: return "Grade 3 (Moderately deep lines, visible at rest and movement)"
    elif p <= 75: return "Grade 4 (Deep, persistent wrinkles with visible folds)"
    else: return "Grade 5 (Very deep wrinkles, pronounced folds)"


def grade_acne(p):
    if p <= 25: return "Grade 1 (Mostly comedones, little/no inflammation)"
    elif p <= 50: return "Grade 2 (Papules/pustules with mild inflammation)"
    elif p <= 75: return "Grade 3 (Numerous papules, pustules, occasional nodules)"
    else: return "Grade 4 (Severe nodules, cysts, widespread scarring)"


def grade_pigmentation(p):
    if p == 0: return "Grade 0 (Normal skin tone with no visible pigmentation)"
    elif p <= 25: return "Grade 1 (Mild brown patches or spots)"
    elif p <= 50: return "Grade 2 (Moderate uneven tone)"
    else: return "Grade 3 (Severe pigmentation covering large areas)"


def grade_pores(p):
    if p == 0: return "Grade 0 (Barely visible pores)"
    elif p <= 25: return "Grade 1 (Mild pore visibility)"
    elif p <= 50: return "Grade 2 (Noticeable pores)"
    else: return "Grade 3 (Large, prominent pores)"


def grade_hydration(p):
    if p <= 33: return "Grade 1 (Well hydrated)"
    elif p <= 66: return "Grade 2 (Moderate dehydration)"
    else: return "Grade 3 (Severe dehydration)"


def severity_label(percent):
    if percent <= 33: return "Mild"
    elif percent <= 66: return "Moderate"
    else: return "Severe"


# =========================
# DETECTED TEXT
# =========================
def build_detected_text(category, severity):
    s = severity.lower()

    mappings = {
        "wrinkles": {
            "mild": "Fine surface lines are present but minimal.",
            "moderate": "Visible wrinkles are noticeable at rest and with expression.",
            "severe": "Deep and prominent wrinkles detected across multiple regions."
        },
        "acne": {
            "mild": "Almost no breakouts or comedones with minimal inflammation.",
            "moderate": "Inflamed acne lesions are visibly present.",
            "severe": "Severe acne with widespread inflammation and deeper lesions."
        },
        "pores": {
            "mild": "Slight pore visibility with minimal enlargement.",
            "moderate": "Noticeable pore enlargement across key facial zones.",
            "severe": "Strong pore prominence with significant enlargement."
        },
        "pigmentation": {
            "mild": "Light unevenness or a few small dark spots.",
            "moderate": "Moderate pigmentation patches are visibly noticeable.",
            "severe": "Widespread pigmentation with strong uneven tone."
        },
        "hydration": {
            "mild": "Skin appears well-hydrated with minimal dryness.",
            "moderate": "Moderate dryness visible with uneven moisture retention.",
            "severe": "Significant dehydration signs with flakiness or dull texture."
        }
    }

    return mappings.get(category, {}).get(s, "")


# =========================
# HIGH-LEVEL ANALYSIS WRAPPER
# =========================
def get_comprehensive_analysis(image_path):
    raw = analyze_skin_complete(image_path)
    if not raw:
        return None

    # FRONTEND SCORES (Higher is better)
    hydration = compute_hydration_score(raw["hydration"])
    pig = 100 - compute_pigmentation_score(raw["pigmentation"])
    acne = 100 - compute_acne_score(raw["acne"])
    pores = 100 - compute_pores_score(raw["pores"])
    wrinkles = 100 - compute_wrinkles_score(raw["wrinkles"])

    # BACKEND SEVERITY
    sev_pig = 100 - pig
    sev_acne = 100 - acne
    sev_pores = 100 - pores
    sev_wrinkles = 100 - wrinkles
    sev_hydration = 100 - hydration

    grades = {
        "hydration": grade_hydration(sev_hydration),
        "pigmentation": grade_pigmentation(sev_pig),
        "acne": grade_acne(sev_acne),
        "pores": grade_pores(sev_pores),
        "wrinkles": grade_wrinkles(sev_wrinkles),
    }

    severity_output = {
        "wrinkles": {
            "label": severity_label(sev_wrinkles),
            "text": build_detected_text("wrinkles", severity_label(sev_wrinkles))
        },
        "acne": {
            "label": severity_label(sev_acne),
            "text": build_detected_text("acne", severity_label(sev_acne))
        },
        "pores": {
            "label": severity_label(sev_pores),
            "text": build_detected_text("pores", severity_label(sev_pores))
        },
        "pigmentation": {
            "label": severity_label(sev_pig),
            "text": build_detected_text("pigmentation", severity_label(sev_pig))
        },
        "hydration": {
            "label": severity_label(sev_hydration),
            "text": build_detected_text("hydration", severity_label(sev_hydration))
        }
    }

    return {
        "raw_data": raw,
        "scores": {
            "hydration": hydration,
            "pigmentation": pig,
            "acne": acne,
            "pores": pores,
            "wrinkles": wrinkles
        },
        "grades": grades,
        "severity_info": severity_output,
        "age_analysis": raw["age_analysis"],
        "metadata": {
            "analyzed_at": datetime.now().isoformat(),
            "model_used": MODEL_COMBINED
        }
    }
# =========================
# HTML REPORT GENERATOR
# =========================
def generate_html_report(analysis, output_path="new_report.html"):
    """Injects analysis values into the HTML template."""

    with open("report_template.html", "r", encoding="utf-8") as f:
        html = f.read()

    # Scores
    html = html.replace("{{wrinkles_score}}", str(analysis["scores"]["wrinkles"]))
    html = html.replace("{{acne_score}}", str(analysis["scores"]["acne"]))
    html = html.replace("{{pores_score}}", str(analysis["scores"]["pores"]))
    html = html.replace("{{pigmentation_score}}", str(analysis["scores"]["pigmentation"]))
    html = html.replace("{{hydration_score}}", str(analysis["scores"]["hydration"]))

    # Grades
    html = html.replace("{{wrinkles_grade}}", analysis["grades"]["wrinkles"])
    html = html.replace("{{acne_grade}}", analysis["grades"]["acne"])
    html = html.replace("{{pores_grade}}", analysis["grades"]["pores"])
    html = html.replace("{{pigmentation_grade}}", analysis["grades"]["pigmentation"])
    html = html.replace("{{hydration_grade}}", analysis["grades"]["hydration"])

    # Severity labels + text
    html = html.replace("{{wrinkles_severity_label}}", analysis["severity_info"]["wrinkles"]["label"])
    html = html.replace("{{wrinkles_detected_text}}", analysis["severity_info"]["wrinkles"]["text"])

    html = html.replace("{{acne_severity_label}}", analysis["severity_info"]["acne"]["label"])
    html = html.replace("{{acne_detected_text}}", analysis["severity_info"]["acne"]["text"])

    html = html.replace("{{pores_severity_label}}", analysis["severity_info"]["pores"]["label"])
    html = html.replace("{{pores_detected_text}}", analysis["severity_info"]["pores"]["text"])

    html = html.replace("{{pig_severity_label}}", analysis["severity_info"]["pigmentation"]["label"])
    html = html.replace("{{pig_detected_text}}", analysis["severity_info"]["pigmentation"]["text"])

    html = html.replace("{{hydration_severity_label}}", analysis["severity_info"]["hydration"]["label"])
    html = html.replace("{{hydration_detected_text}}", analysis["severity_info"]["hydration"]["text"])

    # Write final HTML
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(html)

    return output_path