# Project export: Employ AI

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: UC Berkeley AI Hackathon 2025
- Tagline: Whether you're trying to get a change or find your first job, a daunting application lays before you. Employ AI helps you clear this challenge by making this process easier and more efficient.
- Devpost: https://devpost.com/software/employ-ai
- GitHub: http://github.com/ssridhar128/employAI
- Result: winner (Groq: Best Creative Use of Groq)
- Team: 4 GitHub contributor(s) — ssridhar128 (11 commits), Unnati Goyal (4 commits), speechara18 (2 commits), Subhash Srinivasa - Developer (1 commits)

## Devpost submission (written by the team)

### Inspiration

As computer science students, we are all too familiar with the challenges of breaking into the job market. The modern application process is time-consuming, repetitive, and often demoralizing, especially when juggling coursework, internships, and personal responsibilities. We wanted to build something that would genuinely help students and job-seekers like us: a tool that removes friction from the process, boosts confidence, and actually provides value. That’s how Employ AI was born.

### What it does

Employ AI streamlines two of the most daunting tasks in the job search process: Personalized Cold Email Generator Upload your resume, and we’ll extract your key experiences using AI. Then, with just a few inputs (company name, recruiter name, email), we generate a polished, personalized cold outreach email—ready to send via Gmail integration. Mock Interview Coach with Feedback Upload your resume and specify your target job. We generate 10 intelligent interview questions tailored to your resume and career goals. You can record your spoken responses, and we’ll analyze your delivery using acoustic features (energy, entropy, ZCR) and provide real-time feedback—helping you improve both your content and communication.

### How we built it

Backend: Built using Flask, Firebase (Firestore, Auth, and Cloud Storage), and PyMuPDF for PDF processing. Authentication: Google OAuth 2.0 for Gmail integration and Firebase Auth for signup/login. Audio & AI Features: Audio processing with FFmpeg and feature extraction using signal processing techniques. Natural language understanding with LLaMA-3 through Groq API for both email generation and mock interview logic. Frontend: HTML templates rendered via Flask’s Jinja2 with custom UI for uploading, form inputs, and dynamic question flows.

### Challenges we ran into

Audio File Processing in Flask: Supporting webm-to-wav conversion on the fly required careful integration with FFmpeg. Session Management: Keeping track of resume uploads, question progression, and user state across multiple endpoints was tricky in Flask. Gmail API Integration: Setting up OAuth scopes and redirect URIs while keeping user tokens secure was non-trivial. Groq/OpenAI Rate Limits: Ensuring fast and consistent generation from a third-party API required retry logic and error handling.

### Accomplishments we're proud of

Built a full-stack AI-driven product in a short time that feels useful and personal. Integrated multiple technologies (PDF parsing, audio analysis, generative AI, OAuth) in a cohesive, user-friendly flow. Successfully deployed a working end-to-end Gmail integration that sends real cold emails. Designed a feedback system that’s both data-driven and user-friendly for mock interviews.

### What we learned

How to build robust, session-aware web applications with Flask. Real-world use of OAuth 2.0 with multiple scopes (profile, email, Gmail send). How to extract meaningful insights from raw audio signals using ZCR, entropy, and energy The value of iteration, especially in natural language prompts and generative outputs

### What's next

Voice Tone Feedback: Incorporate sentiment and tone analysis into audio feedback Job-Specific Cold Emails: Add job scraping or integration with platforms like LinkedIn or Indeed to auto-tailor the cold email Question Difficulty Tuning: Let users choose beginner, intermediate, or expert mock questions Progress Tracking: Save user feedback over time to show improvement Mobile Optimization: Bring the full experience to mobile so users can practice on-the-go.

## README (from the GitHub repository)

# aiCareerAid

## Detected evidence (automated analysis)

Indexed codebase: 29 recognized source files, 102 KB.
- CSS (language) — detected in the code
- Flask (technology) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- Python (language) — detected in the code
- Firebase (technology) — claimed on Devpost, not found in the code

## Codebase structure (from repository index)

### Files (31 of 31)

```
.gitignore
apptest.py
audio.py
audiotest.py
full_process.py
post_audio.py
questions_utils.py
README.md
render.yaml
requirements.txt
resume_utils.py
static/confetti.js
static/email.css
static/home.css
static/question.css
static/resume.css
static/style.css
templates/generate-email.html
templates/generate-questions.html
templates/home.html
templates/interview-landing.html
templates/login.html
templates/questions.html
templates/resume-upload.html
templates/sent.html
templates/signup.html
templates/summary.html
utils/audio.py
utils/post_audio.py
utils/questions_utils.py
utils/resume_utils.py
```

### Dependencies

- requirements.txt: annotated-types@==0.7.0, anyio@==4.9.0, Authlib@==1.6.0, blinker@==1.9.0, CacheControl@==0.14.3, cachetools@==5.5.2, certifi@==2025.6.15, cffi@==1.17.1, charset-normalizer@==3.4.2, click@==8.1.8, contourpy@==1.3.0, cryptography@==45.0.4, cycler@==0.12.1, deprecation@==2.1.0, distro@==1.9.0, eyeD3@==0.9.8, filetype@==1.2.0, firebase-admin@==6.9.0, Flask@==3.1.1, flask-cors@==6.0.1, fonttools@==4.58.4, google-api-core@==2.25.1, google-api-python-client@==2.172.0, google-auth@==2.40.3, google-auth-httplib2@==0.2.0, google-cloud-core@==2.4.3, google-cloud-firestore@==2.21.0, google-cloud-storage@==3.1.1, google-crc32c@==1.7.1, google-resumable-media@==2.7.2, googleapis-common-protos@==1.70.0, groq@==0.28.0, grpcio@==1.73.0, grpcio-status@==1.73.0, h11@==0.16.0, h2@==4.2.0, hpack@==4.1.0, httpcore@==1.0.9, httplib2@==0.22.0, httpx@==0.28.1, hyperframe@==6.1.0, idna@==3.10, itsdangerous@==2.2.0, Jinja2@==3.1.6, kiwisolver@==1.4.7, MarkupSafe@==3.0.2, matplotlib@==3.9.4, msgpack@==1.1.1, numpy@==2.0.2, packaging@==25.0, pillow@==11.2.1, proto-plus@==1.26.1, protobuf@==6.31.1, pyasn1@==0.6.1, pyasn1_modules@==0.4.2, PyAudio@==0.2.14, pyAudioAnalysis@==0.3.14, pycparser@==2.22, pydantic@==2.11.7, pydantic_core@==2.33.2, pydub@==0.25.1, PyJWT@==2.10.1, PyMuPDF@==1.26.1, pyparsing@==3.2.3, PyPDF2@==3.0.1, python-dateutil@==2.9.0.post0, python-dotenv@==1.1.0, requests@==2.32.4, rsa@==4.9.1, scipy@==1.13.1, six@==1.17.0, sniffio@==1.3.1, tqdm@==4.67.1, typing_extensions@==4.14.0, typing-inspection@==0.4.1, uritemplate@==4.2.0, urllib3@==2.4.0, Werkzeug@==3.1.3

### Recent commits (newest first)

- Audio Recording Functional v1
- Update questions.html
- Update style.css
- Update full_process.py
- Create render.yaml
- Update requirements.txt
- Working Draft
- Update email.css
- Create resume.css
- Update resume-upload.html
- Update app.py
- updates
- updates
- Add files via upload
- Update signup.html
- Update apptest2.py
- Delete app_copy.py
- Delete apptest.py
- Delete app.py
- google authentication completed

## Key source files (fetched from GitHub, selected and truncated for size)

### requirements.txt

```
annotated-types==0.7.0
anyio==4.9.0
# audioop-lts==0.2.1
Authlib==1.6.0
blinker==1.9.0
CacheControl==0.14.3
cachetools==5.5.2
certifi==2025.6.15
cffi==1.17.1
charset-normalizer==3.4.2
click==8.1.8
contourpy==1.3.0
cryptography==45.0.4
cycler==0.12.1
deprecation==2.1.0
distro==1.9.0
eyeD3==0.9.8
filetype==1.2.0
firebase-admin==6.9.0
Flask==3.1.1
flask-cors==6.0.1
fonttools==4.58.4
google-api-core==2.25.1
google-api-python-client==2.172.0
google-auth==2.40.3
google-auth-httplib2==0.2.0
google-cloud-core==2.4.3
google-cloud-firestore==2.21.0
google-cloud-storage==3.1.1
google-crc32c==1.7.1
google-resumable-media==2.7.2
googleapis-common-protos==1.70.0
groq==0.28.0
grpcio==1.73.0
grpcio-status==1.73.0
h11==0.16.0
h2==4.2.0
hpack==4.1.0
httpcore==1.0.9
httplib2==0.22.0
httpx==0.28.1
hyperframe==6.1.0
idna==3.10
itsdangerous==2.2.0
Jinja2==3.1.6
kiwisolver==1.4.7
MarkupSafe==3.0.2
matplotlib==3.9.4
msgpack==1.1.1
numpy==2.0.2
packaging==25.0
pillow==11.2.1
proto-plus==1.26.1
protobuf==6.31.1
pyasn1==0.6.1
pyasn1_modules==0.4.2
PyAudio==0.2.14
pyAudioAnalysis==0.3.14
pycparser==2.22
pydantic==2.11.7
pydantic_core==2.33.2
pydub==0.25.1
PyJWT==2.10.1
PyMuPDF==1.26.1
pyparsing==3.2.3
PyPDF2==3.0.1
python-dateutil==2.9.0.post0
python-dotenv==1.1.0
requests==2.32.4
rsa==4.9.1
scipy==1.13.1
six==1.17.0
sniffio==1.3.1
tqdm==4.67.1
typing-inspection==0.4.1
typing_extensions==4.14.0
uritemplate==4.2.0
urllib3==2.4.0
Werkzeug==3.1.3

```

