|
import gradio as gr |
|
import pandas as pd |
|
import logging |
|
from pathlib import Path |
|
from docx import Document |
|
from typing import Optional, List |
|
from dataclasses import dataclass |
|
from dotenv import load_dotenv |
|
from src.archive.sample_inputs import INTERVIEW_QUESTION, JOB_REQUIREMENTS |
|
from src.configs.database.firebase import write_user_data, read_all_users |
|
from src.llm.llm import get_llm |
|
from src.service.emotion_recognition import EmotionRecognition |
|
from src.service.resume_parser import ResumeParser |
|
from src.utils.utils import ( |
|
parse_yaml_string, |
|
extract_audio, |
|
audio2text, |
|
sample_frames, |
|
) |
|
from src.template.grading_prompt import ( |
|
GRADE_RESPONSE_PROMPT, |
|
RANKING_AND_FEEDBACK_PROMPT, |
|
) |
|
|
|
load_dotenv() |
|
|
|
|
|
|
|
|
|
|
|
|
|
BASE_DIR = Path(".").resolve() |
|
|
|
LLM_CONFIG_FILE = BASE_DIR / "src/configs/llm/openai-gpt-3.5-turbo.yaml" |
|
RESUME_PARSER_CONFIG_FILE = BASE_DIR / "src/configs/parser/llamaparse_en.yaml" |
|
OUTPUT_AUDIO_FILE_EMPTY = BASE_DIR / "src/output/audio_output.wav" |
|
OUTPUT_REPORT_FILE_EMPTY = BASE_DIR / "src/output/report.docx" |
|
|
|
|
|
@dataclass |
|
class ProcessingResult: |
|
candidate_name: Optional[str] = None |
|
candidate_score: Optional[int] = None |
|
candidate_feedbacks: Optional[List[str]] = None |
|
feedback_md: Optional[str] = None |
|
interview_question: Optional[str] = None |
|
job_requirements: Optional[str] = None |
|
error_message: Optional[str] = None |
|
|
|
|
|
class GradioInterface: |
|
VALID_VIDEO_EXTENSIONS = {".mp4", ".avi", ".mkv"} |
|
VALID_RESUME_EXTENSIONS = {".pdf"} |
|
|
|
def __init__(self): |
|
self.parser = None |
|
self.llm = None |
|
self.logger = None |
|
self.candidate_feedback = pd.DataFrame(columns=["Name", "Score", "Feedback"]) |
|
self.setup_logging() |
|
self.initialize_services() |
|
|
|
def setup_logging(self): |
|
logging.basicConfig( |
|
level=logging.INFO, |
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", |
|
) |
|
self.logger = logging.getLogger(__name__) |
|
|
|
def initialize_services(self): |
|
try: |
|
self.llm = get_llm(str(LLM_CONFIG_FILE)) |
|
self.parser = ResumeParser(str(RESUME_PARSER_CONFIG_FILE)) |
|
except Exception as e: |
|
self.logger.error(f"Failed to initialize services: {str(e)}") |
|
raise |
|
|
|
def validate_inputs( |
|
self, |
|
video_path: Optional[str], |
|
resume_path: Optional[str], |
|
interview_questions: Optional[str], |
|
job_requirements: Optional[str], |
|
) -> Optional[str]: |
|
if not video_path: |
|
return "Please upload an interview video." |
|
if not resume_path: |
|
return "Please upload a resume (PDF)." |
|
if not interview_questions: |
|
return "Please provide interview questions." |
|
if not job_requirements: |
|
return "Please provide job requirements." |
|
if not self._validate_file_format(video_path, self.VALID_VIDEO_EXTENSIONS): |
|
return "Invalid video format." |
|
if not self._validate_file_format(resume_path, self.VALID_RESUME_EXTENSIONS): |
|
return "Please submit resume in PDF format." |
|
return None |
|
|
|
def _validate_file_format(self, file_path: str, valid_extensions: set) -> bool: |
|
return isinstance(file_path, str) and any( |
|
file_path.lower().endswith(ext) for ext in valid_extensions |
|
) |
|
|
|
def process_video(self, video_path: str) -> Optional[str]: |
|
OUTPUT_AUDIO_FILE = extract_audio(video_path, str(OUTPUT_AUDIO_FILE_EMPTY)) |
|
audio_text = audio2text(OUTPUT_AUDIO_FILE) |
|
return audio_text |
|
|
|
def analyze_emotions(self, video_path: str) -> Optional[str]: |
|
frames = sample_frames(video_path, sample_rate=8) |
|
emotions = EmotionRecognition.detect_face_emotions(frames) |
|
emotions_dict = EmotionRecognition.process_emotions(emotions) |
|
conf_score = emotions_dict["conf"] |
|
return conf_score |
|
|
|
def process_resume(self, resume_path: str) -> Optional[str]: |
|
resume_md = self.parser.parse_resume_to_markdown(resume_path) |
|
return resume_md |
|
|
|
def format_feedback_to_markdown(self, feedback_df: pd.DataFrame) -> str: |
|
if feedback_df.empty: |
|
return "No feedback available." |
|
|
|
name = feedback_df["Name"].iloc[0] |
|
score = feedback_df["Score"].iloc[0] |
|
|
|
|
|
markdown_text = f""" |
|
# Candidate Assessment Report 📝 |
|
|
|
## Candidate Name ✨ |
|
{name} |
|
|
|
## Candidate Overall Score 🎯 |
|
{score}/100 |
|
|
|
## Detailed Feedback 🛠️ |
|
""" |
|
|
|
for idx, row in feedback_df.iterrows(): |
|
markdown_text += f"- {row['Feedback']}\n\n" |
|
|
|
return markdown_text |
|
|
|
def get_feedback( |
|
self, |
|
itv_question: str, |
|
job_requirements: str, |
|
conf_score: str, |
|
audio_text: str, |
|
resume_md: str, |
|
) -> pd.DataFrame: |
|
|
|
formatted_grading_prompt = GRADE_RESPONSE_PROMPT.format( |
|
interview_question=itv_question, |
|
conf_score=conf_score, |
|
response_text=audio_text, |
|
) |
|
|
|
grade = self.llm.complete(formatted_grading_prompt) |
|
|
|
formatted_ranking_prompt = RANKING_AND_FEEDBACK_PROMPT.format( |
|
job_requirements=job_requirements, |
|
interview_feedback=grade, |
|
resume_text=resume_md, |
|
) |
|
rank_and_feedback = self.llm.complete(formatted_ranking_prompt) |
|
|
|
expected_keys = ["name", "score", "feedback"] |
|
rank_and_feedback_dict = parse_yaml_string( |
|
yaml_string=rank_and_feedback, expected_keys=expected_keys, cleanup=True |
|
) |
|
|
|
return pd.DataFrame( |
|
{ |
|
"Name": rank_and_feedback_dict["name"], |
|
"Score": rank_and_feedback_dict["score"], |
|
"Feedback": rank_and_feedback_dict["feedback"], |
|
} |
|
) |
|
|
|
def process_submission( |
|
self, |
|
video_path: str, |
|
resume_path: str, |
|
interview_questions: str, |
|
job_title: str, |
|
job_requirements: str, |
|
) -> ProcessingResult: |
|
try: |
|
|
|
error_message = self.validate_inputs( |
|
video_path, resume_path, interview_questions, job_requirements |
|
) |
|
if error_message: |
|
return ProcessingResult(error_message=error_message) |
|
|
|
|
|
video_transcript = self.process_video(video_path) |
|
emotion_analysis = self.analyze_emotions(video_path) |
|
resume_analysis = self.process_resume(resume_path) |
|
|
|
feedback_list = self.get_feedback( |
|
interview_questions, |
|
job_requirements, |
|
emotion_analysis, |
|
video_transcript, |
|
resume_analysis, |
|
) |
|
|
|
|
|
self.candidate_feedback = pd.concat( |
|
[self.candidate_feedback, feedback_list], ignore_index=True |
|
) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
write_user_data( |
|
self.candidate_feedback["Name"].iloc[0], |
|
self.candidate_feedback["Score"].iloc[0], |
|
interview_questions, |
|
job_title, |
|
job_requirements, |
|
self.candidate_feedback["Feedback"].tolist(), |
|
) |
|
|
|
feedback_md = self.format_feedback_to_markdown(self.candidate_feedback) |
|
|
|
return ProcessingResult( |
|
candidate_name=self.candidate_feedback["Name"].iloc[0], |
|
candidate_score=self.candidate_feedback["Score"].iloc[0], |
|
candidate_feedbacks=self.candidate_feedback["Feedback"].tolist(), |
|
feedback_md=feedback_md, |
|
interview_question=interview_questions, |
|
job_requirements=job_requirements, |
|
) |
|
|
|
except Exception as e: |
|
self.logger.error(f"Error in process_submission: {str(e)}") |
|
return ProcessingResult( |
|
error_message=f"An error occurred during processing: {str(e)}" |
|
) |
|
|
|
def save_report( |
|
self, |
|
candidate_name, |
|
candidate_score, |
|
candidate_feedback, |
|
interview_question, |
|
job_requirements, |
|
) -> Optional[str]: |
|
try: |
|
if self.candidate_feedback.empty: |
|
return None |
|
|
|
doc = Document() |
|
doc.add_heading(f"Interview Analysis Report - {candidate_name}", 0) |
|
doc.add_heading("Interview Questions", 1) |
|
doc.add_paragraph(interview_question) |
|
doc.add_heading("Job Requirements", 1) |
|
doc.add_paragraph(job_requirements) |
|
doc.add_heading("Overall Score", 1) |
|
paragraph = doc.add_paragraph() |
|
paragraph.add_run(f"{candidate_score}/100").bold = True |
|
doc.add_heading("Detailed Feedback", 1) |
|
|
|
for feedback in candidate_feedback: |
|
doc.add_paragraph(f"• {feedback}") |
|
|
|
doc.save(str(OUTPUT_REPORT_FILE_EMPTY)) |
|
return str(OUTPUT_REPORT_FILE_EMPTY) |
|
|
|
except Exception as e: |
|
self.logger.error(f"Error saving report: {str(e)}") |
|
return None |
|
|
|
def create_interface(self) -> gr.Blocks: |
|
|
|
theme = gr.themes.Ocean( |
|
primary_hue="pink", |
|
secondary_hue="rose", |
|
font="Chalkboard", |
|
) |
|
|
|
with gr.Blocks(title="HR Interview Analysis System", theme=theme) as demo: |
|
gr.Markdown("# HR Interview Analysis System") |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
video_input = gr.Video(label="Upload Interview Video", format="mp4") |
|
resume_input = gr.File( |
|
label="Upload Resume (PDF)", file_types=[".pdf"] |
|
) |
|
|
|
with gr.Row(): |
|
question_input = gr.Textbox( |
|
label="Interview Questions", |
|
lines=5, |
|
placeholder="Enter the interview questions here...", |
|
) |
|
|
|
with gr.Row(): |
|
job_title_input = gr.Textbox( |
|
label="Job Title", |
|
lines=5, |
|
placeholder="Enter the job title here...", |
|
) |
|
requirements_input = gr.Textbox( |
|
label="Job Requirements", |
|
lines=5, |
|
placeholder="Enter the job requirements here...", |
|
) |
|
|
|
submit_button = gr.Button("Analyze Interview", variant="primary") |
|
|
|
|
|
error_output = gr.Markdown(visible=False) |
|
|
|
with gr.Tabs(): |
|
with gr.Tab("Analysis Results"): |
|
feedback_output_md = gr.Markdown( |
|
label="Candidate Assessment", |
|
value="No assessment available yet.", |
|
) |
|
|
|
save_button = gr.Button("Generate Report", variant="secondary") |
|
report_output = gr.File(label="Download Report") |
|
|
|
with gr.Tab("Candidates List"): |
|
candidates_df = gr.Dataframe( |
|
headers=[ |
|
"Name", |
|
"Job Title", |
|
"Interview Question", |
|
"Score", |
|
"Feedback", |
|
], |
|
datatype=["str", "str", "str", "int", "str"], |
|
row_count=(0, "dynamic"), |
|
col_count=(5, "fixed"), |
|
value=read_all_users(), |
|
interactive=True, |
|
wrap=True, |
|
) |
|
|
|
refresh_button = gr.Button("Refresh Candidates List") |
|
|
|
refresh_button.click( |
|
fn=lambda: read_all_users(), |
|
inputs=[], |
|
outputs=[candidates_df], |
|
) |
|
|
|
candidate_name_state = gr.State() |
|
candidate_score_state = gr.State() |
|
candidate_feedbacks_state = gr.State() |
|
interview_question_state = gr.State() |
|
job_requirements_state = gr.State() |
|
|
|
|
|
submit_button.click( |
|
fn=lambda video, resume, questions, job_title, requirements: ( |
|
lambda result: ( |
|
result.candidate_name, |
|
result.candidate_score, |
|
result.candidate_feedbacks, |
|
result.feedback_md, |
|
result.interview_question, |
|
result.job_requirements, |
|
result.error_message, |
|
) |
|
)( |
|
self.process_submission( |
|
video, resume, questions, job_title, requirements |
|
) |
|
), |
|
inputs=[ |
|
video_input, |
|
resume_input, |
|
question_input, |
|
job_title_input, |
|
requirements_input, |
|
], |
|
outputs=[ |
|
candidate_name_state, |
|
candidate_score_state, |
|
candidate_feedbacks_state, |
|
feedback_output_md, |
|
interview_question_state, |
|
job_requirements_state, |
|
error_output, |
|
], |
|
) |
|
|
|
save_button.click( |
|
fn=self.save_report, |
|
inputs=[ |
|
candidate_name_state, |
|
candidate_score_state, |
|
candidate_feedbacks_state, |
|
interview_question_state, |
|
job_requirements_state, |
|
], |
|
outputs=[report_output], |
|
) |
|
|
|
return demo |
|
|
|
|
|
def launch_app(): |
|
app = GradioInterface() |
|
interface = app.create_interface() |
|
interface.launch(server_name="0.0.0.0", server_port=7860, share=True, debug=True) |
|
|
|
|
|
if __name__ == "__main__": |
|
launch_app() |
|
|