husseinelsaadi commited on
Commit
2ae57cb
·
1 Parent(s): d8acd61

Initial commit for Hugging Face Space

Browse files
.huggingface/spaces.yml ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ sdk: static
2
+ app_file: app.py
3
+ python_version: "3.10"
4
+ hardware: gpu
.venv/bin/huggingface-cli ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ '''exec' "/Users/husseinelsaadi/Documents/Data Science USAL/Spring 24-25/FYP - Codingo/Codingo/.venv/bin/python3.12" "$0" "$@"
3
+ ' '''
4
+ # -*- coding: utf-8 -*-
5
+ import re
6
+ import sys
7
+ from huggingface_hub.commands.huggingface_cli import main
8
+ if __name__ == '__main__':
9
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
10
+ sys.exit(main())
.venv/bin/tiny-agents ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/sh
2
+ '''exec' "/Users/husseinelsaadi/Documents/Data Science USAL/Spring 24-25/FYP - Codingo/Codingo/.venv/bin/python3.12" "$0" "$@"
3
+ ' '''
4
+ # -*- coding: utf-8 -*-
5
+ import re
6
+ import sys
7
+ from huggingface_hub.inference._mcp.cli import app
8
+ if __name__ == '__main__':
9
+ sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
10
+ sys.exit(app())
backend/app.py CHANGED
@@ -6,6 +6,9 @@ import sys
6
  import json
7
  from datetime import datetime
8
 
 
 
 
9
  # Adjust sys.path for import flexibility
10
  current_dir = os.path.dirname(os.path.abspath(__file__))
11
  parent_dir = os.path.dirname(current_dir)
@@ -20,6 +23,7 @@ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
20
 
21
  # Import and initialize DB
22
  from backend.models.database import db, Job, Application, init_db
 
23
  init_db(app)
24
 
25
  # Import models/routes AFTER initializing the DB
@@ -42,10 +46,12 @@ login_manager = LoginManager()
42
  login_manager.login_view = 'auth.login'
43
  login_manager.init_app(app)
44
 
 
45
  @login_manager.user_loader
46
  def load_user(user_id):
47
  return db.session.get(User, int(user_id))
48
 
 
49
  # Register auth blueprint
50
  app.register_blueprint(auth_bp)
51
 
@@ -73,41 +79,44 @@ def handle_resume_upload(file):
73
  return None, str(e), None
74
 
75
 
 
76
  # Routes
77
  @app.route('/')
78
  def index():
79
  return render_template('index.html')
80
 
 
81
  @app.route('/jobs')
82
  def jobs():
83
  all_jobs = Job.query.order_by(Job.date_posted.desc()).all()
84
  return render_template('jobs.html', jobs=all_jobs)
85
 
 
86
  @app.route('/job/<int:job_id>')
87
  def job_detail(job_id):
88
  job = Job.query.get_or_404(job_id)
89
  return render_template('job_detail.html', job=job)
90
 
 
91
  @app.route('/apply/<int:job_id>', methods=['GET', 'POST'])
92
  @login_required
93
  def apply(job_id):
94
  job = Job.query.get_or_404(job_id)
95
- form = JobApplicationForm()
96
- form.job_id.data = job_id
97
 
98
- if form.validate_on_submit():
99
- resume_file = form.resume.data
 
100
 
101
- features, error, filename = handle_resume_upload(resume_file)
102
- features_json = json.dumps(features or {})
 
103
 
104
  application = Application(
105
  job_id=job_id,
106
- name=form.name.data,
107
- email=form.email.data,
108
- resume_path=filename,
109
- cover_letter=form.cover_letter.data,
110
- extracted_features=features_json
111
  )
112
 
113
  db.session.add(application)
@@ -116,10 +125,20 @@ def apply(job_id):
116
  flash('Your application has been submitted successfully!', 'success')
117
  return redirect(url_for('jobs'))
118
 
119
- return render_template('apply.html', form=form, job=job)
 
 
 
 
 
 
 
 
 
120
 
121
  @app.route('/parse_resume', methods=['POST'])
122
  def parse_resume():
 
123
  file = request.files.get('resume')
124
  features, error, _ = handle_resume_upload(file)
125
 
@@ -136,14 +155,28 @@ def parse_resume():
136
  "email": features.get('email', ''),
137
  "mobile_number": features.get('mobile_number', ''),
138
  "skills": features.get('skills', []),
139
- "experience": features.get('experience', [])
 
 
140
  }
141
  return response, 200
142
 
 
 
 
 
 
 
 
 
 
 
 
 
143
 
144
 
145
  if __name__ == '__main__':
146
  print("Starting Codingo application...")
147
  with app.app_context():
148
  db.create_all()
149
- app.run(debug=True, port=5001)
 
6
  import json
7
  from datetime import datetime
8
 
9
+ from backend.routes.interview_api import interview_api
10
+ app.register_blueprint(interview_api, url_prefix="/api")
11
+
12
  # Adjust sys.path for import flexibility
13
  current_dir = os.path.dirname(os.path.abspath(__file__))
14
  parent_dir = os.path.dirname(current_dir)
 
23
 
24
  # Import and initialize DB
25
  from backend.models.database import db, Job, Application, init_db
26
+
27
  init_db(app)
28
 
29
  # Import models/routes AFTER initializing the DB
 
46
  login_manager.login_view = 'auth.login'
47
  login_manager.init_app(app)
48
 
49
+
50
  @login_manager.user_loader
51
  def load_user(user_id):
52
  return db.session.get(User, int(user_id))
53
 
54
+
55
  # Register auth blueprint
56
  app.register_blueprint(auth_bp)
57
 
 
79
  return None, str(e), None
80
 
81
 
82
+
83
  # Routes
84
  @app.route('/')
85
  def index():
86
  return render_template('index.html')
87
 
88
+
89
  @app.route('/jobs')
90
  def jobs():
91
  all_jobs = Job.query.order_by(Job.date_posted.desc()).all()
92
  return render_template('jobs.html', jobs=all_jobs)
93
 
94
+
95
  @app.route('/job/<int:job_id>')
96
  def job_detail(job_id):
97
  job = Job.query.get_or_404(job_id)
98
  return render_template('job_detail.html', job=job)
99
 
100
+
101
  @app.route('/apply/<int:job_id>', methods=['GET', 'POST'])
102
  @login_required
103
  def apply(job_id):
104
  job = Job.query.get_or_404(job_id)
 
 
105
 
106
+ if request.method == 'POST':
107
+ file = request.files.get('resume')
108
+ features, error, _ = handle_resume_upload(file)
109
 
110
+ if error or not features:
111
+ flash("Resume parsing failed. Please try again.", "danger")
112
+ return render_template('apply.html', job=job)
113
 
114
  application = Application(
115
  job_id=job_id,
116
+ user_id=current_user.id,
117
+ name=current_user.username,
118
+ email=current_user.email,
119
+ extracted_features=json.dumps(features)
 
120
  )
121
 
122
  db.session.add(application)
 
125
  flash('Your application has been submitted successfully!', 'success')
126
  return redirect(url_for('jobs'))
127
 
128
+ return render_template('apply.html', job=job)
129
+
130
+
131
+ @app.route('/my_applications')
132
+ @login_required
133
+ def my_applications():
134
+ """View user's applications"""
135
+ applications = Application.query.filter_by(user_id=current_user.id).order_by(Application.date_applied.desc()).all()
136
+ return render_template('my_applications.html', applications=applications)
137
+
138
 
139
  @app.route('/parse_resume', methods=['POST'])
140
  def parse_resume():
141
+ """API endpoint for parsing resume and returning features"""
142
  file = request.files.get('resume')
143
  features, error, _ = handle_resume_upload(file)
144
 
 
155
  "email": features.get('email', ''),
156
  "mobile_number": features.get('mobile_number', ''),
157
  "skills": features.get('skills', []),
158
+ "experience": features.get('experience', []),
159
+ "education": features.get('education', []),
160
+ "summary": features.get('summary', '')
161
  }
162
  return response, 200
163
 
164
+ @app.route("/interview/<int:job_id>")
165
+ @login_required
166
+ def interview_page(job_id):
167
+ job = Job.query.get_or_404(job_id)
168
+ application = Application.query.filter_by(user_id=current_user.id, job_id=job_id).first()
169
+
170
+ if not application or not application.extracted_features:
171
+ flash("Please apply for this job and upload your resume first.", "warning")
172
+ return redirect(url_for('job_detail', job_id=job_id))
173
+
174
+ cv_data = json.loads(application.extracted_features)
175
+ return render_template("interview.html", job=job, cv=cv_data)
176
 
177
 
178
  if __name__ == '__main__':
179
  print("Starting Codingo application...")
180
  with app.app_context():
181
  db.create_all()
182
+ app.run(debug=True, port=5001)
backend/form/AuthForms.py CHANGED
@@ -1,7 +1,8 @@
1
-
2
  from flask_wtf import FlaskForm
3
- from wtforms import StringField, PasswordField, SubmitField, SelectField
4
- from wtforms.validators import DataRequired, Email, EqualTo
 
 
5
 
6
  class LoginForm(FlaskForm):
7
  email = StringField('Email', validators=[DataRequired(), Email()])
@@ -9,9 +10,9 @@ class LoginForm(FlaskForm):
9
  submit = SubmitField('Login')
10
 
11
  class SignupForm(FlaskForm):
12
- username = StringField('Username', validators=[DataRequired()])
13
  email = StringField('Email', validators=[DataRequired(), Email()])
14
- password = PasswordField('Password', validators=[DataRequired()])
15
  confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
16
- role = SelectField('Role', choices=[('unemployed', 'Unemployed'), ('recruiter', 'Recruiter')])
17
  submit = SubmitField('Sign Up')
 
 
1
  from flask_wtf import FlaskForm
2
+ from flask_wtf.file import FileField, FileAllowed
3
+ from wtforms import StringField, PasswordField, SubmitField, SelectField, TextAreaField, BooleanField, RadioField
4
+ from wtforms.validators import DataRequired, Email, EqualTo, Length, Optional
5
+
6
 
7
  class LoginForm(FlaskForm):
8
  email = StringField('Email', validators=[DataRequired(), Email()])
 
10
  submit = SubmitField('Login')
11
 
12
  class SignupForm(FlaskForm):
13
+ username = StringField('Username', validators=[DataRequired(), Length(min=3, max=20)])
14
  email = StringField('Email', validators=[DataRequired(), Email()])
15
+ password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
16
  confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
17
+ role = SelectField('Role', choices=[('unemployed', 'Job Seeker'), ('recruiter', 'Recruiter')], default='unemployed')
18
  submit = SubmitField('Sign Up')
backend/instance/codingo.db CHANGED
Binary files a/backend/instance/codingo.db and b/backend/instance/codingo.db differ
 
backend/models/database.py CHANGED
@@ -4,41 +4,52 @@ Database models for the Codingo application.
4
 
5
  from flask_sqlalchemy import SQLAlchemy
6
  from datetime import datetime
 
7
 
8
  db = SQLAlchemy()
9
 
10
  class Job(db.Model):
11
- """Job model representing a job posting."""
 
12
  id = db.Column(db.Integer, primary_key=True)
13
- title = db.Column(db.String(100), nullable=False)
14
- company = db.Column(db.String(100), nullable=False)
15
- location = db.Column(db.String(100), nullable=False)
16
  description = db.Column(db.Text, nullable=False)
17
- requirements = db.Column(db.Text, nullable=False)
 
 
18
  date_posted = db.Column(db.DateTime, default=datetime.utcnow)
19
 
20
- # Relationships
21
- applications = db.relationship('Application', backref='job', lazy=True, cascade="all, delete-orphan")
22
 
23
  def __repr__(self):
24
- return f"Job('{self.title}', '{self.company}')"
25
-
26
 
27
  class Application(db.Model):
28
- """Application model representing a job application."""
 
29
  id = db.Column(db.Integer, primary_key=True)
30
- job_id = db.Column(db.Integer, db.ForeignKey('job.id'), nullable=False)
 
 
31
  name = db.Column(db.String(100), nullable=False)
32
  email = db.Column(db.String(100), nullable=False)
33
- resume_path = db.Column(db.String(255), nullable=False)
34
- cover_letter = db.Column(db.Text, nullable=False)
35
- extracted_features = db.Column(db.Text, nullable=True) # JSON string of extracted features
36
- status = db.Column(db.String(50), default='applied') # applied, reviewed, shortlisted, rejected, hired
37
  date_applied = db.Column(db.DateTime, default=datetime.utcnow)
38
 
 
 
39
  def __repr__(self):
40
  return f"Application('{self.name}', '{self.email}', Job ID: {self.job_id})"
41
 
 
 
 
 
 
42
 
43
  # Initialize the database
44
  def init_db(app):
@@ -46,36 +57,38 @@ def init_db(app):
46
  db.init_app(app)
47
 
48
  with app.app_context():
 
49
  db.create_all()
50
 
51
  # Add sample data if jobs table is empty
52
  if Job.query.count() == 0:
53
  sample_jobs = [
54
- {
55
- 'title': 'Senior Python Developer',
56
- 'company': 'TechCorp',
57
- 'location': 'Remote',
58
- 'description': 'We are looking for an experienced Python developer to join our team. You will be responsible for developing and maintaining our backend services.',
59
- 'requirements': 'Strong Python skills, experience with Flask or Django, knowledge of SQL and NoSQL databases, 5+ years of experience.'
60
- },
61
- {
62
- 'title': 'Data Scientist',
63
- 'company': 'DataInsights',
64
- 'location': 'New York, NY',
65
- 'description': 'Join our data science team to develop machine learning models and extract insights from large datasets.',
66
- 'requirements': 'Masters or PhD in Computer Science, Statistics or related field. Experience with Python, PyTorch, and scikit-learn.'
67
- },
68
- {
69
- 'title': 'Frontend Developer',
70
- 'company': 'WebSolutions',
71
- 'location': 'San Francisco, CA',
72
- 'description': 'Develop responsive and user-friendly web applications with modern JavaScript frameworks.',
73
- 'requirements': 'Proficiency in HTML, CSS, JavaScript, and React.js. Experience with RESTful APIs and version control systems.'
74
- }
 
75
  ]
76
 
77
- for job_data in sample_jobs:
78
- job = Job(**job_data)
79
- db.session.add(job)
80
 
81
- db.session.commit()
 
 
4
 
5
  from flask_sqlalchemy import SQLAlchemy
6
  from datetime import datetime
7
+ import json
8
 
9
  db = SQLAlchemy()
10
 
11
  class Job(db.Model):
12
+ __tablename__ = 'jobs'
13
+
14
  id = db.Column(db.Integer, primary_key=True)
15
+ role = db.Column(db.String(100), nullable=False)
 
 
16
  description = db.Column(db.Text, nullable=False)
17
+ seniority = db.Column(db.String(50), nullable=False)
18
+ skills = db.Column(db.Text, nullable=False) # JSON string
19
+ company = db.Column(db.String(100), nullable=False)
20
  date_posted = db.Column(db.DateTime, default=datetime.utcnow)
21
 
22
+ recruiter_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True)
23
+ recruiter = db.relationship('User', backref='posted_jobs')
24
 
25
  def __repr__(self):
26
+ return f"<Job {self.role} at {self.company}>"
 
27
 
28
  class Application(db.Model):
29
+ __tablename__ = 'applications'
30
+
31
  id = db.Column(db.Integer, primary_key=True)
32
+ job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'), nullable=False)
33
+ user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
34
+
35
  name = db.Column(db.String(100), nullable=False)
36
  email = db.Column(db.String(100), nullable=False)
37
+ resume_path = db.Column(db.String(255), nullable=True)
38
+ cover_letter = db.Column(db.Text, nullable=True)
39
+ extracted_features = db.Column(db.Text, nullable=True)
40
+ status = db.Column(db.String(50), default='applied')
41
  date_applied = db.Column(db.DateTime, default=datetime.utcnow)
42
 
43
+ user = db.relationship('User', backref='applications')
44
+
45
  def __repr__(self):
46
  return f"Application('{self.name}', '{self.email}', Job ID: {self.job_id})"
47
 
48
+ def get_profile_data(self):
49
+ try:
50
+ return json.loads(self.extracted_features) if self.extracted_features else {}
51
+ except:
52
+ return {}
53
 
54
  # Initialize the database
55
  def init_db(app):
 
57
  db.init_app(app)
58
 
59
  with app.app_context():
60
+ from backend.models.user import User
61
  db.create_all()
62
 
63
  # Add sample data if jobs table is empty
64
  if Job.query.count() == 0:
65
  sample_jobs = [
66
+ Job(
67
+ role='Senior Python Developer',
68
+ description='Experienced developer needed for backend systems.',
69
+ seniority='Senior',
70
+ skills=json.dumps(['Python', 'Flask', 'SQL', 'AWS']),
71
+ company='TechCorp'
72
+ ),
73
+ Job(
74
+ role='Data Scientist',
75
+ description='ML model development and statistical analysis.',
76
+ seniority='Mid',
77
+ skills=json.dumps(['Python', 'scikit-learn', 'Pandas', 'Spark']),
78
+ company='DataInsights'
79
+ ),
80
+ Job(
81
+ role='Frontend Developer',
82
+ description='Modern web frontend development with React.',
83
+ seniority='Junior',
84
+ skills=json.dumps(['HTML', 'CSS', 'JavaScript', 'React']),
85
+ company='WebSolutions'
86
+ ),
87
+
88
  ]
89
 
90
+ for job in sample_jobs:
91
+ db.session.add(job)
 
92
 
93
+ db.session.commit()
94
+ print("Sample jobs added to database.")
backend/models/user.py CHANGED
@@ -1,6 +1,7 @@
1
  from flask_login import UserMixin
2
  from werkzeug.security import generate_password_hash, check_password_hash
3
- from backend.models.database import db # Use shared db instance only
 
4
 
5
  class User(UserMixin, db.Model):
6
  __tablename__ = 'user'
 
1
  from flask_login import UserMixin
2
  from werkzeug.security import generate_password_hash, check_password_hash