### render.yaml

```yaml
services:
  - type: web
    name: employAI
    env: python
    buildCommand: "python apptest.py"
    startCommand: gunicorn app:app

```

### resume_utils.py

```python
import PyPDF2
import os
from groq import Groq
import json
import re

def extract_text(pdf_path):
    with open(pdf_path, 'rb') as pdf:
        reader = PyPDF2.PdfReader(pdf, strict=False)
        pdf_text = []

        for i,page in enumerate(reader.pages):
            content = page.extract_text()
            if content:
                cleaned_lines = []
                lines = content.splitlines()
                for line in lines:
                    stripped_line = line.strip()
                    if stripped_line:
                        cleaned_lines.append(stripped_line)

                cleaned = '\n'.join(cleaned_lines)
                pdf_text.append(f"--- Page {i+1} ---\n{cleaned}")

        return '\n\n'.join(pdf_text)
    
def res_sum(job, resume_text):
    client = Groq(api_key='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')

    system_prompt = """
    You are a resume parsing assistant. Extract structured data from resumes that is most relevant to the job they are applying for. Return only valid JSON with no extra text.
    """

    user_prompt = f"""
    ### Instructions
    Applicant for: {job}
    Parse the below resume in its entirety. Identify which skills are most relevant to the above job. 
    Also scan through the full descriptions of each job experience mentioned and condense it to the parts that are most relevant.
    Put this below in the requested format and specfications)
    - full_name (string)
    - skills (list of strings)
    - job_experience (list of relevant experiences based on the job they are applying to)
        - company (string)
        - title (string)
        - duration (string)
        - description (string, 2 sentence summarizing what the person did after reading the entire experience)

    ### Context
    "Skills" often appear in bullet points or after section headers like "Technical Skills".
    "Job Experience" includes company names, job titles, durations of employment, and what they did.

    ### Input
    {resume_text}

    ### Expected Output
    {{{{  
        "full_name": "",
        "skills": [],
        "job_experience": []
     }}}}
    ###
    """

    chat_completion = client.chat.completions.create(
        messages=[
            { "role": "system", "content": system_prompt },
            { "role": "user", "content": user_prompt }
        ],
        model="gemma2-9b-it", 
        temperature=0.2,         # deterministic output
        max_tokens=700,
        stop=["###"],            # optional: stops after expected output
        seed=700                 # optional: makes output repeatable
    )
    
    raw_output = chat_completion.choices[0].message.content
    return raw_output
```

### questions_utils.py

```python
import json
from groq import Groq
import os

api_key = os.getenv("GROQ_API_KEY")
def mock_inter(res, job, industry, level):
    output_format = {
        "question1": [""],
        "question2": [""],
        "question3": [""],
        "question4": [""],
        "question5": [""],
        "question6": [""],
        "question7": [""],
        "question8": [""],
        "question9": [""],
        "question10": [""]
    }

    expected_output = json.dumps(output_format, indent=4)
    
    client = Groq(api_key=api_key)


    system_prompt ="""
    You are a mock interviewer. Use the below industry, job information, and level, as well as provided resume information of the user to generate 10 mock interview questions. Return only valid JSON with no extra text.
    """
    
    user_prompt = f"""
    ### Inputs
    Industry: {industry}
    level: {level}
    Applicant for: {job}
    User Relevant Resume Sections (in JSON format): {res}
    
    ### Instructions
    You are to help the user get a job in the mentioned industry, level and position.
    First, use these criteria to get information on the industry and job, like necessary skills, qualifications, and ideal candidate metrics
    Next, parse the Relevant resume info to find the user's strengths(stuff to talk more about in interviews) and weaknesses as per the above industry and job info
    Now, Using the metrics found and the strengths and weaknesses, generate a list of mock interview questions that ensure that the user can capitalize on their strenghts and shield their weaknesses and be prepared for an interview.
    Ensure question difficulty and relevance is based on levels (intern, junior, etc is lower level, vs senior, manager, etc is higher)

    Make Sure to get 10 Questions, 60-70 percent of which should be role and industry specific, rest should be behavioral or other type questions that hiring managers would want
    Behavioral questions are where the candidate must describe past experiences or situations, and how that impacts their skills, abilities, and character. These are not always industry specific.
    An example of a behavioral question is: Tell me about a time when you realized you needed a deeper level of subject matter expertise to do your job well. What did you do about it? What was the outcome? Is there anything you would have done differently?

    Remember to place this exactly in the specified format, with no other text whatsoever. Remember to get 10 full questions

    ### Context
    "Industry" is usually the broader field in which the applicant is applying in 
    "Job" is the specific position in the industry they want
    "Level" is the type of job and amount of experience needed

    ### Expected Output(exactly as specified below, no other text whatsoever)
    {expected_output}
    
    """
    messages=[
        { "role": "system", "content": system_prompt },
        { "role": "user", "content": user_prompt }
    ]

    chat_completion = client.chat.completions.create(
        messages= messages,
        model="gemma2-9b-it", 
        temperature=0.2,         # deterministic output
        max_tokens=7000,
        stop=["###"],            # optional: stops after expected output
        seed=700,                 # optional: makes output repeatable
        response_format={ "type": "json_object", "json_object": expected_output }  # ensures output is in JSON format
    )

    messages.append({ "role": "assistant", "content": chat_completion.choices[0].message.content})
    print("User prompt:\n", user_prompt) #debug to remove
    print("Model reply:\n", chat_completion.choices[0].message.content)
    return chat_completion.choices[0].message.content

```

