deepakkumarsoni commited on
Commit
da30de9
·
1 Parent(s): 3c1e580

aicoach backend 2

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ 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
+ models/face_landmarker.task filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use a Python 3.10 slim image
2
+ FROM python:3.10-slim
3
+
4
+ ENV TZ=UTC
5
+
6
+ # Install system dependencies
7
+ # libgl1 replaces libgl1-mesa-glx in newer Debian versions for OpenCV/MediaPipe
8
+ RUN apt-get update && apt-get install -y \
9
+ espeak \
10
+ && rm -rf /var/lib/apt/lists/*
11
+
12
+ # Set up a new user named "user" with user ID 1000 (Required by Hugging Face)
13
+ RUN useradd -m -u 1000 user
14
+ USER user
15
+ ENV HOME=/home/user \
16
+ PATH=/home/user/.local/bin:$PATH
17
+
18
+ WORKDIR $HOME/app
19
+
20
+ # 1. Copy requirements first to leverage Docker caching
21
+ # If your requirements.txt is inside a 'backend' folder, change this to: COPY --chown=user backend/requirements.txt .
22
+ COPY --chown=user requirements.txt .
23
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
24
+
25
+ # 2. Copy the rest of the files
26
+ COPY --chown=user . .
27
+
28
+ # 3. Start the application
29
+ # If main.py is in the root of your Space, use:
30
+ # CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
31
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "2"]
32
+
33
+ # IF main.py is inside a 'backend' folder, use this instead:
34
+ # CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Aicoach2
3
- emoji: 🌍
4
- colorFrom: yellow
5
- colorTo: purple
6
  sdk: docker
7
  pinned: false
 
 
8
  ---
9
 
10
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: Aicoach
3
+ emoji: 💻
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
+ short_description: InterviewMinutes Dockerized Backend
10
  ---
11
 
12
  Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
app/__init__.py ADDED
File without changes
app/auth.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import bcrypt
2
+ import hashlib
3
+ from jose import jwt
4
+ from datetime import datetime, timedelta, timezone
5
+ from .core.config import settings
6
+
7
+ ALGORITHM = "HS256"
8
+
9
+ def normalize_password(password: str):
10
+ return hashlib.sha256(password.encode()).hexdigest()
11
+
12
+ def hash_password(password: str):
13
+ password_bytes = normalize_password(password).encode('utf-8')
14
+ salt = bcrypt.gensalt()
15
+ hashed = bcrypt.hashpw(password_bytes, salt)
16
+ return hashed.decode('utf-8')
17
+
18
+ def verify_password(plain: str, hashed: str):
19
+ password_bytes = normalize_password(plain).encode('utf-8')
20
+ hashed_bytes = hashed.encode('utf-8')
21
+ try:
22
+ return bcrypt.checkpw(password_bytes, hashed_bytes)
23
+ except Exception:
24
+ return False
25
+
26
+ def create_token(data: dict):
27
+ to_encode = data.copy()
28
+ expire = datetime.now(timezone.utc) + timedelta(days=1)
29
+ to_encode.update({"exp": expire})
30
+ return jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
app/core/config.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic_settings import BaseSettings, SettingsConfigDict
2
+ from typing import Optional
3
+
4
+ class Settings(BaseSettings):
5
+ # Database Config
6
+ MYSQL_USER: str
7
+ MYSQL_PASSWORD: str
8
+ MYSQL_HOST: str
9
+ MYSQL_DB: str
10
+
11
+ # Auth Config
12
+ SECRET_KEY: str
13
+ GOOGLE_CLIENT_ID: Optional[str] = None
14
+ GOOGLE_CLIENT_SECRET: Optional[str] = None
15
+
16
+ # External APIs
17
+ GEMINI_API_KEY: str
18
+
19
+ @property
20
+ def DATABASE_URL(self) -> str:
21
+ return f"mysql+pymysql://{self.MYSQL_USER}:{self.MYSQL_PASSWORD}@{self.MYSQL_HOST}/{self.MYSQL_DB}"
22
+
23
+ model_config = SettingsConfigDict(env_file=".env", extra="ignore")
24
+
25
+ settings = Settings()
26
+
27
+
app/database.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from sqlalchemy import create_engine
3
+ from sqlalchemy.orm import sessionmaker, declarative_base
4
+ from .core.config import settings
5
+
6
+ DATABASE_URL = f"mysql+pymysql://{settings.MYSQL_USER}:{settings.MYSQL_PASSWORD}@{settings.MYSQL_HOST}/{settings.MYSQL_DB}"
7
+
8
+ engine = create_engine(
9
+ DATABASE_URL,
10
+ pool_size=10,
11
+ max_overflow=20,
12
+ pool_recycle=3600,
13
+ pool_pre_ping=True,
14
+ connect_args={"init_command": "SET time_zone='+00:00'"}
15
+ )
16
+
17
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
18
+
19
+ Base = declarative_base()
app/models.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ from sqlalchemy import Column, Integer, String, TIMESTAMP, ForeignKey, Float, Text, Boolean, func
3
+ from .database import Base
4
+ from sqlalchemy.orm import relationship
5
+ from datetime import datetime, timezone
6
+
7
+ class InterviewSlot(Base):
8
+ __tablename__ = "interview_slots"
9
+
10
+ id = Column(Integer, primary_key=True)
11
+ user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
12
+ # Use timezone=True to help SQLAlchemy handle the UTC conversion
13
+ start_time = Column(TIMESTAMP(timezone=True), nullable=True)
14
+ is_active = Column(Boolean, default=False)
15
+
16
+ class User(Base):
17
+ __tablename__ = "users"
18
+
19
+ id = Column(Integer, primary_key=True, index=True)
20
+ email = Column(String(255))
21
+ password_hash = Column(String(255))
22
+ full_name = Column(String(100), nullable=False)
23
+ google_id = Column(String(255))
24
+ # FIX: Use func.now() so MySQL generates the timestamp at insertion
25
+ created_at = Column(TIMESTAMP, server_default=func.now())
26
+
27
+ interviews = relationship("InterviewSession", back_populates="user")
28
+
29
+ class InterviewSession(Base):
30
+ __tablename__ = "interview_sessions"
31
+
32
+ id = Column(Integer, primary_key=True, index=True)
33
+ user_id = Column(Integer, ForeignKey("users.id"))
34
+ session_uuid = Column(String(255), unique=True)
35
+ # FIX: Removed parentheses from datetime.now
36
+ created_at = Column(TIMESTAMP, default=func.now())
37
+
38
+ user = relationship("User", back_populates="interviews")
39
+ turns = relationship("InterviewTurn", back_populates="session")
40
+
41
+ class InterviewTurn(Base):
42
+ __tablename__ = "interview_turns"
43
+
44
+ id = Column(Integer, primary_key=True, index=True)
45
+ session_id = Column(Integer, ForeignKey("interview_sessions.id"))
46
+ question = Column(Text)
47
+ answer = Column(Text)
48
+ wpm = Column(Integer)
49
+ accuracy = Column(Float)
50
+ fillers = Column(String(255))
51
+ dominant_behavior = Column(String(50))
52
+
53
+ session = relationship("InterviewSession", back_populates="turns")
54
+
55
+ # ... repeat for Resume class ...
56
+ class Resume(Base):
57
+ __tablename__ = "resumes"
58
+ id = Column(Integer, primary_key=True, index=True)
59
+ user_id = Column(Integer, ForeignKey("users.id"))
60
+ file_name = Column(String(255))
61
+ file_path = Column(String(355))
62
+ # FIX: Use func.now()
63
+ uploaded_at = Column(TIMESTAMP, server_default=func.now())
64
+
65
+ user = relationship("User")
app/oauth.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from authlib.integrations.starlette_client import OAuth
2
+ from .core.config import settings
3
+
4
+ oauth = OAuth()
5
+
6
+ oauth.register(
7
+ name="google",
8
+ client_id=settings.GOOGLE_CLIENT_ID,
9
+ client_secret=settings.GOOGLE_CLIENT_SECRET,
10
+ server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
11
+ client_kwargs={"scope": "openid email profile"}
12
+ )
app/routes/__init__.py ADDED
File without changes
app/routes/analysis_routes.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import uuid
4
+ import json
5
+ import re
6
+ import numpy as np
7
+ import pypdf
8
+ import docx
9
+ import anyio
10
+ from fastapi import APIRouter, File, UploadFile, Form, HTTPException, Depends, Request
11
+ from fastapi.responses import FileResponse
12
+ from sentence_transformers import SentenceTransformer
13
+ from sklearn.metrics.pairwise import cosine_similarity
14
+ from google import genai
15
+ from google.genai import types
16
+ from ..core.config import settings
17
+ from sqlalchemy.orm import Session
18
+ from ..database import SessionLocal
19
+ from .. import models
20
+
21
+ router = APIRouter()
22
+
23
+ nlp_model = SentenceTransformer('all-mpnet-base-v2', device="cpu")
24
+ client = genai.Client(api_key=settings.GEMINI_API_KEY)
25
+
26
+ MODEL_ID = "gemini-2.5-flash"
27
+ chat_sessions = {}
28
+
29
+
30
+ def get_db():
31
+ db = SessionLocal()
32
+ try:
33
+ yield db
34
+ finally:
35
+ db.close()
36
+
37
+ def get_embedding(text):
38
+ words = text.split()
39
+ chunk_size = 300
40
+ chunks = [' '.join(words[i:i + chunk_size]) for i in range(0, len(words), chunk_size)]
41
+ if not chunks: return np.zeros(768)
42
+ chunk_embeddings = nlp_model.encode(chunks)
43
+ return np.mean(chunk_embeddings, axis=0)
44
+
45
+ async def extract_text_from_file(file: UploadFile):
46
+ content = await file.read()
47
+ if file.filename.endswith(".pdf"):
48
+ pdf_reader = pypdf.PdfReader(io.BytesIO(content))
49
+ return "".join([page.extract_text() or "" for page in pdf_reader.pages])
50
+ elif file.filename.endswith(".docx"):
51
+ doc = docx.Document(io.BytesIO(content))
52
+ return "\n".join([para.text for para in doc.paragraphs])
53
+ elif file.filename.endswith(".txt"):
54
+ return content.decode("utf-8")
55
+ return ""
56
+
57
+
58
+ def extract_text_from_path(file_path: str):
59
+ if not os.path.exists(file_path):
60
+ return ""
61
+
62
+ if file_path.endswith(".pdf"):
63
+ with open(file_path, "rb") as f:
64
+ pdf_reader = pypdf.PdfReader(f)
65
+ return "".join([page.extract_text() or "" for page in pdf_reader.pages])
66
+ elif file_path.endswith(".docx"):
67
+ doc = docx.Document(file_path)
68
+ return "\n".join([para.text for para in doc.paragraphs])
69
+ elif file_path.endswith(".txt"):
70
+ with open(file_path, "r", encoding="utf-8") as f:
71
+ return f.read()
72
+ return ""
73
+
74
+
75
+ def extract_json(text):
76
+ match = re.search(r'\{.*\}', text, re.DOTALL)
77
+ return match.group(0) if match else text
78
+
79
+ @router.post("/analyze")
80
+ async def analyze_match(resume: UploadFile = File(...), jd_file: UploadFile = File(None), jd_text: str = Form(None)):
81
+ if not jd_file and not jd_text:
82
+ raise HTTPException(status_code=400, detail="Provide JD file or text.")
83
+
84
+ resume_content = await extract_text_from_file(resume)
85
+ jd_content = await extract_text_from_file(jd_file) if jd_file else jd_text
86
+
87
+ if not resume_content.strip() or not jd_content.strip():
88
+ raise HTTPException(status_code=400, detail="Extraction failed.")
89
+
90
+ r_emb = await anyio.to_thread.run_sync(get_embedding, resume_content)
91
+ j_emb = await anyio.to_thread.run_sync(get_embedding, jd_content)
92
+
93
+ score = cosine_similarity([r_emb], [j_emb])[0][0]
94
+
95
+ return {"match_percentage": round(float(score) * 100, 2), "status": "Success"}
96
+
97
+
98
+
99
+
100
+
101
+ @router.get("/resumes")
102
+ async def get_user_resumes(user_id: int, db: Session = Depends(get_db)):
103
+ return db.query(models.Resume).filter(models.Resume.user_id == user_id).all()
104
+
105
+ @router.get("/resumes/download/{resume_id}")
106
+ async def download_resume(resume_id: int, db: Session = Depends(get_db)):
107
+ res_db = db.query(models.Resume).filter(models.Resume.id == resume_id).first()
108
+ if not res_db or not os.path.exists(res_db.file_path):
109
+ raise HTTPException(status_code=404, detail="File not found")
110
+ return FileResponse(res_db.file_path, filename=res_db.file_name)
111
+
112
+ @router.post("/upload-resume")
113
+ async def upload_resume(user_id: int = Form(...), file: UploadFile = File(...), db: Session = Depends(get_db)):
114
+ upload_dir = "uploads/resumes"
115
+ os.makedirs(upload_dir, exist_ok=True)
116
+
117
+ file_path = os.path.join(upload_dir, f"{uuid.uuid4()}_{file.filename}")
118
+ with open(file_path, "wb") as buffer:
119
+ buffer.write(await file.read())
120
+
121
+ new_resume = models.Resume(user_id=user_id, file_name=file.filename, file_path=file_path)
122
+ db.add(new_resume)
123
+ db.commit()
124
+ return {"message": "Resume saved to library"}
125
+
126
+ @router.delete("/resumes/{resume_id}")
127
+ async def delete_resume(resume_id: int, db: Session = Depends(get_db)):
128
+ res_db = db.query(models.Resume).filter(models.Resume.id == resume_id).first()
129
+ if not res_db:
130
+ raise HTTPException(status_code=404, detail="Resume not found")
131
+
132
+ if os.path.exists(res_db.file_path):
133
+ os.remove(res_db.file_path)
134
+
135
+ db.delete(res_db)
136
+ db.commit()
137
+ return {"message": "Resume deleted successfully"}
138
+
139
+
140
+
141
+ @router.post("/aianalyze")
142
+ async def analyze_match(resume: UploadFile = File(...), jd_file: UploadFile = File(None), jd_text: str = Form(None)):
143
+ if not jd_file and not jd_text:
144
+ raise HTTPException(status_code=400, detail="Provide JD file or text.")
145
+
146
+ resume_content = await extract_text_from_file(resume)
147
+ jd_content = await extract_text_from_file(jd_file) if jd_file else jd_text
148
+
149
+ if not resume_content.strip() or not jd_content.strip():
150
+ raise HTTPException(status_code=400, detail="Extraction failed.")
151
+
152
+ prompt = (
153
+ f"You are an expert HR recruiter. Analyze the match between the following Resume and Job Description (JD).\n\n"
154
+ f"### Resume:\n{resume_content}\n\n"
155
+ f"### Job Description:\n{jd_content}\n\n"
156
+ "Return a JSON object with the following fields:\n"
157
+ "- match_percentage (int): overall compatibility score from 0-100\n"
158
+ "- summary (str): a 2-3 sentence overview of the candidate's fit\n"
159
+ "- key_matches (list): specific skills or experiences that align with the JD\n"
160
+ "- missing_skills (list): critical requirements from the JD not found in the resume\n"
161
+ "- suggestions (list): tips to improve the resume for this specific role\n"
162
+ "Provide ONLY the JSON object, no introductory text."
163
+ )
164
+
165
+ try:
166
+ response = client.models.generate_content(
167
+ model=MODEL_ID,
168
+ contents=prompt,
169
+ config=types.GenerateContentConfig(
170
+ response_mime_type="application/json"
171
+ )
172
+ )
173
+
174
+ analysis_data = json.loads(extract_json(response.text))
175
+ return {"match_percentage": round(float(analysis_data["match_percentage"]),2), "status": "Success"}
176
+ # return {**analysis_data, "status": "Success"}
177
+
178
+ except Exception as e:
179
+ # Fallback if the AI fails or JSON parsing errors out
180
+ raise HTTPException(status_code=500, detail=f"AI Analysis failed: {str(e)}")
181
+
182
+
183
+
184
+ @router.post("/multiplematch")
185
+ async def match_multiple_resumes(
186
+ user_id: int = Form(...),
187
+ jd_file: UploadFile = File(None),
188
+ jd_text: str = Form(None),
189
+ db: Session = Depends(get_db)
190
+ ):
191
+
192
+ if not jd_file and not jd_text:
193
+ raise HTTPException(status_code=400, detail="Provide JD file or text.")
194
+
195
+ jd_content = await extract_text_from_file(jd_file) if jd_file else jd_text
196
+ if not jd_content.strip():
197
+ raise HTTPException(status_code=400, detail="JD content is empty.")
198
+
199
+ j_emb = await anyio.to_thread.run_sync(get_embedding, jd_content)
200
+
201
+ user_resumes = db.query(models.Resume).filter(models.Resume.user_id == user_id).all()
202
+
203
+ if not user_resumes:
204
+ return {"results": [], "message": "No resumes found for this user."}
205
+
206
+ results = []
207
+
208
+ for res in user_resumes:
209
+ try:
210
+ resume_text = await anyio.to_thread.run_sync(extract_text_from_path, res.file_path)
211
+
212
+ if not resume_text.strip():
213
+ results.append({
214
+ "resume_id": res.id,
215
+ "file_name": res.file_name,
216
+ "match_percentage": 0,
217
+ "error": "Could not extract text"
218
+ })
219
+ continue
220
+
221
+ r_emb = await anyio.to_thread.run_sync(get_embedding, resume_text)
222
+ score = cosine_similarity([r_emb], [j_emb])[0][0]
223
+
224
+ results.append({
225
+ "resume_id": res.id,
226
+ "file_name": res.file_name,
227
+ "match_percentage": round(float(score) * 100, 2)
228
+ })
229
+ except Exception as e:
230
+ results.append({
231
+ "resume_id": res.id,
232
+ "file_name": res.file_name,
233
+ "match_percentage": 0,
234
+ "error": str(e)
235
+ })
236
+
237
+ return {"results": results, "status": "Success"}
238
+
239
+
240
+ @router.post("/aimultianalyse")
241
+ async def ai_multiple_match(
242
+ user_id: int = Form(...),
243
+ jd_file: UploadFile = File(None),
244
+ jd_text: str = Form(None),
245
+ db: Session = Depends(get_db)
246
+ ):
247
+ # 1. Extract JD Text
248
+ if not jd_file and not jd_text:
249
+ raise HTTPException(status_code=400, detail="Provide JD file or text.")
250
+
251
+ jd_content = await extract_text_from_file(jd_file) if jd_file else jd_text
252
+ if not jd_content.strip():
253
+ raise HTTPException(status_code=400, detail="JD Extraction failed.")
254
+
255
+ # 2. Retrieve all resumes for this user from DB
256
+ user_resumes = db.query(models.Resume).filter(models.Resume.user_id == user_id).all()
257
+ if not user_resumes:
258
+ return {"results": [], "message": "No resumes found for this user."}
259
+
260
+ # 3. Extract text from all resumes
261
+ resume_data = []
262
+ for res in user_resumes:
263
+ text = await anyio.to_thread.run_sync(extract_text_from_path, res.file_path)
264
+ if text.strip():
265
+ resume_data.append({"id": res.id, "name": res.file_name, "content": text})
266
+
267
+ if not resume_data:
268
+ return {"results": [], "message": "No readable resumes found."}
269
+
270
+ resumes_prompt = "\n\n".join([f"RESUME ID {r['id']} ({r['name']}):\n{r['content']}" for r in resume_data])
271
+
272
+ prompt = (
273
+ f"You are an AI Recruitment Assistant. Compare the following list of resumes against the Job Description (JD).\n\n"
274
+ f"### Job Description:\n{jd_content}\n\n"
275
+ f"### Candidate Resumes:\n{resumes_prompt}\n\n"
276
+ "Return a JSON object with a key 'results' containing a list of objects. Each object must have:\n"
277
+ "- resume_id (int): the ID provided above\n"
278
+ "- file_name (str): the name of the file provided above\n"
279
+ "- match_percentage (int): 0-100 score\n"
280
+ "- brief_reason (str): why this candidate is or isn't a fit\n"
281
+ "- top_skills (list): matching skills found\n"
282
+ "- improvement_suggestions (str): suggestions for improving the resume\n"
283
+ "Sort the list by match_percentage in descending order. Provide ONLY JSON."
284
+ )
285
+
286
+ try:
287
+ # 5. Call Gemini API
288
+ response = client.models.generate_content(
289
+ model=MODEL_ID,
290
+ contents=prompt,
291
+ config=types.GenerateContentConfig(response_mime_type="application/json")
292
+ )
293
+
294
+ analysis_data = json.loads(extract_json(response.text))
295
+ # return {**analysis_data, "status": "Success"}
296
+ return {"results": analysis_data.get("results", []), "status": "Success"}
297
+
298
+ except Exception as e:
299
+ raise HTTPException(status_code=500, detail=f"AI Multiple Analysis failed: {str(e)}")
app/routes/auth_routes.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, Request, HTTPException
2
+ from sqlalchemy.orm import Session
3
+ from ..database import SessionLocal
4
+ from .. import models, schemas, auth
5
+ from ..oauth import oauth
6
+ from fastapi.responses import RedirectResponse, JSONResponse
7
+ from jose import jwt, JWTError
8
+ from ..core.config import settings
9
+
10
+
11
+
12
+ router = APIRouter()
13
+
14
+ def get_db():
15
+ db = SessionLocal()
16
+ try:
17
+ yield db
18
+ finally:
19
+ db.close()
20
+
21
+ @router.post("/register")
22
+ def register(user: schemas.UserCreate, db: Session = Depends(get_db)):
23
+
24
+ existing = db.query(models.User).filter(
25
+ models.User.email == user.email
26
+ ).first()
27
+
28
+ if existing:
29
+ return {"error": "User exists"}
30
+
31
+ new_user = models.User(
32
+ full_name=user.full_name,
33
+ email=user.email,
34
+ password_hash=auth.hash_password(user.password)
35
+ )
36
+
37
+ db.add(new_user)
38
+ db.commit()
39
+
40
+ token = auth.create_token({"sub": new_user.email})
41
+
42
+ response = JSONResponse({"message": "User registered"})
43
+
44
+ response.set_cookie(
45
+ key="access_token",
46
+ value=token,
47
+ httponly=True,
48
+ secure=True,
49
+ samesite="none",
50
+ path="/"
51
+ )
52
+
53
+ return response
54
+
55
+
56
+ @router.post("/login")
57
+ def login(user: schemas.UserLogin, db: Session = Depends(get_db)):
58
+
59
+ db_user = db.query(models.User).filter(
60
+ models.User.email == user.email
61
+ ).first()
62
+
63
+ if not db_user:
64
+ return {"error": "Invalid credentials"}
65
+ if not auth.verify_password(user.password, db_user.password_hash):
66
+ raise HTTPException(status_code=401, detail="Invalid credentials")
67
+
68
+ token = auth.create_token({"sub": db_user.email})
69
+
70
+ response = JSONResponse({"message": "Login success"})
71
+
72
+ response.set_cookie(
73
+ key="access_token",
74
+ value=token,
75
+ httponly=True,
76
+ secure=True,
77
+ samesite="none",
78
+ path="/"
79
+ )
80
+
81
+ return response
82
+
83
+
84
+ @router.get("/google")
85
+ async def google_login(request: Request):
86
+ redirect_uri = request.url_for("google_callback")
87
+ return await oauth.google.authorize_redirect(request, redirect_uri)
88
+
89
+
90
+ @router.get("/google/callback")
91
+ async def google_callback(request: Request, db: Session = Depends(get_db)):
92
+
93
+ token = await oauth.google.authorize_access_token(request)
94
+ user_info = token["userinfo"]
95
+
96
+ db_user = db.query(models.User).filter(
97
+ models.User.email == user_info["email"]
98
+ ).first()
99
+
100
+ if not db_user:
101
+ db_user = models.User(
102
+ email=user_info["email"],
103
+ google_id=user_info["sub"]
104
+ )
105
+ db.add(db_user)
106
+ db.commit()
107
+
108
+ jwt_token = auth.create_token({"sub": user_info["email"]})
109
+
110
+
111
+ response = RedirectResponse("https://d33paksoni.github.io/aicoach-infosys/dashboard")
112
+
113
+ response.set_cookie(
114
+ key="access_token",
115
+ value=jwt_token,
116
+ httponly=True,
117
+ secure=True,
118
+ samesite="none",
119
+ path="/"
120
+ )
121
+
122
+ return response
123
+
124
+
125
+ def get_current_user(request: Request):
126
+
127
+ token = request.cookies.get("access_token")
128
+
129
+ if not token:
130
+ return None
131
+
132
+ try:
133
+ payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
134
+ return payload.get("sub")
135
+
136
+ except JWTError:
137
+ return None
138
+
139
+
140
+
141
+ @router.get("/me")
142
+ def get_me(request: Request, db: Session = Depends(get_db)):
143
+
144
+ user_email = get_current_user(request)
145
+
146
+ if not user_email:
147
+ raise HTTPException(status_code=401, detail="Not authenticated")
148
+
149
+ db_user = db.query(models.User).filter(models.User.email == user_email).first()
150
+
151
+ if not db_user:
152
+ raise HTTPException(status_code=404, detail="User not found")
153
+
154
+ return {
155
+ "full_name": db_user.full_name,
156
+ "id": db_user.id,
157
+ "email": user_email
158
+ }
159
+
160
+
161
+
162
+ @router.post("/logout")
163
+ def logout():
164
+
165
+ response = JSONResponse({"message": "Logged out"})
166
+
167
+ response.delete_cookie(
168
+ key="access_token",
169
+ path="/",
170
+ samesite="none",
171
+ secure=True,
172
+ httponly=True
173
+ )
174
+
175
+ return response
176
+
app/routes/session_routes.py ADDED
@@ -0,0 +1,203 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # from fastapi import APIRouter, Depends, HTTPException
2
+ # from sqlalchemy.orm import Session
3
+ # from ..database import SessionLocal
4
+ # from .. import models, schemas
5
+
6
+ # router = APIRouter()
7
+
8
+ # def get_db():
9
+ # db = SessionLocal()
10
+ # try: yield db
11
+ # finally: db.close()
12
+
13
+ # @router.post("/save-session")
14
+ # def save_interview_session(data: schemas.InterviewSaveRequest, db: Session = Depends(get_db)):
15
+ # try:
16
+ # session_record = db.query(models.InterviewSession).filter(
17
+ # models.InterviewSession.session_uuid == data.session_id
18
+ # ).first()
19
+
20
+ # if not session_record:
21
+ # session_record = models.InterviewSession(session_uuid=data.session_id, user_id=data.user_id)
22
+ # db.add(session_record)
23
+ # db.flush()
24
+
25
+ # for turn in data.turns:
26
+ # new_turn = models.InterviewTurn(
27
+ # session_id=session_record.id,
28
+ # question=turn.question,
29
+ # answer=turn.answer,
30
+ # wpm=turn.wpm,
31
+ # accuracy=turn.accuracy,
32
+ # fillers=turn.fillers,
33
+ # dominant_behavior=turn.commonBehavior
34
+ # )
35
+ # db.add(new_turn)
36
+
37
+ # db.commit()
38
+ # return {"status": "success"}
39
+ # except Exception as e:
40
+ # db.rollback()
41
+ # raise HTTPException(status_code=500, detail=str(e))
42
+
43
+ # @router.get("/get-sessions")
44
+ # def get_sessions(user_id: int, db: Session = Depends(get_db)):
45
+ # sessions = db.query(models.InterviewSession).filter(
46
+ # models.InterviewSession.user_id == user_id
47
+ # ).order_by(models.InterviewSession.created_at.desc()).all()
48
+
49
+ # return [{
50
+ # "id": s.id, "session_uuid": s.session_uuid, "created_at": s.created_at,
51
+ # "turn_count": db.query(models.InterviewTurn).filter(models.InterviewTurn.session_id == s.id).count()
52
+ # } for s in sessions]
53
+
54
+ # @router.get("/session-details/{session_id}")
55
+ # def get_session_details(session_id: int, db: Session = Depends(get_db)):
56
+ # return db.query(models.InterviewTurn).filter(models.InterviewTurn.session_id == session_id).all()
57
+
58
+
59
+
60
+ from fastapi import APIRouter, Depends, HTTPException
61
+ from sqlalchemy.orm import Session
62
+ from ..database import SessionLocal
63
+ from .. import models, schemas
64
+ from datetime import datetime, timezone, timedelta
65
+
66
+ router = APIRouter()
67
+
68
+ def get_db():
69
+ db = SessionLocal()
70
+ try: yield db
71
+ finally: db.close()
72
+
73
+
74
+
75
+ def refresh_slots(db: Session):
76
+ expiry_time = datetime.now(timezone.utc) - timedelta(minutes=12)
77
+ expired_slots = db.query(models.InterviewSlot).filter(
78
+ models.InterviewSlot.is_active == True,
79
+ models.InterviewSlot.start_time < expiry_time
80
+ ).all()
81
+
82
+ for slot in expired_slots:
83
+ slot.is_active = False
84
+ slot.user_id = None
85
+ db.commit()
86
+
87
+ def available_slots(db: Session):
88
+ refresh_slots(db)
89
+ return db.query(models.InterviewSlot).filter(
90
+ models.InterviewSlot.is_active == False
91
+ ).first()
92
+
93
+
94
+
95
+ @router.get("/check-slots")
96
+ def check_slots(db: Session = Depends(get_db)):
97
+
98
+ available_slot = available_slots(db)
99
+ if available_slot:
100
+ return {"status": "available", "slot_id": available_slot.id}
101
+
102
+
103
+ earliest_session = db.query(models.InterviewSlot).filter(
104
+ models.InterviewSlot.is_active == True
105
+ ).order_by(models.InterviewSlot.start_time.asc()).first()
106
+
107
+ if not earliest_session:
108
+ return {"status": "error", "message": "No slots configured."}
109
+
110
+ now = datetime.now(timezone.utc)
111
+ start_time = earliest_session.start_time
112
+ if start_time.tzinfo is None:
113
+ start_time = start_time.replace(tzinfo=timezone.utc)
114
+
115
+ elapsed = now - start_time
116
+ total_duration_seconds = 15 * 60
117
+ remaining_seconds = max(0, total_duration_seconds - elapsed.total_seconds())
118
+
119
+
120
+ return {"status": "full",
121
+ "wait_time_seconds": int(remaining_seconds),
122
+ "message": "All interview slots are currently full."}
123
+
124
+
125
+ @router.post("/acquire-slot")
126
+ def acquire_slot(data: schemas.SlotRequest, db: Session = Depends(get_db)):
127
+
128
+ available_slot = available_slots(db)
129
+
130
+ if available_slot:
131
+ available_slot.user_id = data.user_id
132
+ available_slot.start_time = datetime.now(timezone.utc)
133
+ available_slot.is_active = True
134
+ db.commit()
135
+ return {"status": "success", "slot_id": available_slot.id}
136
+ else:
137
+ return {
138
+ "status": "full",
139
+ "message": "All interview slots are currently full. Please wait and try again."}
140
+
141
+
142
+
143
+
144
+ @router.post("/release-slot/{user_id}")
145
+ def release_slot(user_id: int, db: Session = Depends(get_db)):
146
+ slot = db.query(models.InterviewSlot).filter(
147
+ models.InterviewSlot.user_id == user_id
148
+ ).first()
149
+ if slot:
150
+ slot.is_active = False
151
+ slot.user_id = None
152
+ db.commit()
153
+ return {"status": "released"}
154
+
155
+
156
+
157
+
158
+
159
+
160
+ @router.post("/save-session")
161
+ def save_interview_session(data: schemas.InterviewSaveRequest, db: Session = Depends(get_db)):
162
+ try:
163
+ session_record = db.query(models.InterviewSession).filter(
164
+ models.InterviewSession.session_uuid == data.session_id
165
+ ).first()
166
+
167
+ if not session_record:
168
+ session_record = models.InterviewSession(session_uuid=data.session_id, user_id=data.user_id)
169
+ db.add(session_record)
170
+ db.flush()
171
+
172
+ for turn in data.turns:
173
+ new_turn = models.InterviewTurn(
174
+ session_id=session_record.id,
175
+ question=turn.question,
176
+ answer=turn.answer,
177
+ wpm=turn.wpm,
178
+ accuracy=turn.accuracy,
179
+ fillers=turn.fillers,
180
+ dominant_behavior=turn.commonBehavior
181
+ )
182
+ db.add(new_turn)
183
+
184
+ db.commit()
185
+ return {"status": "success"}
186
+ except Exception as e:
187
+ db.rollback()
188
+ raise HTTPException(status_code=500, detail=str(e))
189
+
190
+ @router.get("/get-sessions")
191
+ def get_sessions(user_id: int, db: Session = Depends(get_db)):
192
+ sessions = db.query(models.InterviewSession).filter(
193
+ models.InterviewSession.user_id == user_id
194
+ ).order_by(models.InterviewSession.created_at.desc()).all()
195
+
196
+ return [{
197
+ "id": s.id, "session_uuid": s.session_uuid, "created_at": s.created_at,
198
+ "turn_count": db.query(models.InterviewTurn).filter(models.InterviewTurn.session_id == s.id).count()
199
+ } for s in sessions]
200
+
201
+ @router.get("/session-details/{session_id}")
202
+ def get_session_details(session_id: int, db: Session = Depends(get_db)):
203
+ return db.query(models.InterviewTurn).filter(models.InterviewTurn.session_id == session_id).all()
app/schemas.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, EmailStr
2
+
3
+ class UserCreate(BaseModel):
4
+ full_name: str
5
+ email: EmailStr
6
+ password: str
7
+
8
+ class UserLogin(BaseModel):
9
+ email: EmailStr
10
+ password: str
11
+
12
+ from typing import List, Optional
13
+
14
+ class TurnData(BaseModel):
15
+ question: str
16
+ answer: str
17
+ wpm: int
18
+ accuracy: float
19
+ fillers: str
20
+ commonBehavior: str
21
+
22
+ class InterviewSaveRequest(BaseModel):
23
+ session_id: str
24
+ user_id: Optional[int] = None
25
+ turns: List[TurnData]
26
+
27
+ class SlotRequest(BaseModel):
28
+ user_id: int
29
+
30
+ class SlotResponse(BaseModel):
31
+ status: str
32
+ slot_id: Optional[int] = None
33
+ wait_time_seconds: Optional[int] = 0
34
+ message: Optional[str] = None
35
+
36
+ class WaitingStatus(BaseModel):
37
+ active_users: int
38
+ estimated_wait_minutes: float
39
+ wait_time_seconds: int
main.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from starlette.middleware.sessions import SessionMiddleware
4
+ from contextlib import asynccontextmanager
5
+ import torch
6
+
7
+ from app.routes import auth_routes, analysis_routes, vision_routes, session_routes
8
+ from app.core.config import settings
9
+
10
+
11
+ @asynccontextmanager
12
+ async def lifespan(app: FastAPI):
13
+ from app.database import SessionLocal
14
+ from app.models import InterviewSlot
15
+
16
+ torch.set_num_threads(1)
17
+ print("AI Interview Coach: Starting up and loading models...")
18
+
19
+ db = SessionLocal()
20
+ # Check if slots exist, if not, create them
21
+ if db.query(InterviewSlot).count() == 0:
22
+ db.add(InterviewSlot(id=1, is_active=False))
23
+ db.add(InterviewSlot(id=2, is_active=False))
24
+ db.commit()
25
+ db.close()
26
+
27
+ yield
28
+ print("AI Interview Coach: Shutting down...")
29
+
30
+ app = FastAPI(
31
+ title="AI Interview Coach",
32
+ lifespan=lifespan
33
+ )
34
+
35
+ app.add_middleware(
36
+ CORSMiddleware,
37
+ allow_origins=[
38
+ "http://localhost:5173",
39
+ "https://d33paksoni.github.io",
40
+ "https://www.thedeepaksoni.life"],
41
+ allow_credentials=True,
42
+ allow_methods=["*"],
43
+ allow_headers=["*"],
44
+ )
45
+
46
+ app.add_middleware(
47
+ SessionMiddleware,
48
+ secret_key=settings.SECRET_KEY,
49
+ same_site="none"
50
+ )
51
+
52
+ app.include_router(auth_routes.router, prefix="/auth", tags=["Authentication"])
53
+ app.include_router(analysis_routes.router, tags=["NLP & AI"])
54
+ app.include_router(session_routes.router, tags=["History"])
55
+
56
+ if __name__ == "__main__":
57
+ import uvicorn
58
+ uvicorn.run(app, host="0.0.0.0", port=8000, workers=2)
59
+
60
+
61
+
62
+
requirements.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ sqlalchemy
4
+ pymysql
5
+ passlib[bcrypt]
6
+ python-jose
7
+ authlib
8
+ python-dotenv
9
+ pydantic[email]
10
+ httpx
11
+ numpy
12
+ pypdf
13
+ python-docx
14
+ sentence-transformers
15
+ scikit-learn
16
+ opencv-python
17
+ pillow
18
+ python-multipart
19
+ pydantic-settings
20
+ google-genai
21
+ itsdangerous
uploads/resumes/a.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ a