| """ |
| FastAPI Server Entry Point for AI Resume Scanner & Feedback Dashboard. |
| Project 1 — AI & Generative AI Fellowship Program |
| """ |
|
|
| import os |
| import shutil |
| from pathlib import Path |
| from typing import Optional |
|
|
| from fastapi import FastAPI, UploadFile, File, Form, HTTPException |
| from fastapi.staticfiles import StaticFiles |
| from fastapi.responses import HTMLResponse, FileResponse |
| from fastapi.middleware.cors import CORSMiddleware |
| from dotenv import load_dotenv |
|
|
| from resume_scanner.assessor import ResumeAssessor, AssessorError |
| from resume_scanner.extractor import extract_text_from_file, prepare_scanner_inputs, ExtractionError |
| from resume_scanner.models import Assessment |
|
|
| load_dotenv() |
|
|
| app = FastAPI( |
| title="AI Resume Scanner API", |
| description="Automated resume vs job description screening and feedback engine powered by Gemini Flash.", |
| version="1.0.0", |
| ) |
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| UPLOAD_DIR = Path("data/uploads") |
| UPLOAD_DIR.mkdir(parents=True, exist_ok=True) |
|
|
| |
| STATIC_DIR = Path("static") |
| STATIC_DIR.mkdir(parents=True, exist_ok=True) |
| app.mount("/static", StaticFiles(directory="static"), name="static") |
|
|
|
|
| @app.get("/", response_class=HTMLResponse) |
| async def serve_dashboard(): |
| """Serves the interactive web UI dashboard.""" |
| index_file = STATIC_DIR / "index.html" |
| if index_file.exists(): |
| return FileResponse(index_file) |
| return HTMLResponse("<h1>AI Resume Scanner API is running. Please add static/index.html</h1>") |
|
|
|
|
| @app.get("/api/health") |
| def health_check(): |
| return { |
| "status": "online", |
| "service": "AI Resume Scanner API", |
| "version": "1.0.0", |
| } |
|
|
|
|
| @app.get("/api/sample", response_model=Assessment) |
| @app.get("/api/demo") |
| def run_sample_assessment(): |
| """ |
| Runs an assessment using the preloaded sample resume and sample JD for quick UI demo. |
| """ |
| sample_resume = Path("data/sample_resume.txt") |
| sample_jd = Path("data/sample_jd.txt") |
|
|
| if not sample_resume.exists() or not sample_jd.exists(): |
| raise HTTPException( |
| status_code=404, |
| detail="Sample files not found in data/ directory.", |
| ) |
|
|
| try: |
| resume_text, jd_text = prepare_scanner_inputs(str(sample_resume), str(sample_jd)) |
| assessor = ResumeAssessor() |
| return assessor.assess(resume_text, jd_text) |
| except Exception as exc: |
| raise HTTPException(status_code=500, detail=str(exc)) |
|
|
|
|
| @app.post("/api/scan", response_model=Assessment) |
| async def scan_resume( |
| resume_file: UploadFile = File(..., description="Candidate resume (.pdf or .txt)"), |
| jd_file: Optional[UploadFile] = File(None, description="Job description file (.pdf or .txt)"), |
| jd_text: Optional[str] = Form(None, description="Raw job description text"), |
| ): |
| """ |
| Accepts candidate resume and job description (file or raw text), |
| extracts text safely, and returns validated Pydantic Assessment JSON. |
| """ |
| |
| resume_ext = Path(resume_file.filename or "").suffix.lower() |
| if resume_ext not in (".pdf", ".txt", ".docx", ".doc"): |
| raise HTTPException( |
| status_code=400, |
| detail=f"Unsupported resume file extension '{resume_ext}'. Only .pdf, .txt, .docx, and .doc allowed.", |
| ) |
|
|
| resume_path = UPLOAD_DIR / f"resume_{resume_file.filename}" |
| with open(resume_path, "wb") as buffer: |
| shutil.copyfileobj(resume_file.file, buffer) |
|
|
| try: |
| |
| if jd_file and jd_file.filename: |
| jd_ext = Path(jd_file.filename).suffix.lower() |
| if jd_ext not in (".pdf", ".txt", ".docx", ".doc"): |
| raise HTTPException( |
| status_code=400, |
| detail=f"Unsupported job description file extension '{jd_ext}'. Only .pdf, .txt, .docx, and .doc allowed.", |
| ) |
| jd_path = UPLOAD_DIR / f"jd_{jd_file.filename}" |
| with open(jd_path, "wb") as buffer: |
| shutil.copyfileobj(jd_file.file, buffer) |
| resume_extracted, jd_extracted = prepare_scanner_inputs(str(resume_path), str(jd_path)) |
| elif jd_text and jd_text.strip(): |
| resume_extracted = extract_text_from_file(str(resume_path), label="Resume") |
| jd_extracted = jd_text.strip() |
| else: |
| raise HTTPException( |
| status_code=400, |
| detail="Please provide either a job description file or paste job description text.", |
| ) |
|
|
| assessor = ResumeAssessor() |
| assessment = assessor.assess(resume_extracted, jd_extracted) |
| return assessment |
|
|
| except ExtractionError as exc: |
| raise HTTPException(status_code=400, detail=f"Extraction Error: {exc}") |
| except AssessorError as exc: |
| raise HTTPException(status_code=500, detail=f"AI Assessment Error: {exc}") |
| except Exception as exc: |
| raise HTTPException(status_code=500, detail=str(exc)) |
| finally: |
| |
| if resume_path.exists(): |
| try: |
| resume_path.unlink() |
| except Exception: |
| pass |