### audio.py

```python
import os
from groq import Groq
# import speechbrain
from pyAudioAnalysis import audioBasicIO, ShortTermFeatures
import numpy as np
import pyaudio 
import wave
import time
import threading

# from speechbrain.inference.interfaces import foreign_class

FORMAT = pyaudio.paInt16
CHANNELS = 1
RATE = 8000
CHUNK = 2048
UPLOAD_FOLDER = "uploads"
OUTPUT_FILENAME = os.path.join(UPLOAD_FOLDER, "answer.wav")
api_key = os.getenv("GROQ_API_KEY")
api_key1 = os.getenv("LMNT_API_KEY")

def audio_to_text(audio_path):
    client = Groq(api_key=api_key)
    filename = os.path.dirname(audio_path) + audio_path

    with open(filename, "rb") as file:
        chat_completion = client.audio.transcriptions.create(
            model="whisper-large-v3", 
            temperature=0,         # deterministic output
            response_format="verbose_json",
            file=file,
            language="en"
        )
        
        return chat_completion

# def emotion(audio_path):  
#     classifier = foreign_class(source="speechbrain/emotion-recognition-wav2vec2-IEMOCAP", pymodule_file="custom_interface.py", classname="CustomEncoderWav2vec2Classifier")
#     out_prob, score, index, text_lab = classifier.classify_file(audio_path)
#     print(text_lab)
#     return text_lab

def audio_features(audio_path):
    # Load audio file (make sure it's WAV, mono)
    [Fs, x] = audioBasicIO.read_audio_file(audio_path)

    # If stereo, use only one channel
    if x.ndim > 1:
        x = x[:, 0]

    # Extract features with window=50ms and step=25ms
    F, f_names = ShortTermFeatures.feature_extraction(x, Fs, 0.050 * Fs, 0.025 * Fs)

    # Find indexes of features you want
    energy_idx = f_names.index('energy')
    zcr_idx = f_names.index('zcr')
    spectral_entropy_idx = f_names.index('spectral_entropy')

    # Extract those features across all frames
    energy = F[energy_idx, :]
    zcr = F[zcr_idx, :]
    spectral_entropy = F[spectral_entropy_idx, :]
    # Compute mean values as summary statistics
    mean_energy = np.mean(energy)
    mean_zcr = np.mean(zcr)
    mean_spectral_entropy = np.mean(spectral_entropy)
    return mean_energy, mean_zcr, mean_spectral_entropy

def groqInput(energy, zcr, entropy):   
    energyS = ""
    if (energy <= 0.003):
        energyS = "The candidate is whispering or mumbling theroughout the response"; 
    elif (energy > 0.003 and energy <= 0.01):
        energyS = "The candidate is tends to be whispering/mumbling or is quiet through out the response"
    elif (energy > 0.01 and energy <= 0.04):
        energyS = "The candidate had a normal conversation, however, in a few parts they were quiet"
    elif (energy > 0.04 and energy <= 0.1):
        energyS = "The candidate talked in loud voice, so they were passionate"
    else: 
        energyS = "The candidate tended to shout"

    entroS = ""
    if (entropy <= 1.3):
        entroS = "The candidate has a steady, tonal, almost flat response (monotone)"; 
    elif (entropy > 1.3 and entropy <= 2.3):
        entroS = "The candidate has a controlled and focused speech"
    elif (entropy > 2.3 and entropy <= 3.3):
        entroS = "The candidate is speaking in a normal tone for the most part"
    elif (entropy > 3.3 and entropy <= 4.5):
        entroS = "The candidate seems to vary in tone a lot"
    else: 
        entroS = "The candidate tone is completely unstructured"

    zcrS = ""
    if (zcr <= 0.1):
        zcrS = "Clear Speech means they spoke steady"; 
    elif (zcr <= 0.15):
        zcrS = "Somewhat messy means they spoke steady but with mix of shaking (a little nervous)"
    else:
        zcrS = "Very Unclear means not sure if they were speaking or if it was just noise from audio"
    
    return energyS, entroS, zcrS

def record():
    os.makedirs(UPLOAD_FOLDER, exist_ok=True)
    audio = pyaudio.PyAudio()
    stream = audio.open(format=FORMAT, channels = CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK)

    frames = []
    
    input("press ENTER to start recording.")
    print("Recording... Press ENTER again to stop")

    recording = True

    def stop_recording():
        nonlocal recording
        input()
        recording = False
        print("Stopping recording...")

    stopper_thread = threading.Thread(target=stop_recording)
    stopper_thread.start()

    while recording:
        data = stream.read(CHUNK)
        frames.append(data)


    stopper_thread.join()
    stream.stop_stream()
    stream.close()
    audio.terminate()

    waveFile = wave.open(OUTPUT_FILENAME, 'wb')
    waveFile.setnchannels(CHANNELS)
    waveFile.setsampwidth(audio.get_sample_size(FORMAT))
    waveFile.setframerate(RATE)
    waveFile.writeframes(b''.join(frames))
    waveFile.close()

    return OUTPUT_FILENAME

```