3
+ from backend.models.database import db # Use the shared db object
4
+
5
 
6
  class User(UserMixin, db.Model):
7
  __tablename__ = 'user'
backend/routes/auth.py CHANGED
@@ -1,11 +1,42 @@
1
- from flask import Blueprint, render_template, redirect, url_for, flash, request
2
- from flask_login import login_user, logout_user, login_required
 
 
 
 
 
3
  from backend.form.AuthForms import LoginForm, SignupForm
4
  from backend.models.user import User
5
  from backend.models.database import db
6
 
7
  auth_bp = Blueprint('auth', __name__)
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  @auth_bp.route('/login', methods=['GET', 'POST'])
10
  def login():
11
  form = LoginForm()
@@ -18,6 +49,7 @@ def login():
18
  flash('Invalid credentials', 'danger')
19
  return render_template('login.html', form=form)
20
 
 
21
  @auth_bp.route('/signup', methods=['GET', 'POST'])
22
  def signup():
23
  form = SignupForm()
@@ -34,12 +66,17 @@ def signup():
34
  user.set_password(form.password.data)
35
  db.session.add(user)
36
  db.session.commit()
37
- flash('Account created successfully!', 'success')
38
- return redirect(url_for('auth.login'))
 
 
 
39
  return render_template('signup.html', form=form)
40
 
 
41
  @auth_bp.route('/logout')
42
  @login_required
43
  def logout():
44
  logout_user()
45
- return redirect(url_for('auth.login'))
 
 
1
+ from flask import Blueprint, render_template, redirect, url_for, flash, request, jsonify
2
+ from flask_login import login_user, logout_user, login_required, current_user
3
+ from werkzeug.utils import secure_filename
4
+ import os
5
+ import json
6
+ from datetime import datetime
7
+
8
  from backend.form.AuthForms import LoginForm, SignupForm
9
  from backend.models.user import User
10
  from backend.models.database import db
11
 
12
  auth_bp = Blueprint('auth', __name__)
13
 
14
+
15
+ def handle_resume_upload(file):
16
+ """
17
+ Save uploaded file temporarily, extract features, then clean up.
18
+ Returns (features_dict, error_message, filename)
19
+ """
20
+ if not file or file.filename == '':
21
+ return None, "No file uploaded", None
22
+
23
+ try:
24
+ # Get current directory for temp folder
25
+ current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
26
+ filename = secure_filename(file.filename)
27
+ filepath = os.path.join(current_dir, 'temp', filename)
28
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
29
+ file.save(filepath)
30
+
31
+ features = extract_resume_features(filepath)
32
+ os.remove(filepath) # Clean up after parsing
33
+
34
+ return features, None, filename
35
+ except Exception as e:
36
+ print(f"Error in handle_resume_upload: {e}")
37
+ return None, str(e), None
38
+
39
+
40
  @auth_bp.route('/login', methods=['GET', 'POST'])
41
  def login():
42
  form = LoginForm()
 
49
  flash('Invalid credentials', 'danger')
50
  return render_template('login.html', form=form)
51
 
52
+
53
  @auth_bp.route('/signup', methods=['GET', 'POST'])
54
  def signup():
55
  form = SignupForm()
 
66
  user.set_password(form.password.data)
67
  db.session.add(user)
68
  db.session.commit()
69
+
70
+ # Log the user in and redirect to profile completion
71
+ login_user(user)
72
+ flash('Account created successfully! Welcome to Codingo.', 'success')
73
+ return redirect(url_for('index'))
74
  return render_template('signup.html', form=form)
75
 
76
+
77
  @auth_bp.route('/logout')
78
  @login_required
79
  def logout():
80
  logout_user()
