pdf / analysis.py
aniket9909's picture
Update analysis.py
7d13bd0 verified
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