### audiotest.py

```python
import os
import json
from flask import Flask, Blueprint, app, request, jsonify, redirect, render_template
from werkzeug.utils import secure_filename
from utils.post_audio import feedback, follow_up_thread
from utils.resume_utils import extract_text, res_sum
from utils.questions_utils import mock_inter
from utils.audio import record, audio_features, groqInput, audio_to_text
from flask import session

app.secret_key = os.getenv("SESSION_SECRET_KEY")

full_process_bp = Blueprint("full", __name__)
UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = {"pdf", "wav"}

def allowed_file(filename):
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS


@full_process_bp.route("/resume", methods=["POST"])
def resume():
    file = request.files.get("resume")
    job = request.form.get("job")
    industry = request.form.get("industry")
    level = request.form.get("level")

    if not file or not allowed_file(file.filename):
        return jsonify({"error": "Invalid or missing resume file."}), 400
    if not job or not industry or not level:
        return jsonify({"error": "Missing job, industry, or level."}), 400

    os.makedirs(UPLOAD_FOLDER, exist_ok=True)
    filename = secure_filename(file.filename)
    file_path = os.path.join(UPLOAD_FOLDER, filename)
    file.save(file_path)

    resume_text = extract_text(file_path)
    while(True):
        try:
            summary_json = res_sum(job, resume_text)
            break
        except:
            continue
    
    session["resume"] = summary_json
    session["job"] = job
    session["industry"] = industry
    session["level"] = level
    
    return redirect("/questions-loading")

@full_process_bp.route("/questions-loading")
def loading_page():
    return render_template("questions_loading.html")

@full_process_bp.route("/generate-questions")
def questions():
    summary_json = session.get("resume")
    job = session.get("job")
    industry = session.get("industry")
    level = session.get("level")

    summary_str = json.dumps(summary_json)
    question_str = mock_inter(summary_str, job, industry, level)
    questions_json = json.loads(question_str)
    session["questions"] = questions_json
    session["responses"] = {}
    session["feedbacks"] = {}
    session["curr_index"] = 1
    session["zcr"] = []
    session["entropy"] = []
    session["energy"] = []

    return redirect("/questions")

@full_process_bp.route("/record", methods=["POST"])
def audio_recording(): 
    file_path = record()
    zcr = session.get("zcr", [])
    energy = session.get("energy", [])
    entropy = session.get("entropy", [])

    store = audio_features(file_path)
    zcr.append(store[0])
    energy.append(store[1])
    entropy.append(store[2])
    session["zcr"] =zcr
    session["energy"] = energy
    session["entropy"] =entropy
    responses = session.get("responses")
    q_key = f"question{session.get('curr_index')}"
    responses.setdefault(q_key, []).append(audio_to_text(file_path))
    session["responses"] = responses

    return jsonify({"message": "Audio processed successfully"})

@full_process_bp.route("/followupfeedback", methods=["GET"])
def post_quest():
    questions = session.get("questions")
    q_thread = questions[f"question{session.get('curr_index')}"]
    question = ""
    if(len(q_thread) == 1):
        responses = session.get("responses")
        r_thread = responses[f"question{session.get('curr_index')}"]
        question = follow_up_thread(q_thread,r_thread, session.get("resume"), session.get("job"), session.get("industry"), session.get("level"))["new_q"]
    if(question != ""):
        q_thread.append(question)
        questions[f"question{session.get('curr_index')}"] = q_thread
        session["questions"] = questions
        return redirect("/record")
    else:
        fback = feedback(q_thread, r_thread, 
                session.get("energy"), session.get("entropy"), session.get("zcr"), session.get("resume"))
        feedbacks = session.get("feedbacks")
        feedbacks[f"question{session.get('curr_index')}"] = fback
        session["feedbacks"] = feedbacks
        return jsonify({
            f"Message": "Feedback saved for question {session.get('curr_index')}. Click next to continue.",
            f"question{session.get('curr_index')} feedback": fback
        })
    
@full_process_bp.route("/current_question", methods=["POST"])
def get_q():
    index = session.get("curr_index")

    if (index < 1 or index > 10):
        return jsonify({"error": "No more questions"}), 404
    
    quest = session.get("questions")[f"question{index}"]
    
    return quest

@full_process_bp.route("/next_question", methods=["GET"])
def next_q():
    index = session.get("curr_index")

    if (index+1 > 10):
        return jsonify({"error": "No more questions"}), 404
    
    session["curr_index"] = index +1

    session["zcr"] = []
    session["energy"] = []
    session["entropy"] = []
    return jsonify({"message": f"Moved to question {index + 1}"})


    






    
    
# @app.route("/main")
# def main():
#     job_position = "Software Engineer"
#     resume = extract_text("Unnati_resume.pdf")
#     resume_info = res_sum(job_position, resume)
#     resume_info = shorten(resume_info, [], [])
#     print(resume_info)
#     industry_type = "Tech"
#     experience_level = "Intern"

#     questions = eval(mock_inter(resume_info, job_position, industry_type, experience_level))
#     responses={}
#     feedbacks = {}
    
#     print(questions)
#     for i in range(1, 3):
#         zcr = [-1,-1]
#         energy = [-1,-1]
#         entropy = [-1,-1]

#         question_key = f"question{i}"
#         if question_key in questions:
#             question_text = questions[question_key][0]
#             print(f"Question {i}: {question_text}")
#             record()
#             zcr[0], energy[0] , entropy[0]  = audio_features("answer.wav")
#             responses[question_key] = [audio_to_text("answer.wav")]
#             questions[questio
[truncated — 629 more characters]
```

