Sumit404 commited on
Commit
4676010
·
verified ·
1 Parent(s): c5c8709

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -549
app.py CHANGED
@@ -1,558 +1,98 @@
1
  import gradio as gr
2
  import pdfplumber
3
  import spacy
4
- import re
5
- import html
6
  import os
7
- import logging
8
- import subprocess
9
- from urllib.parse import quote
10
 
11
- # Set up logging
12
- logging.basicConfig(level=logging.INFO)
13
- logger = logging.getLogger(__name__)
14
 
15
- # Load or download spaCy model
16
- try:
17
- nlp = spacy.load("en_core_web_sm")
18
- logger.info("spaCy model 'en_core_web_sm' loaded successfully.")
19
- except Exception as e:
20
- logger.warning(f"spaCy model 'en_core_web_sm' not found. Attempting to download: {str(e)}")
21
  try:
22
- subprocess.run(["python", "-m", "spacy", "download", "en_core_web_sm"], check=True)
23
- nlp = spacy.load("en_core_web_sm")
24
- logger.info("spaCy model 'en_core_web_sm' downloaded and loaded successfully.")
25
- except Exception as download_error:
26
- logger.error(f"Failed to download spaCy model: {str(download_error)}")
27
- raise Exception("Cannot proceed without 'en_core_web_sm' model. Please ensure it is installed.")
28
-
29
- # HTML template using Tailwind CSS and AOS (provided by user)
30
- HTML_TEMPLATE = """
31
- <!DOCTYPE html>
32
- <html lang="en">
33
- <head>
34
- <meta charset="UTF-8">
35
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
36
- <title>{NAME} - IT Portfolio</title>
37
- <script src="https://cdn.tailwindcss.com"></script>
38
- <link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
39
- <style>
40
- body {{
41
- font-family: 'Inter', sans-serif;
42
- scroll-behavior: smooth;
43
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
44
- }}
45
- .section-heading {{
46
- font-size: 1.25rem sm:1.5rem lg:2rem;
47
- font-weight: 800;
48
- color: #1a202c;
49
- margin-bottom: 1.25rem;
50
- text-transform: uppercase;
51
- letter-spacing: 1px;
52
- }}
53
- .card {{
54
- background: rgba(255, 255, 255, 0.9);
55
- border-radius: 1rem;
56
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
57
- padding: 1rem sm:1.5rem;
58
- margin-bottom: 1rem;
59
- transition: transform 0.3s ease, box-shadow 0.3s ease;
60
- transform: perspective(1000px) rotateX(0deg) rotateY(0deg);
61
- }}
62
- .card:hover {{
63
- transform: perspective(1000px) rotateX(2deg) rotateY(2deg);
64
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
65
- }}
66
- .navbar {{
67
- backdrop-filter: blur(12px);
68
- background: rgba(255, 255, 255, 0.1);
69
- border-bottom: 1px solid rgba(255, 255, 255, 0.2);
70
- }}
71
- .nav-link {{
72
- position: relative;
73
- transition: color 0.3s ease;
74
- }}
75
- .nav-link:hover::after {{
76
- content: '';
77
- position: absolute;
78
- width: 100%;
79
- height: 2px;
80
- bottom: -4px;
81
- left: 0;
82
- background: #4a90e2;
83
- animation: underline 0.3s ease forwards;
84
- }}
85
- @keyframes underline {{
86
- from {{ width: 0; }}
87
- to {{ width: 100%; }}
88
- }}
89
- .skill-bar {{
90
- height: 8px;
91
- background: #e2e8f0;
92
- border-radius: 4px;
93
- overflow: hidden;
94
- margin-top: 0.5rem;
95
- }}
96
- .skill-progress {{
97
- height: 100%;
98
- background: linear-gradient(90deg, #4a90e2, #63b3ed);
99
- transition: width 1s ease-in-out;
100
- }}
101
- .hero-text {{
102
- animation: fadeInUp 1s ease-out;
103
- }}
104
- @keyframes fadeInUp {{
105
- from {{ opacity: 0; transform: translateY(20px); }}
106
- to {{ opacity: 1; transform: translateY(0); }}
107
- }}
108
- .btn-3d {{
109
- position: relative;
110
- overflow: hidden;
111
- transition: transform 0.2s ease;
112
- }}
113
- .btn-3d:hover {{
114
- transform: translateY(-2px);
115
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
116
- }}
117
- .btn-3d::before {{
118
- content: '';
119
- position: absolute;
120
- top: 0;
121
- left: -100%;
122
- width: 100%;
123
- height: 100%;
124
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
125
- transition: 0.5s;
126
- }}
127
- .btn-3d:hover::before {{
128
- left: 100%;
129
- }}
130
- .profile-img {{
131
- border: 4px solid white;
132
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
133
- }}
134
- .hamburger {{
135
- display: none;
136
- }}
137
- .nav-menu {{
138
- display: flex;
139
- }}
140
- @media (max-width: 640px) {{
141
- .hamburger {{
142
- display: block;
143
- cursor: pointer;
144
- }}
145
- .nav-menu {{
146
- display: none;
147
- flex-direction: column;
148
- position: absolute;
149
- top: 64px;
150
- right: 0;
151
- width: 100%;
152
- background: rgba(255, 255, 255, 0.95);
153
- padding: 0.75rem;
154
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
155
- }}
156
- .nav-menu.active {{
157
- display: flex;
158
- }}
159
- .nav-link {{
160
- padding: 0.5rem 1rem;
161
- text-align: center;
162
- }}
163
- .section-heading {{
164
- font-size: 1rem;
165
- }}
166
- .card {{
167
- padding: 0.75rem;
168
- }}
169
- .profile-img {{
170
- width: 100px;
171
- height: 100px;
172
- }}
173
- .hero-text h1 {{
174
- font-size: 1.75rem;
175
- }}
176
- .hero-text p {{
177
- font-size: 0.875rem;
178
- }}
179
- .btn-3d {{
180
- padding: 0.5rem 1rem;
181
- font-size: 0.75rem;
182
- }}
183
- }}
184
- @media (min-width: 641px) and (max-width: 768px) {{
185
- .profile-img {{
186
- width: 120px;
187
- height: 120px;
188
- }}
189
- }}
190
- </style>
191
- </head>
192
- <body>
193
- <!-- Navbar -->
194
- <nav class="navbar fixed top-0 w-full z-50">
195
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
196
- <div class="flex justify-between h-16">
197
- <div class="flex items-center">
198
- <span class="text-base sm:text-lg lg:text-xl font-bold text-gray-800">{NAME}</span>
199
- </div>
200
- <div class="flex items-center">
201
- <div class="hamburger sm:hidden">
202
- <svg class="w-5 h-5 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
203
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
204
- </svg>
205
- </div>
206
- <div class="nav-menu sm:flex sm:items-center sm:space-x-2 lg:space-x-4">
207
- <a href="#home" class="nav-link text-gray-600 hover:text-gray-900 px-2 py-2 rounded-md text-xs sm:text-sm font-medium">Home</a>
208
- <a href="#about" class="nav-link text-gray-600 hover:text-gray-900 px-2 py-2 rounded-md text-xs sm:text-sm font-medium">About</a>
209
- <a href="#education" class="nav-link text-gray-600 hover:text-gray-900 px-2 py-2 rounded-md text-xs sm:text-sm font-medium">Education</a>
210
- <a href="#experience" class="nav-link text-gray-600 hover:text-gray-900 px-2 py-2 rounded-md text-xs sm:text-sm font-medium">Experience</a>
211
- <a href="#projects" class="nav-link text-gray-600 hover:text-gray-900 px-2 py-2 rounded-md text-xs sm:text-sm font-medium">Projects</a>
212
- <a href="#skills" class="nav-link text-gray-600 hover:text-gray-900 px-2 py-2 rounded-md text-xs sm:text-sm font-medium">Skills</a>
213
- <a href="#involvement" class="nav-link text-gray-600 hover:text-gray-900 px-2 py-2 rounded-md text-xs sm:text-sm font-medium">Involvement</a>
214
- <a href="#contact" class="nav-link text-gray-600 hover:text-gray-900 px-2 py-2 rounded-md text-xs sm:text-sm font-medium">Contact</a>
215
- </div>
216
- </div>
217
- </div>
218
- </div>
219
- </nav>
220
-
221
- <!-- Hero Section -->
222
- <section id="home" class="min-h-screen flex items-center justify-center bg-gradient-to-r from-blue-600 to-purple-700">
223
- <div class="text-center text-white hero-text" data-aos="fade-up">
224
- <img src="https://via.placeholder.com/150" alt="{NAME}" class="profile-img w-24 h-24 sm:w-36 sm:h-36 lg:w-48 lg:h-48 rounded-full mx-auto mb-3 sm:mb-4 object-cover">
225
- <h1 class="text-xl sm:text-3xl lg:text-5xl font-bold mb-3 sm:mb-4">{NAME}</h1>
226
- <p class="text-sm sm:text-base lg:text-xl mb-4 sm:mb-6">{SKILLS_SUMMARY}</p>
227
- <a href="#contact" class="btn-3d bg-white text-blue-600 px-3 sm:px-4 py-1 sm:py-2 rounded-full font-semibold text-xs sm:text-base">Get in Touch</a>
228
- </div>
229
- </section>
230
-
231
- <!-- About Section -->
232
- <section id="about" class="py-8 sm:py-12 lg:py-20">
233
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
234
- <h2 class="section-heading text-center" data-aos="fade-up">About Me</h2>
235
- <div class="card" data-aos="fade-up" data-aos-delay="100">
236
- <p class="text-gray-700 leading-relaxed text-xs sm:text-sm lg:text-base">
237
- I’m {NAME}, a passionate IT professional specializing in electrical engineering and machine learning. I thrive in dynamic environments, driving innovation through projects like Short-term Traffic Prediction Using DTC. My expertise includes building scalable solutions with a focus on tensor completion and traffic data analysis.
238
- </p>
239
- </div>
240
- </div>
241
- </section>
242
-
243
- <!-- Education Section -->
244
- <section id="education" class="py-8 sm:py-12 lg:py-20 bg-gray-50">
245
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
246
- <h2 class="section-heading text-center" data-aos="fade-up">Education</h2>
247
- {EDUCATION}
248
- </div>
249
- </section>
250
-
251
- <!-- Experience Section -->
252
- <section id="experience" class="py-8 sm:py-12 lg:py-20">
253
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
254
- <h2 class="section-heading text-center" data-aos="fade-up">Experience</h2>
255
- {EXPERIENCE}
256
- </div>
257
- </section>
258
-
259
- <!-- Projects Section -->
260
- <section id="projects" class="py-8 sm:py-12 lg:py-20 bg-gray-50">
261
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
262
- <h2 class="section-heading text-center" data-aos="fade-up">Projects</h2>
263
- {PROJECTS}
264
- </div>
265
- </section>
266
-
267
- <!-- Skills Section -->
268
- <section id="skills" class="py-8 sm:py-12 lg:py-20">
269
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
270
- <h2 class="section-heading text-center" data-aos="fade-up">Skills</h2>
271
- <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3 sm:gap-4 lg:gap-6">
272
- {SKILLS}
273
- </div>
274
- </div>
275
- </section>
276
-
277
- <!-- Involvement Section -->
278
- <section id="involvement" class="py-8 sm:py-12 lg:py-20 bg-gray-50">
279
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
280
- <h2 class="section-heading text-center" data-aos="fade-up">Involvement</h2>
281
- {INVOLVEMENT}
282
- </div>
283
- </section>
284
-
285
- <!-- Contact Section -->
286
- <section id="contact" class="py-8 sm:py-12 lg:py-20 bg-gray-50">
287
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
288
- <h2 class="section-heading text-center" data-aos="fade-up">Contact</h2>
289
- <div class="card" data-aos="fade-up" data-aos-delay="100">
290
- <p class="text-gray-700 text-xs sm:text-sm">Email: <a href="mailto:{EMAIL}" class="text-blue-600 hover:underline">{EMAIL}</a></p>
291
- <p class="text-gray-700 text-xs sm:text-sm">Phone: {PHONE}</p>
292
- </div>
293
- </div>
294
- </section>
295
-
296
- <!-- Footer -->
297
- <footer class="bg-gray-900 text-white py-4 sm:py-6">
298
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
299
- <p class="text-xs sm:text-sm">© 2025 {NAME}. All rights reserved.</p>
300
- </div>
301
- </footer>
302
-
303
- <!-- Scripts -->
304
- <script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
305
- <script>
306
- // Initialize AOS
307
- AOS.init({{
308
- duration: 800,
309
- once: true
310
- }});
311
-
312
- // Hamburger Menu Toggle
313
- const hamburger = document.querySelector('.hamburger');
314
- const navMenu = document.querySelector('.nav-menu');
315
- hamburger.addEventListener('click', () => {{
316
- navMenu.classList.toggle('active');
317
- }});
318
-
319
- // Close menu when clicking a link
320
- document.querySelectorAll('.nav-link').forEach(link => {{
321
- link.addEventListener('click', (e) => {{
322
- e.preventDefault();
323
- navMenu.classList.remove('active');
324
- const targetId = link.getAttribute('href').substring(1);
325
- const targetSection = document.getElementById(targetId);
326
- targetSection.scrollIntoView({{ behavior: 'smooth' }});
327
- }});
328
- }});
329
-
330
- // Skill Bar Animation
331
- const skillBars = document.querySelectorAll('.skill-progress');
332
- const animateSkills = () => {{
333
- skillBars.forEach(bar => {{
334
- const progress = bar.getAttribute('data-progress');
335
- bar.style.width = `${{progress}}%`;
336
- }});
337
- }};
338
- window.addEventListener('scroll', () => {{
339
- const skillsSection = document.getElementById('skills');
340
- const sectionTop = skillsSection.getBoundingClientRect().top;
341
- if (sectionTop < window.innerHeight * 0.8) {{
342
- animateSkills();
343
- window.removeEventListener('scroll', animateSkills);
344
- }}
345
- }});
346
- </script>
347
- </body>
348
- </html>
349
- """
350
-
351
- def extract_text_from_pdf(file_path):
352
- try:
353
- with pdfplumber.open(file_path) as pdf:
354
- text = ""
355
- for page in pdf.pages:
356
- page_text = page.extract_text()
357
- if page_text:
358
- text += page_text
359
- logger.info("Successfully extracted text from PDF.")
360
- return text
361
  except Exception as e:
362
- logger.error(f"Error reading PDF: {str(e)}")
363
- return f"Error reading PDF: {str(e)}"
364
-
365
- def extract_details(resume_text):
366
- try:
367
- # Sanitize the resume text
368
- resume_text = resume_text.replace('\n', ' ').replace('"', '"').replace("'", "'").strip()
369
- doc = nlp(resume_text)
370
- lines = resume_text.split('.')
371
-
372
- # Extract name (assume first line is the name)
373
- name = lines[0].strip().split(',')[0] if lines and lines[0].strip() else "[Your Name]"
374
- name = html.escape(name.title())
375
-
376
- # Extract contact details
377
- email = "your.email@example.com"
378
- phone = "555-555-5555"
379
- email_pattern = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
380
- phone_pattern = r'\b\d{3}-\d{3}-\d{4}\b|\+\d{2}-\d{10}\b'
381
- for line in lines:
382
- if re.search(email_pattern, line):
383
- email = html.escape(re.search(email_pattern, line).group())
384
- if re.search(phone_pattern, line):
385
- phone = html.escape(re.search(phone_pattern, line).group())
386
-
387
- # Extract education
388
- education_keywords = ["degree", "university", "college", "master", "bachelor", "b.tech", "m.tech", "school", "12th", "10th"]
389
- education = [line.strip() for line in lines if any(keyword.lower() in line.lower() for keyword in education_keywords)]
390
- education_html = ""
391
- for i, edu in enumerate(education, 1):
392
- edu = html.escape(edu)
393
- education_html += f"""
394
- <div class="card" data-aos="fade-up" data-aos-delay="{100 * i}">
395
- <p class="text-gray-700 text-xs sm:text-sm">{edu}</p>
396
- </div>
397
- """
398
- education_html = education_html if education else """
399
- <div class="card" data-aos="fade-up" data-aos-delay="100">
400
- <p class="text-gray-700 text-xs sm:text-sm">Not provided</p>
401
- </div>
402
- """
403
-
404
- # Extract experience
405
- experience_keywords = ["intern", "software", "research", "engineer", "developer", "mentor", "guide"]
406
- experience = [line.strip() for line in lines if any(keyword.lower() in line.lower() for keyword in experience_keywords)]
407
- experience_html = ""
408
- for i, exp in enumerate(experience, 1):
409
- exp = html.escape(exp)
410
- experience_html += f"""
411
- <div class="card" data-aos="fade-up" data-aos-delay="{100 * i}">
412
- <p class="text-gray-700 text-xs sm:text-sm">{exp}</p>
413
- </div>
414
- """
415
- experience_html = experience_html if experience else """
416
- <div class="card" data-aos="fade-up" data-aos-delay="100">
417
- <p class="text-gray-700 text-xs sm:text-sm">Not provided</p>
 
 
 
 
 
418
  </div>
419
- """
420
-
421
- # Extract projects
422
- project_keywords = ["project", "developed", "built", "implemented"]
423
- projects = [line.strip() for line in lines if any(keyword.lower() in line.lower() for keyword in project_keywords)]
424
- projects_html = ""
425
- for i, proj in enumerate(projects, 1):
426
- proj = html.escape(proj)
427
- projects_html += f"""
428
- <div class="card" data-aos="fade-up" data-aos-delay="{100 * i}">
429
- <p class="text-gray-700 text-xs sm:text-sm">{proj}</p>
430
- </div>
431
- """
432
- projects_html = projects_html if projects else """
433
- <div class="card" data-aos="fade-up" data-aos-delay="100">
434
- <p class="text-gray-700 text-xs sm:text-sm">Not provided</p>
435
- </div>
436
- """
437
-
438
- # Extract skills
439
- skills_line = next((line for line in lines if "skills" in line.lower()), "")
440
- known_skills = ["python", "javascript", "nlp", "cnns", "aws", "flask", "fastapi", "sql", "docker", "kubernetes", "teamwork", "communication", "c++", "matlab", "html/css"]
441
- skills = []
442
- if skills_line:
443
- for skill in known_skills:
444
- if skill in skills_line.lower() and skill not in skills:
445
- skills.append(skill)
446
- trending_skills = ["Docker", "Kubernetes", "SQL"]
447
- for skill in trending_skills:
448
- if skill.lower() not in [s.lower() for s in skills]:
449
- skills.append(skill)
450
- skills_html = ""
451
- for i, skill in enumerate(skills, 1):
452
- skill = html.escape(skill.title())
453
- skills_html += f"""
454
- <div class="card" data-aos="fade-up" data-aos-delay="{100 * i}">
455
- <h3 class="text-xs sm:text-sm lg:text-base font-semibold text-gray-800">{skill}</h3>
456
- <div class="skill-bar"><div class="skill-progress" style="width: 0%;" data-progress="{80 + (i % 20)}"></div></div>
457
- </div>
458
- """
459
- skills_html = skills_html if skills else """
460
- <div class="card" data-aos="fade-up" data-aos-delay="100">
461
- <p class="text-gray-700 text-xs sm:text-sm">Not provided</p>
462
- </div>
463
- """
464
- skills_summary = ", ".join(skills[:3]) if skills else "IT Professional"
465
-
466
- # Extract involvement
467
- involvement_keywords = ["co-captain", "volunteer", "participant", "member"]
468
- involvement = [line.strip() for line in lines if any(keyword.lower() in line.lower() for keyword in involvement_keywords)]
469
- involvement_html = ""
470
- for i, inv in enumerate(involvement, 1):
471
- inv = html.escape(inv)
472
- involvement_html += f"""
473
- <div class="card" data-aos="fade-up" data-aos-delay="{100 * i}">
474
- <p class="text-gray-700 text-xs sm:text-sm">{inv}</p>
475
- </div>
476
- """
477
- involvement_html = involvement_html if involvement else """
478
- <div class="card" data-aos="fade-up" data-aos-delay="100">
479
- <p class="text-gray-700 text-xs sm:text-sm">Not provided</p>
480
- </div>
481
- """
482
-
483
- logger.info("Successfully extracted details from resume.")
484
- return name, email, phone, education_html, experience_html, projects_html, skills_html, involvement_html, skills_summary, skills
485
- except Exception as e:
486
- logger.error(f"Error extracting details: {str(e)}")
487
- return "[Your Name]", "your.email@example.com", "555-555-5555", f"Error: {str(e)}", f"Error: {str(e)}", f"Error: {str(e)}", f"Error: {str(e)}", f"Error: {str(e)}", "IT Professional", []
488
-
489
- def generate_portfolio(file, theme="Light"):
490
- try:
491
- if not file:
492
- logger.error("No file uploaded.")
493
- return "Error: No file uploaded. Please upload a resume file (PDF or text).", "Error: No file uploaded. Please upload a resume file (PDF or text).", None
494
-
495
- # In Hugging Face Spaces, file.name is the path to the uploaded file
496
- file_path = file.name if hasattr(file, 'name') else file
497
-
498
- # Extract text from the file
499
- if file_path.endswith('.pdf'):
500
- resume_text = extract_text_from_pdf(file_path)
501
- else:
502
- with open(file_path, 'r', encoding='utf-8') as f:
503
- resume_text = f.read()
504
-
505
- if "Error" in resume_text:
506
- logger.error(f"Failed to extract text: {resume_text}")
507
- return resume_text, resume_text, None
508
-
509
- # Extract details from the resume
510
- name, email, phone, education_html, experience_html, projects_html, skills_html, involvement_html, skills_summary, skills = extract_details(resume_text)
511
-
512
- # Generate the portfolio HTML
513
- html_code = HTML_TEMPLATE.format(
514
- NAME=name,
515
- EMAIL=email,
516
- PHONE=phone,
517
- EDUCATION=education_html,
518
- EXPERIENCE=experience_html,
519
- PROJECTS=projects_html,
520
- SKILLS=skills_html,
521
- INVOLVEMENT=involvement_html,
522
- SKILLS_SUMMARY=skills_summary
523
- )
524
-
525
- # Save to index.html
526
- with open("index.html", "w", encoding="utf-8") as f:
527
- f.write(html_code)
528
-
529
- # Display the full website in an iframe
530
- html_preview = f"""
531
- <iframe src="/file=index.html" width="100%" height="600px" style="border:none;"></iframe>
532
- <p style="text-align: center;"><a href="/file=index.html" target="_blank">View Full Portfolio in New Tab</a></p>
533
- """
534
-
535
- logger.info("Successfully generated portfolio.")
536
- return html_preview, html_code, None
537
- except Exception as e:
538
- logger.error(f"Error generating portfolio: {str(e)}")
539
- return f"Error generating portfolio: {str(e)}", f"Error generating portfolio: {str(e)}", None
540
-
541
- # Gradio Interface
542
- with gr.Blocks() as interface:
543
- gr.Markdown("# PortGen - IT Portfolio Builder")
544
- file_input = gr.File(label="Upload Resume (PDF or Text)", file_types=[".pdf", ".txt"])
545
- theme_input = gr.Dropdown(label="Portfolio Theme", choices=["Light", "Dark"], value="Light")
546
- generate_btn = gr.Button("Generate Portfolio")
547
-
548
- html_output = gr.HTML(label="Portfolio Preview")
549
- code_output = gr.Textbox(label="Downloadable HTML/CSS Code", lines=10)
550
- error_output = gr.Textbox(label="Error (if any)", visible=False)
551
-
552
- generate_btn.click(
553
- fn=generate_portfolio,
554
- inputs=[file_input, theme_input],
555
- outputs=[html_output, code_output, error_output]
556
- )
557
-
558
- interface.launch()
 
