Spaces:
Running
Running
| 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 | |