### post_audio.py

```python
import json
import os
from groq import Groq

api_key = os.getenv("GROQ_API_KEY")

def feedback(question, response, energy, entropy, zcr, resume_info):
    client = Groq(api_key=api_key)
    expected_output = {
        "feedback": ""
    }
    system_prompt = """
    You are a mock interviewer giving feedback. Use the text translation and information about candidates audio response, like their energy and confidence, to create response. Also the question they answered is provided. 
    Return this in a PARAGRAPH only in valid JSON format!
    """

    user_prompt = f"""
    ### Input
    resume: {resume_info}
    Question thread: {question}
    Response thread: {response}
    Energy levels of candidate(energy): {energy}
    Randomness of the persons tone(entropy): {entropy}
    Clearity of voice(zcr): {zcr}
    
    ### Context
    resume has the user's resume information
    Question thread has either one or two questions asked (base questions and its follow up)
    Response thread has the answer to these  questions in order
    the energy, entropy, and zcr values are also lists that are in the same order as the question thread. i.e first zcr value is mea zcr for first response, etc.
    Energy, entropy, and zcr are all lists as they have 2 inputs (one for each question) 
    IMPORTANT: if there was only one question in the question thread, then only use the first index for each of ZCR, entropy, and energy lists


    ### Instructions
    First, read through the entire response. Then, write detailed feedback on the response using the topics below as guidance. 
    Be sure to include **specific suggestions** on how the response could be improved (e.g., rephrasing, providing examples, elaborating further).
    If this is not the first response in the full conversation, be sure to give feedback with that context in mind. 

    Topics to cover in your feedback:
    1) Was the response directed toward the question, or did the candidate go off-topic?
    2) Was the explanation clear and easy to follow?
    3) Did the response go beyond surface-level ideas?
    4) Were the best examples or reasoning from resume used to support claims?
    5) Was there anything specifc resume they could have used? 

    Next, assess the candidate's **demeanor** based on the tone of their response:
    - Was the energy level appropriate?
    - Did the tone feel too random or inconsistent?
    - Was the communication clear and professional?

    Offer advice on any adjustments they should make to their tone, clarity, or energy level.
    Compile all of this into 2 or 3 sentences of feedback.
    

    ### 
    #Output it exactly as in the below format, all as the value for the key "feedback", do not create any other keys.
    ### Expected Output
    {expected_output}
    ###
    """
    chat_completion = client.chat.completions.create(
        messages=[
        { "role": "system", "content": system_prompt },
        { "role": "user", "content": user_prompt }],
        model="llama-3.1-8b-instant", 
        temperature=0.2,        
        max_tokens=2000,
        stop=["###"],           
        seed=700,
        response_format={ "type": "json_object", "json_object": expected_output }             
    )
    return chat_completion.choices[0].message.content

def follow_up_thread(question, response, res, job, industry, level):
    client = Groq(api_key=api_key)
    system_prompt = """
    You are a mock interviewer who wants to test if the user is a good candidate for the job. Use the context and the user's interview response to assess if they are to be selected.
    Return this only in valid JSON format!
    """

    expected_output = { "new_q": ""}

    user_prompt = f"""
    ### Input
    Resume: {res}
    Job: {job}
    Industry: {industry}
    level: {level}
    Question thread: {question}
    Response thread: {response}

    ### Context
    First, use these criteria to get information on the industry, job and level, like necessary skills, qualifications, and ideal candidate metrics
    Next, parse the Relevant resume info to find the user's strengths(stuff to talk more about in interviews) and weaknesses as per the above industry and job info
    In this interview, You want to address the user's strengths and weaknesses, and ensure they are a good candidate for the job. 
    None of these metrics or strengths, weaknesses, etc should be returned in the output, just used as context.

    "Question thread" is the list of questions in this question thread
    "Response thread" is the list of responses to the question thread, in order.
    
    
    ### Instructions
    Read through the question and response thread. If the user has not sufficiently answered the question, 
    generate a follow-up question that digs deeper into the topic, or clarifies the user's response. Otherwise, 
    return an EMPTY string.

    REMEMBER: You only get one follow-up question, so if you ask one, make sure it is a good one that will help the user improve their response
    and that it is detailed enough to not need multiple more follow-up questions.

    ### Expected Output
        {expected_output}
    ###
    """
    chat_completion = client.chat.completions.create(
        messages=[
        { "role": "system", "content": system_prompt },
        { "role": "user", "content": user_prompt }],
        model="llama-3.1-8b-instant", 
        temperature=0.2,         # deterministic output
        max_tokens=700,
        stop=["###"],            # optional: stops after expected output
        seed=700,
        response_format={ "type": "json_object", "json_object": expected_output }                 # optional: makes output repeatable
    )

    return chat_completion.choices[0].message.content

def shorten(res, questions, responses):
    client = Groq(api_key=api_key)
    system_prompt = """
    You are a summarizer. You are to shorten information about a resume and interview questions and responses as much as possible, while still r
[truncated — 1167 more characters]
```

