Spaces:
Runtime error
Runtime error
Download app.py from Muskuu/interview-evaluator: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/spaces/Muskuu/interview-evaluator/resolve/main/app.py
- Command line
-
hf download hf://spaces/Muskuu/interview-evaluator/app.py
-
curl -L -o app.py https://huggingface.co/spaces/Muskuu/interview-evaluator/resolve/main/app.py
11.5 kB
| import gradio as gr | |
| import torch | |
| import re | |
| import random | |
| import os | |
| from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig | |
| from peft import PeftModel | |
| from datasets import load_dataset | |
| model = None | |
| tokenizer = None | |
| eval_data = None | |
| QUESTION_COL = None | |
| ANSWER_COL = None | |
| BASE_MODEL_ID = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" | |
| ADAPTER_PATH = "./qlora_interview_adapter" # your saved adapter folder | |
| # LOAD MODEL | |
| def load_model(): | |
| global model, tokenizer, eval_data, QUESTION_COL, ANSWER_COL | |
| print("Loading tokenizer...") | |
| tokenizer = AutoTokenizer.from_pretrained( | |
| ADAPTER_PATH if os.path.exists(ADAPTER_PATH) else BASE_MODEL_ID | |
| ) | |
| tokenizer.pad_token = tokenizer.eos_token | |
| # 4-bit QLoRA config | |
| bnb_config = BitsAndBytesConfig( | |
| load_in_4bit=True, | |
| bnb_4bit_quant_type="nf4", | |
| bnb_4bit_compute_dtype=torch.float16 | |
| ) | |
| print("Loading base model in 4-bit...") | |
| base = AutoModelForCausalLM.from_pretrained( | |
| BASE_MODEL_ID, | |
| quantization_config=bnb_config, | |
| device_map="auto" | |
| ) | |
| # Load your fine-tuned LoRA adapter if it exists | |
| if os.path.exists(ADAPTER_PATH): | |
| print(f"Loading QLoRA adapter from {ADAPTER_PATH}...") | |
| model = PeftModel.from_pretrained(base, ADAPTER_PATH) | |
| print("QLoRA adapter loaded!") | |
| else: | |
| print("No adapter found using base TinyLlama ") | |
| model = base | |
| model.eval() | |
| # Load dataset for question pool | |
| print("Loading interview dataset...") | |
| dataset = load_dataset("Aiman1234/Interview-questions", split="train") | |
| dataset = dataset.select(range(min(500, len(dataset)))) | |
| cols = dataset.column_names | |
| for c in cols: | |
| lc = c.strip().lower() | |
| if "question" in lc: QUESTION_COL = c | |
| if "answer" in lc: ANSWER_COL = c | |
| split = dataset.train_test_split(test_size=0.2, seed=42) | |
| eval_data = split["test"] | |
| print(f"Ready! {len(eval_data)} eval questions loaded.") | |
| # SAME FUNCTIONS FROM YOUR NOTEBOOK | |
| def generate_model_answer(question, max_new_tokens=300): | |
| """Step 10 from your notebook β generate reference answer from fine-tuned model.""" | |
| prompt = f"### Instruction:\n{question}\n\n### Response:\n" | |
| inputs = tokenizer(prompt, return_tensors="pt").to(model.device) | |
| with torch.no_grad(): | |
| output = model.generate( | |
| **inputs, | |
| max_new_tokens=max_new_tokens, | |
| temperature=0.3, | |
| do_sample=True, | |
| pad_token_id=tokenizer.eos_token_id | |
| ) | |
| new_tokens = output[0][inputs['input_ids'].shape[1]:] | |
| return tokenizer.decode(new_tokens, skip_special_tokens=True).strip() | |
| def compute_ngram_similarity(text1, text2, n=2): | |
| """BLEU-inspired n-gram overlap.""" | |
| def get_ngrams(text, n): | |
| words = re.findall(r'\w+', text.lower()) | |
| return set(zip(*[words[i:] for i in range(n)])) | |
| ngrams1 = get_ngrams(text1, n) | |
| ngrams2 = get_ngrams(text2, n) | |
| if not ngrams1 or not ngrams2: | |
| return 0.0 | |
| overlap = ngrams1 & ngrams2 | |
| precision = len(overlap) / len(ngrams1) | |
| recall = len(overlap) / len(ngrams2) | |
| if precision + recall == 0: | |
| return 0.0 | |
| return 2 * precision * recall / (precision + recall) | |
| def llm_evaluate(question, model_answer, user_answer, max_new_tokens=250): | |
| """ LLM-based evaluation using the fine-tuned model.""" | |
| eval_prompt = f"""### Instruction: | |
| You are an AI interview evaluator. Compare the user's answer with the expected answer. | |
| Question: {question} | |
| Expected Answer: {model_answer[:400]} | |
| User Answer: {user_answer} | |
| """ | |
| inputs = tokenizer(eval_prompt, return_tensors="pt").to(model.device) | |
| with torch.no_grad(): | |
| output = model.generate( | |
| **inputs, | |
| max_new_tokens=max_new_tokens, | |
| temperature=0.2, | |
| do_sample=True, | |
| pad_token_id=tokenizer.eos_token_id | |
| ) | |
| new_tokens = output[0][inputs['input_ids'].shape[1]:] | |
| return tokenizer.decode(new_tokens, skip_special_tokens=True).strip() | |
| def parse_evaluation(llm_output, ngram_score): | |
| """ parse score and feedback.""" | |
| score_match = re.search(r'Score:\s*(\d+(?:\.\d+)?)/10', llm_output, re.IGNORECASE) | |
| feedback_match = re.search(r'Feedback:\s*(.+)', llm_output, re.IGNORECASE | re.DOTALL) | |
| score = float(score_match.group(1)) if score_match else round(ngram_score * 10, 1) | |
| feedback = feedback_match.group(1).split('\n')[0].strip() if feedback_match else ( | |
| "Good answer!" if ngram_score >= 0.7 else | |
| "Partial answer. Consider expanding on key points." if ngram_score >= 0.4 else | |
| "Needs improvement. Review key concepts." | |
| ) | |
| return max(0.0, min(10.0, score)), feedback | |
| def evaluate_answer_fast(user_answer, dataset_answer): | |
| """ | |
| Fast evaluation using only n-gram similarity against the dataset answer. | |
| No model inference β instant results. | |
| """ | |
| ngram_sim_1 = compute_ngram_similarity(user_answer, dataset_answer, n=1) | |
| ngram_sim_2 = compute_ngram_similarity(user_answer, dataset_answer, n=2) | |
| ngram_score = (ngram_sim_1 + ngram_sim_2) / 2 | |
| score = round(ngram_score * 10, 1) | |
| if ngram_score >= 0.7: | |
| feedback = "Great answer! Strong overlap with the reference answer." | |
| elif ngram_score >= 0.4: | |
| feedback = "Partial answer. Consider expanding on key points from the reference." | |
| else: | |
| feedback = "Needs improvement. Review the reference answer and key concepts." | |
| return score, feedback, round(ngram_score * 100, 1) | |
| # SESSION STATE HELPERS | |
| def get_random_questions(n=5): | |
| indices = random.sample(range(len(eval_data)), min(n, len(eval_data))) | |
| questions = [eval_data[i][QUESTION_COL] for i in indices] | |
| dataset_answers = [eval_data[i][ANSWER_COL] for i in indices] | |
| return questions, dataset_answers | |
| def start_session(num_q): | |
| """Pick questions, return first one.""" | |
| questions, dataset_answers = get_random_questions(int(num_q)) | |
| state = { | |
| "questions": questions, | |
| "dataset_answers": dataset_answers, | |
| "current": 0, | |
| "results": [], | |
| "total": int(num_q) | |
| } | |
| q = questions[0] | |
| progress = f"Question 1 / {num_q}" | |
| return state, q, progress, "", "", "", gr.update(visible=False), gr.update(visible=True), gr.update(visible=False) | |
| def submit_answer(user_answer, state): | |
| """Evaluate current answer, show result.""" | |
| if not user_answer.strip(): | |
| return state, "β οΈ Please type your answer first.", "", "", gr.update(visible=False) | |
| idx = state["current"] | |
| q = state["questions"][idx] | |
| dataset_answer = state["dataset_answers"][idx] | |
| score, feedback, ngram_pct = evaluate_answer_fast(user_answer, dataset_answer) | |
| state["results"].append({ | |
| "question": q, | |
| "user_answer": user_answer, | |
| "score": score, | |
| "feedback": feedback, | |
| "ngram_pct": ngram_pct | |
| }) | |
| score_display = ( | |
| f"## Score: {score:.1f} / 10\n\n" | |
| f"**Feedback:** {feedback}\n\n" | |
| f"**N-gram Similarity (BLEU-style):** {ngram_pct}%\n\n" | |
| f"---\n\n" | |
| f"### Reference Answer\n" | |
| f"{dataset_answer}" | |
| ) | |
| is_last = state["current"] >= state["total"] - 1 | |
| next_label = "See Final Results β" if is_last else "Next Question β" | |
| return state, "", score_display, next_label, gr.update(visible=True) | |
| def next_question(state): | |
| """Move to next question or show summary.""" | |
| state["current"] += 1 | |
| if state["current"] >= state["total"]: | |
| # Build summary | |
| results = state["results"] | |
| avg_score = sum(r["score"] for r in results) / len(results) | |
| if avg_score >= 8.5: verdict = "π Outstanding β You are very well prepared!" | |
| elif avg_score >= 7: verdict = "β Strong performance β A few areas to polish." | |
| elif avg_score >= 5.5: verdict = "π Good effort β Review feedback to improve." | |
| elif avg_score >= 4: verdict = "π Keep studying β Focus on key concepts." | |
| else: verdict = "πͺ Don't give up β Practice makes perfect!" | |
| summary = f"## Session Complete!\n\n**Average Score: {avg_score:.1f} / 10**\n\n{verdict}\n\n---\n\n" | |
| for i, r in enumerate(results, 1): | |
| summary += f"**Q{i}:** {r['question']}\n" | |
| summary += f"- Score: {r['score']:.1f}/10 | N-gram: {r['ngram_pct']}%\n" | |
| summary += f"- Feedback: {r['feedback']}\n\n" | |
| return (state, "Session complete!", f"0 / {state['total']}", | |
| gr.update(visible=False), gr.update(visible=True), | |
| summary, gr.update(visible=False)) | |
| q = state["questions"][state["current"]] | |
| progress = f"Question {state['current']+1} / {state['total']}" | |
| return (state, q, progress, | |
| gr.update(visible=True), gr.update(visible=False), | |
| "", gr.update(visible=False)) | |
| # GRADIO UI | |
| with gr.Blocks(title="AI Interview Evaluator β QLoRA") as demo: | |
| state = gr.State({}) | |
| gr.Markdown(""" | |
| # AI Interview Evaluator | |
| """, elem_classes="title-block") | |
| # ββ SETUP PANEL ββ | |
| with gr.Group(visible=True) as setup_panel: | |
| gr.Markdown("### Start a new session") | |
| num_q = gr.Dropdown( | |
| choices=["3","5","8","10"], | |
| value="5", | |
| label="Number of Questions" | |
| ) | |
| start_btn = gr.Button(" Start Interview Session", variant="primary", size="lg") | |
| # ββ INTERVIEW PANEL ββ | |
| with gr.Group(visible=False) as interview_panel: | |
| progress_label = gr.Markdown("Question 1 / 5") | |
| question_box = gr.Textbox( | |
| label="π Question", | |
| interactive=False, | |
| lines=3, | |
| elem_classes="question-box" | |
| ) | |
| answer_box = gr.Textbox( | |
| label="βοΈ Your Answer", | |
| placeholder="Type your answer here... take your time.", | |
| lines=6 | |
| ) | |
| with gr.Row(): | |
| submit_btn = gr.Button("Evaluate My Answer β ", variant="primary") | |
| next_btn = gr.Button("Next Question β", variant="secondary", visible=False) | |
| result_box = gr.Markdown("", label="Evaluation Result") | |
| # ββ SUMMARY PANEL ββ | |
| with gr.Group(visible=False) as summary_panel: | |
| summary_box = gr.Markdown("") | |
| restart_btn = gr.Button("π Start New Session", variant="primary") | |
| # ββ WIRING ββ | |
| start_btn.click( | |
| fn=start_session, | |
| inputs=[num_q], | |
| outputs=[state, question_box, progress_label, answer_box, result_box, | |
| result_box, setup_panel, interview_panel, summary_panel] | |
| ) | |
| submit_btn.click( | |
| fn=submit_answer, | |
| inputs=[answer_box, state], | |
| outputs=[state, answer_box, result_box, next_btn, next_btn] | |
| ) | |
| next_btn.click( | |
| fn=next_question, | |
| inputs=[state], | |
| outputs=[state, question_box, progress_label, | |
| interview_panel, summary_panel, | |
| summary_box, next_btn] | |
| ) | |
| restart_btn.click( | |
| fn=lambda: (gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)), | |
| outputs=[setup_panel, interview_panel, summary_panel] | |
| ) | |
| # STARTUP | |
| if __name__ == "__main__": | |
| load_model() | |
| demo.launch( | |
| css=""" | |
| footer { display: none !important; } | |
| .title-block { text-align: center; } | |
| """ | |
| ) |