Spaces:
Running
Running
File size: 9,164 Bytes
40f5138 3cb4983 0dc9435 e7b1e22 3cb4983 5942210 e7b1e22 3cb4983 78bb61a 243b6fb 30f82a6 3cb4983 e7b1e22 3cb4983 e7b1e22 243b6fb 3cb4983 e7b1e22 3cb4983 e7b1e22 3cb4983 e7b1e22 fa605c6 243b6fb fa605c6 4cb647e 3cb4983 243b6fb 3cb4983 243b6fb 3cb4983 243b6fb 3cb4983 4cb647e 3cb4983 4a0564f e7b1e22 3cb4983 20efc7b e7b1e22 3cb4983 b8d023d 50ed549 b8d023d 50ed549 b8d023d e7b1e22 3cb4983 e7b1e22 3cb4983 e7b1e22 3cb4983 e7b1e22 3cb4983 243b6fb 3cb4983 4a0564f 3cb4983 e7b1e22 3cb4983 f4c538b 3cb4983 94759d3 3cb4983 94759d3 3cb4983 0dc9435 f4c538b 40f5138 f4c538b 3cb4983 f4c538b 3cb4983 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 |
# https://binkhoale1812-interview-ai.hf.space/
# Interview Q&A – FastAPI backend
import base64, io, json, logging, os, tempfile
import re
from pathlib import Path
from typing import Dict
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, FileResponse
from fastapi.staticfiles import StaticFiles
# AI / LLM
from google import genai
from google.genai import types
# ASR
import numpy as np
from pydub import AudioSegment
from transformers import WhisperProcessor, WhisperForConditionalGeneration
# Misc
from PIL import Image
##############################################################################
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
if not GEMINI_API_KEY:
raise RuntimeError("❌ GEMINI_API_KEY must be set as env var")
ASR_MODEL_ID = "openai/whisper-small.en"
ASR_LANGUAGE = "en"
SAMPLE_RATE = 16_000
##############################################################################
app = FastAPI(title="Interview Q&A Assistant", docs_url="/docs")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], allow_methods=["*"], allow_headers=["*"],
)
app.mount("/statics", StaticFiles(directory="statics"), name="statics")
# Enable Logging for Debugging
import psutil
import logging
# Set up app-specific logger
logger = logging.getLogger("triage-response")
logger.setLevel(logging.INFO) # Set to DEBUG only when needed
# Set log format
formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(message)s")
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)
# Suppress noisy libraries like pymongo, urllib3, etc.
for noisy in ["pymongo", "urllib3", "httpx", "uvicorn", "uvicorn.error", "uvicorn.access"]:
logging.getLogger(noisy).setLevel(logging.WARNING)
# Monitor Resources Before Startup
def check_system_resources():
memory = psutil.virtual_memory()
cpu = psutil.cpu_percent(interval=1)
disk = psutil.disk_usage("/")
# Defines log info messages
logger.info(f"🔍 System Resources - RAM: {memory.percent}%, CPU: {cpu}%, Disk: {disk.percent}%")
if memory.percent > 85:
logger.warning("⚠️ High RAM usage detected!")
if cpu > 90:
logger.warning("⚠️ High CPU usage detected!")
if disk.percent > 90:
logger.warning("⚠️ High Disk usage detected!")
check_system_resources()
##############################################################################
# Global ASR (lazy-loaded)
processor = model = None
def build_prompt(question: str) -> str:
return (
"You are a helpful career-coach AI. Answer the following interview "
"question clearly and concisely (≤200 words). Use markdown when helpful.\n\n"
f"Interview question: \"{question.strip()}\""
)
def memory_mb() -> float:
return round(psutil.Process().memory_info().rss / 1_048_576, 1)
@app.on_event("startup")
async def load_models():
global processor, model
cache = Path("model_cache"); cache.mkdir(exist_ok=True)
processor = WhisperProcessor.from_pretrained(ASR_MODEL_ID, cache_dir=cache)
model = WhisperForConditionalGeneration.from_pretrained(ASR_MODEL_ID, cache_dir=cache)
forced = processor.get_decoder_prompt_ids(task="transcribe", language="english")
model.config.forced_decoder_ids = forced
model.to("cpu").eval()
logger.info("[MODEL] 🔊 Whisper loaded ✔")
@app.get("/")
async def root() -> FileResponse: # serve SPA
logger.info("[STATIC] Serving frontend")
return FileResponse(Path("statics/index.html"))
##############################################################################
# ── MAIN ENDPOINTS ──────────────────────────────────────────────────────────
def call_gemini(prompt: str, vision_parts=None) -> str:
client = genai.Client(api_key=GEMINI_API_KEY)
kwargs: Dict = {}
if vision_parts: # multimodal call
kwargs["contents"] = vision_parts + [{"text": prompt}]
else:
kwargs["contents"] = prompt
resp = client.models.generate_content(
model="gemini-2.5-flash-preview-04-17", **kwargs
)
try:
resp = client.models.generate_content(
model="gemini-2.5-flash-preview-04-17", **kwargs
)
# Check for at least one valid candidate
if not resp.candidates:
raise RuntimeError("No candidates returned from Gemini")
# Start at first index
candidate = resp.candidates[0]
if candidate.content is None or not hasattr(candidate.content, "parts"):
raise RuntimeError("Gemini candidate missing content parts")
# Join all .text fields in case Gemini responds in multiple parts.
text = "".join(part.text for part in candidate.content.parts if hasattr(part, "text"))
if not text.strip():
raise RuntimeError("Gemini response contained empty text")
# Success
logger.info(f"[LLM] ✅ Response received: {text[:100]}...")
return text.strip()
# Fail
except Exception as e:
logger.error(f"[LLM] ❌ Gemini API error: {e}")
raise RuntimeError("Gemini API response format error")
@app.post("/voice-transcribe")
async def voice_transcribe(file: UploadFile = File(...)):
if file.content_type not in {"audio/wav", "audio/x-wav", "audio/mpeg"}:
raise HTTPException(415, "Unsupported audio type")
# Write temporary audio file
with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
tmp.write(await file.read()); tmp_path = tmp.name
# Audio processing and transcription
try:
seg = AudioSegment.from_file(tmp_path).set_frame_rate(SAMPLE_RATE).set_channels(1)
audio = np.array(seg.get_array_of_samples()).astype(np.float32) / (2 ** 15)
inputs = processor(audio, sampling_rate=SAMPLE_RATE, return_tensors="pt")
ids = model.generate(inputs.input_features.to(model.device))
question = processor.decode(ids[0], skip_special_tokens=True).strip()
if not question:
raise ValueError("No speech detected")
logger.info(f"[MODEL] Transcribed text: {question}")
answer = call_gemini(build_prompt(question))
return JSONResponse({"question": question, "answer": answer, "memory_mb": memory_mb()})
finally:
os.remove(tmp_path)
# Route sending question as image (PNG/JPEG)
@app.post("/image-question")
async def image_question(file: UploadFile = File(...)):
if file.content_type not in {"image/png", "image/jpeg"}:
raise HTTPException(415, "Unsupported image type")
# Read file and decode
raw = await file.read()
b64 = base64.b64encode(raw).decode()
# Send image data
vision_part = [{
"inline_data": {
"mime_type": file.content_type,
"data": b64
}
}]
# Ask Gemini to return JSON splitting Q&A
prompt = (
"From the screenshot, extract all English interview question(s). "
"There may be multiple questions. For each, provide a concise answer (≤200 words).\n\n"
"Return only valid JSON as a list of objects:\n"
"[\n"
" {\"question\": \"...\", \"answer\": \"...\"},\n"
" {\"question\": \"...\", \"answer\": \"...\"},\n"
" ...\n"
"]\n\n"
"Do not include explanations or additional formatting — only output raw JSON."
)
# Send prompt and image
text = call_gemini(prompt, vision_part)
try: # Parsed from JSON (rm bracket and markdown)
cleaned = re.sub(r"^```json\s*|\s*```$", "", text.strip(), flags=re.IGNORECASE | re.MULTILINE)
parsed = json.loads(cleaned)
try:
# If it's a list of Q&A
if isinstance(parsed, list):
return JSONResponse(parsed)
# Fallback: single object
elif isinstance(parsed, dict):
question = str(parsed.get("question", "")).strip()
answer = str(parsed.get("answer", "")).strip()
return JSONResponse([{"question": question, "answer": answer}])
except Exception as e:
raise ValueError("Unexpected JSON format from Gemini")
# Remove accidental outer quotes if double-wrapped
if question.startswith("{") or answer.startswith("{"):
raise ValueError("Wrapped JSON detected inside field")
except Exception as e:
logger.warning(f"[PARSE] Failed to cleanly extract JSON fields: {e}")
return JSONResponse([{
"question": "[Extracted from screenshot]",
"answer": text.strip()
}])
# Text based question (both voice transcribe or edit question)
@app.post("/text-question")
async def text_question(payload: Dict):
question = (payload.get("question") or "").strip()
if not question:
raise HTTPException(400, "question is required")
answer = call_gemini(build_prompt(question))
return JSONResponse({"question": question, "answer": answer, "memory_mb": memory_mb()})
|