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 from mimetypes import guess_type # ========================= # CONFIGURATION # ========================= API_KEY = os.getenv("GEMINI_API_KEY") MODEL_COMBINED = "models/gemini-2.5-flash" _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() mime_type, _ = guess_type(image_path) mime_type = mime_type or "image/jpeg" image_part = Part.from_bytes(data=image_bytes, mime_type=mime_type) # 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 if not response or not response.candidates: raise RuntimeError("Unable to process image at this time") parts = response.candidates[0].content.parts text_chunks = [p.text for p in parts if hasattr(p, "text") and p.text] if not text_chunks: raise RuntimeError("Unable to process image at this time") clean_text = "\n".join(text_chunks) clean_text = clean_text.replace("```json", "").replace("```", "").strip() # Convert to dict try: result = json.loads(clean_text) except json.JSONDecodeError: raise RuntimeError("Unable to process image at this time") # 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, user_info, 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"]) # User Info html = html.replace("{{full_name}}", str(user_info.get("name", ""))) html = html.replace("{{age}}", str(user_info.get("age", ""))) html = html.replace("{{phone}}", str(user_info.get("phone", ""))) html = html.replace("{{gender}}", str(user_info.get("gender", ""))) # Write final HTML with open(output_path, "w", encoding="utf-8") as f: f.write(html) return output_path