81
+ flash('You have been logged out successfully.', 'info')
82
+ return redirect(url_for('auth.login'))
backend/routes/interview_api.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import uuid
3
+ import json
4
+
5
+ from flask import Blueprint, request, jsonify, url_for
6
+ from flask_login import login_required, current_user
7
+
8
+ from backend.models.database import db, Job, Application
9
+ from backend.services.interview_engine import (
10
+ generate_first_question,
11
+ edge_tts_to_file_sync,
12
+ whisper_stt
13
+ )
14
+
15
+
16
+ interview_api = Blueprint("interview_api", __name__)
17
+
18
+ @interview_api.route("/api/start_interview", methods=["POST"])
19
+ @login_required
20
+ def start_interview():
21
+ data = request.get_json()
22
+ job_id = data.get("job_id")
23
+
24
+ job = Job.query.get_or_404(job_id)
25
+ application = Application.query.filter_by(user_id=current_user.id, job_id=job_id).first()
26
+
27
+ if not application or not application.extracted_features:
28
+ return jsonify({"error": "No application/profile data found."}), 400
29
+
30
+ try:
31
+ profile = json.loads(application.extracted_features)
32
+ except:
33
+ return jsonify({"error": "Invalid profile JSON"}), 500
34
+
35
+ question = generate_first_question(profile, job)
36
+
37
+ audio_filename = f"q_{uuid.uuid4().hex}.wav"
38
+ audio_path = os.path.join("static", "audio", audio_filename)
39
+ os.makedirs(os.path.dirname(audio_path), exist_ok=True)
40
+
41
+ edge_tts_to_file_sync(question, audio_path)
42
+
43
+ return jsonify({
44
+ "question": question,
45
+ "audio_url": url_for("static", filename=f"audio/{audio_filename}")
46
+ })
47
+
48
+ @interview_api.route("/api/transcribe_audio", methods=["POST"])
49
+ @login_required
50
+ def transcribe_audio():
51
+ audio_file = request.files.get("audio")
52
+ if not audio_file:
53
+ return jsonify({"error": "No audio file received."}), 400
54
+
55
+ filename = f"user_audio_{uuid.uuid4().hex}.wav"
56
+ path = os.path.join("temp", filename)
57
+ os.makedirs("temp", exist_ok=True)
58
+ audio_file.save(path)
59
+
60
+ transcript = whisper_stt(path)
61
+ os.remove(path)
62
+
63
+ return jsonify({"transcript": transcript})
64
+
65
+ @interview_api.route("/api/process_answer", methods=["POST"])
66
+ @login_required
67
+ def process_answer():
68
+ data = request.get_json()
69
+ answer = data.get("answer", "")
70
+ question_idx = data.get("questionIndex", 0)
71
+
72
+ # For now: just return a fake next question
73
+ fake_next_question = f"Follow-up question {question_idx + 2}: Why should we hire you?"
74
+ audio_filename = f"q_{uuid.uuid4().hex}.wav"
75
+ audio_path = os.path.join("static", "audio", audio_filename)
76
+ os.makedirs(os.path.dirname(audio_path), exist_ok=True)
77
+
78
+ edge_tts_to_file_sync(fake_next_question, audio_path)
79
+
80
+ return jsonify({
81
+ "success": True,
82
+ "nextQuestion": fake_next_question,
83
+ "audioUrl": url_for("static", filename=f"audio/{audio_filename}"),
84
+ "evaluation": {
85
+ "score": "medium",
86
+ "feedback": "Good answer, but be more specific."
87
+ },
88
+ "isComplete": question_idx >= 2,
89
+ "summary": [] # optional: you can fill this later
90
+ })
91
+
92
+
93
+
backend/services/interview_engine.py ADDED
@@ -0,0 +1,1958 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import os
3
+ import json
4
+ from langchain_groq import ChatGroq
5
+ from langchain_community.embeddings import HuggingFaceEmbeddings
6
+ from langchain_community.vectorstores import Qdrant
7
+ from langchain.prompts import PromptTemplate
8
+ from langchain.chains import LLMChain
9
+ from langchain.retrievers import ContextualCompressionRetriever
10
+ from langchain.retrievers.document_compressors import CohereRerank
11
+ from qdrant_client import QdrantClient
12
+ import cohere
13
+ import json
14
+ import re
15
+ import time
16
+ from collections import defaultdict
17
+
18
+
19
+ from qdrant_client.http import models
20
+ from qdrant_client.models import PointStruct
21
+ from sklearn.feature_extraction.text import TfidfVectorizer
22
+ from sklearn.neighbors import NearestNeighbors
23
+ from transformers import AutoTokenizer
24
+ #from langchain_huggingface import HuggingFaceEndpoint
25
+ from langchain_community.embeddings import HuggingFaceEmbeddings
26
+ import numpy as np
27
+ import os
28
+ from dotenv import load_dotenv
29
+ from enum import Enum
30
+ import time
31
+ from inputimeout import inputimeout, TimeoutOccurred
32
+
33
+
34
+ # Import Qdrant client and models (adjust based on your environment)
35
+ from qdrant_client import QdrantClient
36
+ from qdrant_client.http.models import VectorParams, Distance, Filter, FieldCondition, MatchValue
37
+ from qdrant_client.http.models import PointStruct, Filter, FieldCondition, MatchValue, SearchRequest
38
+ import traceback
39
+ from transformers import pipeline
40
+
41
+ from textwrap import dedent
42
+ import json
43
+ import logging
44
+
45
+ from transformers import pipeline,BitsAndBytesConfig
46
+
47
+
48
+ import os
49
+
50
+ cohere_api_key = os.getenv("COHERE_API_KEY")
51
+ chat_groq_api = os.getenv("GROQ_API_KEY")
52
+ hf_api_key = os.getenv("HF_API_KEY")
53
+ qdrant_api = os.getenv("QDRANT_API_KEY")
54
+ qdrant_url = os.getenv("QDRANT_API_URL")
55
+
56
+ print("GROQ API Key:", chat_groq_api)
57
+ print("QDRANT API Key:", qdrant_api)
58
+ print("QDRANT API URL:", qdrant_url)
59
+ print("Cohere API Key:", cohere_api_key)
60
+
61
+
62
+ from qdrant_client import QdrantClient
63
+
64
+ qdrant_client = QdrantClient(
65
+ url="https://313b1ceb-057f-4b7b-89f5-7b19a213fe65.us-east-1-0.aws.cloud.qdrant.io:6333",
66
+ api_key="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.w13SPZbljbSvt9Ch_0r034QhMFlmEr4ctXqLo2zhxm4",
67
+ )
68
+
69
+ print(qdrant_client.get_collections())
70
+
71
+ class CustomChatGroq:
72
+ def __init__(self, temperature, model_name, api_key):
73
+ self.temperature = temperature
74
+ self.model_name = model_name
75
+ self.api_key = api_key
76
+ self.api_url = "https://api.groq.com/openai/v1/chat/completions"
77
+
78
+ def predict(self, prompt):
79
+ """Send a request to the Groq API and return the generated response."""
80
+ try:
81
+ headers = {
82
+ "Authorization": f"Bearer {self.api_key}",
83
+ "Content-Type": "application/json"
84
+ }
85
+
86
+ payload = {
87
+ "model": self.model_name,
88
+ "messages": [{"role": "system", "content": "You are an AI interviewer."},
89
+ {"role": "user", "content": prompt}],
90
+ "temperature": self.temperature,
91
+ "max_tokens": 150
92
+ }
93
+
94
+ response = requests.post(self.api_url, headers=headers, json=payload, timeout=10)
95
+ response.raise_for_status() # Raise an error for HTTP codes 4xx/5xx
96
+
97
+ data = response.json()
98
+
99
+ # Extract response text based on Groq API response format
100
+ if "choices" in data and len(data["choices"]) > 0:
101
+ return data["choices"][0]["message"]["content"].strip()
102
+
103
+ logging.warning("Unexpected response structure from Groq API")
104
+ return "Interviewer: Could you tell me more about your relevant experience?"
105
+
106
+ except requests.exceptions.RequestException as e:
107
+ logging.error(f"ChatGroq API error: {e}")
108
+ return "Interviewer: Due to a system issue, let's move on to another question."
109
+
110
+ groq_llm = ChatGroq(
111
+ temperature=0.7,
112
+ model_name="llama-3.3-70b-versatile",
113
+ api_key=chat_groq_api
114
+ )
115
+
116
+ from huggingface_hub import login
117
+ import os
118
+
119
+ HF_TOKEN = os.getenv("HF_TOKEN")
120
+
121
+ if HF_TOKEN:
122
+ login(HF_TOKEN)
123
+ else:
124
+ raise EnvironmentError("Missing HF_TOKEN environment variable.")
125
+
126
+ #Load mistral Model
127
+ from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
128
+ import torch
129
+ print(torch.cuda.is_available())
130
+
131
+ MODEL_PATH = "mistralai/Mistral-7B-Instruct-v0.3"
132
+ #MODEL_PATH = "tiiuae/falcon-rw-1b"
133
+
134
+ bnb_config = BitsAndBytesConfig(
135
+ load_in_8bit=True,
136
+ )
137
+
138
+ mistral_tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH,token=hf_api_key)
139
+
140
+ judge_llm = AutoModelForCausalLM.from_pretrained(
141
+ MODEL_PATH,
142
+ quantization_config=bnb_config,torch_dtype=torch.float16,
143
+ device_map="auto",
144
+ token=hf_api_key
145
+ )
146
+ judge_llm.config.pad_token_id = judge_llm.config.eos_token_id
147
+
148
+
149
+ print(judge_llm.hf_device_map)
150
+
151
+ judge_pipeline = pipeline(
152
+ "text-generation",
153
+ model=judge_llm,
154
+ tokenizer=mistral_tokenizer,
155
+ max_new_tokens=128,
156
+ temperature=0.3,
157
+ top_p=0.9,
158
+ do_sample=True, # Optional but recommended with temperature/top_p
159
+ repetition_penalty=1.1,
160
+ )
161
+
162
+
163
+ output = judge_pipeline("Q: What is Python?\nA:", max_new_tokens=128)[0]['generated_text']
164
+ print(output)
165
+
166
+
167
+
168
+ # embedding model
169
+ from sentence_transformers import SentenceTransformer
170
+
171
+ class LocalEmbeddings:
172
+ def __init__(self, model_name="all-MiniLM-L6-v2"):
173
+ self.model = SentenceTransformer(model_name)
174
+
175
+ def embed_query(self, text):
176
+ return self.model.encode(text).tolist()
177
+
178
+ def embed_documents(self, documents):
179
+ return self.model.encode(documents).tolist()
180
+
181
+
182
+ embeddings = LocalEmbeddings()
183
+
184
+ # import cohere
185
+ qdrant_client = QdrantClient(url=qdrant_url, api_key=qdrant_api,check_compatibility=False)
186
+ co = cohere.Client(api_key=cohere_api_key)
187
+
188
+ class EvaluationScore(str, Enum):
189
+ POOR = "Poor"
190
+ MEDIUM = "Medium"
191
+ GOOD = "Good"
192
+ EXCELLENT = "Excellent"
193
+
194
+ # Cohere Reranker
195
+ class CohereReranker:
196
+ def __init__(self, client):
197
+ self.client = client
198
+
199
+ def compress_documents(self, documents, query):
200
+ if not documents:
201
+ return []
202
+ doc_texts = [doc.page_content for doc in documents]
203
+ try:
204
+ reranked = self.client.rerank(
205
+ query=query,
206
+ documents=doc_texts,
207
+ model="rerank-english-v2.0",
208
+ top_n=5
209
+ )
210
+ return [documents[result.index] for result in reranked.results]
211
+ except Exception as e:
212
+ logging.error(f"Error in CohereReranker.compress_documents: {e}")
213
+ return documents[:5]
214
+
215
+ reranker = CohereReranker(co)
216
+
217
+ def load_data_from_json(file_path):
218
+ """Load interview Q&A data from a JSON file."""
219
+ try:
220
+ with open(file_path, "r", encoding="utf-8") as f:
221
+ data = json.load(f)
222
+ job_role_buckets = defaultdict(list)
223
+ for idx, item in enumerate(data):
224
+ try:
225
+ job_role = item["Job Role"].lower().strip()
226
+ question = item["Questions"].strip()
227
+ answer = item["Answers"].strip()
228
+ job_role_buckets[job_role].append({"question": question, "answer": answer})
229
+ except KeyError as e:
230
+ logging.warning(f"Skipping item {idx}: missing key {e}")
231
+ return job_role_buckets # <--- You missed this!
232
+ except Exception as e:
233
+ logging.error(f"Error loading data: {e}")
234
+ raise
235
+
236
+
237
+ def verify_qdrant_collection(collection_name='interview_questions'):
238
+ """Verify if a Qdrant collection exists with the correct configuration."""
239
+ try:
240
+ collection_info = qdrant_client.get_collection(collection_name)
241
+ vector_size = collection_info.config.params.vectors.size
242
+ logging.info(f"Collection '{collection_name}' exists with vector size: {vector_size}")
243
+ return True
244
+ except Exception as e:
245
+ logging.warning(f"Collection '{collection_name}' not found: {e}")
246
+ return False
247
+
248
+
249
+
250
+
251
+ def store_data_to_qdrant(data, collection_name='interview_questions', batch_size=100):
252
+ """Store interview data in the Qdrant vector database."""
253
+ try:
254
+ # Check if collection exists, otherwise create it
255
+ if not verify_qdrant_collection(collection_name):
256
+ try:
257
+ qdrant_client.create_collection(
258
+ collection_name=collection_name,
259
+ vectors_config=VectorParams(size=384, distance=Distance.COSINE)
260
+ )
261
+ logging.info(f"Created collection '{collection_name}'")
262
+ except Exception as e:
263
+ logging.error(f"Error creating collection: {e}\n{traceback.format_exc()}")
264
+ return False
265
+
266
+ points = []
267
+ point_id = 0
268
+ total_points = sum(len(qa_list) for qa_list in data.values())
269
+ processed = 0
270
+
271
+ for job_role, qa_list in data.items():
272
+ for entry in qa_list:
273
+ try:
274
+ emb = embeddings.embed_query(entry["question"])
275
+ print(f"Embedding shape: {len(emb)}")
276
+
277
+ if not emb or len(emb) != 384:
278
+ logging.warning(f"Skipping point {point_id} due to invalid embedding length: {len(emb)}")
279
+ continue
280
+
281
+ points.append(PointStruct(
282
+ id=point_id,
283
+ vector=emb,
284
+ payload={
285
+ "job_role": job_role,
286
+ "question": entry["question"],
287
+ "answer": entry["answer"]
288
+ }
289
+ ))
290
+ point_id += 1
291
+ processed += 1
292
+
293
+ # Batch upload
294
+ if len(points) >= batch_size:
295
+ try:
296
+ qdrant_client.upsert(collection_name=collection_name, points=points)
297
+ logging.info(f"Stored {processed}/{total_points} points ({processed/total_points*100:.1f}%)")
298
+ except Exception as upsert_err:
299
+ logging.error(f"Error during upsert: {upsert_err}\n{traceback.format_exc()}")
300
+ points = []
301
+
302
+ except Exception as embed_err:
303
+ logging.error(f"Embedding error for point {point_id}: {embed_err}\n{traceback.format_exc()}")
304
+
305
+ # Final batch upload
306
+ if points:
307
+ try:
308
+ qdrant_client.upsert(collection_name=collection_name, points=points)
309
+ logging.info(f"Stored final batch of {len(points)} points")
310
+ except Exception as final_upsert_err:
311
+ logging.error(f"Error during final upsert: {final_upsert_err}\n{traceback.format_exc()}")
312
+
313
+ # Final verification
314
+ try:
315
+ count = qdrant_client.count(collection_name=collection_name, exact=True).count
316
+ print("Current count:", count)
317
+ logging.info(f"✅ Successfully stored {count} points in Qdrant")
318
+ if count != total_points:
319
+ logging.warning(f"Expected {total_points} points but stored {count}")
320
+ except Exception as count_err:
321
+ logging.error(f"Error verifying stored points: {count_err}\n{traceback.format_exc()}")
322
+
323
+ return True
324
+
325
+ except Exception as e:
326
+ logging.error(f"Error storing data to Qdrant: {e}\n{traceback.format_exc()}")
327
+ return False
328
+
329
+ # to ensure cosine similarity use
330
+ info = qdrant_client.get_collection("interview_questions")
331
+ print(info.config.params.vectors.distance)
332
+
333
+ def extract_all_roles_from_qdrant(collection_name='interview_questions'):
334
+ """ Extract all unique job roles from the Qdrant vector store """
335
+ try:
336
+ all_roles = set()
337
+ scroll_offset = None
338
+
339
+ while True:
340
+ response = qdrant_client.scroll(
341
+ collection_name=collection_name,
342
+ limit=200,
343
+ offset=scroll_offset,
344
+ with_payload=True
345
+ )
346
+ points, next_page_offset = response
347
+
348
+ if not points:
349
+ break
350
+
351
+ for point in points:
352
+ role = point.payload.get("job_role", "").strip().lower()
353
+ if role:
354
+ all_roles.add(role)
355
+
356
+ if not next_page_offset:
357
+ break
358
+
359
+ scroll_offset = next_page_offset
360
+
361
+ if not all_roles:
362
+ logging.warning("[Qdrant] No roles found in payloads.")
363
+ else:
364
+ logging.info(f"[Qdrant] Extracted {len(all_roles)} unique job roles.")
365
+
366
+ return list(all_roles)
367
+ except Exception as e:
368
+ logging.error(f"Error extracting roles from Qdrant: {e}")
369
+ return []
370
+
371
+ import numpy as np
372
+ import logging
373
+ from sklearn.metrics.pairwise import cosine_similarity
374
+
375
+ def find_similar_roles(user_role, all_roles, top_k=3):
376
+ """
377
+ Find the most similar job roles to the given user_role using embeddings.
378
+ """
379
+ try:
380
+ # Clean inputs
381
+ user_role = user_role.strip().lower()
382
+ if not user_role or not all_roles or not isinstance(all_roles, list):
383
+ logging.warning("Invalid input for role similarity")
384
+ return []
385
+
386
+ # Embed user role
387
+ try:
388
+ user_embedding = embeddings.embed_query(user_role)
389
+ if user_embedding is None:
390
+ logging.error("User embedding is None")
391
+ return []
392
+ except Exception as e:
393
+ logging.error(f"Error embedding user role: {type(e).__name__}: {e}")
394
+ return []
395
+
396
+ # Embed all roles
397
+ try:
398
+ role_embeddings = []
399
+ valid_roles = []
400
+ for role in all_roles:
401
+ emb = embeddings.embed_query(role.lower())
402
+ if emb is not None:
403
+ role_embeddings.append(emb)
404
+ valid_roles.append(role)
405
+ else:
406
+ logging.warning(f"Skipping role with no embedding: {role}")
407
+ except Exception as e:
408
+ logging.error(f"Error embedding all roles: {type(e).__name__}: {e}")
409
+ return []
410
+
411
+ if not role_embeddings:
412
+ logging.error("All role embeddings failed")
413
+ return []
414
+
415
+ # Compute similarities
416
+ similarities = cosine_similarity([user_embedding], role_embeddings)[0]
417
+ top_indices = np.argsort(similarities)[::-1][:top_k]
418
+
419
+ similar_roles = [valid_roles[i] for i in top_indices]
420
+ logging.debug(f"Similar roles to '{user_role}': {similar_roles}")
421
+ return similar_roles
422
+
423
+ except Exception as e:
424
+ logging.error(f"Error finding similar roles: {type(e).__name__}: {e}", exc_info=True)
425
+ return []
426
+
427
+ # RETREIVE ALL DATA RELATED TO THE JOB ROLE NOT JUST TOP_K
428
+ def get_role_questions(job_role):
429
+ try:
430
+ if not job_role:
431
+ logging.warning("Job role is empty.")
432
+ return []
433
+
434
+ filter_by_role = Filter(
435
+ must=[FieldCondition(
436
+ key="job_role",
437
+ match=MatchValue(value=job_role.lower())
438
+ )]
439
+ )
440
+
441
+ all_results = []
442
+ offset = None
443
+ while True:
444
+ results, next_page_offset = qdrant_client.scroll(
445
+ collection_name="interview_questions",
446
+ scroll_filter=filter_by_role,
447
+ with_payload=True,
448
+ with_vectors=False,
449
+ limit=100, # batch size
450
+ offset=offset
451
+ )
452
+ all_results.extend(results)
453
+
454
+ if not next_page_offset:
455
+ break
456
+ offset = next_page_offset
457
+
458
+ parsed_results = [{
459
+ "question": r.payload.get("question"),
460
+ "answer": r.payload.get("answer"),
461
+ "job_role": r.payload.get("job_role")
462
+ } for r in all_results]
463
+
464
+ return parsed_results
465
+
466
+ except Exception as e:
467
+ logging.error(f"Error fetching role questions: {type(e).__name__}: {e}", exc_info=True)
468
+ return []
469
+
470
+ def retrieve_interview_data(job_role, all_roles):
471
+ """
472
+ Retrieve all interview Q&A for a given job role.
473
+ Falls back to similar roles if no data found.
474
+ Args:
475
+ job_role (str): Input job role (can be misspelled)
476
+ all_roles (list): Full list of available job roles
477
+ Returns:
478
+ list: List of QA dicts with keys: 'question', 'answer', 'job_role'
479
+ """
480
+ import logging
481
+ logging.basicConfig(level=logging.INFO)
482
+
483
+ job_role = job_role.strip().lower()
484
+ seen_questions = set()
485
+ final_results = []
486
+
487
+ # Step 1: Try exact match (fetch all questions for role)
488
+ logging.info(f"Trying to fetch all data for exact role: '{job_role}'")
489
+ exact_matches = get_role_questions(job_role)
490
+
491
+ for qa in exact_matches:
492
+ question = qa["question"]
493
+ if question and question not in seen_questions:
494
+ seen_questions.add(question)
495
+ final_results.append(qa)
496
+
497
+ if final_results:
498
+ logging.info(f"Found {len(final_results)} QA pairs for exact role '{job_role}'")
499
+ return final_results
500
+
501
+ logging.warning(f"No data found for role '{job_role}'. Trying similar roles...")
502
+
503
+ # Step 2: No matches — find similar roles
504
+ similar_roles = find_similar_roles(job_role, all_roles, top_k=3)
505
+
506
+ if not similar_roles:
507
+ logging.warning("No similar roles found.")
508
+ return []
509
+
510
+ logging.info(f"Found similar roles: {similar_roles}")
511
+
512
+ # Step 3: Retrieve data for each similar role (all questions)
513
+ for role in similar_roles:
514
+ logging.info(f"Fetching data for similar role: '{role}'")
515
+ role_qa = get_role_questions(role)
516
+
517
+ for qa in role_qa:
518
+ question = qa["question"]
519
+ if question and question not in seen_questions:
520
+ seen_questions.add(question)
521
+ final_results.append(qa)
522
+
523
+ logging.info(f"Retrieved total {len(final_results)} QA pairs from similar roles")
524
+ return final_results
525
+
526
+ import random
527
+
528
+ def random_context_chunks(retrieved_data, k=3):
529
+ chunks = random.sample(retrieved_data, k)
530
+ return "\n\n".join([f"Q: {item['question']}\nA: {item['answer']}" for item in chunks])
531
+
532
+ import json
533
+ import logging
534
+ import re
535
+ from typing import Dict
536
+
537
+ def eval_question_quality(
538
+ question: str,
539
+ job_role: str,
540
+ seniority: str
541
+ ) -> Dict[str, str]:
542
+ """
543
+ Evaluate the quality of a generated interview question using Groq LLM.
544
+ Returns a structured JSON with score, reasoning, and suggestions.
545
+ """
546
+ import time, json
547
+
548
+ prompt = f"""
549
+ You are a senior AI hiring expert evaluating the quality of an interview question for a {seniority} {job_role} role.
550
+
551
+ Evaluate the question based on:
552
+ - Relevance to the role and level
553
+ - Clarity and conciseness
554
+ - Depth of technical insight
555
+
556
+ ---
557
+ Question: {question}
558
+ ---
559
+
560
+ Respond only with a valid JSON like:
561
+ {{
562
+ "Score": "Poor" | "Medium" | "Good" | "Excellent",
563
+ "Reasoning": "short justification",
564
+ "Improvements": ["tip1", "tip2"]
565
+ }}
566
+ """
567
+
568
+ try:
569
+ start = time.time()
570
+ response = groq_llm.invoke(prompt)
571
+ print("⏱️ eval_question_quality duration:", round(time.time() - start, 2), "s")
572
+
573
+ # Extract JSON safely
574
+ start_idx = response.rfind("{")
575
+ end_idx = response.rfind("}") + 1
576
+ json_str = response[start_idx:end_idx]
577
+ result = json.loads(json_str)
578
+
579
+ if result.get("Score") in {"Poor", "Medium", "Good", "Excellent"}:
580
+ return result
581
+ else:
582
+ raise ValueError("Invalid Score value in model output")
583
+
584
+ except Exception as e:
585
+ print(f"⚠️ eval_question_quality fallback: {e}")
586
+ return {
587
+ "Score": "Poor",
588
+ "Reasoning": "Evaluation failed, using fallback.",
589
+ "Improvements": [
590
+ "Ensure the question is relevant and clear.",
591
+ "Avoid vague or overly generic phrasing.",
592
+ "Include role-specific context if needed."
593
+ ]
594
+ }
595
+
596
+ def evaluate_answer(
597
+ question: str,
598
+ answer: str,
599
+ ref_answer: str,
600
+ job_role: str,
601
+ seniority: str,
602
+ ) -> Dict[str, str]:
603
+ """
604
+ Fast and structured answer evaluation using Groq LLM (e.g. Mixtral or LLaMA 3).
605
+ """
606
+ import time, json
607
+ from langchain_core.messages import AIMessage
608
+
609
+ prompt = f"""
610
+ You are a technical interviewer evaluating a candidate for a {seniority} {job_role} role.
611
+
612
+ Evaluate the response based on:
613
+ - Technical correctness
614
+ - Clarity
615
+ - Relevance
616
+ - Structure
617
+
618
+ ---
619
+ Question: {question}
620
+ Candidate Answer: {answer}
621
+ Reference Answer: {ref_answer}
622
+ ---
623
+
624
+ Respond ONLY with valid JSON in the following format:
625
+ {{
626
+ "Score": "Poor" | "Medium" | "Good" | "Excellent",
627
+ "Reasoning": "short justification",
628
+ "Improvements": ["tip1", "tip2"]
629
+ }}
630
+ """
631
+
632
+ try:
633
+ start = time.time()
634
+ raw = groq_llm.invoke(prompt)
635
+ print("⏱️ evaluate_answer duration:", round(time.time() - start, 2), "s")
636
+
637
+ if isinstance(raw, AIMessage):
638
+ output = raw.content
639
+ else:
640
+ output = str(raw)
641
+
642
+ print("🔍 Raw Groq Response:\n", output)
643
+
644
+ start_idx = output.rfind("{")
645
+ end_idx = output.rfind("}") + 1
646
+ json_str = output[start_idx:end_idx]
647
+
648
+ result = json.loads(json_str)
649
+ if result.get("Score") in {"Poor", "Medium", "Good", "Excellent"}:
650
+ return result
651
+ else:
652
+ raise ValueError("Invalid score value")
653
+
654
+ except Exception as e:
655
+ print(f"⚠️ evaluate_answer fallback: {e}")
656
+ return {
657
+ "Score": "Poor",
658
+ "Reasoning": "Failed to evaluate properly. Defaulted to Poor.",
659
+ "Improvements": [
660
+ "Be more specific",
661
+ "Add technical details",
662
+ "Structure the answer clearly"
663
+ ]
664
+ }
665
+
666
+ # SAME BUT USING LLAMA 3.3 FROM GROQ
667
+ def generate_reference_answer(question, job_role, seniority):
668
+ """
669
+ Generates a high-quality reference answer using Groq-hosted LLaMA model.
670
+ Args:
671
+ question (str): Interview question to answer.
672
+ job_role (str): Target job role (e.g., "Frontend Developer").
673
+ seniority (str): Experience level (e.g., "Mid-Level").
674
+ Returns:
675
+ str: Clean, generated reference answer or error message.
676
+ """
677
+ try:
678
+ # Clean, role-specific prompt
679
+ prompt = f"""You are a {seniority} {job_role}.
680
+ Q: {question}
681
+ A:"""
682
+
683
+ # Use Groq-hosted model to generate the answer
684
+ ref_answer = groq_llm.predict(prompt)
685
+
686
+ if not ref_answer.strip():
687
+ return "Reference answer not generated."
688
+
689
+ return ref_answer.strip()
690
+
691
+ except Exception as e:
692
+ logging.error(f"Error generating reference answer: {e}", exc_info=True)
693
+ return "Unable to generate reference answer due to an error"
694
+
695
+
696
+
697
+ def build_interview_prompt(conversation_history, user_response, context, job_role, skills, seniority,
698
+ difficulty_adjustment=None, voice_label=None, face_label=None, effective_confidence=None):
699
+ """Build a prompt for generating the next interview question with adaptive difficulty and fairness logic."""
700
+
701
+ interview_template = """
702
+ You are an AI interviewer conducting a real-time interview for a {job_role} position.
703
+ Your objective is to thoroughly evaluate the candidate's suitability for the role using smart, structured, and adaptive questioning.
704
+ ---
705
+ Interview Rules and Principles:
706
+ - The **baseline difficulty** of questions must match the candidate’s seniority level (e.g., junior, mid-level, senior).
707
+ - Use your judgment to increase difficulty **slightly** if the candidate performs well, or simplify if they struggle — but never drop below the expected baseline for their level.
708
+ - Avoid asking extremely difficult questions to junior candidates unless they’ve clearly demonstrated advanced knowledge.
709
+ - Be fair: candidates for the same role should be evaluated within a consistent difficulty range.
710
+ - Adapt your line of questioning gradually and logically based on the **overall flow**, not just the last answer.
711
+ - Include real-world problem-solving scenarios to test how the candidate thinks and behaves practically.
712
+ - You must **lead** the interview and make intelligent decisions about what to ask next.
713
+ ---
714
+ Context Use:
715
+ {context_instruction}
716
+ Note:
717
+ If no relevant context was retrieved or the previous answer is unclear, you must still generate a thoughtful interview question using your own knowledge. Do not skip generation. Avoid default or fallback responses — always try to generate a meaningful and fair next question.
718
+ ---
719
+ Job Role: {job_role}
720
+ Seniority Level: {seniority}
721
+ Skills Focus: {skills}
722
+ Difficulty Setting: {difficulty} (based on {difficulty_adjustment})
723
+ ---
724
+ Recent Conversation History:
725
+ {history}
726
+ Candidate's Last Response:
727
+ "{user_response}"
728
+ Evaluation of Last Response:
729
+ {response_evaluation}
730
+ Voice Tone: {voice_label}
731
+ ---
732
+ ---
733
+ Important:
734
+ If no relevant context was retrieved or the previous answer is unclear or off-topic,
735
+ you must still generate a meaningful and fair interview question using your own knowledge and best practices.
736
+ Do not skip question generation or fall back to default/filler responses.
737
+ ---
738
+ Guidelines for Next Question:
739
+ - If this is the beginning of the interview, start with a question about the candidate’s background or experience.
740
+ - Base the difficulty primarily on the seniority level, with light adjustment from recent performance.
741
+ - Focus on core skills, real-world applications, and depth of reasoning.
742
+ - Ask only one question. Be clear and concise.
743
+ Generate the next interview question now:
744
+ """
745
+
746
+ # Calculate difficulty phrase
747
+ if difficulty_adjustment == "harder":
748
+ difficulty = f"slightly more challenging than typical for {seniority}"
749
+ elif difficulty_adjustment == "easier":
750
+ difficulty = f"slightly easier than typical for {seniority}"
751
+ else:
752
+ difficulty = f"appropriate for {seniority}"
753
+
754
+ # Choose context logic
755
+ if context.strip():
756
+ context_instruction = (
757
+ "Use both your own expertise and the provided context from relevant interview datasets. "
758
+ "You can either build on questions from the dataset or generate your own."
759
+ )
760
+ context = context.strip()
761
+ else:
762
+ context_instruction = (
763
+ "No specific context retrieved. Use your own knowledge and best practices to craft a question."
764
+ )
765
+ context = "" # Let it be actually empty!
766
+
767
+
768
+ # Format conversation history (last 6 exchanges max)
769
+ recent_history = conversation_history[-6:] if len(conversation_history) > 6 else conversation_history
770
+ formatted_history = "\n".join([f"{msg['role'].capitalize()}: {msg['content']}" for msg in recent_history])
771
+
772
+ # Add evaluation summary if available
773
+
774
+ if conversation_history and conversation_history[-1].get("evaluation"):
775
+ eval_data = conversation_history[-1]["evaluation"][-1]
776
+ response_evaluation = f"""
777
+ - Score: {eval_data.get('Score', 'N/A')}
778
+ - Reasoning: {eval_data.get('Reasoning', 'N/A')}
779
+ - Improvements: {eval_data.get('Improvements', 'N/A')}
780
+ """
781
+ else:
782
+ response_evaluation = "No evaluation available yet."
783
+
784
+
785
+ # Fill the template
786
+ prompt = interview_template.format(
787
+ job_role=job_role,
788
+ seniority=seniority,
789
+ skills=skills,
790
+ difficulty=difficulty,
791
+ difficulty_adjustment=difficulty_adjustment if difficulty_adjustment else "default seniority",
792
+ context_instruction=context_instruction,
793
+ context=context,
794
+ history=formatted_history,
795
+ user_response=user_response,
796
+ response_evaluation=response_evaluation.strip(),
797
+ voice_label=voice_label or "unknown",
798
+ )
799
+
800
+ return prompt
801
+
802
+
803
+ def generate_llm_interview_report(
804
+ interview_state, logged_samples, job_role, seniority
805
+ ):
806
+ from collections import Counter
807
+
808
+ # Helper for converting score to 1–5
809
+ def score_label(label):
810
+ mapping = {
811
+ "confident": 5, "calm": 4, "neutral": 3, "nervous": 2, "anxious": 1, "unknown": 3
812
+ }
813
+ return mapping.get(label.lower(), 3)
814
+
815
+ def section_score(vals):
816
+ return round(sum(vals)/len(vals), 2) if vals else "N/A"
817
+
818
+ # Aggregate info
819
+ scores, voice_conf, face_conf, comm_scores = [], [], [], []
820
+ tech_details, comm_details, emotion_details, relevance_details, problem_details = [], [], [], [], []
821
+
822
+ for entry in logged_samples:
823
+ answer_eval = entry.get("answer_evaluation", {})
824
+ score = answer_eval.get("Score", "Not Evaluated")
825
+ reasoning = answer_eval.get("Reasoning", "")
826
+ if score.lower() in ["excellent", "good", "medium", "poor"]:
827
+ score_map = {"excellent": 5, "good": 4, "medium": 3, "poor": 2}
828
+ scores.append(score_map[score.lower()])
829
+ # Section details
830
+ tech_details.append(reasoning)
831
+ comm_details.append(reasoning)
832
+ # Emotions/confidence
833
+ voice_conf.append(score_label(entry.get("voice_label", "unknown")))
834
+ face_conf.append(score_label(entry.get("face_label", "unknown")))
835
+ # Communication estimate
836
+ if entry["user_answer"]:
837
+ length = len(entry["user_answer"].split())
838
+ comm_score = min(5, max(2, length // 30))
839
+ comm_scores.append(comm_score)
840
+
841
+ # Compute averages for sections
842
+ avg_problem = section_score(scores)
843
+ avg_tech = section_score(scores)
844
+ avg_comm = section_score(comm_scores)
845
+ avg_emotion = section_score([(v+f)/2 for v, f in zip(voice_conf, face_conf)])
846
+
847
+ # Compute decision heuristics
848
+ section_averages = [avg_problem, avg_tech, avg_comm, avg_emotion]
849
+ numeric_avgs = [v for v in section_averages if isinstance(v, (float, int))]
850
+ avg_overall = round(sum(numeric_avgs) / len(numeric_avgs), 2) if numeric_avgs else 0
851
+
852
+ # Hiring logic (you can customize thresholds)
853
+ if avg_overall >= 4.5:
854
+ verdict = "Strong Hire"
855
+ elif avg_overall >= 4.0:
856
+ verdict = "Hire"
857
+ elif avg_overall >= 3.0:
858
+ verdict = "Conditional Hire"
859
+ else:
860
+ verdict = "No Hire"
861
+
862
+ # Build LLM report prompt
863
+ transcript = "\n\n".join([
864
+ f"Q: {e['generated_question']}\nA: {e['user_answer']}\nScore: {e.get('answer_evaluation',{}).get('Score','')}\nReasoning: {e.get('answer_evaluation',{}).get('Reasoning','')}"
865
+ for e in logged_samples
866
+ ])
867
+
868
+ prompt = f"""
869
+ You are a senior technical interviewer at a major tech company.
870
+ Write a structured, realistic hiring report for this {seniority} {job_role} interview, using these section scores (scale 1–5, with 5 best):
871
+ Section-wise Evaluation
872
+ 1. *Problem Solving & Critical Thinking*: {avg_problem}
873
+ 2. *Technical Depth & Knowledge*: {avg_tech}
874
+ 3. *Communication & Clarity*: {avg_comm}
875
+ 4. *Emotional Composure & Confidence*: {avg_emotion}
876
+ 5. *Role Relevance*: 5
877
+ *Transcript*
878
+ {transcript}
879
+ Your report should have the following sections:
880
+ 1. *Executive Summary* (realistic, hiring-committee style)
881
+ 2. *Section-wise Comments* (for each numbered category above, with short paragraph citing specifics)
882
+ 3. *Strengths & Weaknesses* (list at least 2 for each)
883
+ 4. *Final Verdict*: {verdict}
884
+ 5. *Recommendations* (2–3 for future improvement)
885
+ Use realistic language. If some sections are N/A or lower than others, comment honestly.
886
+ Interview Report:
887
+ """
888
+ # LLM call, or just return prompt for review
889
+ return groq_llm.predict(prompt)
890
+
891
+ def get_user_info():
892
+ """
893
+ Collects essential information from the candidate before starting the interview.
894
+ Returns a dictionary with keys: name, job_role, seniority, skills
895
+ """
896
+ import logging
897
+ logging.info("Collecting user information...")
898
+
899
+ print("Welcome to the AI Interview Simulator!")
900
+ print("Let’s set up your mock interview.\n")
901
+
902
+ # Get user name
903
+ name = input("What is your name? ").strip()
904
+ while not name:
905
+ print("Please enter your name.")
906
+ name = input("What is your name? ").strip()
907
+
908
+ # Get job role
909
+ job_role = input(f"Hi {name}, what job role are you preparing for? (e.g. Frontend Developer) ").strip()
910
+ while not job_role:
911
+ print("Please specify the job role.")
912
+ job_role = input("What job role are you preparing for? ").strip()
913
+
914
+ # Get seniority level
915
+ seniority_options = ["Entry-level", "Junior", "Mid-Level", "Senior", "Lead"]
916
+ print("\nSelect your experience level:")
917
+ for i, option in enumerate(seniority_options, 1):
918
+ print(f"{i}. {option}")
919
+
920
+ seniority_choice = None
921
+ while seniority_choice not in range(1, len(seniority_options)+1):
922
+ try:
923
+ seniority_choice = int(input("Enter the number corresponding to your level: "))
924
+ except ValueError:
925
+ print(f"Please enter a number between 1 and {len(seniority_options)}")
926
+
927
+ seniority = seniority_options[seniority_choice - 1]
928
+
929
+ # Get skills
930
+ skills_input = input(f"\nWhat are your top skills relevant to {job_role}? (Separate with commas): ")
931
+ skills = [skill.strip() for skill in skills_input.split(",") if skill.strip()]
932
+
933
+ while not skills:
934
+ print("Please enter at least one skill.")
935
+ skills_input = input("Your top skills (comma-separated): ")
936
+ skills = [skill.strip() for skill in skills_input.split(",") if skill.strip()]
937
+
938
+ # Confirm collected info
939
+ print("\n Interview Setup Complete!")
940
+ print(f"Name: {name}")
941
+ print(f"Job Role: {job_role}")
942
+ print(f"Experience Level: {seniority}")
943
+ print(f"Skills: {', '.join(skills)}")
944
+ print("\nStarting your mock interview...\n")
945
+
946
+ return {
947
+ "name": name,
948
+ "job_role": job_role,
949
+ "seniority": seniority,
950
+ "skills": skills
951
+ }
952
+
953
+ import threading
954
+
955
+ def wait_for_user_response(timeout=200):
956
+ """Wait for user input with timeout. Returns '' if no response."""
957
+ user_input = []
958
+
959
+ def get_input():
960
+ answer = input("Your Answer (within timeout): ").strip()
961
+ user_input.append(answer)
962
+
963
+ thread = threading.Thread(target=get_input)
964
+ thread.start()
965
+ thread.join(timeout)
966
+
967
+ return user_input[0] if user_input else ""
968
+
969
+ import json
970
+ from datetime import datetime
971
+ from time import time
972
+ import random
973
+
974
+ def interview_loop(max_questions, timeout_seconds=300, collection_name="interview_questions", judge_pipeline=None, save_path="interview_log.json"):
975
+
976
+
977
+ user_info = get_user_info()
978
+ job_role = user_info['job_role']
979
+ seniority = user_info['seniority']
980
+ skills = user_info['skills']
981
+
982
+ all_roles = extract_all_roles_from_qdrant(collection_name=collection_name)
983
+ retrieved_data = retrieve_interview_data(job_role, all_roles)
984
+ context_data = random_context_chunks(retrieved_data, k=4)
985
+
986
+ conversation_history = []
987
+ interview_state = {
988
+ "questions": [],
989
+ "user_answer": [],
990
+ "job_role": job_role,
991
+ "seniority": seniority,
992
+ "start_time": time()
993
+ }
994
+
995
+ # Store log for evaluation
996
+ logged_samples = []
997
+
998
+ difficulty_adjustment = None
999
+
1000
+ for i in range(max_questions):
1001
+ last_user_response = conversation_history[-1]['content'] if conversation_history else ""
1002
+
1003
+ # Generate question prompt
1004
+ prompt = build_interview_prompt(
1005
+ conversation_history=conversation_history,
1006
+ user_response=last_user_response,
1007
+ context=context_data,
1008
+ job_role=job_role,
1009
+ skills=skills,
1010
+ seniority=seniority,
1011
+ difficulty_adjustment=difficulty_adjustment
1012
+ )
1013
+ question = groq_llm.predict(prompt)
1014
+ question_eval = eval_question_quality(question, job_role, seniority)
1015
+
1016
+ conversation_history.append({'role': "Interviewer", "content": question})
1017
+ print(f"Interviewer: Q{i + 1} : {question}")
1018
+
1019
+ # Wait for user answer
1020
+ start_time = time()
1021
+ user_answer = wait_for_user_response(timeout=timeout_seconds)
1022
+ response_time = time() - start_time
1023
+
1024
+ skipped = False
1025
+ answer_eval = None
1026
+ ref_answer = None
1027
+
1028
+ if not user_answer:
1029
+ print("No Response Received, moving to next question.")
1030
+ user_answer = None
1031
+ skipped = True
1032
+ difficulty_adjustment = "medium"
1033
+ else:
1034
+ conversation_history.append({"role": "Candidate", "content": user_answer})
1035
+
1036
+ ref_answer = generate_reference_answer(question, job_role, seniority)
1037
+ answer_eval = evaluate_answer(
1038
+ question=question,
1039
+ answer=user_answer,
1040
+ ref_answer=ref_answer,
1041
+ job_role=job_role,
1042
+ seniority=seniority,
1043
+ judge_pipeline=judge_pipeline
1044
+ )
1045
+
1046
+
1047
+ interview_state["user_answer"].append(user_answer)
1048
+ # Append inline evaluation for history
1049
+ conversation_history[-1].setdefault('evaluation', []).append({
1050
+ "technical_depth": {
1051
+ "score": answer_eval['Score'],
1052
+ "Reasoning": answer_eval['Reasoning']
1053
+ }
1054
+ })
1055
+
1056
+ # Adjust difficulty
1057
+ score = answer_eval['Score'].lower()
1058
+ if score == "excellent":
1059
+ difficulty_adjustment = "harder"
1060
+ elif score in ['poor', 'medium']:
1061
+ difficulty_adjustment = "easier"
1062
+ else:
1063
+ difficulty_adjustment = None
1064
+
1065
+ # Store for local logging
1066
+ logged_samples.append({
1067
+ "job_role": job_role,
1068
+ "seniority": seniority,
1069
+ "skills": skills,
1070
+ "context": context_data,
1071
+ "prompt": prompt,
1072
+ "generated_question": question,
1073
+ "question_evaluation": question_eval,
1074
+ "user_answer": user_answer,
1075
+ "reference_answer": ref_answer,
1076
+ "answer_evaluation": answer_eval,
1077
+ "skipped": skipped
1078
+ })
1079
+
1080
+ # Store state
1081
+ interview_state['questions'].append({
1082
+ "question": question,
1083
+ "question_evaluation": question_eval,
1084
+ "user_answer": user_answer,
1085
+ "answer_evaluation": answer_eval,
1086
+ "skipped": skipped
1087
+ })
1088
+
1089
+ interview_state['end_time'] = time()
1090
+ report = generate_llm_interview_report(interview_state, job_role, seniority)
1091
+ print("Report : _____________________\n")
1092
+ print(report)
1093
+ print('______________________________________________')
1094
+
1095
+ # Save full interview logs to JSON
1096
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1097
+ filename = f"{save_path.replace('.json', '')}_{timestamp}.json"
1098
+ with open(filename, "w", encoding="utf-8") as f:
1099
+ json.dump(logged_samples, f, indent=2, ensure_ascii=False)
1100
+
1101
+ print(f" Interview log saved to {filename}")
1102
+ print("____________________________________\n")
1103
+
1104
+ print(f"interview state : {interview_state}")
1105
+ return interview_state, report
1106
+
1107
+ from sklearn.metrics import precision_score, recall_score, f1_score
1108
+ import numpy as np
1109
+ # build ground truth for retrieving data for testing
1110
+
1111
+ def build_ground_truth(all_roles):
1112
+ gt = {}
1113
+ for role in all_roles:
1114
+ qa_list = get_role_questions(role)
1115
+ gt[role] = set(q["question"] for q in qa_list if q["question"])
1116
+ return gt
1117
+
1118
+
1119
+ def evaluate_retrieval(job_role, all_roles, k=10):
1120
+ """
1121
+ Evaluate retrieval quality using Precision@k, Recall@k, and F1@k.
1122
+ Args:
1123
+ job_role (str): The input job role to search for.
1124
+ all_roles (list): List of all available job roles in the system.
1125
+ k (int): Top-k retrieved questions to evaluate.
1126
+ Returns:
1127
+ dict: Evaluation metrics including precision, recall, and f1.
1128
+ """
1129
+
1130
+ # Step 1: Ground Truth (all exact questions stored for this role)
1131
+ ground_truth_qs = set(
1132
+ q["question"].strip()
1133
+ for q in get_role_questions(job_role)
1134
+ if q.get("question")
1135
+ )
1136
+
1137
+ if not ground_truth_qs:
1138
+ print(f"[!] No ground truth found for role: {job_role}")
1139
+ return {}
1140
+
1141
+ # Step 2: Retrieved Questions (may include fallback roles)
1142
+ retrieved_qas = retrieve_interview_data(job_role, all_roles)
1143
+ retrieved_qs = [q["question"].strip() for q in retrieved_qas if q.get("question")]
1144
+
1145
+ # Step 3: Take top-k retrieved (you can also do full if needed)
1146
+ retrieved_top_k = retrieved_qs[:k]
1147
+
1148
+ # Step 4: Binary relevance (1 if in ground truth, 0 if not)
1149
+ y_true = [1 if q in ground_truth_qs else 0 for q in retrieved_top_k]
1150
+ y_pred = [1] * len(y_true) # all retrieved are treated as predicted relevant
1151
+
1152
+ precision = precision_score(y_true, y_pred, zero_division=0)
1153
+ recall = recall_score(y_true, y_pred, zero_division=0)
1154
+ f1 = f1_score(y_true, y_pred, zero_division=0)
1155
+
1156
+ print(f" Retrieval Evaluation for role: '{job_role}' (Top-{k})")
1157
+ print(f"Precision@{k}: {precision:.2f}")
1158
+ print(f"Recall@{k}: {recall:.2f}")
1159
+ print(f"F1@{k}: {f1:.2f}")
1160
+ print(f"Relevant Retrieved: {sum(y_true)}/{len(y_true)}")
1161
+ print("–" * 40)
1162
+
1163
+ return {
1164
+ "job_role": job_role,
1165
+ "precision": precision,
1166
+ "recall": recall,
1167
+ "f1": f1,
1168
+ "relevant_retrieved": sum(y_true),
1169
+ "total_retrieved": len(y_true),
1170
+ "ground_truth_count": len(ground_truth_qs),
1171
+ }
1172
+
1173
+
1174
+ k_values = [5, 10, 20]
1175
+ all_roles = extract_all_roles_from_qdrant(collection_name="interview_questions")
1176
+
1177
+ results = []
1178
+
1179
+ for k in k_values:
1180
+ for role in all_roles:
1181
+ metrics = evaluate_retrieval(role, all_roles, k=k)
1182
+ if metrics: # only if we found ground truth
1183
+ metrics["k"] = k
1184
+ results.append(metrics)
1185
+
1186
+ import pandas as pd
1187
+
1188
+ df = pd.DataFrame(results)
1189
+ summary = df.groupby("k")[["precision", "recall", "f1"]].mean().round(3)
1190
+ print(summary)
1191
+
1192
+
1193
+ def extract_job_details(job_description):
1194
+ """Extract job details such as title, skills, experience level, and years of experience from the job description."""
1195
+ title_match = re.search(r"(?i)(?:seeking|hiring) a (.+?) to", job_description)
1196
+ job_title = title_match.group(1) if title_match else "Unknown"
1197
+
1198
+ skills_match = re.findall(r"(?i)(?:Proficiency in|Experience with|Knowledge of) (.+?)(?:,|\.| and| or)", job_description)
1199
+ skills = list(set([skill.strip() for skill in skills_match])) if skills_match else []
1200
+
1201
+ experience_match = re.search(r"(\d+)\+? years of experience", job_description)
1202
+ if experience_match:
1203
+ years_experience = int(experience_match.group(1))
1204
+ experience_level = "Senior" if years_experience >= 5 else "Mid" if years_experience >= 3 else "Junior"
1205
+ else:
1206
+ years_experience = None
1207
+ experience_level = "Unknown"
1208
+
1209
+ return {
1210
+ "job_title": job_title,
1211
+ "skills": skills,
1212
+ "experience_level": experience_level,
1213
+ "years_experience": years_experience
1214
+ }
1215
+
1216
+ import re
1217
+ from docx import Document
1218
+ import textract
1219
+ from PyPDF2 import PdfReader
1220
+
1221
+ JOB_TITLES = [
1222
+ "Accountant", "Data Scientist", "Machine Learning Engineer", "Software Engineer",
1223
+ "Developer", "Analyst", "Researcher", "Intern", "Consultant", "Manager",
1224
+ "Engineer", "Specialist", "Project Manager", "Product Manager", "Administrator",
1225
+ "Director", "Officer", "Assistant", "Coordinator", "Supervisor"
1226
+ ]
1227
+
1228
+ def clean_filename_name(filename):
1229
+ # Remove file extension
1230
+ base = re.sub(r"\.[^.]+$", "", filename)
1231
+ base = base.strip()
1232
+
1233
+ # Remove 'cv' or 'CV' words
1234
+ base_clean = re.sub(r"\bcv\b", "", base, flags=re.IGNORECASE).strip()
1235
+
1236
+ # If after removing CV it's empty, return None
1237
+ if not base_clean:
1238
+ return None
1239
+
1240
+ # If it contains any digit, return None (unreliable)
1241
+ if re.search(r"\d", base_clean):
1242
+ return None
1243
+
1244
+ # Replace underscores/dashes with spaces, capitalize
1245
+ base_clean = base_clean.replace("_", " ").replace("-", " ")
1246
+ return base_clean.title()
1247
+
1248
+ def looks_like_job_title(line):
1249
+ for title in JOB_TITLES:
1250
+ pattern = r"\b" + re.escape(title.lower()) + r"\b"
1251
+ if re.search(pattern, line.lower()):
1252
+ return True
1253
+ return False
1254
+
1255
+ def extract_name_from_text(lines):
1256
+ # Try first 3 lines for a name, skipping job titles
1257
+ for i in range(min(1, len(lines))):
1258
+ line = lines[i].strip()
1259
+ if looks_like_job_title(line):
1260
+ return "unknown"
1261
+ if re.search(r"\d", line): # skip lines with digits
1262
+ continue
1263
+ if len(line.split()) > 4 or len(line) > 40: # too long or many words
1264
+ continue
1265
+ # If line has only uppercase words, it's probably not a name
1266
+ if line.isupper():
1267
+ continue
1268
+ # Passed checks, return title-cased line as name
1269
+ return line.title()
1270
+ return None
1271
+
1272
+ def extract_text_from_file(file_path):
1273
+ if file_path.endswith('.pdf'):
1274
+ reader = PdfReader(file_path)
1275
+ text = "\n".join(page.extract_text() or '' for page in reader.pages)
1276
+ elif file_path.endswith('.docx'):
1277
+ doc = Document(file_path)
1278
+ text = "\n".join([para.text for para in doc.paragraphs])
1279
+ else: # For .doc or fallback
1280
+ text = textract.process(file_path).decode('utf-8')
1281
+ return text.strip()
1282
+
1283
+ def extract_candidate_details(file_path):
1284
+ text = extract_text_from_file(file_path)
1285
+ lines = [line.strip() for line in text.splitlines() if line.strip()]
1286
+
1287
+ # Extract name
1288
+ filename = file_path.split("/")[-1] # just filename, no path
1289
+ name = clean_filename_name(filename)
1290
+ if not name:
1291
+ name = extract_name_from_text(lines)
1292
+ if not name:
1293
+ name = "Unknown"
1294
+
1295
+ # Extract skills (basic version)
1296
+ skills = []
1297
+ skills_section = re.search(r"Skills\s*[:\-]?\s*(.+)", text, re.IGNORECASE)
1298
+ if skills_section:
1299
+ raw_skills = skills_section.group(1)
1300
+ skills = [s.strip() for s in re.split(r",|\n|•|-", raw_skills) if s.strip()]
1301
+
1302
+ return {
1303
+ "name": name,
1304
+ "skills": skills
1305
+ }
1306
+
1307
+ # import gradio as gr
1308
+ # import time
1309
+ # import tempfile
1310
+ # import numpy as np
1311
+ # import scipy.io.wavfile as wavfile
1312
+ # import os
1313
+ # import json
1314
+ # from transformers import BarkModel, AutoProcessor
1315
+ # import torch, gc
1316
+ # import whisper
1317
+ # from transformers import Wav2Vec2Processor, Wav2Vec2ForSequenceClassification
1318
+ # import librosa
1319
+
1320
+ # import torch
1321
+ # print(torch.cuda.is_available()) # ✅ Tells you if GPU is available
1322
+ # torch.cuda.empty_cache()
1323
+ # gc.collect()
1324
+
1325
+
1326
+ # # Bark TTS
1327
+ # print("🔁 Loading Bark model...")
1328
+ # model_bark = BarkModel.from_pretrained("suno/bark").to("cuda" if torch.cuda.is_available() else "cpu")
1329
+ # print("✅ Bark model loaded")
1330
+ # print("🔁 Loading Bark processor...")
1331
+ # processor_bark = AutoProcessor.from_pretrained("suno/bark")
1332
+ # print("✅ Bark processor loaded")
1333
+ # bark_voice_preset = "v2/en_speaker_5"
1334
+
1335
+ # def bark_tts(text):
1336
+ # print(f"🔁 Synthesizing TTS for: {text}")
1337
+
1338
+ # # Process the text
1339
+ # inputs = processor_bark(text, return_tensors="pt", voice_preset=bark_voice_preset)
1340
+
1341
+ # # Move tensors to device
1342
+ # input_ids = inputs["input_ids"].to(model_bark.device)
1343
+
1344
+ # start = time.time()
1345
+
1346
+ # # Generate speech with only the required parameters
1347
+ # with torch.no_grad():
1348
+ # speech_values = model_bark.generate(
1349
+ # input_ids=input_ids,
1350
+ # do_sample=True,
1351
+ # fine_temperature=0.4,
1352
+ # coarse_temperature=0.8
1353
+ # )
1354
+
1355
+ # print(f"✅ Bark finished in {round(time.time() - start, 2)}s")
1356
+
1357
+ # # Convert to audio
1358
+ # speech = speech_values.cpu().numpy().squeeze()
1359
+ # speech = (speech * 32767).astype(np.int16)
1360
+
1361
+ # temp_wav = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
1362
+ # wavfile.write(temp_wav.name, 22050, speech)
1363
+
1364
+ # return temp_wav.name
1365
+
1366
+
1367
+
1368
+
1369
+
1370
+ # # Whisper STT
1371
+ # print("🔁 Loading Whisper model...")
1372
+ # whisper_model = whisper.load_model("base", device="cuda")
1373
+ # print("✅ Whisper model loaded")
1374
+ # def whisper_stt(audio_path):
1375
+ # if not audio_path or not os.path.exists(audio_path): return ""
1376
+ # result = whisper_model.transcribe(audio_path)
1377
+ # return result["text"]
1378
+
1379
+ # seniority_mapping = {
1380
+ # "Entry-level": 1, "Junior": 2, "Mid-Level": 3, "Senior": 4, "Lead": 5
1381
+ # }
1382
+
1383
+
1384
+ # # --- 2. Gradio App ---
1385
+
1386
+ # with gr.Blocks(theme=gr.themes.Soft()) as demo:
1387
+ # user_data = gr.State({})
1388
+ # interview_state = gr.State({})
1389
+ # missing_fields_state = gr.State([])
1390
+
1391
+ # # --- UI Layout ---
1392
+ # with gr.Column(visible=True) as user_info_section:
1393
+ # gr.Markdown("## Candidate Information")
1394
+ # cv_file = gr.File(label="Upload CV")
1395
+ # job_desc = gr.Textbox(label="Job Description")
1396
+ # start_btn = gr.Button("Continue", interactive=False)
1397
+
1398
+ # with gr.Column(visible=False) as missing_section:
1399
+ # gr.Markdown("## Missing Information")
1400
+ # name_in = gr.Textbox(label="Name", visible=False)
1401
+ # role_in = gr.Textbox(label="Job Role", visible=False)
1402
+ # seniority_in = gr.Dropdown(list(seniority_mapping.keys()), label="Seniority", visible=False)
1403
+ # skills_in = gr.Textbox(label="Skills", visible=False)
1404
+ # submit_btn = gr.Button("Submit", interactive=False)
1405
+
1406
+ # with gr.Column(visible=False) as interview_pre_section:
1407
+ # pre_interview_greeting_md = gr.Markdown()
1408
+ # start_interview_final_btn = gr.Button("Start Interview")
1409
+
1410
+ # with gr.Column(visible=False) as interview_section:
1411
+ # gr.Markdown("## Interview in Progress")
1412
+ # question_audio = gr.Audio(label="Listen", interactive=False, autoplay=True)
1413
+ # question_text = gr.Markdown()
1414
+ # user_audio_input = gr.Audio(sources=["microphone"], type="filepath", label="1. Record Audio Answer")
1415
+ # stt_transcript = gr.Textbox(label="Transcribed Answer (edit if needed)")
1416
+ # confirm_btn = gr.Button("Confirm Answer")
1417
+ # evaluation_display = gr.Markdown()
1418
+ # interview_summary = gr.Markdown(visible=False)
1419
+
1420
+ # # --- UI Logic ---
1421
+
1422
+ # def validate_start_btn(cv_file, job_desc):
1423
+ # return gr.update(interactive=(cv_file is not None and hasattr(cv_file, "name") and bool(job_desc and job_desc.strip())))
1424
+ # cv_file.change(validate_start_btn, [cv_file, job_desc], start_btn)
1425
+ # job_desc.change(validate_start_btn, [cv_file, job_desc], start_btn)
1426
+
1427
+ # def process_and_route_initial(cv_file, job_desc):
1428
+ # details = extract_candidate_details(cv_file.name)
1429
+ # job_info = extract_job_details(job_desc)
1430
+ # data = {
1431
+ # "name": details.get("name", "unknown"), "job_role": job_info.get("job_title", "unknown"),
1432
+ # "seniority": job_info.get("experience_level", "unknown"), "skills": job_info.get("skills", [])
1433
+ # }
1434
+ # missing = [k for k, v in data.items() if (isinstance(v, str) and v.lower() == "unknown") or not v]
1435
+ # if missing:
1436
+ # return data, missing, gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
1437
+ # else:
1438
+ # greeting = f"Hello {data['name']}, your profile is ready. Click 'Start Interview' when ready."
1439
+ # return data, missing, gr.update(visible=False), gr.update(visible=False), gr.update(visible=True, value=greeting)
1440
+ # start_btn.click(
1441
+ # process_and_route_initial,
1442
+ # [cv_file, job_desc],
1443
+ # [user_data, missing_fields_state, user_info_section, missing_section, pre_interview_greeting_md]
1444
+ # )
1445
+
1446
+ # def show_missing(missing):
1447
+ # if missing is None: missing = []
1448
+ # return gr.update(visible="name" in missing), gr.update(visible="job_role" in missing), gr.update(visible="seniority" in missing), gr.update(visible="skills" in missing)
1449
+ # missing_fields_state.change(show_missing, missing_fields_state, [name_in, role_in, seniority_in, skills_in])
1450
+
1451
+ # def validate_fields(name, role, seniority, skills, missing):
1452
+ # if not missing: return gr.update(interactive=False)
1453
+ # all_filled = all([(not ("name" in missing) or bool(name.strip())), (not ("job_role" in missing) or bool(role.strip())), (not ("seniority" in missing) or bool(seniority)), (not ("skills" in missing) or bool(skills.strip())),])
1454
+ # return gr.update(interactive=all_filled)
1455
+ # for inp in [name_in, role_in, seniority_in, skills_in]:
1456
+ # inp.change(validate_fields, [name_in, role_in, seniority_in, skills_in, missing_fields_state], submit_btn)
1457
+
1458
+ # def complete_manual(data, name, role, seniority, skills):
1459
+ # if data["name"].lower() == "unknown": data["name"] = name
1460
+ # if data["job_role"].lower() == "unknown": data["job_role"] = role
1461
+ # if data["seniority"].lower() == "unknown": data["seniority"] = seniority
1462
+ # if not data["skills"]: data["skills"] = [s.strip() for s in skills.split(",")]
1463
+ # greeting = f"Hello {data['name']}, your profile is ready. Click 'Start Interview' to begin."
1464
+ # return data, gr.update(visible=False), gr.update(visible=True), gr.update(value=greeting)
1465
+ # submit_btn.click(complete_manual, [user_data, name_in, role_in, seniority_in, skills_in], [user_data, missing_section, interview_pre_section, pre_interview_greeting_md])
1466
+
1467
+ # def start_interview(data):
1468
+ # # --- Advanced state with full logging ---
1469
+ # state = {
1470
+ # "questions": [], "answers": [], "face_labels": [], "voice_labels": [], "timings": [],
1471
+ # "question_evaluations": [], "answer_evaluations": [], "effective_confidences": [],
1472
+ # "conversation_history": [],
1473
+ # "difficulty_adjustment": None,
1474
+ # "question_idx": 0, "max_questions": 3, "q_start_time": time.time(),
1475
+ # "log": []
1476
+ # }
1477
+ # # --- Optionally: context retrieval here (currently just blank) ---
1478
+ # context = ""
1479
+ # prompt = build_interview_prompt(
1480
+ # conversation_history=[], user_response="", context=context, job_role=data["job_role"],
1481
+ # skills=data["skills"], seniority=data["seniority"], difficulty_adjustment=None,
1482
+ # voice_label="neutral", face_label="neutral"
1483
+ # )
1484
+ # #here the original one
1485
+ # # first_q = groq_llm.predict(prompt)
1486
+ # # # Evaluate Q for quality
1487
+ # # q_eval = eval_question_quality(first_q, data["job_role"], data["seniority"], None)
1488
+ # # state["questions"].append(first_q)
1489
+ # # state["question_evaluations"].append(q_eval)
1490
+
1491
+ # #here the testing one
1492
+ # first_q = groq_llm.predict(prompt)
1493
+ # q_eval = {
1494
+ # "Score": "N/A",
1495
+ # "Reasoning": "Skipped to reduce processing time",
1496
+ # "Improvements": []
1497
+ # }
1498
+ # state["questions"].append(first_q)
1499
+ # state["question_evaluations"].append(q_eval)
1500
+
1501
+
1502
+ # state["conversation_history"].append({'role': 'Interviewer', 'content': first_q})
1503
+ # start = time.perf_counter()
1504
+ # audio_path = bark_tts(first_q)
1505
+ # print("⏱️ Bark TTS took", time.perf_counter() - start, "seconds")
1506
+
1507
+ # # LOG
1508
+ # state["log"].append({"type": "question", "question": first_q, "question_eval": q_eval, "timestamp": time.time()})
1509
+ # return state, gr.update(visible=False), gr.update(visible=True), audio_path, f"*Question 1:* {first_q}"
1510
+ # start_interview_final_btn.click(start_interview, [user_data], [interview_state, interview_pre_section, interview_section, question_audio, question_text])
1511
+
1512
+ # def transcribe(audio_path):
1513
+ # return whisper_stt(audio_path)
1514
+ # user_audio_input.change(transcribe, user_audio_input, stt_transcript)
1515
+
1516
+ # def process_answer(transcript, audio_path, state, data):
1517
+ # if not transcript:
1518
+ # return state, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
1519
+
1520
+ # elapsed = round(time.time() - state.get("q_start_time", time.time()), 2)
1521
+ # state["timings"].append(elapsed)
1522
+ # state["answers"].append(transcript)
1523
+ # state["conversation_history"].append({'role': 'Candidate', 'content': transcript})
1524
+
1525
+ # # --- 1. Emotion analysis (simplified for testing) ---
1526
+ # voice_label = "neutral"
1527
+ # face_label = "neutral"
1528
+ # state["voice_labels"].append(voice_label)
1529
+ # state["face_labels"].append(face_label)
1530
+
1531
+ # # --- 2. Evaluate previous Q and Answer ---
1532
+ # last_q = state["questions"][-1]
1533
+ # q_eval = state["question_evaluations"][-1] # Already in state
1534
+ # ref_answer = generate_reference_answer(last_q, data["job_role"], data["seniority"])
1535
+ # answer_eval = evaluate_answer(last_q, transcript, ref_answer, data["job_role"], data["seniority"], None)
1536
+ # state["answer_evaluations"].append(answer_eval)
1537
+ # answer_score = answer_eval.get("Score", "medium") if answer_eval else "medium"
1538
+
1539
+ # # --- 3. Adaptive difficulty ---
1540
+ # if answer_score == "excellent":
1541
+ # state["difficulty_adjustment"] = "harder"
1542
+ # elif answer_score in ("medium", "poor"):
1543
+ # state["difficulty_adjustment"] = "easier"
1544
+ # else:
1545
+ # state["difficulty_adjustment"] = None
1546
+
1547
+ # # --- 4. Effective confidence (simplified) ---
1548
+ # eff_conf = {"effective_confidence": 0.6}
1549
+ # state["effective_confidences"].append(eff_conf)
1550
+
1551
+ # # --- LOG ---
1552
+ # state["log"].append({
1553
+ # "type": "answer",
1554
+ # "question": last_q,
1555
+ # "answer": transcript,
1556
+ # "answer_eval": answer_eval,
1557
+ # "ref_answer": ref_answer,
1558
+ # "face_label": face_label,
1559
+ # "voice_label": voice_label,
1560
+ # "effective_confidence": eff_conf,
1561
+ # "timing": elapsed,
1562
+ # "timestamp": time.time()
1563
+ # })
1564
+
1565
+ # # --- Next or End ---
1566
+ # qidx = state["question_idx"] + 1
1567
+ # if qidx >= state["max_questions"]:
1568
+ # # Save as JSON (optionally)
1569
+ # timestamp = time.strftime("%Y%m%d_%H%M%S")
1570
+ # log_file = f"interview_log_{timestamp}.json"
1571
+ # with open(log_file, "w", encoding="utf-8") as f:
1572
+ # json.dump(state["log"], f, indent=2, ensure_ascii=False)
1573
+ # # Report
1574
+ # summary = "# Interview Summary\n"
1575
+ # for i, q in enumerate(state["questions"]):
1576
+ # summary += (f"\n### Q{i + 1}: {q}\n"
1577
+ # f"- *Answer*: {state['answers'][i]}\n"
1578
+ # f"- *Q Eval*: {state['question_evaluations'][i]}\n"
1579
+ # f"- *A Eval*: {state['answer_evaluations'][i]}\n"
1580
+ # f"- *Time*: {state['timings'][i]}s\n")
1581
+ # summary += f"\n\n⏺ Full log saved as {log_file}."
1582
+ # return (state, gr.update(visible=True, value=summary), gr.update(value=None), gr.update(value=None), gr.update(value=None), gr.update(value=None), gr.update(visible=True, value=f"Last Detected — Face: {face_label}, Voice: {voice_label}"))
1583
+ # else:
1584
+ # # --- Build next prompt using adaptive difficulty ---
1585
+ # state["question_idx"] = qidx
1586
+ # state["q_start_time"] = time.time()
1587
+ # context = "" # You can add your context logic here
1588
+ # prompt = build_interview_prompt(
1589
+ # conversation_history=state["conversation_history"],
1590
+ # user_response=transcript,
1591
+ # context=context,
1592
+ # job_role=data["job_role"],
1593
+ # skills=data["skills"],
1594
+ # seniority=data["seniority"],
1595
+ # difficulty_adjustment=state["difficulty_adjustment"],
1596
+ # voice_label=voice_label,
1597
+ # )
1598
+ # next_q = groq_llm.predict(prompt)
1599
+ # # Evaluate Q quality
1600
+ # q_eval = eval_question_quality(next_q, data["job_role"], data["seniority"], None)
1601
+ # state["questions"].append(next_q)
1602
+ # state["question_evaluations"].append(q_eval)
1603
+ # state["conversation_history"].append({'role': 'Interviewer', 'content': next_q})
1604
+ # state["log"].append({"type": "question", "question": next_q, "question_eval": q_eval, "timestamp": time.time()})
1605
+ # audio_path = bark_tts(next_q)
1606
+ # # Display evaluations
1607
+ # eval_md = f"*Last Answer Eval:* {answer_eval}\n\n*Effective Confidence:* {eff_conf}"
1608
+ # return (
1609
+ # state, gr.update(visible=False), audio_path, f"*Question {qidx + 1}:* {next_q}",
1610
+ # gr.update(value=None), gr.update(value=None),
1611
+ # gr.update(visible=True, value=eval_md),
1612
+ # )
1613
+ # # Replace your confirm_btn.click with this:
1614
+ # confirm_btn.click(
1615
+ # process_answer,
1616
+ # [stt_transcript, user_audio_input, interview_state, user_data], # Added None for video_path
1617
+ # [interview_state, interview_summary, question_audio, question_text, user_audio_input, stt_transcript, evaluation_display]
1618
+ # ).then(
1619
+ # lambda: (gr.update(value=None), gr.update(value=None)), None, [user_audio_input, stt_transcript]
1620
+ # )
1621
+
1622
+ # demo.launch(debug=True)
1623
+ import gradio as gr
1624
+ import time
1625
+ import tempfile
1626
+ import numpy as np
1627
+ import scipy.io.wavfile as wavfile
1628
+ import os
1629
+ import json
1630
+ import edge_tts
1631
+ import torch, gc
1632
+ from faster_whisper import WhisperModel
1633
+ import asyncio
1634
+ import threading
1635
+ from concurrent.futures import ThreadPoolExecutor
1636
+
1637
+ print(torch.cuda.is_available())
1638
+ torch.cuda.empty_cache()
1639
+ gc.collect()
1640
+
1641
+ # Global variables for lazy loading
1642
+ faster_whisper_model = None
1643
+ tts_voice = "en-US-AriaNeural"
1644
+
1645
+
1646
+ # Thread pool for async operations
1647
+ executor = ThreadPoolExecutor(max_workers=2)
1648
+
1649
+ # Add after your imports
1650
+ if torch.cuda.is_available():
1651
+ print(f"🔥 CUDA Available: {torch.cuda.get_device_name(0)}")
1652
+ print(f"🔥 CUDA Memory: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f} GB")
1653
+ # Set default device
1654
+ torch.cuda.set_device(0)
1655
+ else:
1656
+ print("⚠️ CUDA not available, using CPU")
1657
+
1658
+ def load_models_lazy():
1659
+ """Load models only when needed"""
1660
+ global faster_whisper_model
1661
+
1662
+ device = "cuda" if torch.cuda.is_available() else "cpu"
1663
+ print(f"🔁 Using device: {device}")
1664
+
1665
+ if faster_whisper_model is None:
1666
+ print("🔁 Loading Faster-Whisper model...")
1667
+ compute_type = "float16" if device == "cuda" else "int8"
1668
+ faster_whisper_model = WhisperModel("base", device=device, compute_type=compute_type)
1669
+ print(f"✅ Faster-Whisper model loaded on {device}")
1670
+
1671
+
1672
+ async def edge_tts_to_file(text, output_path="tts.wav", voice=tts_voice):
1673
+ communicate = edge_tts.Communicate(text, voice)
1674
+ await communicate.save(output_path)
1675
+ return output_path
1676
+
1677
+ def tts_async(text):
1678
+ loop = asyncio.new_event_loop()
1679
+ asyncio.set_event_loop(loop)
1680
+ return executor.submit(loop.run_until_complete, edge_tts_to_file(text))
1681
+
1682
+
1683
+
1684
+
1685
+ def whisper_stt(audio_path):
1686
+ """STT using Faster-Whisper"""
1687
+ if not audio_path or not os.path.exists(audio_path):
1688
+ return ""
1689
+
1690
+ load_models_lazy()
1691
+ print("🔁 Transcribing with Faster-Whisper")
1692
+
1693
+ segments, _ = faster_whisper_model.transcribe(audio_path)
1694
+ transcript = " ".join(segment.text for segment in segments)
1695
+ return transcript.strip()
1696
+
1697
+
1698
+ seniority_mapping = {
1699
+ "Entry-level": 1, "Junior": 2, "Mid-Level": 3, "Senior": 4, "Lead": 5
1700
+ }
1701
+
1702
+ with gr.Blocks(theme=gr.themes.Soft()) as demo:
1703
+ user_data = gr.State({})
1704
+ interview_state = gr.State({})
1705
+ missing_fields_state = gr.State([])
1706
+ tts_future = gr.State(None) # Store async TTS future
1707
+
1708
+ with gr.Column(visible=True) as user_info_section:
1709
+ gr.Markdown("## Candidate Information")
1710
+ cv_file = gr.File(label="Upload CV")
1711
+ job_desc = gr.Textbox(label="Job Description")
1712
+ start_btn = gr.Button("Continue", interactive=False)
1713
+
1714
+ with gr.Column(visible=False) as missing_section:
1715
+ gr.Markdown("## Missing Information")
1716
+ name_in = gr.Textbox(label="Name", visible=False)
1717
+ role_in = gr.Textbox(label="Job Role", visible=False)
1718
+ seniority_in = gr.Dropdown(list(seniority_mapping.keys()), label="Seniority", visible=False)
1719
+ skills_in = gr.Textbox(label="Skills", visible=False)
1720
+ submit_btn = gr.Button("Submit", interactive=False)
1721
+
1722
+ with gr.Column(visible=False) as interview_pre_section:
1723
+ pre_interview_greeting_md = gr.Markdown()
1724
+ start_interview_final_btn = gr.Button("Start Interview")
1725
+ loading_status = gr.Markdown("", visible=False)
1726
+
1727
+ with gr.Column(visible=False) as interview_section:
1728
+ gr.Markdown("## Interview in Progress")
1729
+ question_audio = gr.Audio(label="Listen", interactive=False, autoplay=True)
1730
+ question_text = gr.Markdown()
1731
+ user_audio_input = gr.Audio(sources=["microphone"], type="filepath", label="1. Record Audio Answer")
1732
+ stt_transcript = gr.Textbox(label="Transcribed Answer (edit if needed)")
1733
+ confirm_btn = gr.Button("Confirm Answer")
1734
+ evaluation_display = gr.Markdown()
1735
+ interview_summary = gr.Markdown(visible=False)
1736
+
1737
+ def validate_start_btn(cv_file, job_desc):
1738
+ return gr.update(interactive=(cv_file is not None and hasattr(cv_file, "name") and bool(job_desc and job_desc.strip())))
1739
+
1740
+ cv_file.change(validate_start_btn, [cv_file, job_desc], start_btn)
1741
+ job_desc.change(validate_start_btn, [cv_file, job_desc], start_btn)
1742
+
1743
+ def process_and_route_initial(cv_file, job_desc):
1744
+ details = extract_candidate_details(cv_file.name)
1745
+ job_info = extract_job_details(job_desc)
1746
+ data = {
1747
+ "name": details.get("name", "unknown"),
1748
+ "job_role": job_info.get("job_title", "unknown"),
1749
+ "seniority": job_info.get("experience_level", "unknown"),
1750
+ "skills": job_info.get("skills", [])
1751
+ }
1752
+ missing = [k for k, v in data.items() if (isinstance(v, str) and v.lower() == "unknown") or not v]
1753
+ if missing:
1754
+ return data, missing, gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
1755
+ else:
1756
+ greeting = f"Hello {data['name']}, your profile is ready. Click 'Start Interview' when ready."
1757
+ return data, missing, gr.update(visible=False), gr.update(visible=False), gr.update(visible=True, value=greeting)
1758
+
1759
+ start_btn.click(process_and_route_initial, [cv_file, job_desc], [user_data, missing_fields_state, user_info_section, missing_section, pre_interview_greeting_md])
1760
+
1761
+ def show_missing(missing):
1762
+ if missing is None: missing = []
1763
+ return gr.update(visible="name" in missing), gr.update(visible="job_role" in missing), gr.update(visible="seniority" in missing), gr.update(visible="skills" in missing)
1764
+
1765
+ missing_fields_state.change(show_missing, missing_fields_state, [name_in, role_in, seniority_in, skills_in])
1766
+
1767
+ def validate_fields(name, role, seniority, skills, missing):
1768
+ if not missing: return gr.update(interactive=False)
1769
+ all_filled = all([(not ("name" in missing) or bool(name.strip())), (not ("job_role" in missing) or bool(role.strip())), (not ("seniority" in missing) or bool(seniority)), (not ("skills" in missing) or bool(skills.strip()))])
1770
+ return gr.update(interactive=all_filled)
1771
+
1772
+ for inp in [name_in, role_in, seniority_in, skills_in]:
1773
+ inp.change(validate_fields, [name_in, role_in, seniority_in, skills_in, missing_fields_state], submit_btn)
1774
+
1775
+ def complete_manual(data, name, role, seniority, skills):
1776
+ if data["name"].lower() == "unknown": data["name"] = name
1777
+ if data["job_role"].lower() == "unknown": data["job_role"] = role
1778
+ if data["seniority"].lower() == "unknown": data["seniority"] = seniority
1779
+ if not data["skills"]: data["skills"] = [s.strip() for s in skills.split(",")]
1780
+ greeting = f"Hello {data['name']}, your profile is ready. Click 'Start Interview' to begin."
1781
+ return data, gr.update(visible=False), gr.update(visible=True), gr.update(value=greeting)
1782
+
1783
+ submit_btn.click(complete_manual, [user_data, name_in, role_in, seniority_in, skills_in], [user_data, missing_section, interview_pre_section, pre_interview_greeting_md])
1784
+
1785
+ async def start_interview(data):
1786
+ # Initialize interview state
1787
+ state = {
1788
+ "questions": [],
1789
+ "answers": [],
1790
+ "timings": [],
1791
+ "question_evaluations": [],
1792
+ "answer_evaluations": [],
1793
+ "conversation_history": [],
1794
+ "difficulty_adjustment": None,
1795
+ "question_idx": 0,
1796
+ "max_questions": 3,
1797
+ "q_start_time": time.time(),
1798
+ "log": []
1799
+ }
1800
+
1801
+ # Build prompt for first question
1802
+ context = ""
1803
+ prompt = build_interview_prompt(
1804
+ conversation_history=[],
1805
+ user_response="",
1806
+ context=context,
1807
+ job_role=data["job_role"],
1808
+ skills=data["skills"],
1809
+ seniority=data["seniority"],
1810
+ difficulty_adjustment=None,
1811
+ voice_label="neutral"
1812
+ )
1813
+
1814
+ # Generate first question
1815
+ start = time.time()
1816
+ first_q = groq_llm.predict(prompt)
1817
+ print("⏱️ Groq LLM Response Time:", round(time.time() - start, 2), "seconds")
1818
+ q_eval = {
1819
+ "Score": "N/A",
1820
+ "Reasoning": "Skipped to reduce processing time",
1821
+ "Improvements": []
1822
+ }
1823
+
1824
+ state["questions"].append(first_q)
1825
+ state["question_evaluations"].append(q_eval)
1826
+ state["conversation_history"].append({'role': 'Interviewer', 'content': first_q})
1827
+
1828
+ # Generate audio with Bark (wait for it)
1829
+ start = time.perf_counter()
1830
+ cleaned_text = first_q.strip().replace("\n", " ")
1831
+ audio_path = await edge_tts_to_file(first_q)
1832
+ print("⏱️ TTS (edge-tts) took", round(time.perf_counter() - start, 2), "seconds")
1833
+
1834
+ # Log question
1835
+ state["log"].append({
1836
+ "type": "question",
1837
+ "question": first_q,
1838
+ "question_eval": q_eval,
1839
+ "timestamp": time.time()
1840
+ })
1841
+
1842
+ return (
1843
+ state,
1844
+ gr.update(visible=False), # Hide interview_pre_section
1845
+ gr.update(visible=True), # Show interview_section
1846
+ audio_path, # Set audio
1847
+ f"*Question 1:* {first_q}" # Set question text
1848
+ )
1849
+
1850
+ # Hook into Gradio
1851
+ start_interview_final_btn.click(
1852
+ fn=start_interview,
1853
+ inputs=[user_data],
1854
+ outputs=[interview_state, interview_pre_section, interview_section, question_audio, question_text],
1855
+ concurrency_limit=1
1856
+ )
1857
+
1858
+
1859
+ def transcribe(audio_path):
1860
+ return whisper_stt(audio_path)
1861
+
1862
+ user_audio_input.change(transcribe, user_audio_input, stt_transcript)
1863
+
1864
+ async def process_answer(transcript, audio_path, state, data):
1865
+ start = time.time()
1866
+ if not transcript:
1867
+ return state, gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update()
1868
+
1869
+ elapsed = round(time.time() - state.get("q_start_time", time.time()), 2)
1870
+ state["timings"].append(elapsed)
1871
+ state["answers"].append(transcript)
1872
+ state["conversation_history"].append({'role': 'Candidate', 'content': transcript})
1873
+
1874
+ last_q = state["questions"][-1]
1875
+ q_eval = state["question_evaluations"][-1]
1876
+ ref_answer = generate_reference_answer(last_q, data["job_role"], data["seniority"])
1877
+ answer_eval = await asyncio.get_event_loop().run_in_executor(
1878
+ executor,
1879
+ evaluate_answer,
1880
+ last_q, transcript, ref_answer, data["job_role"], data["seniority"]
1881
+ )
1882
+
1883
+ state["answer_evaluations"].append(answer_eval)
1884
+ answer_score = answer_eval.get("Score", "medium") if answer_eval else "medium"
1885
+
1886
+ if answer_score == "excellent":
1887
+ state["difficulty_adjustment"] = "harder"
1888
+ elif answer_score in ("medium", "poor"):
1889
+ state["difficulty_adjustment"] = "easier"
1890
+ else:
1891
+ state["difficulty_adjustment"] = None
1892
+
1893
+ state["log"].append({
1894
+ "type": "answer", "question": last_q, "answer": transcript,
1895
+ "answer_eval": answer_eval, "ref_answer": ref_answer,
1896
+ "timing": elapsed, "timestamp": time.time()
1897
+ })
1898
+
1899
+ qidx = state["question_idx"] + 1
1900
+ if qidx >= state["max_questions"]:
1901
+ timestamp = time.strftime("%Y%m%d_%H%M%S")
1902
+ log_file = f"interview_log_{timestamp}.json"
1903
+ with open(log_file, "w", encoding="utf-8") as f:
1904
+ json.dump(state["log"], f, indent=2, ensure_ascii=False)
1905
+ summary = "# Interview Summary\n"
1906
+ for i, q in enumerate(state["questions"]):
1907
+ summary += (f"\n### Q{i + 1}: {q}\n"
1908
+ f"- *Answer*: {state['answers'][i]}\n"
1909
+ f"- *Q Eval*: {state['question_evaluations'][i]}\n"
1910
+ f"- *A Eval*: {state['answer_evaluations'][i]}\n"
1911
+ f"- *Time*: {state['timings'][i]}s\n")
1912
+ summary += f"\n\n⏺ Full log saved as {log_file}."
1913
+ return state, gr.update(visible=True, value=summary), gr.update(value=None), gr.update(value=None), gr.update(value=None), gr.update(value=None), gr.update(visible=False)
1914
+ else:
1915
+ state["question_idx"] = qidx
1916
+ state["q_start_time"] = time.time()
1917
+ context = ""
1918
+ prompt = build_interview_prompt(
1919
+ conversation_history=state["conversation_history"],
1920
+ user_response=transcript, context=context,
1921
+ job_role=data["job_role"], skills=data["skills"],
1922
+ seniority=data["seniority"], difficulty_adjustment=state["difficulty_adjustment"],
1923
+ voice_label="neutral"
1924
+ )
1925
+ start = time.time()
1926
+ next_q = groq_llm.predict(prompt)
1927
+ print("⏱️ Groq LLM Response Time:", round(time.time() - start, 2), "seconds")
1928
+ start = time.time()
1929
+ q_eval_future = executor.submit(
1930
+ eval_question_quality,
1931
+ next_q, data["job_role"], data["seniority"]
1932
+ )
1933
+ q_eval = q_eval_future.result()
1934
+ print("⏱️ Evaluation time:", round(time.time() - start, 2), "seconds")
1935
+ state["questions"].append(next_q)
1936
+ state["question_evaluations"].append(q_eval)
1937
+ state["conversation_history"].append({'role': 'Interviewer', 'content': next_q})
1938
+ state["log"].append({"type": "question", "question": next_q, "question_eval": q_eval, "timestamp": time.time()})
1939
+
1940
+ audio_path = await edge_tts_to_file(next_q)
1941
+
1942
+
1943
+ eval_md = f"*Last Answer Eval:* {answer_eval}"
1944
+ print("✅ process_answer time:", round(time.time() - start, 2), "s")
1945
+ return state, gr.update(visible=False), audio_path, f"*Question {qidx + 1}:* {next_q}", gr.update(value=None), gr.update(value=None), gr.update(visible=True, value=eval_md)
1946
+
1947
+
1948
+ confirm_btn.click(
1949
+ fn=process_answer,
1950
+ inputs=[stt_transcript, user_audio_input, interview_state, user_data],
1951
+ outputs=[interview_state, interview_summary, question_audio, question_text, user_audio_input, stt_transcript,
1952
+ evaluation_display],
1953
+ concurrency_limit=1
1954
+ ).then(
1955
+ lambda: (gr.update(value=None), gr.update(value=None)), None, [user_audio_input, stt_transcript]
1956
+ )
1957
+
1958
+ demo.launch(debug=True)
backend/templates/apply.html CHANGED
@@ -24,87 +24,62 @@
24
 
25
  <div class="card">
26
  <div class="card-header">
27
- <h2>Complete Your Application</h2>
28
- <p>Upload your resume to auto-fill fields or complete the form manually</p>
29
  </div>
30
 
31
  <div class="card-body">
 
32
  <form method="POST" enctype="multipart/form-data">
33
- {{ form.hidden_tag() }}
34
- {{ form.job_id }}
35
-
36
  <div class="form-group">
37
  <label for="resume">Upload Resume</label>
38
- {{ form.resume(class="form-control", id="resume", required=True) }}
39
- </div>
40
-
41
- <div class="form-group">
42
- {{ form.name.label }}
43
- {{ form.name(class="form-control", id="name", placeholder="Enter your full name") }}
44
  </div>
45
 
46
- <div class="form-group">
47
- {{ form.email.label }}
48
- {{ form.email(class="form-control", id="email", placeholder="Enter your email address") }}
49
- </div>
50
-
51
- <div class="form-group">
52
- <label for="phone">Phone Number</label>
53
- <input type="text" id="phone" class="form-control" placeholder="Enter your phone number">
54
- </div>
55
-
56
- <div class="form-group">
57
- <label for="skills">Skills</label>
58
- <textarea id="skills" class="form-control"
59
- placeholder="Skills extracted from your resume..."></textarea>
60
- </div>
61
-
62
- <div class="form-group">
63
- <label for="experience">Previous Experience</label>
64
- <textarea id="experience" class="form-control"
65
- placeholder="Experience extracted from your resume..."></textarea>
66
- </div>
67
-
68
- <div class="form-group">
69
- {{ form.cover_letter.label }}
70
- {{ form.cover_letter(class="form-control", placeholder="Enter your cover letter here...") }}
71
- </div>
72
-
73
- <div style="display: flex; justify-content: space-between; align-items: center; margin-top: 2rem;">
74
- <a href="{{ url_for('job_detail', job_id=job.id) }}" class="btn btn-outline">Cancel</a>
75
- {{ form.submit(class="btn btn-primary") }}
76
  </div>
77
  </form>
78
 
 
 
 
79
  </div>
80
  </div>
81
  </section>
82
 
83
- <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
84
- <script>
85
- $(document).ready(function () {
86
- $("#resume").on("change", function () {
87
- var formData = new FormData();
88
- formData.append("resume", $("#resume")[0].files[0]);
89
-
90
- $.ajax({
91
- url: "{{ url_for('parse_resume') }}",
92
- type: "POST",
93
- data: formData,
94
- processData: false,
95
- contentType: false,
96
- success: function (data) {
97
- $("#name").val(data.name);
98
- $("#email").val(data.email);
99
- $("#phone").val(data.mobile_number);
100
- $("#skills").val(data.skills.join(", "));
101
- $("#experience").val(data.experience.join("\n"));
102
- },
103
- error: function () {
104
- alert("Error parsing the resume. Please try again.");
105
- }
106
- });
107
- });
108
- });
109
- </script>
110
- {% endblock %}
 
 
 
 
 
 
 
 
24
 
25
  <div class="card">
26
  <div class="card-header">
27
+ <h2>Submit Your Application</h2>
28
+ <p>Please upload your resume (PDF, DOCX). Our system will extract your info automatically.</p>
29
  </div>
30
 
31
  <div class="card-body">
32
+ <!-- Application Form -->
33
  <form method="POST" enctype="multipart/form-data">
 
 
 
34
  <div class="form-group">
35
  <label for="resume">Upload Resume</label>
36
+ <input type="file" name="resume" id="resume" class="form-control" required accept=".pdf,.doc,.docx">
 
 
 
 
 
37
  </div>
38
 
39
+ <div class="application-actions" style="margin-top: 2rem;">
40
+ <button type="submit" class="btn btn-primary">Submit Application</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  </div>
42
  </form>
43
 
44
+ <div style="margin-top: 1.5rem; text-align: center;">
45
+ <a href="{{ url_for('job_detail', job_id=job.id) }}" class="btn btn-outline">Back to Job Details</a>
46
+ </div>
47
  </div>
48
  </div>
49
  </section>
50
 
51
+ <style>
52
+ .form-group label {
53
+ font-weight: 600;
54
+ color: var(--primary);
55
+ margin-bottom: 0.5rem;
56
+ display: block;
57
+ }
58
+
59
+ .form-control {
60
+ width: 100%;
61
+ padding: 0.75rem;
62
+ font-size: 1rem;
63
+ border-radius: 6px;
64
+ border: 1px solid #ccc;
65
+ }
66
+
67
+ .application-actions {
68
+ text-align: center;
69
+ }
70
+
71
+ .btn-primary {
72
+ background: linear-gradient(135deg, var(--primary), var(--secondary));
73
+ color: white;
74
+ padding: 0.75rem 1.5rem;
75
+ font-weight: 500;
76
+ border: none;
77
+ border-radius: 6px;
78
+ cursor: pointer;
79
+ }
80
+
81
+ .btn-primary:hover {
82
+ opacity: 0.9;
83
+ }
84
+ </style>
85
+ {% endblock %}
backend/templates/interview.html ADDED
@@ -0,0 +1,833 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AI Interview - Interactive Session</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ display: flex;
19
+ justify-content: center;
20
+ align-items: center;
21
+ padding: 20px;
22
+ }
23
+
24
+ .interview-container {
25
+ background: white;
26
+ border-radius: 20px;
27
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
28
+ width: 100%;
29
+ max-width: 800px;
30
+ min-height: 600px;
31
+ display: flex;
32
+ flex-direction: column;
33
+ overflow: hidden;
34
+ }
35
+
36
+ .header {
37
+ background: linear-gradient(45deg, #4CAF50, #45a049);
38
+ color: white;
39
+ padding: 20px;
40
+ text-align: center;
41
+ position: relative;
42
+ }
43
+
44
+ .header h1 {
45
+ font-size: 1.8rem;
46
+ margin-bottom: 5px;
47
+ }
48
+
49
+ .header p {
50
+ opacity: 0.9;
51
+ font-size: 0.9rem;
52
+ }
53
+
54
+ .chat-area {
55
+ flex: 1;
56
+ padding: 20px;
57
+ display: flex;
58
+ flex-direction: column;
59
+ gap: 20px;
60
+ max-height: 400px;
61
+ overflow-y: auto;
62
+ }
63
+
64
+ .ai-message {
65
+ display: flex;
66
+ align-items: flex-start;
67
+ gap: 15px;
68
+ animation: slideIn 0.5s ease-out;
69
+ }
70
+
71
+ .ai-avatar {
72
+ width: 60px;
73
+ height: 60px;
74
+ border-radius: 50%;
75
+ background: linear-gradient(45deg, #667eea, #764ba2);
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ color: white;
80
+ font-weight: bold;
81
+ font-size: 1.2rem;
82
+ position: relative;
83
+ flex-shrink: 0;
84
+ }
85
+
86
+ .ai-avatar.talking {
87
+ animation: pulse 1.5s infinite;
88
+ }
89
+
90
+ .ai-avatar.talking::before {
91
+ content: '';
92
+ position: absolute;
93
+ width: 100%;
94
+ height: 100%;
95
+ border-radius: 50%;
96
+ background: rgba(102, 126, 234, 0.3);
97
+ animation: ripple 1.5s infinite;
98
+ }
99
+
100
+ .message-bubble {
101
+ background: #f8f9fa;
102
+ border-radius: 15px;
103
+ padding: 15px 20px;
104
+ max-width: 70%;
105
+ position: relative;
106
+ }
107
+
108
+ .message-bubble::before {
109
+ content: '';
110
+ position: absolute;
111
+ left: -10px;
112
+ top: 15px;
113
+ width: 0;
114
+ height: 0;
115
+ border-top: 10px solid transparent;
116
+ border-bottom: 10px solid transparent;
117
+ border-right: 10px solid #f8f9fa;
118
+ }
119
+
120
+ .user-message {
121
+ display: flex;
122
+ justify-content: flex-end;
123
+ animation: slideIn 0.5s ease-out;
124
+ }
125
+
126
+ .user-bubble {
127
+ background: linear-gradient(45deg, #4CAF50, #45a049);
128
+ color: white;
129
+ border-radius: 15px;
130
+ padding: 15px 20px;
131
+ max-width: 70%;
132
+ position: relative;
133
+ }
134
+
135
+ .user-bubble::before {
136
+ content: '';
137
+ position: absolute;
138
+ right: -10px;
139
+ top: 15px;
140
+ width: 0;
141
+ height: 0;
142
+ border-top: 10px solid transparent;
143
+ border-bottom: 10px solid transparent;
144
+ border-left: 10px solid #4CAF50;
145
+ }
146
+
147
+ .controls-area {
148
+ background: #f8f9fa;
149
+ padding: 20px;
150
+ border-top: 1px solid #e9ecef;
151
+ }
152
+
153
+ .recording-section {
154
+ display: flex;
155
+ flex-direction: column;
156
+ gap: 15px;
157
+ }
158
+
159
+ .mic-container {
160
+ display: flex;
161
+ justify-content: center;
162
+ align-items: center;
163
+ gap: 20px;
164
+ }
165
+
166
+ .mic-button {
167
+ width: 80px;
168
+ height: 80px;
169
+ border-radius: 50%;
170
+ border: none;
171
+ background: linear-gradient(45deg, #ff6b6b, #ee5a52);
172
+ color: white;
173
+ font-size: 1.8rem;
174
+ cursor: pointer;
175
+ transition: all 0.3s ease;
176
+ display: flex;
177
+ align-items: center;
178
+ justify-content: center;
179
+ position: relative;
180
+ }
181
+
182
+ .mic-button:hover {
183
+ transform: scale(1.05);
184
+ box-shadow: 0 8px 25px rgba(255, 107, 107, 0.3);
185
+ }
186
+
187
+ .mic-button.recording {
188
+ background: linear-gradient(45deg, #ff4757, #ff3742);
189
+ animation: recordPulse 1s infinite;
190
+ }
191
+
192
+ .mic-button.recording::before {
193
+ content: '';
194
+ position: absolute;
195
+ width: 100%;
196
+ height: 100%;
197
+ border-radius: 50%;
198
+ background: rgba(255, 71, 87, 0.3);
199
+ animation: ripple 1s infinite;
200
+ }
201
+
202
+ .recording-status {
203
+ font-size: 0.9rem;
204
+ color: #666;
205
+ text-align: center;
206
+ }
207
+
208
+ .transcript-area {
209
+ background: white;
210
+ border: 2px solid #e9ecef;
211
+ border-radius: 10px;
212
+ padding: 15px;
213
+ font-size: 0.9rem;
214
+ color: #333;
215
+ min-height: 60px;
216
+ max-height: 120px;
217
+ overflow-y: auto;
218
+ transition: border-color 0.3s ease;
219
+ }
220
+
221
+ .transcript-area:focus {
222
+ outline: none;
223
+ border-color: #4CAF50;
224
+ }
225
+
226
+ .action-buttons {
227
+ display: flex;
228
+ gap: 10px;
229
+ justify-content: center;
230
+ margin-top: 15px;
231
+ }
232
+
233
+ .btn {
234
+ padding: 12px 24px;
235
+ border: none;
236
+ border-radius: 8px;
237
+ cursor: pointer;
238
+ font-size: 1rem;
239
+ font-weight: 500;
240
+ transition: all 0.3s ease;
241
+ display: flex;
242
+ align-items: center;
243
+ gap: 8px;
244
+ }
245
+
246
+ .btn-primary {
247
+ background: linear-gradient(45deg, #4CAF50, #45a049);
248
+ color: white;
249
+ }
250
+
251
+ .btn-primary:hover {
252
+ transform: translateY(-2px);
253
+ box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3);
254
+ }
255
+
256
+ .btn-secondary {
257
+ background: #6c757d;
258
+ color: white;
259
+ }
260
+
261
+ .btn-secondary:hover {
262
+ background: #545b62;
263
+ transform: translateY(-2px);
264
+ }
265
+
266
+ .btn:disabled {
267
+ opacity: 0.6;
268
+ cursor: not-allowed;
269
+ transform: none;
270
+ }
271
+
272
+ .question-counter {
273
+ position: absolute;
274
+ top: 15px;
275
+ right: 20px;
276
+ background: rgba(255, 255, 255, 0.2);
277
+ padding: 5px 12px;
278
+ border-radius: 20px;
279
+ font-size: 0.8rem;
280
+ }
281
+
282
+ .loading {
283
+ display: inline-block;
284
+ width: 20px;
285
+ height: 20px;
286
+ border: 3px solid rgba(255, 255, 255, 0.3);
287
+ border-radius: 50%;
288
+ border-top-color: white;
289
+ animation: spin 1s ease-in-out infinite;
290
+ }
291
+
292
+ .summary-panel {
293
+ display: none;
294
+ background: white;
295
+ border-radius: 15px;
296
+ padding: 30px;
297
+ margin: 20px;
298
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
299
+ }
300
+
301
+ .summary-panel h2 {
302
+ color: #333;
303
+ margin-bottom: 20px;
304
+ text-align: center;
305
+ }
306
+
307
+ .summary-item {
308
+ background: #f8f9fa;
309
+ border-radius: 10px;
310
+ padding: 15px;
311
+ margin-bottom: 15px;
312
+ border-left: 4px solid #4CAF50;
313
+ }
314
+
315
+ .summary-item h4 {
316
+ color: #333;
317
+ margin-bottom: 8px;
318
+ }
319
+
320
+ .summary-item p {
321
+ color: #666;
322
+ margin-bottom: 5px;
323
+ line-height: 1.4;
324
+ }
325
+
326
+ .evaluation-score {
327
+ display: inline-block;
328
+ background: #4CAF50;
329
+ color: white;
330
+ padding: 4px 8px;
331
+ border-radius: 15px;
332
+ font-size: 0.8rem;
333
+ font-weight: bold;
334
+ }
335
+
336
+ @keyframes slideIn {
337
+ from {
338
+ opacity: 0;
339
+ transform: translateY(20px);
340
+ }
341
+ to {
342
+ opacity: 1;
343
+ transform: translateY(0);
344
+ }
345
+ }
346
+
347
+ @keyframes pulse {
348
+ 0%, 100% { transform: scale(1); }
349
+ 50% { transform: scale(1.1); }
350
+ }
351
+
352
+ @keyframes ripple {
353
+ 0% {
354
+ transform: scale(1);
355
+ opacity: 1;
356
+ }
357
+ 100% {
358
+ transform: scale(1.4);
359
+ opacity: 0;
360
+ }
361
+ }
362
+
363
+ @keyframes recordPulse {
364
+ 0%, 100% { transform: scale(1); }
365
+ 50% { transform: scale(1.05); }
366
+ }
367
+
368
+ @keyframes spin {
369
+ to { transform: rotate(360deg); }
370
+ }
371
+
372
+ @media (max-width: 768px) {
373
+ .interview-container {
374
+ margin: 10px;
375
+ min-height: 90vh;
376
+ }
377
+
378
+ .mic-button {
379
+ width: 70px;
380
+ height: 70px;
381
+ font-size: 1.5rem;
382
+ }
383
+
384
+ .message-bubble, .user-bubble {
385
+ max-width: 85%;
386
+ }
387
+
388
+ .header h1 {
389
+ font-size: 1.5rem;
390
+ }
391
+ }
392
+
393
+ /* Hide default audio controls */
394
+ audio {
395
+ display: none;
396
+ }
397
+ </style>
398
+ </head>
399
+ <body>
400
+ <div class="interview-container">
401
+ <div class="header">
402
+ <div class="question-counter">
403
+ Question <span id="currentQuestionNum">1</span> of <span id="totalQuestions">3</span>
404
+ </div>
405
+ <h1>🤖 AI Interview Assistant</h1>
406
+ <p>Answer thoughtfully and take your time</p>
407
+ </div>
408
+
409
+ <div class="chat-area" id="chatArea">
410
+ <div class="ai-message">
411
+ <div class="ai-avatar" id="aiAvatar">AI</div>
412
+ <div class="message-bubble">
413
+ <div id="loadingMessage">
414
+ <div class="loading"></div>
415
+ <span style="margin-left: 10px;">Generating your first question...</span>
416
+ </div>
417
+ </div>
418
+ </div>
419
+ </div>
420
+
421
+ <div class="controls-area">
422
+ <div class="recording-section">
423
+ <div class="mic-container">
424
+ <button class="mic-button" id="micButton" disabled>
425
+ <span id="micIcon">🎤</span>
426
+ </button>
427
+ </div>
428
+ <div class="recording-status" id="recordingStatus">
429
+ Click the microphone to record your answer
430
+ </div>
431
+
432
+ <div class="transcript-area" id="transcriptArea" contenteditable="true" placeholder="Your transcribed answer will appear here...">
433
+ </div>
434
+
435
+ <div class="action-buttons">
436
+ <button class="btn btn-primary" id="confirmButton" disabled>
437
+ <span>Confirm Answer</span>
438
+ <span id="confirmLoading" style="display: none;">
439
+ <div class="loading" style="width: 16px; height: 16px; border-width: 2px;"></div>
440
+ </span>
441
+ </button>
442
+ <button class="btn btn-secondary" id="retryRecording" style="display: none;">
443
+ 🔄 Re-record
444
+ </button>
445
+ </div>
446
+ </div>
447
+ </div>
448
+
449
+ <!-- Summary Panel (hidden initially) -->
450
+ <div class="summary-panel" id="summaryPanel">
451
+ <h2>📋 Interview Summary</h2>
452
+ <div id="summaryContent"></div>
453
+ <div style="text-align: center; margin-top: 30px;">
454
+ <button class="btn btn-primary" onclick="window.close()">
455
+ Complete Interview
456
+ </button>
457
+ </div>
458
+ </div>
459
+ </div>
460
+
461
+ <!-- Hidden audio element for TTS playback -->
462
+ <audio id="ttsAudio" preload="auto"></audio>
463
+
464
+ <script>
465
+ const JOB_ID = {{ job.id }};
466
+ class AIInterviewer {
467
+ constructor() {
468
+ this.currentQuestionIndex = 0;
469
+ this.totalQuestions = 3;
470
+ this.isRecording = false;
471
+ this.mediaRecorder = null;
472
+ this.audioChunks = [];
473
+ this.interviewData = {
474
+ questions: [],
475
+ answers: [],
476
+ evaluations: []
477
+ };
478
+
479
+ this.initializeElements();
480
+ this.initializeInterview();
481
+ }
482
+
483
+ initializeElements() {
484
+ this.chatArea = document.getElementById('chatArea');
485
+ this.micButton = document.getElementById('micButton');
486
+ this.micIcon = document.getElementById('micIcon');
487
+ this.recordingStatus = document.getElementById('recordingStatus');
488
+ this.transcriptArea = document.getElementById('transcriptArea');
489
+ this.confirmButton = document.getElementById('confirmButton');
490
+ this.confirmLoading = document.getElementById('confirmLoading');
491
+ this.retryButton = document.getElementById('retryRecording');
492
+ this.aiAvatar = document.getElementById('aiAvatar');
493
+ this.ttsAudio = document.getElementById('ttsAudio');
494
+ this.summaryPanel = document.getElementById('summaryPanel');
495
+ this.currentQuestionNum = document.getElementById('currentQuestionNum');
496
+ this.totalQuestionsSpan = document.getElementById('totalQuestions');
497
+
498
+ this.bindEvents();
499
+ }
500
+
501
+ bindEvents() {
502
+ this.micButton.addEventListener('mousedown', () => this.startRecording());
503
+ this.micButton.addEventListener('mouseup', () => this.stopRecording());
504
+ this.micButton.addEventListener('mouseleave', () => this.stopRecording());
505
+ this.micButton.addEventListener('touchstart', (e) => {
506
+ e.preventDefault();
507
+ this.startRecording();
508
+ });
509
+ this.micButton.addEventListener('touchend', (e) => {
510
+ e.preventDefault();
511
+ this.stopRecording();
512
+ });
513
+
514
+ this.confirmButton.addEventListener('click', () => this.submitAnswer());
515
+ this.retryButton.addEventListener('click', () => this.resetRecording());
516
+
517
+ this.transcriptArea.addEventListener('input', () => {
518
+ const hasText = this.transcriptArea.textContent.trim().length > 0;
519
+ this.confirmButton.disabled = !hasText;
520
+ });
521
+
522
+ this.ttsAudio.addEventListener('play', () => {
523
+ this.aiAvatar.classList.add('talking');
524
+ });
525
+
526
+ this.ttsAudio.addEventListener('ended', () => {
527
+ this.aiAvatar.classList.remove('talking');
528
+ this.enableControls();
529
+ });
530
+
531
+ this.ttsAudio.addEventListener('error', (e) => {
532
+ console.error('Audio playback error:', e);
533
+ this.aiAvatar.classList.remove('talking');
534
+ this.enableControls();
535
+ });
536
+ }
537
+
538
+ async initializeInterview() {
539
+ try {
540
+ const response = await fetch('/api/start_interview', {
541
+ method: 'POST',
542
+ headers: {
543
+ 'Content-Type': 'application/json'
544
+ },
545
+ body: JSON.stringify({ job_id: JOB_ID })
546
+ });
547
+
548
+ const data = await response.json();
549
+ if (data.success) {
550
+ this.displayQuestion(data.question, data.audioUrl);
551
+ this.interviewData.questions.push(data.question);
552
+ } else {
553
+ this.showError('Failed to start interview. Please try again.');
554
+ }
555
+ } catch (error) {
556
+ console.error('Error starting interview:', error);
557
+ this.showError('Connection error. Please check your internet connection.');
558
+ }
559
+ }
560
+
561
+ displayQuestion(question, audioUrl = null) {
562
+ // Remove loading message
563
+ const loadingMsg = document.getElementById('loadingMessage');
564
+ if (loadingMsg) {
565
+ loadingMsg.remove();
566
+ }
567
+
568
+ // Create question message
569
+ const messageDiv = document.createElement('div');
570
+ messageDiv.className = 'ai-message';
571
+ messageDiv.innerHTML = `
572
+ <div class="ai-avatar talking">AI</div>
573
+ <div class="message-bubble">
574
+ <p>${question}</p>
575
+ </div>
576
+ `;
577
+
578
+ this.chatArea.appendChild(messageDiv);
579
+ this.chatArea.scrollTop = this.chatArea.scrollHeight;
580
+
581
+ // Update question counter
582
+ this.currentQuestionNum.textContent = this.currentQuestionIndex + 1;
583
+
584
+ // Play audio if available
585
+ if (audioUrl) {
586
+ this.playQuestionAudio(audioUrl);
587
+ } else {
588
+ // Enable controls if no audio
589
+ setTimeout(() => this.enableControls(), 1000);
590
+ }
591
+ }
592
+
593
+ playQuestionAudio(audioUrl) {
594
+ this.ttsAudio.src = audioUrl;
595
+ this.ttsAudio.play().catch(error => {
596
+ console.error('Audio play error:', error);
597
+ this.enableControls();
598
+ });
599
+ }
600
+
601
+ enableControls() {
602
+ this.micButton.disabled = false;
603
+ this.recordingStatus.textContent = 'Click and hold to record your answer';
604
+
605
+ // Remove talking animation from avatar
606
+ const avatars = this.chatArea.querySelectorAll('.ai-avatar');
607
+ avatars.forEach(avatar => avatar.classList.remove('talking'));
608
+ }
609
+
610
+ async startRecording() {
611
+ if (this.isRecording) return;
612
+
613
+ try {
614
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
615
+ this.mediaRecorder = new MediaRecorder(stream);
616
+ this.audioChunks = [];
617
+
618
+ this.mediaRecorder.ondataavailable = (event) => {
619
+ this.audioChunks.push(event.data);
620
+ };
621
+
622
+ this.mediaRecorder.onstop = () => {
623
+ this.processRecording();
624
+ stream.getTracks().forEach(track => track.stop());
625
+ };
626
+
627
+ this.mediaRecorder.start();
628
+ this.isRecording = true;
629
+
630
+ // Update UI
631
+ this.micButton.classList.add('recording');
632
+ this.micIcon.textContent = '🔴';
633
+ this.recordingStatus.textContent = 'Recording... Release to stop';
634
+
635
+ } catch (error) {
636
+ console.error('Error starting recording:', error);
637
+ this.recordingStatus.textContent = 'Microphone access denied. Please allow microphone access and try again.';
638
+ }
639
+ }
640
+
641
+ stopRecording() {
642
+ if (!this.isRecording || !this.mediaRecorder) return;
643
+
644
+ this.mediaRecorder.stop();
645
+ this.isRecording = false;
646
+
647
+ // Update UI
648
+ this.micButton.classList.remove('recording');
649
+ this.micIcon.textContent = '🎤';
650
+ this.recordingStatus.textContent = 'Processing audio...';
651
+ }
652
+
653
+ async processRecording() {
654
+ const audioBlob = new Blob(this.audioChunks, { type: 'audio/wav' });
655
+ const formData = new FormData();
656
+ formData.append('audio', audioBlob, 'recording.wav');
657
+
658
+ try {
659
+ const response = await fetch('/api/transcribe', {
660
+ method: 'POST',
661
+ body: formData
662
+ });
663
+
664
+ const data = await response.json();
665
+ if (data.success && data.transcript) {
666
+ this.transcriptArea.textContent = data.transcript;
667
+ this.confirmButton.disabled = false;
668
+ this.retryButton.style.display = 'inline-flex';
669
+ this.recordingStatus.textContent = 'Transcription complete. Review and confirm your answer.';
670
+ } else {
671
+ this.recordingStatus.textContent = 'Transcription failed. Please try recording again.';
672
+ }
673
+ } catch (error) {
674
+ console.error('Error processing recording:', error);
675
+ this.recordingStatus.textContent = 'Error processing audio. Please try again.';
676
+ }
677
+ }
678
+
679
+ resetRecording() {
680
+ this.transcriptArea.textContent = '';
681
+ this.confirmButton.disabled = true;
682
+ this.retryButton.style.display = 'none';
683
+ this.recordingStatus.textContent = 'Click and hold to record your answer';
684
+ }
685
+
686
+ async submitAnswer() {
687
+ const answer = this.transcriptArea.textContent.trim();
688
+ if (!answer) return;
689
+
690
+ // Show loading state
691
+ this.confirmButton.disabled = true;
692
+ this.confirmLoading.style.display = 'inline-block';
693
+ this.confirmButton.querySelector('span').style.display = 'none';
694
+
695
+ // Add user message to chat
696
+ this.addUserMessage(answer);
697
+
698
+ try {
699
+ const response = await fetch('/api/process_answer', {
700
+ method: 'POST',
701
+ headers: {
702
+ 'Content-Type': 'application/json'
703
+ },
704
+ body: JSON.stringify({
705
+ answer: answer,
706
+ questionIndex: this.currentQuestionIndex
707
+ })
708
+ });
709
+
710
+ const data = await response.json();
711
+
712
+ if (data.success) {
713
+ this.interviewData.answers.push(answer);
714
+ this.interviewData.evaluations.push(data.evaluation);
715
+
716
+ if (data.isComplete) {
717
+ this.showInterviewSummary(data.summary);
718
+ } else {
719
+ this.currentQuestionIndex++;
720
+ this.displayQuestion(data.nextQuestion, data.audioUrl);
721
+ this.interviewData.questions.push(data.nextQuestion);
722
+ this.resetForNextQuestion();
723
+ }
724
+ } else {
725
+ this.showError('Failed to process answer. Please try again.');
726
+ }
727
+
728
+ } catch (error) {
729
+ console.error('Error submitting answer:', error);
730
+ this.showError('Connection error. Please try again.');
731
+ } finally {
732
+ // Reset button state
733
+ this.confirmLoading.style.display = 'none';
734
+ this.confirmButton.querySelector('span').style.display = 'inline';
735
+ }
736
+ }
737
+
738
+ addUserMessage(message) {
739
+ const messageDiv = document.createElement('div');
740
+ messageDiv.className = 'user-message';
741
+ messageDiv.innerHTML = `
742
+ <div class="user-bubble">
743
+ <p>${message}</p>
744
+ </div>
745
+ `;
746
+
747
+ this.chatArea.appendChild(messageDiv);
748
+ this.chatArea.scrollTop = this.chatArea.scrollHeight;
749
+ }
750
+
751
+ resetForNextQuestion() {
752
+ this.transcriptArea.textContent = '';
753
+ this.confirmButton.disabled = true;
754
+ this.retryButton.style.display = 'none';
755
+ this.recordingStatus.textContent = 'Wait for the next question...';
756
+ this.micButton.disabled = true;
757
+ }
758
+
759
+ showInterviewSummary(summaryData) {
760
+ const summaryContent = document.getElementById('summaryContent');
761
+ let summaryHtml = '';
762
+
763
+ this.interviewData.questions.forEach((question, index) => {
764
+ const answer = this.interviewData.answers[index] || 'No answer provided';
765
+ const evaluation = this.interviewData.evaluations[index] || {};
766
+
767
+ summaryHtml += `
768
+ <div class="summary-item">
769
+ <h4>Question ${index + 1}:</h4>
770
+ <p><strong>Q:</strong> ${question}</p>
771
+ <p><strong>A:</strong> ${answer}</p>
772
+ <p><strong>Score:</strong> <span class="evaluation-score">${evaluation.score || 'N/A'}</span></p>
773
+ <p><strong>Feedback:</strong> ${evaluation.feedback || 'No feedback provided'}</p>
774
+ </div>
775
+ `;
776
+ });
777
+
778
+ summaryContent.innerHTML = summaryHtml;
779
+
780
+ // Hide main interface and show summary
781
+ document.querySelector('.interview-container').style.display = 'none';
782
+ this.summaryPanel.style.display = 'block';
783
+ }
784
+
785
+ showError(message) {
786
+ this.recordingStatus.textContent = message;
787
+ this.recordingStatus.style.color = '#ff4757';
788
+ setTimeout(() => {
789
+ this.recordingStatus.style.color = '#666';
790
+ }, 3000);
791
+ }
792
+ }
793
+
794
+ // Initialize the interview when page loads
795
+ document.addEventListener('DOMContentLoaded', () => {
796
+ new AIInterviewer();
797
+ });
798
+
799
+ // Add placeholder attribute support for contenteditable
800
+ document.addEventListener('DOMContentLoaded', function() {
801
+ const transcriptArea = document.getElementById('transcriptArea');
802
+ const placeholder = transcriptArea.getAttribute('placeholder');
803
+
804
+ function checkPlaceholder() {
805
+ if (transcriptArea.textContent.trim() === '') {
806
+ transcriptArea.style.color = '#999';
807
+ transcriptArea.textContent = placeholder;
808
+ } else if (transcriptArea.textContent === placeholder) {
809
+ transcriptArea.style.color = '#333';
810
+ transcriptArea.textContent = '';
811
+ }
812
+ }
813
+
814
+ transcriptArea.addEventListener('focus', function() {
815
+ if (transcriptArea.textContent === placeholder) {
816
+ transcriptArea.textContent = '';
817
+ transcriptArea.style.color = '#333';
818
+ }
819
+ });
820
+
821
+ transcriptArea.addEventListener('blur', function() {
822
+ if (transcriptArea.textContent.trim() === '') {
823
+ transcriptArea.style.color = '#999';
824
+ transcriptArea.textContent = placeholder;
825
+ }
826
+ });
827
+
828
+ // Initial check
829
+ checkPlaceholder();
830
+ });
831
+ </script>
832
+ </body>
833
+ </html>
backend/utils/luna_phase1.py ADDED
File without changes
instance/codingo.db DELETED
Binary file (24.6 kB)
 
requirements.txt CHANGED
@@ -10,3 +10,49 @@ pyresparser
10
  flask_sqlalchemy
11
  flask_wtf
12
  email-validator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  flask_sqlalchemy
11
  flask_wtf
12
  email-validator
13
+
14
+ # ------
15
+ # Core Scientific Stack
16
+ scipy==1.11.3
17
+ soundfile==0.12.1
18
+ sounddevice==0.4.6
19
+ opencv-python==4.7.0.72
20
+ moviepy==1.0.3
21
+ librosa==0.10.0.post2
22
+
23
+ # Hugging Face Transformers + Whisper
24
+ transformers==4.39.3
25
+ torch==2.1.2
26
+ sentencepiece==0.1.99
27
+ sentence-transformers==2.7.0
28
+ git+https://github.com/openai/whisper.git@c0d2f62
29
+
30
+ # LangChain stack
31
+ langchain==0.3.26
32
+ langchain_groq==0.3.6
33
+ langchain_community==0.3.27
34
+ #langchain_huggingface==0.2.0
35
+ llama-index==0.8.40
36
+ cohere==5.16.1
37
+
38
+ # Vector DB
39
+ qdrant-client==1.14.3
40
+
41
+ # PDF & DOCX parsing
42
+ PyPDF2==3.0.1
43
+ python-docx==1.2.0
44
+
45
+ # Audio / CLI
46
+ ffmpeg-python==0.2.0
47
+ #pyaudio==0.2.14
48
+ inputimeout==1.0.4
49
+ evaluate==0.4.5
50
+ pip==23.3.1
51
+ accelerate==0.29.3
52
+ huggingface_hub==0.20.3
53
+ textract==1.6.3
54
+ bitsandbytes
55
+ faster-whisper==0.10.0
56
+ edge-tts==6.1.2
57
+
58
+