aniket9909 commited on
Commit
36354ac
·
verified ·
1 Parent(s): f533d0a

Upload 18 files

Browse files
.gitattributes CHANGED
@@ -33,3 +33,10 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ img1.png filter=lfs diff=lfs merge=lfs -text
37
+ img2.png filter=lfs diff=lfs merge=lfs -text
38
+ img3.png filter=lfs diff=lfs merge=lfs -text
39
+ img4.png filter=lfs diff=lfs merge=lfs -text
40
+ img5.png filter=lfs diff=lfs merge=lfs -text
41
+ img6.png filter=lfs diff=lfs merge=lfs -text
42
+ logo.png filter=lfs diff=lfs merge=lfs -text
analysis.py ADDED
@@ -0,0 +1,504 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import time
4
+ import hashlib
5
+ from datetime import datetime
6
+ from typing import Optional, Dict, Any
7
+
8
+ from google import genai
9
+ from google.genai.types import Part
10
+
11
+
12
+ # =========================
13
+ # CONFIGURATION
14
+ # =========================
15
+ API_KEY = "AIzaSyCOGp8swGLAyDxvLZAehgmq5nTFye-qgm8"
16
+ MODEL_COMBINED = "models/gemini-2.0-flash-exp"
17
+
18
+ _analysis_cache = {}
19
+ _usage_log = []
20
+
21
+
22
+ # =========================
23
+ # CLIENT / HELPERS
24
+ # =========================
25
+ def load_client():
26
+ return genai.Client(api_key=API_KEY)
27
+
28
+
29
+ def get_image_hash(image_path: str) -> str:
30
+ with open(image_path, "rb") as f:
31
+ return hashlib.md5(f.read()).hexdigest()
32
+
33
+
34
+ def log_api_usage(tokens_used: int, cost: float, success: bool = True):
35
+ _usage_log.append({
36
+ "timestamp": datetime.now().isoformat(),
37
+ "tokens": tokens_used,
38
+ "cost": cost,
39
+ "success": success
40
+ })
41
+
42
+ with open("api_usage.log", "a") as f:
43
+ f.write(f"{datetime.now()},{tokens_used},{cost},{success}\n")
44
+
45
+
46
+ def retry_with_backoff(func, max_retries: int = 3, initial_delay: float = 2.0):
47
+ delay = initial_delay
48
+ for attempt in range(max_retries):
49
+ try:
50
+ return func()
51
+ except Exception as e:
52
+ error_msg = str(e).lower()
53
+
54
+ retryable = any(k in error_msg for k in [
55
+ "500", "503", "502", "504",
56
+ "timeout", "overload", "unavailable",
57
+ "internal error", "service unavailable"
58
+ ])
59
+
60
+ if retryable and attempt < max_retries - 1:
61
+ wait = delay * (2 ** attempt)
62
+ print(f"⚠️ Attempt {attempt+1}/{max_retries} failed: {e}")
63
+ print(f" Retrying in {wait:.1f}s...")
64
+ time.sleep(wait)
65
+ elif attempt == max_retries - 1:
66
+ print(f"❌ All {max_retries} attempts failed: {e}")
67
+ raise
68
+ else:
69
+ # Non-retryable error, raise immediately
70
+ raise
71
+ return None
72
+
73
+
74
+ # =========================
75
+ # MAIN GEMINI SKIN ANALYSIS
76
+ # =========================
77
+ def analyze_skin_complete(
78
+ image_path: str,
79
+ use_cache: bool = True,
80
+ max_retries: int = 3
81
+ ):
82
+
83
+ # Cache key based on image hash
84
+ cache_key = f"complete_v2_{get_image_hash(image_path)}"
85
+ if use_cache and cache_key in _analysis_cache:
86
+ print("✓ Using cached analysis results")
87
+ return _analysis_cache[cache_key]
88
+
89
+ def _call():
90
+ client = load_client()
91
+
92
+ # Read image bytes
93
+ with open(image_path, "rb") as f:
94
+ image_bytes = f.read()
95
+
96
+ image_part = Part.from_bytes(data=image_bytes, mime_type="image/jpeg")
97
+
98
+ # UPDATED FULL PROMPT FROM SCRIPT #2
99
+ prompt = """
100
+ You are an advanced AI skin analysis system. Analyze the face in this image comprehensively.
101
+
102
+ Return STRICT JSON with ALL these fields (use exact field names):
103
+
104
+ {
105
+ "hydration": {
106
+ "texture": float (0.0-1.0, smoothness level),
107
+ "radiance": float (0.0-1.0, natural glow),
108
+ "flakiness": float (0.0-1.0, visible dry flakes - higher is worse),
109
+ "oil_balance": float (0.0-1.0, healthy surface moisture),
110
+ "fine_lines": float (0.0-1.0, dryness lines - higher is worse)
111
+ },
112
+ "pigmentation": {
113
+ "dark_spots": float (0.0-1.0, severity of dark spots),
114
+ "hyperpigmentation": float (0.0-1.0, overall hyperpigmentation),
115
+ "under_eye_pigmentation": float (0.0-1.0, dark circles),
116
+ "redness": float (0.0-1.0, skin redness),
117
+ "melanin_unevenness": float (0.0-1.0, uneven melanin distribution),
118
+ "uv_damage": float (0.0-1.0, visible UV damage),
119
+ "overall_evenness": float (0.0-1.0, overall skin tone evenness)
120
+ },
121
+ "acne": {
122
+ "active_acne": float (0.0-1.0, active breakouts),
123
+ "comedones": float (0.0-1.0, blackheads/whiteheads),
124
+ "cystic_acne": float (0.0-1.0, deep cystic acne),
125
+ "inflammation": float (0.0-1.0, inflammatory response),
126
+ "oiliness": float (0.0-1.0, excess sebum production),
127
+ "scarring": float (0.0-1.0, acne scarring),
128
+ "congestion": float (0.0-1.0, pore congestion)
129
+ },
130
+ "pores": {
131
+ "visibility": float (0.0-1.0, how visible/prominent pores are),
132
+ "size": float (0.0-1.0, average pore size - larger is worse),
133
+ "enlarged_pores": float (0.0-1.0, percentage of enlarged pores),
134
+ "clogged_pores": float (0.0-1.0, degree of pore clogging),
135
+ "texture_roughness": float (0.0-1.0, roughness due to pores),
136
+ "t_zone_prominence": float (0.0-1.0, pore visibility in T-zone),
137
+ "cheek_prominence": float (0.0-1.0, pore visibility on cheeks)
138
+ },
139
+ "wrinkles": {
140
+ "forehead_lines": float (0.0-1.0, horizontal forehead wrinkles),
141
+ "frown_lines": float (0.0-1.0, glabellar lines between eyebrows),
142
+ "crows_feet": float (0.0-1.0, eye corner wrinkles),
143
+ "nasolabial_folds": float (0.0-1.0, nose-to-mouth lines),
144
+ "marionette_lines": float (0.0-1.0, mouth-to-chin lines),
145
+ "under_eye_wrinkles": float (0.0-1.0, fine lines under eyes),
146
+ "lip_lines": float (0.0-1.0, perioral wrinkles around mouth),
147
+ "neck_lines": float (0.0-1.0, horizontal neck wrinkles if visible),
148
+ "overall_severity": float (0.0-1.0, overall wrinkle severity),
149
+ "depth": float (0.0-1.0, average depth of wrinkles),
150
+ "dynamic_wrinkles": float (0.0-1.0, expression-related wrinkles),
151
+ "static_wrinkles": float (0.0-1.0, wrinkles at rest)
152
+ },
153
+ "age_analysis": {
154
+ "fitzpatrick_type": integer (1-6, skin type based on melanin),
155
+ "eye_age": integer (estimated age of eye area),
156
+ "skin_age": integer (estimated overall skin age)
157
+ }
158
+ }
159
+
160
+ DETAILED ANALYSIS GUIDELINES:
161
+
162
+ PORES:
163
+ - Assess pore visibility across different facial zones
164
+ - Consider pore size relative to skin type
165
+ - Note if pores appear stretched, enlarged, or clogged
166
+ - T-zone (forehead, nose, chin) typically has more prominent pores
167
+ - Cheeks may show different pore characteristics
168
+
169
+ WRINKLES:
170
+ - Distinguish between dynamic (expression) and static (at rest) wrinkles
171
+ - Forehead lines: horizontal lines across forehead
172
+ - Frown lines: vertical lines between eyebrows (11 lines)
173
+ - Crow's feet: radiating lines from outer eye corners
174
+ - Nasolabial folds: lines from nose to mouth corners
175
+ - Marionette lines: lines from mouth corners downward
176
+ - Assess depth (superficial vs deep wrinkles)
177
+ - Consider fine lines vs established wrinkles
178
+
179
+ CRITICAL RULES:
180
+ - Return ONLY raw JSON, no markdown formatting
181
+ - No explanations, no text outside JSON
182
+ - All float values must be between 0.0 and 1.0
183
+ - All integer values must be positive integers
184
+ - Base analysis ONLY on visible features in the image
185
+ - Do NOT guess or infer anything not visible
186
+ - Ensure all fields are present in the response
187
+ - If a feature is not visible or applicable, use 0.0
188
+ """
189
+
190
+ # --- API CALL WITH TIMING ---
191
+ start_time = time.time()
192
+ response = client.models.generate_content(
193
+ model=MODEL_COMBINED,
194
+ contents=[prompt, image_part],
195
+ config={"temperature": 0, "top_p": 1, "top_k": 1}
196
+ )
197
+ elapsed = time.time() - start_time
198
+
199
+ # Clean response text
200
+ clean_text = response.text.strip()
201
+ clean_text = clean_text.replace("```json", "").replace("```", "").strip()
202
+
203
+ # Convert to dict
204
+ result = json.loads(clean_text)
205
+
206
+ # Estimate token usage
207
+ estimated_tokens = len(prompt) / 4 + len(clean_text) / 4 + 1000
208
+ cost = (estimated_tokens / 1_000_000) * 0.075
209
+
210
+ log_api_usage(int(estimated_tokens), cost, success=True)
211
+
212
+ print(f"✓ Analysis completed in {elapsed:.2f}s (est. cost: ${cost:.6f})")
213
+
214
+ return result
215
+
216
+ try:
217
+ result = retry_with_backoff(_call, max_retries=max_retries)
218
+ except Exception as e:
219
+ print(f"❌ Final failure: {e}")
220
+ log_api_usage(0, 0, success=False)
221
+ return None
222
+
223
+ if result and use_cache:
224
+ _analysis_cache[cache_key] = result
225
+
226
+ return result
227
+
228
+ # =========================
229
+ # SCORE FUNCTIONS
230
+ # =========================
231
+ def compute_hydration_score(h):
232
+ if not h: return None
233
+ try:
234
+ return round(
235
+ h["radiance"]*30 +
236
+ (1-h["flakiness"])*25 +
237
+ (1-h["fine_lines"])*20 +
238
+ h["oil_balance"]*15 +
239
+ h["texture"]*10,
240
+ 1
241
+ )
242
+ except:
243
+ return None
244
+
245
+
246
+ def compute_pigmentation_score(p):
247
+ if not p: return None
248
+ try:
249
+ return round(
250
+ p["hyperpigmentation"]*30 +
251
+ p["dark_spots"]*25 +
252
+ p["melanin_unevenness"]*20 +
253
+ p["under_eye_pigmentation"]*10 +
254
+ p["uv_damage"]*10 +
255
+ p["redness"]*5,
256
+ 1
257
+ )
258
+ except:
259
+ return None
260
+
261
+
262
+ def compute_acne_score(a):
263
+ if not a: return None
264
+ try:
265
+ return round(
266
+ a["active_acne"]*40 +
267
+ a["comedones"]*20 +
268
+ a["inflammation"]*15 +
269
+ a["cystic_acne"]*15 +
270
+ a["scarring"]*10,
271
+ 1
272
+ )
273
+ except:
274
+ return None
275
+
276
+
277
+ def compute_pores_score(p):
278
+ if not p: return None
279
+ try:
280
+ return round(
281
+ p["visibility"]*25 +
282
+ p["size"]*25 +
283
+ p["enlarged_pores"]*20 +
284
+ p["clogged_pores"]*15 +
285
+ p["texture_roughness"]*15,
286
+ 1
287
+ )
288
+ except:
289
+ return None
290
+
291
+
292
+ def compute_wrinkles_score(w):
293
+ if not w: return None
294
+ try:
295
+ return round(
296
+ w["overall_severity"]*30 +
297
+ w["depth"]*20 +
298
+ w["forehead_lines"]*10 +
299
+ w["crows_feet"]*10 +
300
+ w["nasolabial_folds"]*10 +
301
+ w["frown_lines"]*8 +
302
+ w["static_wrinkles"]*7 +
303
+ w["under_eye_wrinkles"]*5,
304
+ 1
305
+ )
306
+ except:
307
+ return None
308
+
309
+
310
+ # =========================
311
+ # GRADES
312
+ # =========================
313
+ def grade_wrinkles(p):
314
+ if p <= 5: return "Grade 1 (Absent or barely visible fine lines)"
315
+ elif p <= 25: return "Grade 2 (Shallow wrinkles visible only with muscle movement)"
316
+ elif p <= 50: return "Grade 3 (Moderately deep lines, visible at rest and movement)"
317
+ elif p <= 75: return "Grade 4 (Deep, persistent wrinkles with visible folds)"
318
+ else: return "Grade 5 (Very deep wrinkles, pronounced folds)"
319
+
320
+
321
+ def grade_acne(p):
322
+ if p <= 25: return "Grade 1 (Mostly comedones, little/no inflammation)"
323
+ elif p <= 50: return "Grade 2 (Papules/pustules with mild inflammation)"
324
+ elif p <= 75: return "Grade 3 (Numerous papules, pustules, occasional nodules)"
325
+ else: return "Grade 4 (Severe nodules, cysts, widespread scarring)"
326
+
327
+
328
+ def grade_pigmentation(p):
329
+ if p == 0: return "Grade 0 (Normal skin tone with no visible pigmentation)"
330
+ elif p <= 25: return "Grade 1 (Mild brown patches or spots)"
331
+ elif p <= 50: return "Grade 2 (Moderate uneven tone)"
332
+ else: return "Grade 3 (Severe pigmentation covering large areas)"
333
+
334
+
335
+ def grade_pores(p):
336
+ if p == 0: return "Grade 0 (Barely visible pores)"
337
+ elif p <= 25: return "Grade 1 (Mild pore visibility)"
338
+ elif p <= 50: return "Grade 2 (Noticeable pores)"
339
+ else: return "Grade 3 (Large, prominent pores)"
340
+
341
+
342
+ def grade_hydration(p):
343
+ if p <= 33: return "Grade 1 (Well hydrated)"
344
+ elif p <= 66: return "Grade 2 (Moderate dehydration)"
345
+ else: return "Grade 3 (Severe dehydration)"
346
+
347
+
348
+ def severity_label(percent):
349
+ if percent <= 33: return "Mild"
350
+ elif percent <= 66: return "Moderate"
351
+ else: return "Severe"
352
+
353
+
354
+ # =========================
355
+ # DETECTED TEXT
356
+ # =========================
357
+ def build_detected_text(category, severity):
358
+ s = severity.lower()
359
+
360
+ mappings = {
361
+ "wrinkles": {
362
+ "mild": "Fine surface lines are present but minimal.",
363
+ "moderate": "Visible wrinkles are noticeable at rest and with expression.",
364
+ "severe": "Deep and prominent wrinkles detected across multiple regions."
365
+ },
366
+ "acne": {
367
+ "mild": "Almost no breakouts or comedones with minimal inflammation.",
368
+ "moderate": "Inflamed acne lesions are visibly present.",
369
+ "severe": "Severe acne with widespread inflammation and deeper lesions."
370
+ },
371
+ "pores": {
372
+ "mild": "Slight pore visibility with minimal enlargement.",
373
+ "moderate": "Noticeable pore enlargement across key facial zones.",
374
+ "severe": "Strong pore prominence with significant enlargement."
375
+ },
376
+ "pigmentation": {
377
+ "mild": "Light unevenness or a few small dark spots.",
378
+ "moderate": "Moderate pigmentation patches are visibly noticeable.",
379
+ "severe": "Widespread pigmentation with strong uneven tone."
380
+ },
381
+ "hydration": {
382
+ "mild": "Skin appears well-hydrated with minimal dryness.",
383
+ "moderate": "Moderate dryness visible with uneven moisture retention.",
384
+ "severe": "Significant dehydration signs with flakiness or dull texture."
385
+ }
386
+ }
387
+
388
+ return mappings.get(category, {}).get(s, "")
389
+
390
+
391
+ # =========================
392
+ # HIGH-LEVEL ANALYSIS WRAPPER
393
+ # =========================
394
+ def get_comprehensive_analysis(image_path):
395
+ raw = analyze_skin_complete(image_path)
396
+ if not raw:
397
+ return None
398
+
399
+ # FRONTEND SCORES (Higher is better)
400
+ hydration = compute_hydration_score(raw["hydration"])
401
+ pig = 100 - compute_pigmentation_score(raw["pigmentation"])
402
+ acne = 100 - compute_acne_score(raw["acne"])
403
+ pores = 100 - compute_pores_score(raw["pores"])
404
+ wrinkles = 100 - compute_wrinkles_score(raw["wrinkles"])
405
+
406
+ # BACKEND SEVERITY
407
+ sev_pig = 100 - pig
408
+ sev_acne = 100 - acne
409
+ sev_pores = 100 - pores
410
+ sev_wrinkles = 100 - wrinkles
411
+ sev_hydration = 100 - hydration
412
+
413
+ grades = {
414
+ "hydration": grade_hydration(sev_hydration),
415
+ "pigmentation": grade_pigmentation(sev_pig),
416
+ "acne": grade_acne(sev_acne),
417
+ "pores": grade_pores(sev_pores),
418
+ "wrinkles": grade_wrinkles(sev_wrinkles),
419
+ }
420
+
421
+ severity_output = {
422
+ "wrinkles": {
423
+ "label": severity_label(sev_wrinkles),
424
+ "text": build_detected_text("wrinkles", severity_label(sev_wrinkles))
425
+ },
426
+ "acne": {
427
+ "label": severity_label(sev_acne),
428
+ "text": build_detected_text("acne", severity_label(sev_acne))
429
+ },
430
+ "pores": {
431
+ "label": severity_label(sev_pores),
432
+ "text": build_detected_text("pores", severity_label(sev_pores))
433
+ },
434
+ "pigmentation": {
435
+ "label": severity_label(sev_pig),
436
+ "text": build_detected_text("pigmentation", severity_label(sev_pig))
437
+ },
438
+ "hydration": {
439
+ "label": severity_label(sev_hydration),
440
+ "text": build_detected_text("hydration", severity_label(sev_hydration))
441
+ }
442
+ }
443
+
444
+ return {
445
+ "raw_data": raw,
446
+ "scores": {
447
+ "hydration": hydration,
448
+ "pigmentation": pig,
449
+ "acne": acne,
450
+ "pores": pores,
451
+ "wrinkles": wrinkles
452
+ },
453
+ "grades": grades,
454
+ "severity_info": severity_output,
455
+ "age_analysis": raw["age_analysis"],
456
+ "metadata": {
457
+ "analyzed_at": datetime.now().isoformat(),
458
+ "model_used": MODEL_COMBINED
459
+ }
460
+ }
461
+ # =========================
462
+ # HTML REPORT GENERATOR
463
+ # =========================
464
+ def generate_html_report(analysis, output_path="new_report.html"):
465
+ """Injects analysis values into the HTML template."""
466
+
467
+ with open("report_template.html", "r", encoding="utf-8") as f:
468
+ html = f.read()
469
+
470
+ # Scores
471
+ html = html.replace("{{wrinkles_score}}", str(analysis["scores"]["wrinkles"]))
472
+ html = html.replace("{{acne_score}}", str(analysis["scores"]["acne"]))
473
+ html = html.replace("{{pores_score}}", str(analysis["scores"]["pores"]))
474
+ html = html.replace("{{pigmentation_score}}", str(analysis["scores"]["pigmentation"]))
475
+ html = html.replace("{{hydration_score}}", str(analysis["scores"]["hydration"]))
476
+
477
+ # Grades
478
+ html = html.replace("{{wrinkles_grade}}", analysis["grades"]["wrinkles"])
479
+ html = html.replace("{{acne_grade}}", analysis["grades"]["acne"])
480
+ html = html.replace("{{pores_grade}}", analysis["grades"]["pores"])
481
+ html = html.replace("{{pigmentation_grade}}", analysis["grades"]["pigmentation"])
482
+ html = html.replace("{{hydration_grade}}", analysis["grades"]["hydration"])
483
+
484
+ # Severity labels + text
485
+ html = html.replace("{{wrinkles_severity_label}}", analysis["severity_info"]["wrinkles"]["label"])
486
+ html = html.replace("{{wrinkles_detected_text}}", analysis["severity_info"]["wrinkles"]["text"])
487
+
488
+ html = html.replace("{{acne_severity_label}}", analysis["severity_info"]["acne"]["label"])
489
+ html = html.replace("{{acne_detected_text}}", analysis["severity_info"]["acne"]["text"])
490
+
491
+ html = html.replace("{{pores_severity_label}}", analysis["severity_info"]["pores"]["label"])
492
+ html = html.replace("{{pores_detected_text}}", analysis["severity_info"]["pores"]["text"])
493
+
494
+ html = html.replace("{{pig_severity_label}}", analysis["severity_info"]["pigmentation"]["label"])
495
+ html = html.replace("{{pig_detected_text}}", analysis["severity_info"]["pigmentation"]["text"])
496
+
497
+ html = html.replace("{{hydration_severity_label}}", analysis["severity_info"]["hydration"]["label"])
498
+ html = html.replace("{{hydration_detected_text}}", analysis["severity_info"]["hydration"]["text"])
499
+
500
+ # Write final HTML
501
+ with open(output_path, "w", encoding="utf-8") as f:
502
+ f.write(html)
503
+
504
+ return output_path
email-svgrepo-com.svg ADDED
facebook.svg ADDED
icons8-linkedin (1).svg ADDED
icons8-linkedin.svg ADDED
img1.png ADDED

Git LFS Details

  • SHA256: 8eda415d5d9eed3f5d403d550f05cbc735d1b802ca302a9b1c7cd575c6390ef0
  • Pointer size: 131 Bytes
  • Size of remote file: 866 kB
img2.png ADDED

Git LFS Details

  • SHA256: 19780f3cdcb73961a4e3e6eb9dded7b32375610676f98d653ae9b2ea50872ba3
  • Pointer size: 132 Bytes
  • Size of remote file: 1.27 MB
img3.png ADDED

Git LFS Details

  • SHA256: c609972a65c6bcc6120f12e69f280f7db9930705149989abcfd327cf63f70145
  • Pointer size: 132 Bytes
  • Size of remote file: 1.23 MB
img4.png ADDED

Git LFS Details

  • SHA256: bcdadb8e07b3b9f24b2386f5f421004f1560d2aeac26ea8cda72c75fa7ba6264
  • Pointer size: 131 Bytes
  • Size of remote file: 910 kB
img5.png ADDED

Git LFS Details

  • SHA256: e33e2453f994e3740c2f3726dbc34ba78df13538fe134baa1df8684dcf749c1c
  • Pointer size: 132 Bytes
  • Size of remote file: 1.17 MB
img6.png ADDED

Git LFS Details

  • SHA256: 69eb4fb69adb95d0fa33d1db6ea0b97ac2b05c4daf4c941fbd8fc913c2380dd9
  • Pointer size: 131 Bytes
  • Size of remote file: 614 kB
instagram (1).svg ADDED
instagram.svg ADDED
linkedin-logo-svgrepo-com.svg ADDED
linkedin.svg ADDED
logo.png ADDED

Git LFS Details

  • SHA256: 21a4fdd3e6b8ae4c1a0ea11819d5cdf36e87734963a5eed289cfa8486dbf3a97
  • Pointer size: 131 Bytes
  • Size of remote file: 340 kB
pdf_generator.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from playwright.sync_api import sync_playwright
2
+ import os
3
+
4
+ def generate_pdf(html_path, output_path="report.pdf"):
5
+ with sync_playwright() as pw:
6
+ browser = pw.chromium.launch()
7
+ page = browser.new_page()
8
+
9
+ # Normalize path for Playwright
10
+ safe_path = html_path.replace("\\", "/")
11
+
12
+ page.goto(f"file:///{safe_path}")
13
+
14
+ page.pdf(
15
+ path=output_path,
16
+ format="A4",
17
+ print_background=True,
18
+ )
19
+
20
+ browser.close()
21
+
22
+ return output_path
phone-svgrepo-com.svg ADDED