### full_process.py

```python
import os
import json
import fitz
from io import BytesIO
import firebase_admin
from firebase_admin import credentials, firestore, auth as fb_auth, storage
from flask import Flask, Blueprint, app, request, jsonify, redirect, render_template
from werkzeug.utils import secure_filename
from utils.post_audio import feedback, follow_up_thread
from utils.resume_utils import extract_text, res_sum
from utils.questions_utils import mock_inter
from utils.audio import record, audio_features, groqInput, audio_to_text
from flask import session
import subprocess

app.secret_key = os.getenv("SESSION_SECRET_KEY")

full_process_bp = Blueprint("full", __name__)
UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = {"pdf", "wav"}

def allowed_file(filename):
    return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS

def extract_text_from_pdf(pdf_file):
    text = ""
    with fitz.open(stream=pdf_file.read(), filetype="pdf") as doc:
        for page in doc:
            text += page.get_text()
    return text.strip()

@full_process_bp.route("/resume", methods=["POST"])
def resume():
    user_email = session["user_email"]
    db = firestore.client()
    doc = db.collection("users").document(user_email).get()
    job = request.form.get("job")
    industry = request.form.get("industry")
    level = request.form.get("level")
    if not doc.exists or "resume_filename" not in doc.to_dict():
        return redirect("/resume-upload")
    if not job or not industry or not level:
        return jsonify({"error": "Missing job, industry, or level."}), 400
    user_data = doc.to_dict()
    filename = user_data["resume_filename"]
    bucket = storage.bucket()
    blob = bucket.blob(f"resumes/{user_email}/{filename}")
    resume_bytes = blob.download_as_bytes()
    
    resume_text = extract_text_from_pdf(BytesIO(resume_bytes))
    summary_json = res_sum(job, resume_text)
    
    session["resume"] = summary_json
    session["job"] = job
    session["industry"] = industry
    session["level"] = level

    return redirect("/full/questions-loading")

@full_process_bp.route("/generate-questions")
def questions():
    summary_json = session.get("resume")
    job = session.get("job")
    industry = session.get("industry")
    level = session.get("level")

    summary_str = json.dumps(summary_json)
    question_str = mock_inter(summary_str, job, industry, level)
    questions_json = json.loads(question_str)
    session["questions"] = questions_json
    session["responses"] = {}
    session["feedbacks"] = {}
    session["curr_index"] = 1
    session["zcr"] = []
    session["entropy"] = []
    session["energy"] = []

    return redirect("/full/questions")

@full_process_bp.route("/record", methods=["POST"])
def audio_recording():
    audio_file = request.files.get("audio")
    if not audio_file:
        console.log("NO AUDIO FILE")
        return jsonify({"error": "No audio uploaded"}), 400

    
    webm_path = os.path.join(UPLOAD_FOLDER, "temp.webm")
    wav_path = os.path.join(UPLOAD_FOLDER, "temp.wav")
    audio_file.save(webm_path)

    zcr = session.get("zcr", [])
    energy = session.get("energy", [])
    entropy = session.get("entropy", [])

    subprocess.run(["ffmpeg", "-i", webm_path, wav_path, "-y"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)

    store = audio_features(wav_path)
    zcr.append(store[0])
    energy.append(store[1])
    entropy.append(store[2])
    session["zcr"] = zcr
    session["energy"] = energy
    session["entropy"] = entropy

    responses = session.get("responses")
    q_key = f"question{session.get('curr_index')}"
    answer = audio_to_text(wav_path).text
    print(f"AHHHHH q NAME: {answer}")
    
    responses.setdefault(q_key, []).append(answer)
    session["responses"] = responses

    return jsonify({"message": "Audio processed successfully"})

@full_process_bp.route("/followupfeedback", methods=["GET"])
def post_quest():
    index = session.get("curr_index")
    questions = session.get("questions")
    responses = session.get("responses")

    q_key = f"question{index}"
    q_thread = questions.get(q_key, [])
    r_thread = responses.get(q_key, [])

    if len(q_thread) == 1:
        # Only original question so far → generate follow-up
        raw = follow_up_thread(q_thread, r_thread,
                                 session.get("resume"),
                                 session.get("job"),
                                 session.get("industry"),
                                 session.get("level"))
        parsed = json.loads(raw) if isinstance(raw, str) else raw
        new_q = parsed["new_q"]
        if new_q:
            q_thread.append(new_q)
            questions[q_key] = q_thread
            session["questions"] = questions
            return redirect("/full/questions")  # go back to UI for follow-up recording

    # Otherwise, it's time for feedback
    fback = feedback(q_thread, r_thread,
                     session.get("energy"),
                     session.get("entropy"),
                     session.get("zcr"),
                     session.get("resume"))
    feedbacks = session.get("feedbacks", {})
    feedbacks[q_key] = fback
    session["feedbacks"] = feedbacks

    return jsonify({
        "Message": f"Feedback saved for question {index}. Click next to continue.",
        f"{q_key} feedback": fback
    })

@full_process_bp.route("/current_question", methods=["POST"])
def get_q():
    index = session.get("curr_index")

    if (index < 1 or index > 10):
        return jsonify({"error": "No more questions"}), 404
    
    quest = session.get("questions")[f"question{index}"]
    return quest

@full_process_bp.route("/next_question", methods=["GET"])
def next_q():
    index = session.get("curr_index")

    if (index+1 > 10):
        return jsonify({"error": "No more questions"}), 404
    
    session["curr_index"] = index +1

    session["zcr"] = []
    session["energy"] = []
    session["entropy"] = []
    return jsonify({"message": f"Moved
[truncated — 1991 more characters]
```