1
  import gradio as gr
2
  import pdfplumber
3
  import spacy
 
 
4
  import os
 
 
 
5
 
6
+ # Load spaCy model with minimal pipelines to save memory
7
+ nlp = spacy.load("en_core_web_sm", disable=["ner", "lemmatizer"])
 
8
 
9
+ def generate_portfolio(resume_file):
10
+ # Extract text from PDF
 
 
 
 
11
  try:
12
+ with pdfplumber.open(resume_file) as pdf:
13
+ text = "".join(page.extract_text() or "" for page in pdf.pages)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  except Exception as e:
15
+ return f"Error processing PDF: {str(e)}"
16
+
17
+ # Parse resume with spaCy
18
+ doc = nlp(text)
19
+ data = {
20
+ "name": "",
21
+ "summary": "",
22
+ "experience": [],
23
+ "skills": [],
24
+ "contact": ""
25
+ }
26
+ for ent in doc.ents:
27
+ if ent.label_ == "PERSON" and not data["name"]:
28
+ data["name"] = ent.text
29
+ elif ent.label_ == "ORG":
30
+ data["experience"].append({"company": ent.text, "role": ""})
31
+ elif ent.label_ == "EMAIL":
32
+ data["contact"] = ent.text
33
+
34
+ # Extract skills (simple keyword-based)
35
+ skill_keywords = ["python", "javascript", "sql", "communication", "leadership"]
36
+ data["skills"] = [token.text for token in doc if token.text.lower() in skill_keywords]
37
+ data["summary"] = text[:200] + "..." # Truncate for demo
38
+
39
+ # Delete the uploaded file (privacy)
40
+ os.remove(resume_file)
41
+
42
+ # Generate portfolio HTML
43
+ portfolio_html = f"""
44
+ <!DOCTYPE html>
45
+ <html lang="en">
46
+ <head>
47
+ <meta charset="UTF-8">
48
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
49
+ <title>{data['name']}'s Portfolio</title>
50
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
51
+ </head>
52
+ <body>
53
+ <header class="bg-primary text-white text-center py-5">
54
+ <h1>{data['name']}</h1>
55
+ <p>Professional Portfolio</p>
56
+ </header>
57
+ <div class="container my-5">
58
+ <section>
59
+ <h2>About Me</h2>
60
+ <p>{data['summary']}</p>
61
+ </section>
62
+ <section>
63
+ <h2>Experience</h2>
64
+ {''.join([f'<div class="card mb-3"><div class="card-body"><h5 class="card-title">{exp["company"]}</h5><p class="card-text">{exp["role"]}</p></div></div>' for exp in data['experience']])}
65
+ </section>
66
+ <section>
67
+ <h2>Skills</h2>
68
+ <ul class="list-group">
69
+ {''.join([f'<li class="list-group-item">{skill}</li>' for skill in data['skills']])}
70
+ </ul>
71
+ </section>
72
+ <section>
73
+ <h2>Contact</h2>
74
+ <p>Email: {data['contact']}</p>
75
+ </section>
76
  </div>
77
+ <footer class="bg-dark text-white text-center py-3">
78
+ <p>© 2025 {data['name']}</p>
79
+ <p>Generated by Resume-to-Portfolio AI. Your data was processed securely and not retained.</p>
80
+ </footer>
81
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
82
+ </body>
83
+ </html>
84
+ """
85
+ return portfolio_html
86
+
87
+ # Gradio interface
88
+ interface = gr.Interface(
89
+ fn=generate_portfolio,
90
+ inputs=gr.File(label="Upload Resume (PDF)"),
91
+ outputs=gr.HTML(label="Your Portfolio"),
92
+ title="Resume to Portfolio Generator",
93
+ description="Upload your resume to generate a portfolio landing page. Your data is processed securely and not stored.",
94
+ allow_flagging="never"
95
+ )
96
+
97
+ if __name__ == "__main__":
98
+ interface.launch()