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