LiamKhoaLe commited on
Commit
e7b1e22
·
1 Parent(s): b818ff9

Deploy interview assistant app

Browse files
Dockerfile ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Set environment variables (may need to change authority)
4
+ ENV PYTHONDONTWRITEBYTECODE=1 \
5
+ PYTHONUNBUFFERED=1
6
+
7
+ # ENV to inject secret
8
+ # ARG HF_TOKEN
9
+ # ENV HF_TOKEN=${HF_TOKEN}
10
+
11
+ # Create working directory (and authorised token)
12
+ WORKDIR /app
13
+
14
+ # Install SSL root certs and system deps required by pymongo + DNS
15
+ RUN apt-get update && apt-get install -y --no-install-recommends \
16
+ ca-certificates curl dnsutils gcc openssl && \
17
+ rm -rf /var/lib/apt/lists/*
18
+
19
+ # Copy and install dependencies
20
+ COPY requirements.txt .
21
+ RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
22
+
23
+ # Copy source code (app files)
24
+ COPY . .
25
+
26
+ # Expose the port used by Uvicorn
27
+ EXPOSE 7860
28
+
29
+ # Run the FastAPI application using Uvicorn
30
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--log-level", "debug"]
app.py ADDED
@@ -0,0 +1,123 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Access site: https://binkhoale1812-interview-ai.hf.space/
2
+ import os
3
+ import tempfile
4
+ import psutil
5
+ from pathlib import Path
6
+ from typing import Dict
7
+
8
+ from fastapi import FastAPI, File, UploadFile, HTTPException
9
+ from fastapi.middleware.cors import CORSMiddleware
10
+ from fastapi.responses import JSONResponse, FileResponse
11
+ from fastapi.staticfiles import StaticFiles
12
+
13
+ import google.generativeai as genai
14
+ from transformers import pipeline, AutoProcessor, AutoModelForSpeechSeq2Seq
15
+
16
+ ############################################
17
+ # ── Configuration ────────────────────────
18
+ ############################################
19
+
20
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
21
+ if not GEMINI_API_KEY:
22
+ raise RuntimeError("GEMINI_API_KEY environment variable must be set!")
23
+
24
+ # Tiny Whisper model is light enough for CPU Spaces; change if GPU is available
25
+ ASR_MODEL_ID = "openai/whisper-tiny" # ~39 MB
26
+ ASR_LANGUAGE = "en" # Force to English for interview setting
27
+
28
+ ############################################
29
+ # ── FastAPI App ───────────────────────────
30
+ ############################################
31
+
32
+ app = FastAPI(title="Interview Q&A Assistant", docs_url="/docs")
33
+ app.add_middleware(
34
+ CORSMiddleware,
35
+ allow_origins=["*"],
36
+ allow_methods=["*"],
37
+ allow_headers=["*"],
38
+ )
39
+
40
+ # Serve frontend assets
41
+ app.mount("/statics", StaticFiles(directory="statics"), name="statics")
42
+
43
+ ############################################
44
+ # ── Global objects (lazy‑loaded) ──────────
45
+ ############################################
46
+
47
+ asr_pipeline = None # Speech‑to‑text
48
+ llm = None # Gemini model
49
+
50
+
51
+ @app.on_event("startup")
52
+ async def load_models():
53
+ """Load Whisper."""
54
+ global asr_pipeline, llm
55
+ # Whisper tiny – seq2seq pipeline
56
+ asr_pipeline = pipeline(
57
+ "automatic-speech-recognition",
58
+ model=ASR_MODEL_ID,
59
+ chunk_length_s=30,
60
+ torch_dtype="auto",
61
+ device="cpu",
62
+ )
63
+
64
+
65
+ ############################################
66
+ # ── Helpers ───────────────────────────────
67
+ ############################################
68
+
69
+ def build_prompt(question: str) -> str:
70
+ """Craft a prompt that elicits concise, structured answers."""
71
+ return (
72
+ "You are a helpful career‑coach AI. Answer the following interview "
73
+ "question clearly and concisely, offering practical insights when "
74
+ "appropriate.\n\n"
75
+ f"Interview question: \"{question}\""
76
+ )
77
+
78
+ def memory_usage_mb() -> float:
79
+ return psutil.Process().memory_info().rss / 1_048_576 # bytes→MiB
80
+
81
+ ############################################
82
+ # ── Routes ────────────────────────────────
83
+ ############################################
84
+
85
+ @app.get("/")
86
+ async def root() -> FileResponse:
87
+ """Serve the single‑page app."""
88
+ return FileResponse(Path("statics/index.html"))
89
+
90
+
91
+ @app.post("/voice-transcribe")
92
+ async def voice_transcribe(file: UploadFile = File(...)): # noqa: B008
93
+ """Receive audio, transcribe, push to Gemini, return answer."""
94
+ if file.content_type not in {"audio/wav", "audio/x-wav", "audio/mpeg"}:
95
+ raise HTTPException(status_code=415, detail="Unsupported audio type")
96
+ # Save to a temp file (Whisper expects a filename/bytes)
97
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
98
+ tmp.write(await file.read())
99
+ tmp_path = tmp.name
100
+ try:
101
+ # ── 1. Transcribe
102
+ transcript: Dict = asr_pipeline(tmp_path, generate_kwargs={"language": ASR_LANGUAGE})
103
+ question = transcript["text"].strip()
104
+ if not question:
105
+ raise ValueError("Empty transcription")
106
+ # ── 2. LLM answer
107
+ prompt = build_prompt(question)
108
+ # Gemini Flash 2.5 – tuned for short latency
109
+ client = genai.Client(api_key=GEMINI_API_KEY)
110
+ response = client.models.generate_content(
111
+ model="gemini-2.5-flash-preview-04-17",
112
+ contents=prompt
113
+ )
114
+ answer = response.text.strip()
115
+ return JSONResponse(
116
+ {
117
+ "question": question,
118
+ "answer": answer,
119
+ "memory_mb": round(memory_usage_mb(), 1),
120
+ }
121
+ )
122
+ finally:
123
+ os.remove(tmp_path) # Rm audio when done
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core server
2
+ fastapi
3
+ uvicorn[standard]
4
+ aiofiles # Static file serving
5
+ python-multipart # File uploads
6
+
7
+ # Voice‑to‑text (Whisper via 🤗 Transformers)
8
+ transformers>=4.40
9
+ torch
10
+ huggingface_hub
11
+
12
+ # Gemini Flash 2.5
13
+ google-genai
14
+ python-dotenv # Optional – read GOOGLE_API_KEY
15
+
16
+ # Utilities
17
+ psutil # Lightweight health logging
statics/.DS_Store ADDED
Binary file (6.15 kB). View file
 
statics/icon.png ADDED
statics/index.html ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Interview Q&A Assistant</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <link rel="stylesheet" href="/statics/styles.css" />
8
+ <link rel="icon" type="image/png" href="/statics/icon.png" />
9
+ </head>
10
+ <body>
11
+ <main class="container">
12
+ <h1>Interview Q&amp;A Assistant</h1>
13
+ <p class="subtitle">Hold the button, ask your interview question, release to get an answer.</p>
14
+
15
+ <button id="record-button" class="record-btn">🎙 Hold&nbsp;to&nbsp;Ask</button>
16
+
17
+ <section class="output-section">
18
+ <h2>Your Question</h2>
19
+ <pre id="question-output" class="output"></pre>
20
+
21
+ <h2>AI&nbsp;Answer</h2>
22
+ <pre id="answer-output" class="output"></pre>
23
+ </section>
24
+ </main>
25
+
26
+ <script src="/statics/script.js"></script>
27
+ </body>
28
+ </html>
statics/script.js ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /*******************************
2
+ * Interview Q&A Frontend JS *
3
+ *******************************/
4
+
5
+ // Elements
6
+ const recordBtn = document.getElementById("record-button");
7
+ const questionEl = document.getElementById("question-output");
8
+ const answerEl = document.getElementById("answer-output");
9
+
10
+ // Typing animation util
11
+ function typeEffect(el, text, speed = 30) {
12
+ el.textContent = "";
13
+ let idx = 0;
14
+ const timer = setInterval(() => {
15
+ el.textContent += text.charAt(idx);
16
+ idx++;
17
+ if (idx >= text.length) clearInterval(timer);
18
+ }, speed);
19
+ }
20
+
21
+ // Audio recording setup
22
+ let mediaRecorder = null;
23
+ let chunks = [];
24
+
25
+ async function initMedia() {
26
+ try {
27
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
28
+ mediaRecorder = new MediaRecorder(stream);
29
+
30
+ mediaRecorder.ondataavailable = e => chunks.push(e.data);
31
+
32
+ mediaRecorder.onstop = async () => {
33
+ const audioBlob = new Blob(chunks, { type: "audio/wav" });
34
+ chunks = [];
35
+
36
+ // Build form data
37
+ const form = new FormData();
38
+ form.append("file", audioBlob, "record.wav");
39
+
40
+ // UX feedback
41
+ typeEffect(questionEl, "⌛ Transcribing…");
42
+ answerEl.textContent = "";
43
+
44
+ try {
45
+ const res = await fetch("https://binkhoale1812-interview-ai.hf.space/voice-transcribe", { method: "POST", body: form });
46
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
47
+ const data = await res.json();
48
+
49
+ typeEffect(questionEl, data.question || "[no speech detected]");
50
+ setTimeout(() => typeEffect(answerEl, data.answer || "[no answer]"), 500);
51
+ } catch (err) {
52
+ typeEffect(answerEl, "❌ " + err.message);
53
+ }
54
+ };
55
+ } catch (err) {
56
+ alert("Microphone access denied – please allow permissions.");
57
+ }
58
+ }
59
+
60
+ // Hold‑to‑record UX
61
+ function bindRecordBtn() {
62
+ if (!mediaRecorder) return;
63
+ recordBtn.addEventListener("mousedown", () => mediaRecorder.start());
64
+ recordBtn.addEventListener("mouseup", () => mediaRecorder.stop());
65
+
66
+ // Touch devices
67
+ recordBtn.addEventListener("touchstart", e => { e.preventDefault(); mediaRecorder.start(); });
68
+ recordBtn.addEventListener("touchend", e => { e.preventDefault(); mediaRecorder.stop(); });
69
+ }
70
+
71
+ // Init on page load
72
+ window.addEventListener("DOMContentLoaded", async () => {
73
+ await initMedia();
74
+ bindRecordBtn();
75
+ });
statics/styles.css ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Simple, clean aesthetic */
2
+ :root {
3
+ --primary: #0052cc;
4
+ --accent: #ff6666;
5
+ --bg: #f8f9fc;
6
+ --mono: "Courier New", monospace;
7
+ }
8
+
9
+ html,body {
10
+ margin: 0; padding: 0; background: var(--bg); font-family: Arial, sans-serif;
11
+ }
12
+
13
+ .container {
14
+ max-width: 720px; margin: 40px auto; padding: 24px;
15
+ background: #fff; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.08);
16
+ }
17
+
18
+ h1 { margin-top: 0; text-align: center; color: var(--primary); }
19
+ .subtitle { text-align: center; color: #444; margin-bottom: 32px; }
20
+
21
+ .record-btn {
22
+ display: block; margin: 0 auto 24px; padding: 14px 28px;
23
+ background: var(--accent); color: #fff; border: none; border-radius: 50px;
24
+ font-size: 17px; cursor: pointer; transition: background .25s;
25
+ }
26
+
27
+ .record-btn:hover { background: #ff4d4d; }
28
+
29
+ .output-section h2 { margin: 24px 0 8px; color: var(--primary); }
30
+
31
+ .output {
32
+ background: #000; color: #0f0; padding: 16px; min-height: 60px;
33
+ border-radius: 4px; overflow-x: auto; font-family: var(--mono);
34
+ white-space: pre-wrap; word-wrap: break-word;
35
+ }
temp_hf_space ADDED
@@ -0,0 +1 @@
 
 
1
+ Subproject commit b818ff92402f2d47358e68c4219e456d1dc9a815