### templates/interview-landing.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Resume Upload - Employ AI</title>
  <link rel="stylesheet" href="/static/style.css">
</head>
<body class="dark-mode">
  <nav class="navbar">
    <div class="logo"><a href="/">🎓Employ AI</a></div>
    <ul class="nav-links">
      <li><a href="/resume-upload">Resume Upload</a></li>
      <li><a href="/generate-email">Generate Email</a></li>
      <li><a href="/full/resume">Mock Interview</a></li>
      <li><a href="/logout">Log Out</a></li>
    </ul>
  </nav>

  <main class="auth-container">
    <h1>Enter position details</h1>
    <form method="POST" action="/full/resume" enctype="multipart/form-data" class="auth-form">
      <input type="text" name="job" placeholder="Job Title" required><br>
      <input type="text" name="industry" placeholder="Industry" required><br>
      <input type="text" name="level" placeholder="Experience Level" required><br>
      <div class="button-wrapper">
        <button type="submit"><a href="/full/questions-loading">Start Interview</a></button>
      </div>
    </form>
  </main>
</body>
</html>
```

### templates/login.html

```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Login - Employ AI</title>
  <link rel="stylesheet" href="/static/style.css">
</head>
<body class="dark-mode">
  <div class="title">
  <nav class="navbar">
    <div class="logo"><a href="/">🎓Employ AI</a></div>
    <ul class="nav-links">
      <li><a href="/resume-upload">Resume Upload</a></li>
      <li><a href="/generate-email">Generate Email</a></li>
      <li><a href="/full/resume">Mock Interview</a></li>
      <li><a href="/signup">Sign Up</a></li>
    </ul>
  </nav>

  <main class="auth-container">
    <h1>Log In</h1>
    <form method="POST" action="/login" class="auth-form">
      <input type="email" name="email" placeholder="Email" required>
      <input type="password" name="password" placeholder="Password" required>
      <div class="button-wrapper">
        <button type="submit" class="full-btn">Log In</button>
      </div>
    </form>

    <p class="or-divider">or</p>

    <div class="button-wrapper">
      <a href="/login/google" class="google-btn full-btn">
        <img src="https://www.gstatic.com/firebasejs/ui/2.0.0/images/auth/google.svg" alt="Google logo">
        Log in with Google
      </a>
    </div>

    <p>Don't have an account? <a href="/signup">Sign up</a></p>
  </main>
</body>
</html>

```

[19 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]