vadirajkrishna's picture
final changes
9899af7 unverified
Raw
History Blame Contribute Delete
92.1 kB
import asyncio
import csv
import hashlib
import html
import json
import os
import re
from typing import Any
if os.environ.get("SPACE_ID") or os.environ.get("INTERVIEW_COACH_RUNTIME") == "space":
os.environ["CUDA_VISIBLE_DEVICES"] = ""
import gradio as gr
import numpy as np
from agents.evaluator import EvaluationAgent
from agents.hf_chat import HuggingFaceChatModel
from agents.topic_pattern import TopicPatternAgent
from config import (
APP_HOST,
APP_PORT,
BASE_DIR,
GENERAL_LLM_MODEL,
HF_SPACE_MODE,
STREAMING_WHISPER_MODEL,
TOPIC_PATTERN_MODEL,
)
from db.queries import (
add_exchange,
add_evaluation,
add_transcript,
append_exchange_answer,
clear_all_tables,
create_session,
list_all_evaluations,
list_evaluations,
list_exchanges,
update_exchange_answer,
)
from db.schema import init_db
from graph import coach_graph
from nodes.audio import LiveAudioTranscriber, transcribe_audio_array, transcribe_audio_file, warmup_transcriber
from prompts import (
CLARIFICATION_CHECK_SYSTEM_PROMPT,
CLARIFICATION_CHECK_USER_PROMPT,
COACHING_GUIDANCE_SYSTEM_PROMPT,
COACHING_GUIDANCE_USER_PROMPT,
MULTI_EXCHANGE_EXTRACTOR_SYSTEM_PROMPT,
MULTI_EXCHANGE_EXTRACTOR_USER_PROMPT,
QUESTION_LIST_DETECTOR_SYSTEM_PROMPT,
QUESTION_LIST_DETECTOR_USER_PROMPT,
QUESTION_DETECTOR_SYSTEM_PROMPT,
QUESTION_DETECTOR_USER_PROMPT,
TRANSCRIPT_NORMALIZER_REPAIR_SYSTEM_PROMPT,
TRANSCRIPT_NORMALIZER_REPAIR_USER_PROMPT,
TRANSCRIPT_NORMALIZER_SYSTEM_PROMPT,
TRANSCRIPT_NORMALIZER_USER_PROMPT,
)
CSS = """
:root {
--bg: #0b0f14;
--panel: #111827;
--panel-soft: #151c28;
--panel-muted: #0f1622;
--border: #263241;
--border-strong: #344154;
--text: #e7ebf0;
--muted: #9aa5b1;
--accent: #14b8a6;
--accent-strong: #2dd4bf;
--danger: #ef4444;
}
body,
.gradio-container {
background: var(--bg) !important;
color: var(--text) !important;
}
.gradio-container {
max-width: 1280px !important;
margin: 0 auto !important;
padding: 12px 18px !important;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
#app-shell {
display: flex;
flex-direction: column;
gap: 10px;
}
#app-header {
padding: 0;
}
#app-header h1 {
margin: 0;
font-size: 22px;
line-height: 1.15;
font-weight: 700;
}
#app-header p {
margin: 3px 0 0;
color: var(--muted);
font-size: 12px;
}
.form {
border: 1px solid var(--border) !important;
border-radius: 8px !important;
background: var(--panel-muted) !important;
padding: 8px 10px !important;
}
.action-row {
align-items: end;
}
.action-row button,
button {
border-radius: 6px !important;
font-weight: 650 !important;
min-height: 34px !important;
}
button.primary {
background: var(--accent) !important;
border-color: var(--accent) !important;
color: #041412 !important;
}
button.secondary {
background: #1f2937 !important;
border-color: var(--border-strong) !important;
color: var(--text) !important;
}
button.stop {
background: #3b1417 !important;
border-color: #7f1d1d !important;
color: #fecaca !important;
}
.tabs {
border-radius: 8px !important;
}
.tab-nav {
border-bottom: 1px solid var(--border) !important;
}
.tab-nav button {
border-radius: 6px 6px 0 0 !important;
color: var(--muted) !important;
font-weight: 650 !important;
}
.tab-nav button.selected {
color: var(--text) !important;
border-color: var(--accent) !important;
}
.block,
.panel,
.form,
textarea,
input {
border-color: var(--border) !important;
}
textarea,
input {
background: #0d131d !important;
color: var(--text) !important;
border-radius: 6px !important;
}
label,
.label-wrap span {
color: var(--muted) !important;
font-weight: 650 !important;
}
#status_box textarea,
#model_status_box textarea,
#stream_status_box textarea {
color: var(--accent-strong) !important;
font-size: 13px !important;
}
.section-title {
margin: 0 0 6px;
color: var(--muted);
font-size: 11px;
font-weight: 750;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.compact-panel {
border: 1px solid var(--border) !important;
border-radius: 8px !important;
background: var(--panel-muted) !important;
padding: 10px !important;
min-width: 0 !important;
}
#live_grid {
flex-wrap: nowrap !important;
gap: 10px !important;
}
#live_grid > div {
min-width: 0 !important;
}
#live_transcript_box textarea {
min-height: 330px !important;
max-height: 520px !important;
overflow-y: auto !important;
resize: vertical !important;
}
#log_box textarea,
#report_box textarea {
min-height: 500px !important;
}
#status_box textarea {
min-height: 34px !important;
}
#model_status_box textarea {
min-height: 34px !important;
}
#stream_status_box textarea {
min-height: 72px !important;
font-size: 12px !important;
}
.coach-card {
border: 1px solid var(--border);
border-left: 5px solid var(--framework-color);
border-radius: 8px;
background: var(--panel);
padding: 14px;
min-height: 425px;
max-height: 425px;
overflow: auto;
}
.floating-card {
resize: both;
min-width: 220px;
min-height: 145px;
max-width: 100%;
}
.coach-card summary {
cursor: pointer;
list-style: none;
}
.coach-card summary::-webkit-details-marker {
display: none;
}
.card-question {
margin: 0 0 8px;
color: var(--text);
font-size: 14px;
line-height: 1.45;
font-weight: 650;
}
.card-type {
display: inline-block;
margin: 4px 0 8px;
color: var(--accent-strong);
font-size: 13px;
font-weight: 700;
}
.coach-card h3 {
margin: 0 0 6px;
font-size: 17px;
line-height: 1.25;
}
.coach-card p {
margin: 10px 0;
color: var(--text);
line-height: 1.5;
}
.coach-card ol {
margin: 10px 0 0 22px;
padding: 0;
color: #d5dbe3;
}
.coach-card li {
margin-bottom: 6px;
}
.coach-card-stack {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
gap: 10px;
max-height: 425px;
overflow: auto;
padding-right: 4px;
}
.coach-card-stack .coach-card {
min-height: 0;
max-height: none;
box-shadow: 0 14px 32px rgba(0, 0, 0, 0.24);
}
.meta {
color: var(--muted);
font-size: 13px;
}
.review {
color: #fbbf24;
}
@keyframes card-pulse {
0% { box-shadow: 0 0 0 0 rgba(20, 184, 166, 0.35); }
55% { box-shadow: 0 0 0 10px rgba(20, 184, 166, 0); }
100% { box-shadow: 0 0 0 0 rgba(20, 184, 166, 0); }
}
.flash-card {
animation: card-pulse 1.1s ease-out infinite;
}
"""
FRAMEWORK_COLORS = {
"General": "#64748b",
"Behavioral": "#0ea5e9",
"Behavioural": "#0ea5e9",
"Technical": "#22c55e",
"Data Science": "#22c55e",
"AI Engineering": "#14b8a6",
"System Design": "#f59e0b",
"Product Sense": "#ec4899",
"Product Design": "#ec4899",
"Case": "#a855f7",
"Estimation": "#a855f7",
}
LIVE_CARD_LLM_TIMEOUT_SECONDS = 15
LIVE_QUESTION_CONTEXT_LINES = 10
LIVE_QUESTION_CONTEXT_CHARS = 2200
LIVE_QUESTION_PAUSE_SECONDS = 1.5
BROWSER_STREAM_STEP_SECONDS = 2.0
BROWSER_STREAM_CONTEXT_SECONDS = 12.0
evaluator = EvaluationAgent()
general_llm = HuggingFaceChatModel(GENERAL_LLM_MODEL)
topic_pattern_agent = TopicPatternAgent()
live_audio = LiveAudioTranscriber()
topic_model_warmup_task: asyncio.Task | None = None
model_warmup_task: asyncio.Task | None = None
model_status: dict[str, str] = {
"General LLM": "not loaded",
"Topic/steps model": "not loaded",
"Speech-to-text": "not loaded",
}
def ensure_topic_model_warmup() -> None:
global topic_model_warmup_task
if topic_model_warmup_task and not topic_model_warmup_task.done():
return
topic_model_warmup_task = asyncio.create_task(
topic_pattern_agent.analyze("How would you handle class imbalance in a fraud detection model?")
)
def render_model_status() -> str:
statuses = dict(model_status)
if general_llm.is_loaded:
statuses["General LLM"] = "loaded"
if topic_pattern_agent.is_loaded:
statuses["Topic/steps model"] = "loaded"
elif not topic_pattern_agent.enabled:
statuses["Topic/steps model"] = "disabled"
return "\n".join(f"{name}: {status}" for name, status in statuses.items())
def all_models_ready() -> bool:
return (
general_llm.is_loaded
and (topic_pattern_agent.is_loaded or not topic_pattern_agent.enabled)
and model_status.get("Speech-to-text") == "loaded"
)
def render_startup_status() -> str:
if all_models_ready():
return "All models loaded."
return "Loading models..."
def live_detector_timeout_message(detector_name: str = "Question detector") -> str:
if all_models_ready():
return f"{detector_name} is taking longer than expected. Keeping the transcript live; try again in a moment."
return f"{detector_name} is still loading. Try again after startup status shows All models loaded."
async def warmup_all_models():
global model_warmup_task
if model_warmup_task and not model_warmup_task.done():
yield render_startup_status()
return
model_warmup_task = asyncio.current_task()
model_status["General LLM"] = "loading"
model_status["Topic/steps model"] = "loading" if topic_pattern_agent.enabled else "disabled"
model_status["Speech-to-text"] = "loading"
yield render_startup_status()
try:
await general_llm.warmup()
model_status["General LLM"] = "loaded"
except Exception as exc:
model_status["General LLM"] = f"error: {exc}"
yield render_startup_status()
if topic_pattern_agent.enabled:
try:
await topic_pattern_agent.warmup()
model_status["Topic/steps model"] = "loaded"
except Exception as exc:
model_status["Topic/steps model"] = f"error: {exc}"
yield render_startup_status()
try:
await warmup_transcriber(
model=STREAMING_WHISPER_MODEL,
backend="transformers" if HF_SPACE_MODE else None,
)
model_status["Speech-to-text"] = "loaded"
except Exception as exc:
model_status["Speech-to-text"] = f"error: {exc}"
yield render_startup_status() if all_models_ready() else render_model_status()
def ensure_model_warmup_background() -> None:
global model_warmup_task
if model_warmup_task and not model_warmup_task.done():
return
if all_models_ready():
return
async def consume_warmup() -> None:
async for _ in warmup_all_models():
pass
model_warmup_task = asyncio.create_task(consume_warmup())
async def start_session(company: str, role: str) -> tuple[int, str]:
ensure_model_warmup_background()
await init_db()
session_id = await create_session(company=company, role=role)
return session_id, f"Session {session_id} started"
async def coach_question(session_id: int | None, question: str, answer: str) -> tuple[str, str, dict[str, Any]]:
if not session_id:
await init_db()
session_id = await create_session()
classification = await classify_topic_and_steps(question)
if classification.get("model_unavailable"):
state = {
"session_id": session_id,
"question": question,
"answer": answer,
"message": classification["message"],
}
return render_answer_card(state), classification["message"], state
clarification_state = await handle_clarification_if_needed(session_id, question, answer)
if clarification_state:
return render_card(clarification_state), await render_log(session_id), clarification_state
state = await coach_graph.ainvoke(
{
"session_id": session_id,
"raw_text": question,
"question": question,
"answer": answer.strip(),
"framework": classification["type"],
"pattern": classification.get("pattern", classification["type"]),
"steps": classification["steps"],
"confidence": classification["confidence"],
"topic_model_used": classification.get("topic_model_used", ""),
}
)
framework_steps = state.get("steps", [])
state["steps"] = await generate_coaching_cues(
question,
classification["type"],
classification.get("pattern", classification["type"]),
framework_steps,
)
state["framework_steps"] = framework_steps
return render_card(state), await render_log(session_id), state
async def classify_topic_and_steps(question: str) -> dict[str, Any]:
topic_result = await topic_pattern_agent.analyze(question)
if not topic_result:
details = topic_pattern_agent.last_error
suffix = f" Details: {details}" if details else ""
return {
"type": "General",
"steps": [],
"confidence": 0.0,
"model_unavailable": True,
"message": (
"Topic/steps model unavailable. Expected Hugging Face model "
f"{TOPIC_PATTERN_MODEL}.{suffix}"
),
}
return {
"type": topic_result["type"],
"steps": topic_result["steps"],
"confidence": topic_result.get("confidence", 0.0),
"pattern": topic_result.get("pattern", topic_result["type"]),
"topic_model_used": topic_result.get("model", ""),
}
async def normalize_interview_exchange_with_llm(
transcript: str,
question: str = "",
answer: str = "",
prefer_latest: bool = False,
) -> dict[str, Any] | None:
mode_instruction = (
"Extract the latest complete target interviewer question in the transcript window. "
"Ignore earlier target questions that already have candidate answers. "
"If the transcript says 'second question', 'next question', or similar, prefer the target question after that marker. "
"In a technical/ML interview, production checks, data drift, monitoring, and training-serving data issues are target ML/MLOps questions. "
"If first-person answer text follows the latest question, such as 'I will...' or 'I would...', keep that text in answer, not question."
if prefer_latest
else "Extract the clearest complete target interviewer question and its candidate answer, if present."
)
prompt = TRANSCRIPT_NORMALIZER_USER_PROMPT.format(
transcript=transcript,
mode_instruction=mode_instruction,
question=question,
answer=answer,
)
response = await general_llm.generate(
TRANSCRIPT_NORMALIZER_SYSTEM_PROMPT,
prompt,
max_new_tokens=512,
)
if not response:
if not general_llm.last_error:
general_llm.last_error = "Model returned an empty response."
return None
try:
payload = json.loads(extract_json_object(response))
except Exception as exc:
preview = response.replace("\n", " ")[:300]
general_llm.last_error = f"Model returned non-JSON output: {exc}. Output preview: {preview}"
return None
normalized = normalize_normalizer_payload(payload)
if normalized["is_target"] and normalized["complete"] and not normalized["question"]:
repaired = await repair_normalizer_payload_with_llm(transcript, payload)
if repaired:
normalized = normalize_normalizer_payload(repaired)
if (
normalized["question"]
and normalized["answer"]
and normalized["answer"].lower() in normalized["question"].lower()
):
repaired = await repair_normalizer_payload_with_llm(transcript, normalized)
if repaired:
repaired_normalized = normalize_normalizer_payload(repaired)
if repaired_normalized["is_target"] and repaired_normalized["complete"]:
normalized = repaired_normalized
if normalized["is_target"] and normalized["complete"] and not normalized["question"]:
general_llm.last_error = "Normalizer marked target complete but returned an empty question."
normalized["is_target"] = False
normalized["complete"] = False
normalized["reason"] = "Target question was empty."
return normalized
def normalize_normalizer_payload(payload: dict[str, Any]) -> dict[str, Any]:
clean_question = normalize_llm_text(str(payload.get("question", "")))
clean_answer = normalize_llm_text(str(payload.get("answer", "")))
if clean_question and not clean_question.endswith("?"):
clean_question = f"{clean_question.rstrip('.')}?"
return {
"question": clean_question,
"answer": clean_answer,
"is_target": bool(payload.get("is_target", False)),
"complete": bool(payload.get("complete", False)),
"reason": str(payload.get("reason", "")).strip(),
}
async def repair_normalizer_payload_with_llm(transcript: str, payload: dict[str, Any]) -> dict[str, Any] | None:
prompt = TRANSCRIPT_NORMALIZER_REPAIR_USER_PROMPT.format(
transcript=transcript,
payload=json.dumps(payload),
)
response = await general_llm.generate(
TRANSCRIPT_NORMALIZER_REPAIR_SYSTEM_PROMPT,
prompt,
max_new_tokens=384,
)
if not response:
return None
try:
return json.loads(extract_json_object(response))
except Exception as exc:
preview = response.replace("\n", " ")[:300]
general_llm.last_error = f"Normalizer repair returned non-JSON output: {exc}. Output preview: {preview}"
return None
async def extract_all_interview_exchanges_with_llm(transcript: str) -> list[dict[str, Any]]:
prompt = MULTI_EXCHANGE_EXTRACTOR_USER_PROMPT.format(transcript=transcript)
response = await general_llm.generate(
MULTI_EXCHANGE_EXTRACTOR_SYSTEM_PROMPT,
prompt,
max_new_tokens=1400,
)
if not response:
return []
try:
payload = json.loads(extract_json_object(response))
except Exception as exc:
preview = response.replace("\n", " ")[:300]
general_llm.last_error = f"Multi-exchange extractor returned non-JSON output: {exc}. Output preview: {preview}"
return []
exchanges = payload.get("exchanges", [])
if not isinstance(exchanges, list):
return []
cleaned = []
for item in exchanges:
if not isinstance(item, dict):
continue
normalized = normalize_normalizer_payload(item)
if not normalized["is_target"] or not normalized["complete"] or not normalized["question"]:
continue
if not is_target_coaching_question(normalized["question"], item):
continue
cleaned.append(normalized)
return cleaned
def dedupe_extracted_exchanges(exchanges: list[dict[str, Any]]) -> list[dict[str, Any]]:
deduped: list[dict[str, Any]] = []
for exchange in exchanges:
question = normalize_llm_text(str(exchange.get("question", "")))
answer = normalize_llm_text(str(exchange.get("answer", "")))
if not question or question_looks_incomplete(question):
continue
current = {**exchange, "question": question, "answer": answer}
current_key = canonical_question_key(question)
duplicate_index = -1
for index, existing in enumerate(deduped):
existing_key = canonical_question_key(str(existing.get("question", "")))
if question_keys_are_similar(current_key, existing_key):
duplicate_index = index
break
if duplicate_index == -1:
deduped.append(current)
continue
existing = deduped[duplicate_index]
if len(question.split()) > len(str(existing.get("question", "")).split()):
existing["question"] = question
if len(answer) > len(str(existing.get("answer", ""))):
existing["answer"] = answer
return deduped
def repair_missing_answers_from_transcript(
exchanges: list[dict[str, Any]],
transcript: str,
replace_existing: bool = False,
) -> list[dict[str, Any]]:
if not exchanges or not transcript.strip():
return exchanges
repaired: list[dict[str, Any]] = []
cursor = 0
spans: list[tuple[int, int]] = []
for exchange in exchanges:
span = ordered_question_match_span(transcript, str(exchange.get("question", "")), cursor)
spans.append(span)
if span[1] > 0:
cursor = span[1]
for index, exchange in enumerate(exchanges):
item = dict(exchange)
if str(item.get("answer", "")).strip() and not replace_existing:
repaired.append(item)
continue
_, question_end = spans[index]
if question_end < 0:
repaired.append(item)
continue
next_question_start = len(transcript)
for next_start, _ in spans[index + 1 :]:
if next_start > question_end:
next_question_start = next_start
break
answer = clean_extracted_answer_text(transcript[question_end:next_question_start])
if answer:
item["answer"] = answer
item["reason"] = "Recovered answer from transcript."
repaired.append(item)
return repaired
def clean_extracted_answer_text(text: str) -> str:
clean = normalize_llm_text(text.strip(" ?.:-,;"))
if not clean:
return ""
clean = re.sub(r"^(good|great|okay|ok)[,.\s]+(?=(yes|i|we|my|the|in|for)\b)", "", clean, flags=re.I)
clean = re.sub(
r"\b(good|great|okay|ok|thank you|thanks)(?:[.!?,\s]+(?:let'?s move to the next|next question)?)?$",
"",
clean,
flags=re.I,
)
clean = re.sub(
r"\b(that'?s great|that is great|good|great|okay|ok)[.!?,\s]+(?:let'?s go to the next question|let'?s move to the next question|next question)[.!?,\s]*$",
"",
clean,
flags=re.I,
)
return normalize_llm_text(clean)
def normalize_llm_text(text: str) -> str:
return re.sub(r"\s+", " ", text).strip(" -:")
def general_llm_unavailable_message() -> str:
details = f" Details: {general_llm.last_error}" if general_llm.last_error else ""
return f"General LLM unavailable. Expected Hugging Face model {GENERAL_LLM_MODEL}.{details}"
async def handle_clarification_if_needed(
session_id: int,
question: str,
answer: str,
) -> dict[str, Any] | None:
exchanges = await list_exchanges(session_id)
if not exchanges:
return None
previous = exchanges[-1]
if not await is_clarification_question(previous["question"], question, answer):
return None
addition = f"Clarification: {question}"
if answer.strip():
addition = f"{addition}\nCandidate follow-up: {answer.strip()}"
await append_exchange_answer(previous["id"], addition)
result = await classify_topic_and_steps(previous["question"])
cues = await generate_coaching_cues(
previous["question"],
previous["framework_used"],
result.get("pattern", previous["framework_used"]),
result["steps"],
)
return {
"session_id": session_id,
"exchange_id": previous["id"],
"question": previous["question"],
"answer": addition,
"framework": previous["framework_used"],
"pattern": result.get("pattern", previous["framework_used"]),
"steps": cues,
"framework_steps": result["steps"],
"confidence": result["confidence"],
"needs_review": False,
}
async def is_clarification_question(previous_question: str, new_question: str, answer: str) -> bool:
if looks_like_interviewer_prompt(new_question) and not question_keys_are_similar(
canonical_question_key(previous_question),
canonical_question_key(new_question),
):
return False
heuristic = is_clarification_question_heuristic(previous_question, new_question)
llm_result = await is_clarification_question_with_llm(previous_question, new_question, answer)
return llm_result if llm_result is not None else heuristic
async def is_clarification_question_with_llm(
previous_question: str,
new_question: str,
answer: str,
) -> bool | None:
prompt = CLARIFICATION_CHECK_USER_PROMPT.format(
previous_question=previous_question,
new_question=new_question,
answer=answer,
)
response = await general_llm.generate(
CLARIFICATION_CHECK_SYSTEM_PROMPT,
prompt,
max_new_tokens=256,
)
if not response:
return None
try:
payload = json.loads(extract_json_object(response))
return bool(payload.get("is_clarification", False))
except Exception:
return None
def is_clarification_question_heuristic(previous_question: str, new_question: str) -> bool:
previous = previous_question.lower()
current = new_question.lower().strip()
if len(current.split()) < 3:
return False
clarification_starts = (
"do you mean",
"did you mean",
"when you say",
"should i assume",
"can i assume",
"are we assuming",
"are we considering",
"should we consider",
"is it okay if",
"can i clarify",
"could you clarify",
"what do you mean by",
"for this question",
"in this case",
)
if current.startswith(clarification_starts):
return True
clarification_terms = (
"assume",
"clarify",
"constraint",
"scope",
"mean by",
"considering",
"requirement",
"latency",
"scale",
"users",
"time window",
)
overlap = set(re.findall(r"[a-zA-Z]+", previous)).intersection(
set(re.findall(r"[a-zA-Z]+", current))
)
return any(term in current for term in clarification_terms) and bool(overlap)
async def transcribe_and_coach(
session_id: int | None,
audio_input: Any,
typed_transcript: str,
) -> tuple[str, str, str, dict[str, Any]]:
if audio_input is None:
return "Record audio first, then press Transcribe & Coach.", render_answer_card(), "", {}
transcript = await transcribe_audio_input(audio_input, use_space_stt=HF_SPACE_MODE)
if transcript.startswith("[transcription unavailable:"):
return transcript, render_answer_card(), "", {}
normalized = await normalize_interview_exchange_with_llm(transcript)
if (
normalized
and normalized.get("is_target")
and normalized.get("complete")
and normalized.get("question", "").strip()
):
card, log, state = await coach_question(
session_id,
normalized.get("question", ""),
normalized.get("answer", ""),
)
return transcript, card, log, state
state = {
"message": (
"General LLM could not extract a complete DS/ML/AI/System Design question."
if normalized
else general_llm_unavailable_message()
)
}
return transcript, render_answer_card(state), state["message"], state
async def transcribe_browser_recording(
audio_input: Any,
current_transcript: str,
stream_state: dict[str, Any] | None,
) -> tuple[str, str, dict[str, Any]]:
state = stream_state or fresh_stream_state()
if audio_input is None:
transcript = latest_stream_transcript(state, current_transcript)
return transcript, format_stream_status(state, "waiting for browser recording"), state
transcript = await transcribe_audio_input(audio_input, use_space_stt=HF_SPACE_MODE)
if transcript.startswith("[transcription unavailable:"):
state["last_text"] = transcript
return latest_stream_transcript(state, current_transcript), format_stream_status(state, transcript), state
updated_transcript = (
transcript
if HF_SPACE_MODE
else merge_transcript_text(latest_stream_transcript(state, current_transcript), transcript)
)
state["transcript"] = updated_transcript
state["last_text"] = transcript
state["transcriptions"] = int(state.get("transcriptions") or 0) + 1
return updated_transcript, format_stream_status(state, "recording transcribed"), state
async def stream_live_transcript(
session_id: int | None,
audio_input: Any,
current_transcript: str,
stream_state: dict[str, Any] | None,
) -> tuple[str, str, str, dict[str, Any], dict[str, Any], str]:
if audio_input is None:
state = stream_state or fresh_stream_state()
return (
current_transcript or "",
state.get("card_html") or render_answer_card(),
format_stream_status(state, "waiting for microphone"),
state,
state.get("card_state") or {},
await render_live_log(session_id),
)
state = update_stream_state(audio_input, stream_state)
audio_buffer = state.get("audio_buffer")
sample_rate = int(state.get("sample_rate") or 16000)
processed_until = int(state.get("processed_until") or 0)
available = 0 if audio_buffer is None else len(audio_buffer) - processed_until
transcript_so_far = latest_stream_transcript(state, current_transcript)
if HF_SPACE_MODE:
if audio_buffer is not None:
state["processed_until"] = len(audio_buffer)
state["last_window_seconds"] = len(audio_buffer) / sample_rate
state["last_rms"] = audio_rms(audio_buffer)
return (
transcript_so_far,
state.get("card_html") or render_answer_card({"message": "Recording. Transcript will appear after you stop."}),
format_stream_status(state, "recording browser audio"),
state,
state.get("card_state") or {},
await render_live_log(session_id),
)
if available < int(sample_rate * BROWSER_STREAM_STEP_SECONDS):
return (
transcript_so_far,
state.get("card_html") or render_answer_card(),
format_stream_status(state, "buffering audio"),
state,
state.get("card_state") or {},
await render_live_log(session_id),
)
context_samples = int(sample_rate * BROWSER_STREAM_CONTEXT_SECONDS)
end = len(audio_buffer)
start = max(0, end - context_samples)
audio_window = audio_buffer[start:end].copy()
new_audio = audio_buffer[processed_until:end].copy()
state["processed_until"] = end
state["last_window_seconds"] = len(audio_window) / sample_rate
rms = audio_rms(new_audio)
state["last_rms"] = rms
if rms < 0.003:
quiet_seconds = float(state.get("quiet_seconds") or 0.0) + (len(new_audio) / sample_rate)
state["quiet_seconds"] = quiet_seconds
if transcript_so_far:
if quiet_seconds >= LIVE_QUESTION_PAUSE_SECONDS:
card_html, card_state = await monitor_answer_card(
transcript_so_far,
state,
session_id=session_id,
force=False,
fast=True,
pause_check=True,
pause_seconds=quiet_seconds,
)
state["card_html"] = card_html
state["card_state"] = card_state
else:
card_html = current_card_or_status(
state,
f"Pause {quiet_seconds:.1f}s. Waiting for {LIVE_QUESTION_PAUSE_SECONDS:.0f}s before checking question.",
)
state["card_html"] = card_html
card_state = state.get("card_state") or {}
else:
card_html = state.get("card_html") or render_answer_card()
card_state = state.get("card_state") or {}
return (
transcript_so_far,
card_html,
format_stream_status(state, "quiet audio skipped"),
state,
card_state,
await render_live_log(session_id),
)
state["quiet_seconds"] = 0.0
chunk_text = await transcribe_audio_array(
sample_rate,
audio_window,
model=STREAMING_WHISPER_MODEL,
backend="transformers" if HF_SPACE_MODE else None,
temperature=0.0,
condition_on_previous_text=False,
compression_ratio_threshold=1.8,
logprob_threshold=-0.6,
no_speech_threshold=0.35,
)
if not chunk_text or chunk_text.startswith("[transcription unavailable:"):
return (
transcript_so_far,
state.get("card_html") or render_answer_card(),
format_stream_status(state, chunk_text or "no speech detected yet"),
state,
state.get("card_state") or {},
await render_live_log(session_id),
)
if is_repetitive_hallucination(chunk_text):
state["rejected"] = int(state.get("rejected") or 0) + 1
state["last_text"] = f"rejected: {chunk_text[:60]}"
return (
transcript_so_far,
state.get("card_html") or render_answer_card(),
format_stream_status(state, "repeated-word output skipped"),
state,
state.get("card_state") or {},
await render_live_log(session_id),
)
state["transcriptions"] = int(state.get("transcriptions") or 0) + 1
state["last_text"] = chunk_text
updated_transcript = merge_transcript_text(transcript_so_far, chunk_text)
state["transcript"] = updated_transcript
return (
updated_transcript,
state.get("card_html") or render_answer_card({"message": "Listening for a pause before creating a card."}),
format_stream_status(state, "transcribing"),
state,
state.get("card_state") or {},
await render_live_log(session_id),
)
async def start_backend_live_transcript(session_id: int | None):
ensure_model_warmup_background()
await live_audio.start()
monitor_state = fresh_card_monitor_state()
card_html = monitor_state["card_html"]
card_state: dict[str, Any] = {}
card_task: asyncio.Task | None = None
yield (
"",
card_html,
"Capturing system audio via BlackHole/default input",
card_state,
monitor_state,
await render_live_log(session_id),
)
try:
async for transcript, pause_detected, pause_seconds in live_audio.transcript_stream():
if card_task and card_task.done():
try:
card_html, card_state = card_task.result()
except Exception as exc:
monitor_state["last_extraction_message"] = f"Coaching card failed: {exc}"
card_html = current_card_or_status(monitor_state)
card_state = monitor_state.get("card_state") or {}
card_task = None
if pause_detected and (card_task is None or card_task.done()):
card_task = asyncio.create_task(
monitor_answer_card(
transcript,
monitor_state,
session_id=session_id,
force=False,
fast=True,
pause_check=True,
pause_seconds=pause_seconds,
)
)
status_message = (
"Pause detected; coaching card loading in background"
if card_task and not card_task.done()
else "Capturing system audio via BlackHole/default input"
)
yield (
transcript,
card_html,
status_message,
card_state,
monitor_state,
await render_live_log(session_id),
)
finally:
if card_task and not card_task.done():
card_task.cancel()
await asyncio.gather(card_task, return_exceptions=True)
async def stop_backend_live_transcript() -> str:
await live_audio.stop()
return "Stopped live audio capture"
async def transcribe_audio_input(audio_input: Any, use_space_stt: bool = False) -> str:
backend = "transformers" if use_space_stt else None
if isinstance(audio_input, str):
return await transcribe_audio_file(audio_input, backend=backend)
if isinstance(audio_input, tuple) and len(audio_input) == 2:
sample_rate, audio = audio_input
return await transcribe_audio_array(int(sample_rate), np.asarray(audio), backend=backend)
return "[transcription unavailable: unsupported audio input]"
async def process_typed_transcript(
session_id: int | None,
transcript: str,
) -> tuple[str, str, str, dict[str, Any], dict[str, Any]]:
exchanges = dedupe_extracted_exchanges(await extract_all_interview_exchanges_with_llm(transcript))
if exchanges:
cards = []
card_states = []
last_state: dict[str, Any] = {}
log = ""
for exchange in exchanges:
existing_exchange = (
await find_existing_exchange(session_id, exchange["question"])
if session_id
else None
)
if existing_exchange:
answer = exchange.get("answer", "").strip()
existing_answer = str(existing_exchange.get("answer", "")).strip()
if answer and len(answer) > len(existing_answer):
await update_exchange_answer(int(existing_exchange["id"]), answer)
log = await render_log(session_id)
display_answer = answer if answer and len(answer) > len(existing_answer) else existing_answer
last_state = {
"session_id": session_id,
"exchange_id": existing_exchange["id"],
"question": existing_exchange["question"],
"answer": display_answer,
"framework": existing_exchange.get("framework_used", "General"),
"skipped_duplicate": True,
}
classification = await classify_topic_and_steps(str(existing_exchange["question"]))
if not classification.get("model_unavailable"):
framework_steps = classification["steps"]
last_state.update(
{
"framework": classification["type"],
"pattern": classification.get("pattern", classification["type"]),
"steps": await generate_coaching_cues(
str(existing_exchange["question"]),
classification["type"],
classification.get("pattern", classification["type"]),
framework_steps,
),
"framework_steps": framework_steps,
"confidence": classification["confidence"],
}
)
cards.append(render_card(last_state))
card_states.append(last_state)
continue
card, log, last_state = await coach_question(
session_id,
exchange["question"],
exchange.get("answer", ""),
)
cards.append(card)
card_states.append(last_state)
session_id = last_state.get("session_id", session_id)
last_state["processed_exchanges"] = exchanges
card_html = render_cards(cards)
monitor_state = fresh_card_monitor_state()
monitor_state["cards"] = card_states[-4:]
monitor_state["card_html"] = card_html
monitor_state["card_state"] = last_state
if card_states:
monitor_state["last_question"] = str(card_states[-1].get("question", ""))
monitor_state["last_question_hash"] = question_hash_key(monitor_state["last_question"])
return transcript, card_html, log, last_state, monitor_state
normalized = await normalize_interview_exchange_with_llm(transcript)
if (
normalized
and normalized.get("is_target")
and normalized.get("complete")
and normalized.get("question", "").strip()
):
card, log, state = await coach_question(
session_id,
normalized.get("question", ""),
normalized.get("answer", ""),
)
monitor_state = fresh_card_monitor_state()
monitor_state["cards"] = [state]
monitor_state["card_html"] = card
monitor_state["card_state"] = state
monitor_state["last_question"] = str(state.get("question", ""))
monitor_state["last_question_hash"] = question_hash_key(monitor_state["last_question"])
return transcript, card, log, state, monitor_state
state = {
"message": (
"General LLM could not extract a complete DS/ML/AI/System Design question."
if normalized
else general_llm_unavailable_message()
)
}
return transcript, render_answer_card(state), state["message"], state, fresh_card_monitor_state()
def clear_live_state() -> tuple[str, str, dict[str, Any], str, dict[str, Any]]:
state = fresh_stream_state()
return "", render_answer_card(), state, format_stream_status(state, "cleared"), fresh_card_monitor_state()
async def clear_database() -> tuple[None, dict[str, Any], dict[str, Any], str, str, str, str, str, dict[str, Any]]:
await init_db()
await clear_all_tables()
return (
None,
{},
fresh_stream_state(),
"All SQLite tables cleared. Start a new session to continue.",
"",
render_answer_card(),
"",
"",
fresh_card_monitor_state(),
)
async def monitor_answer_card(
transcript: str,
monitor_state: dict[str, Any] | None,
session_id: int | None = None,
force: bool = False,
fast: bool = True,
pause_check: bool = False,
pause_seconds: float = 0.0,
) -> tuple[str, dict[str, Any]]:
monitor_state = monitor_state or fresh_card_monitor_state()
if fast and not (force or pause_check):
monitor_state["last_extraction_message"] = "Listening for the interviewer to finish the question."
return current_card_or_status(monitor_state), monitor_state.get("card_state") or {}
question = await detect_live_question_from_transcript(
transcript,
monitor_state,
pause_seconds=pause_seconds,
)
if not question:
message = monitor_state.get("last_extraction_message", "")
card_html = current_card_or_status(monitor_state, message)
monitor_state["card_html"] = card_html
return card_html, monitor_state.get("card_state") or {}
question_hash = question_hash_key(question)
if not has_new_live_question(question, monitor_state):
monitor_state["last_extraction_message"] = "Latest detected question is already shown."
existing_card_state = monitor_state.get("card_state") or {}
if session_id and existing_card_state and not existing_card_state.get("exchange_id"):
exchange_id = await persist_live_question(session_id, transcript, existing_card_state)
existing_card_state["session_id"] = session_id
existing_card_state["exchange_id"] = exchange_id
monitor_state["card_state"] = existing_card_state
return current_card_or_status(monitor_state), monitor_state.get("card_state") or {}
result = await classify_topic_and_steps(question)
if result.get("model_unavailable"):
monitor_state["last_extraction_message"] = result["message"]
card_html = current_card_or_status(monitor_state, result["message"])
monitor_state["card_html"] = card_html
return card_html, {}
cues = await generate_coaching_cues(
question,
result["type"],
result.get("pattern", result["type"]),
result["steps"],
)
card_state = {
"question": question,
"question_hash": question_hash,
"framework": result["type"],
"pattern": result.get("pattern", result["type"]),
"steps": cues,
"framework_steps": result["steps"],
"confidence": result["confidence"],
"needs_review": result["confidence"] < 0.6,
}
if session_id:
exchange_id = await persist_live_question(session_id, transcript, card_state)
card_state["session_id"] = session_id
card_state["exchange_id"] = exchange_id
cards = update_card_history(monitor_state, card_state)
card_html = render_cards(
[
render_card(card, flash=card.get("question_hash") == question_hash)
for card in cards
]
)
monitor_state["last_question"] = question
monitor_state["last_question_hash"] = question_hash
monitor_state["card_html"] = card_html
monitor_state["card_state"] = card_state
return card_html, card_state
async def persist_live_question(session_id: int, transcript: str, card_state: dict[str, Any]) -> int:
existing_exchange_id = await find_existing_exchange_id(session_id, card_state["question"])
if existing_exchange_id:
await backfill_empty_exchange_answers(session_id, transcript)
return existing_exchange_id
await add_transcript(
session_id=session_id,
raw_text=transcript,
labelled={"question": card_state["question"], "source": "live_detector"},
)
exchange_id = await add_exchange(
session_id=session_id,
question=card_state["question"],
answer="",
framework_used=card_state.get("framework", "General"),
)
await backfill_empty_exchange_answers(session_id, transcript)
return exchange_id
async def backfill_empty_exchange_answers(session_id: int, transcript: str) -> None:
if not transcript.strip():
return
exchanges = await list_exchanges(session_id)
if not exchanges:
return
repair_input = [
{
"question": str(exchange.get("question", "")),
"answer": str(exchange.get("answer", "")),
"is_target": True,
"complete": True,
"exchange_id": exchange.get("id"),
}
for exchange in exchanges
]
repaired = repair_missing_answers_from_transcript(repair_input, transcript)
for original, fixed in zip(exchanges, repaired):
if str(original.get("answer", "")).strip():
continue
answer = str(fixed.get("answer", "")).strip()
if answer:
await update_exchange_answer(int(original["id"]), answer)
async def find_existing_exchange_id(session_id: int, question: str) -> int | None:
existing = await find_existing_exchange(session_id, question)
return int(existing["id"]) if existing else None
async def find_existing_exchange(session_id: int, question: str) -> dict[str, Any] | None:
question_key = canonical_question_key(question)
exchanges = await list_exchanges(session_id)
for exchange in exchanges:
existing_key = canonical_question_key(str(exchange.get("question", "")))
if question_keys_are_similar(question_key, existing_key):
return exchange
return None
async def render_live_log(session_id: int | None) -> str:
if not session_id:
return "Create a session to save live exchanges."
return await render_log(session_id)
def current_card_or_status(state: dict[str, Any], message: str = "") -> str:
if state.get("card_state"):
return state.get("card_html") or render_card(state["card_state"])
return render_answer_card({"message": message or state.get("last_extraction_message", "")})
def update_card_history(state: dict[str, Any], card_state: dict[str, Any], limit: int = 4) -> list[dict[str, Any]]:
question_hash = str(card_state.get("question_hash", ""))
cards = [
card
for card in state.get("cards", [])
if isinstance(card, dict) and str(card.get("question_hash", "")) != question_hash
]
cards.insert(0, card_state)
cards = cards[:limit]
state["cards"] = cards
return cards
async def update_live_card_from_transcript(
transcript: str,
monitor_state: dict[str, Any] | None,
) -> tuple[str, dict[str, Any], dict[str, Any]]:
return await refresh_live_card_from_transcript(transcript, monitor_state, force=False)
async def call_coaching_from_transcript(
session_id: int | None,
transcript: str,
monitor_state: dict[str, Any] | None,
) -> tuple[str, dict[str, Any], dict[str, Any], str]:
ensure_model_warmup_background()
monitor_state = monitor_state or fresh_card_monitor_state()
if not transcript.strip():
monitor_state = fresh_card_monitor_state()
return render_answer_card(), {}, monitor_state, await render_live_log(session_id)
card_html, card_state = await monitor_answer_card(
transcript,
monitor_state,
session_id=session_id,
force=True,
fast=True,
)
return card_html, card_state, monitor_state, await render_live_log(session_id)
async def refresh_live_card_from_transcript(
transcript: str,
monitor_state: dict[str, Any] | None,
force: bool,
) -> tuple[str, dict[str, Any], dict[str, Any]]:
monitor_state = monitor_state or fresh_card_monitor_state()
if not force and not monitor_state.get("cards") and not monitor_state.get("last_question"):
return gr.skip(), gr.skip(), monitor_state
if not transcript.strip():
monitor_state = fresh_card_monitor_state()
return render_answer_card(), {}, monitor_state
card_html, card_state = await monitor_answer_card(
transcript,
monitor_state,
force=force,
fast=True,
)
return card_html, card_state, monitor_state
async def generate_coaching_cues(
question: str,
framework: str,
pattern: str,
fallback_steps: list[str],
) -> list[str]:
cues = await generate_coaching_cues_with_llm(question, framework, pattern, fallback_steps)
if cues:
return cues
return fallback_steps
async def generate_coaching_cues_with_llm(
question: str,
framework: str,
pattern: str,
fallback_steps: list[str],
) -> list[str]:
prompt = COACHING_GUIDANCE_USER_PROMPT.format(
question=question,
framework=framework,
pattern=pattern,
steps="\n".join(f"- {step}" for step in fallback_steps),
)
response = await general_llm.generate(
COACHING_GUIDANCE_SYSTEM_PROMPT,
prompt,
max_new_tokens=384,
)
if not response:
return []
try:
payload = json.loads(extract_json_object(response))
cues = payload.get("cues", [])
if isinstance(cues, list):
return [str(cue).strip() for cue in cues if str(cue).strip()][:6]
except Exception:
return []
return []
def fresh_card_monitor_state() -> dict[str, Any]:
return {
"last_question": "",
"last_question_hash": "",
"card_html": render_answer_card(),
"card_state": {},
"cards": [],
}
async def detect_live_question_from_transcript(
transcript: str,
state: dict[str, Any],
pause_seconds: float = 0.0,
) -> str | None:
previous_question = str(state.get("last_question") or "").strip()
question = await detect_live_question_from_excerpt(
recent_transcript_excerpt(transcript),
state,
previous_question,
pause_seconds,
"transcript tail",
)
if question or not previous_question:
return question
after_previous = transcript_after_question(transcript, previous_question)
if after_previous.strip() == transcript.strip():
return await detect_latest_question_from_list(
recent_transcript_excerpt(transcript),
state,
previous_question,
)
question = await detect_live_question_from_excerpt(
recent_transcript_excerpt(after_previous),
state,
previous_question,
pause_seconds,
"after previous question",
)
if question:
return question
return await detect_latest_question_from_list(
recent_transcript_excerpt(after_previous),
state,
previous_question,
)
async def detect_live_question_from_excerpt(
excerpt: str,
state: dict[str, Any],
previous_question: str,
pause_seconds: float,
source_label: str,
) -> str | None:
if len(excerpt.split()) < 4:
state["last_extraction_message"] = f"Not enough transcript after {source_label} to detect a new question."
return None
prompt = QUESTION_DETECTOR_USER_PROMPT.format(
transcript=excerpt,
pause_seconds=pause_seconds,
previous_question=previous_question or "None",
)
try:
response = await asyncio.wait_for(
general_llm.generate(
QUESTION_DETECTOR_SYSTEM_PROMPT,
prompt,
max_new_tokens=256,
),
timeout=LIVE_CARD_LLM_TIMEOUT_SECONDS,
)
except asyncio.TimeoutError:
state["last_extraction_message"] = live_detector_timeout_message("Question detector")
return None
if not response:
details = f" Details: {general_llm.last_error}" if general_llm.last_error else ""
state["last_extraction_message"] = (
f"General LLM unavailable. Expected Hugging Face model {GENERAL_LLM_MODEL}.{details}"
)
return None
try:
result = json.loads(extract_json_object(response))
except Exception as exc:
preview = response.replace("\n", " ")[:300]
general_llm.last_error = f"Question detector returned non-JSON output: {exc}. Output preview: {preview}"
state["last_extraction_message"] = "Question detector returned invalid JSON."
return None
if not bool(result.get("question_detected")):
if pause_seconds >= LIVE_QUESTION_PAUSE_SECONDS:
state["last_extraction_message"] = (
f"{pause_seconds:.1f}s pause reached, but no complete interviewer question was found in the transcript window."
)
else:
state["last_extraction_message"] = "Listening for the interviewer to finish a question."
return None
return validate_detected_question(result, excerpt, state, source_label)
async def detect_latest_question_from_list(
excerpt: str,
state: dict[str, Any],
previous_question: str,
) -> str | None:
if len(excerpt.split()) < 4:
return None
prompt = QUESTION_LIST_DETECTOR_USER_PROMPT.format(
transcript=excerpt,
previous_question=previous_question or "None",
)
try:
response = await asyncio.wait_for(
general_llm.generate(
QUESTION_LIST_DETECTOR_SYSTEM_PROMPT,
prompt,
max_new_tokens=700,
),
timeout=LIVE_CARD_LLM_TIMEOUT_SECONDS,
)
except asyncio.TimeoutError:
state["last_extraction_message"] = live_detector_timeout_message("Question list detector")
return None
if not response:
return None
try:
result = json.loads(extract_json_object(response))
except Exception as exc:
preview = response.replace("\n", " ")[:300]
general_llm.last_error = f"Question list detector returned non-JSON output: {exc}. Output preview: {preview}"
state["last_extraction_message"] = "Question list detector returned invalid JSON."
return None
questions = result.get("questions", [])
if not isinstance(questions, list):
return None
for item in reversed(questions):
if not isinstance(item, dict):
continue
question = validate_detected_question(item, excerpt, state, "question list fallback")
if question:
return question
state["last_extraction_message"] = "Question list fallback found no new valid interviewer question."
return None
def validate_detected_question(
result: dict[str, Any],
excerpt: str,
state: dict[str, Any],
source_label: str,
) -> str | None:
speaker = str(result.get("speaker", "unknown")).strip().lower()
confidence = str(result.get("confidence", "low")).strip().lower()
if speaker == "candidate":
state["last_extraction_message"] = f"Detector attributed latest text to {speaker or 'unknown'}, so no card was created."
return None
if confidence == "low":
state["last_extraction_message"] = "Question detector confidence was low, so no card was created."
return None
if result.get("is_target") is False:
state["last_extraction_message"] = "Detected question is outside DS/ML/AI/MLOps/System Design scope."
return None
question_value = result.get("question")
question = normalize_llm_text(str(question_value)) if question_value is not None else ""
question = trim_answer_leak_from_question(question)
if not question:
state["last_extraction_message"] = "Question detector did not return a question."
return None
if looks_like_candidate_answer_fragment(question):
state["last_extraction_message"] = "Detector returned candidate answer text, so no coaching card was created."
return None
if not looks_like_interviewer_prompt(question):
state["last_extraction_message"] = "Detector returned an answer fragment, so no coaching card was created."
return None
if question_looks_incomplete(question):
state["last_extraction_message"] = "Detector returned an incomplete question fragment, so no card was created."
return None
if not question_is_grounded_in_transcript(question, excerpt):
state["last_extraction_message"] = f"Detector returned a question that was not grounded in {source_label}."
return None
if not is_target_coaching_question(question, result):
state["last_extraction_message"] = "Detected question is not a target technical interview question."
return None
question_hash = question_hash_key(question)
if not has_new_live_question(question, state):
state["last_extraction_message"] = f"No newer interviewer question detected in {source_label}."
return None
state["last_extraction_message"] = ""
return question
def looks_like_candidate_answer_fragment(question: str) -> bool:
clean = normalize_llm_text(question).lower().rstrip("?!. ")
answer_starts = (
"in supervised learning",
"in unsupervised learning",
"supervised learning",
"unsupervised learning",
"the model ",
"a model ",
"the key difference",
"the main difference",
"the main reason",
"this means",
"it means",
"for example",
"like ",
"i would ",
"i will ",
"i have ",
"i used ",
"yes ",
"no ",
)
return clean.startswith(answer_starts)
def trim_answer_leak_from_question(question: str) -> str:
clean = normalize_llm_text(question)
if not clean:
return ""
trailing_answer_tokens = (" yes", " yeah", " yep", " no", " nope", " sure", " okay", " ok")
lowered = clean.lower().rstrip("?.!, ")
for token in trailing_answer_tokens:
if lowered.endswith(token):
clean = clean[: -len(token)].rstrip(" ?.!,")
break
answer_starts = (
" yes i ",
" yes, i ",
" yeah i ",
" sure i ",
" no i ",
" i have ",
" i've ",
" i used ",
" i would ",
" i will ",
" we used ",
" we have ",
)
lowered = f" {clean.lower()} "
cut_at = -1
for marker in answer_starts:
index = lowered.find(marker)
if index > 0:
cut_at = index
break
if cut_at > 0 and len(clean[:cut_at].split()) >= 5:
clean = clean[:cut_at].rstrip(" ?.!,")
if clean and not clean.endswith("?"):
clean = f"{clean.rstrip('.')}?"
return normalize_llm_text(clean)
def is_target_coaching_question(question: str, result: dict[str, Any] | None = None) -> bool:
result = result or {}
domain = str(result.get("domain", "")).strip().lower()
target_domains = {
"data_science",
"machine_learning",
"ai_engineering",
"mlops",
"statistics",
"analytics",
"coding",
"algorithms",
"system_design",
}
if domain in target_domains:
return True
if domain == "other" or result.get("is_target") is False:
return False
text = normalize_llm_text(question).lower()
domain_terms = (
"accuracy",
"algorithm",
"analytics",
"api",
"classification",
"clustering",
"coding",
"data",
"database",
"deployment",
"drift",
"embedding",
"evaluation",
"feature",
"fraud",
"inference",
"latency",
"learning",
"llm",
"machine",
"metric",
"ml",
"model",
"pipeline",
"precision",
"production",
"recall",
"recommendation",
"recommender",
"regression",
"scaling",
"scalable",
"statistics",
"supervised",
"system",
"system design",
"training",
"unsupervised",
"xgboost",
)
non_domain_terms = (
"this app",
"this tool",
"how does it work here",
"what does it mean",
"extract relevant questions",
"extract the required relevant questions",
"coaching card",
"session log",
"transcript",
)
return any(term in text for term in domain_terms) and not any(term in text for term in non_domain_terms)
def recent_transcript_excerpt(
transcript: str,
max_lines: int = LIVE_QUESTION_CONTEXT_LINES,
max_chars: int = LIVE_QUESTION_CONTEXT_CHARS,
) -> str:
lines = [line.strip() for line in transcript.splitlines() if line.strip()]
excerpt = "\n".join(lines[-max_lines:]) if lines else transcript.strip()
if len(excerpt) > max_chars:
excerpt = excerpt[-max_chars:]
return excerpt.strip()
def transcript_after_question(transcript: str, question: str) -> str:
if not question:
return transcript
transcript_lower = transcript.lower()
question_lower = question.lower().rstrip(" ?.")
position = transcript_lower.rfind(question_lower)
if position >= 0:
return transcript[position + len(question_lower) :].strip(" ?.:-,;") or transcript
match_end = ordered_question_match_end(transcript, question)
if match_end > 0:
return transcript[match_end:].strip(" ?.:-,;") or transcript
return transcript
def ordered_question_match_span(transcript: str, question: str, start_at: int = 0) -> tuple[int, int]:
transcript_tokens = [
(match.group(0).lower(), match.start(), match.end())
for match in re.finditer(r"[a-z0-9]+", transcript.lower())
if match.end() >= start_at
]
question_tokens = [
token
for token in re.findall(r"[a-z0-9]+", question.lower())
if token not in {"is", "the", "a", "an", "and", "or"}
]
if len(question_tokens) < 3:
return -1, -1
best_partial: tuple[int, int, int] | None = None
minimum_match = max(3, int(len(question_tokens) * 0.8))
for start_index, (token, start, end) in enumerate(transcript_tokens):
if token != question_tokens[0]:
continue
question_index = 1
matched = 1
match_end = end
for next_token, _, next_end in transcript_tokens[start_index + 1 :]:
if question_index >= len(question_tokens):
break
if next_token == question_tokens[question_index]:
matched += 1
question_index += 1
match_end = next_end
if matched == len(question_tokens):
return start, match_end
if matched >= minimum_match and (
best_partial is None or matched > best_partial[0]
):
best_partial = (matched, start, match_end)
if best_partial:
return best_partial[1], best_partial[2]
return -1, -1
def ordered_question_match_end(transcript: str, question: str) -> int:
transcript_tokens = [
(match.group(0).lower(), match.start(), match.end())
for match in re.finditer(r"[a-z0-9]+", transcript.lower())
]
question_tokens = [
token
for token in re.findall(r"[a-z0-9]+", question.lower())
if token not in {"is", "the", "a", "an", "and", "or"}
]
if len(question_tokens) < 3:
return -1
needed = len(question_tokens) if len(question_tokens) <= 6 else 6
for start_index, (token, start, _) in enumerate(transcript_tokens):
if token != question_tokens[0]:
continue
question_index = 1
matched = 1
end = start
for next_token, _, next_end in transcript_tokens[start_index + 1 :]:
if question_index >= len(question_tokens):
break
if next_token == question_tokens[question_index]:
matched += 1
question_index += 1
end = next_end
if matched >= needed:
return end
if matched >= needed:
return end
return -1
def question_hash_key(question: str) -> str:
return hashlib.md5(canonical_question_key(question).encode("utf-8")).hexdigest()
def canonical_question_key(question: str) -> str:
text = normalize_llm_text(question).lower()
replacements = {
"what's": "what is",
"whats": "what is",
"you're": "you are",
"you've": "you have",
"can't": "cannot",
}
for source, target in replacements.items():
text = text.replace(source, target)
words = re.findall(r"[a-z0-9]+", text)
stop_words = {"a", "an", "the", "please", "quick", "one", "question"}
words = [word for word in words if word not in stop_words]
return " ".join(remove_adjacent_duplicates(words))
def remove_adjacent_duplicates(words: list[str]) -> list[str]:
cleaned: list[str] = []
for word in words:
if cleaned and cleaned[-1] == word:
continue
cleaned.append(word)
return cleaned
def question_looks_incomplete(question: str) -> bool:
words = re.findall(r"[a-z0-9]+", question.lower())
if len(words) < 5:
return True
return words[-1] in {"and", "or", "between", "with", "of", "to", "for", "in", "on", "the", "a", "an"}
def looks_like_interviewer_prompt(text: str) -> bool:
clean = normalize_llm_text(text).lower()
promptish = re.sub(r"[,:;]", "", clean)
prompt_starts = (
"what ",
"what's ",
"why ",
"how ",
"when ",
"where ",
"which ",
"who ",
"have you ",
"have i ",
"do you ",
"did you ",
"are you ",
"can you ",
"could you ",
"would you ",
"should you ",
"explain ",
"describe ",
"tell me ",
"walk me through ",
"compare ",
"design ",
"solve ",
"evaluate ",
"reason about ",
"discuss ",
"show me ",
"give me ",
"build ",
"implement ",
)
if promptish.startswith(prompt_starts):
return True
setup_starts = (
"your model ",
"you have ",
"given ",
"suppose ",
"imagine ",
"let's say ",
"lets say ",
"in production ",
"in a production ",
"for a ",
)
question_clauses = (
" what ",
" how ",
" why ",
" which ",
" when ",
" where ",
" who ",
" is this ",
" is that ",
" is it ",
" are they ",
" do you ",
" does it ",
" can you ",
" could you ",
" would you ",
" should you ",
" have you ",
)
return promptish.startswith(setup_starts) and any(clause in f" {promptish}" for clause in question_clauses)
def question_is_grounded_in_transcript(question: str, transcript: str) -> bool:
question_tokens = content_tokens(question)
transcript_tokens = content_tokens(transcript)
if len(question_tokens) < 2:
return False
transcript_text = " ".join(transcript_tokens)
opening = " ".join(question_tokens[:2])
if opening and opening in transcript_text:
return True
if question_tokens[0] not in transcript_tokens:
return False
overlap = len(set(question_tokens) & set(transcript_tokens))
return overlap / max(len(set(question_tokens)), 1) >= 0.65
def content_tokens(text: str) -> list[str]:
stop_words = {"a", "an", "the", "is", "are", "was", "were", "to", "of", "in", "on", "for", "and", "or"}
return [
token
for token in re.findall(r"[a-z0-9]+", text.lower())
if token not in stop_words
]
def has_new_live_question(question: str, state: dict[str, Any]) -> bool:
question_key = canonical_question_key(question)
if not question_key:
return False
if question_keys_are_similar(question_key, canonical_question_key(str(state.get("last_question", "")))):
return False
return not any(
question_keys_are_similar(question_key, canonical_question_key(str(card.get("question", ""))))
for card in state.get("cards", [])
if isinstance(card, dict)
)
def question_keys_are_similar(left_key: str, right_key: str) -> bool:
if not left_key or not right_key:
return False
if left_key == right_key:
return True
left_words = left_key.split()
right_words = right_key.split()
if len(left_words) < 5 or len(right_words) < 5:
return False
left = set(left_words)
right = set(right_words)
overlap = len(left & right)
containment = overlap / max(min(len(left), len(right)), 1)
jaccard = overlap / max(len(left | right), 1)
return containment >= 0.86 or jaccard >= 0.78
def extract_json_object(text: str) -> str:
match = re.search(r"\{.*\}", text, flags=re.DOTALL)
return match.group(0) if match else text
def merge_transcript_text(existing: str, incoming: str) -> str:
existing = re.sub(r"\s+", " ", existing).strip()
incoming = re.sub(r"\s+", " ", incoming).strip()
if not existing:
return incoming
if not incoming:
return existing
if incoming.lower().startswith(existing.lower()):
return incoming
if existing.lower().endswith(incoming.lower()):
return existing
overlap = find_text_overlap(existing, incoming)
if overlap:
return f"{existing}{incoming[overlap:]}".strip()
return f"{existing} {incoming}".strip()
def find_text_overlap(existing: str, incoming: str) -> int:
existing_lower = existing.lower()
incoming_lower = incoming.lower()
max_overlap = min(len(existing), len(incoming), 120)
for size in range(max_overlap, 4, -1):
if existing_lower.endswith(incoming_lower[:size]):
return size
return 0
def fresh_stream_state() -> dict[str, Any]:
return {
"sample_rate": None,
"audio_buffer": np.array([], dtype=np.float32),
"transcript": "",
"last_input_audio": np.array([], dtype=np.float32),
"last_seen_samples": 0,
"processed_until": 0,
"chunks": 0,
"transcriptions": 0,
"rejected": 0,
"last_rms": 0.0,
"last_window_seconds": 0.0,
"quiet_seconds": 0.0,
"last_text": "",
}
def update_stream_state(audio_input: Any, stream_state: dict[str, Any] | None) -> dict[str, Any]:
state = stream_state or fresh_stream_state()
if not isinstance(audio_input, tuple) or len(audio_input) != 2:
return state
sample_rate, audio = audio_input
audio = np.asarray(audio)
if audio.ndim > 1:
audio = audio.mean(axis=1)
if np.issubdtype(audio.dtype, np.integer):
audio = audio.astype(np.float32) / np.iinfo(audio.dtype).max
else:
audio = audio.astype(np.float32, copy=False)
last_seen = int(state.get("last_seen_samples") or 0)
previous_input = state.get("last_input_audio")
if previous_input is None:
previous_input = np.array([], dtype=np.float32)
if len(audio) > last_seen and audio_has_previous_prefix(audio, previous_input):
new_audio = audio[last_seen:]
state["last_seen_samples"] = len(audio)
else:
new_audio = audio
state["last_seen_samples"] = len(audio)
state["last_input_audio"] = audio[-int(sample_rate) * 45 :].copy()
audio_buffer = state.get("audio_buffer")
if audio_buffer is None:
audio_buffer = np.array([], dtype=np.float32)
state["sample_rate"] = int(sample_rate)
state["chunks"] = int(state.get("chunks") or 0) + 1
combined = np.concatenate([audio_buffer, new_audio])
max_samples = int(sample_rate) * 45
dropped = max(0, len(combined) - max_samples)
state["audio_buffer"] = combined[-max_samples:]
state["processed_until"] = max(0, int(state.get("processed_until") or 0) - dropped)
return state
def format_stream_status(state: dict[str, Any], message: str) -> str:
sample_rate = int(state.get("sample_rate") or 16000)
audio_buffer = state.get("audio_buffer")
buffered_seconds = 0.0
if audio_buffer is not None:
buffered_seconds = len(audio_buffer) / sample_rate
processed_seconds = int(state.get("processed_until") or 0) / sample_rate
chunks = int(state.get("chunks") or 0)
transcriptions = int(state.get("transcriptions") or 0)
rejected = int(state.get("rejected") or 0)
rms = float(state.get("last_rms") or 0.0)
window_seconds = float(state.get("last_window_seconds") or 0.0)
last_text = str(state.get("last_text") or "").strip()
if last_text:
last_text = f" | last: {last_text[:80]}"
return (
f"{message} | chunks: {chunks} | buffered: {buffered_seconds:.1f}s "
f"| processed: {processed_seconds:.1f}s | runs: {transcriptions} "
f"| window: {window_seconds:.1f}s | rejected: {rejected} | rms: {rms:.4f}{last_text}"
)
def latest_stream_transcript(state: dict[str, Any], current_transcript: str) -> str:
state_transcript = str(state.get("transcript") or "").strip()
visible_transcript = str(current_transcript or "").strip()
if len(visible_transcript) > len(state_transcript):
state["transcript"] = visible_transcript
return visible_transcript
return state_transcript
def audio_has_previous_prefix(audio: np.ndarray, previous_audio: np.ndarray) -> bool:
if previous_audio.size == 0:
return False
compare_samples = min(previous_audio.size, audio.size, 8000)
if compare_samples <= 0:
return False
return bool(np.allclose(audio[:compare_samples], previous_audio[:compare_samples], atol=1e-4))
def audio_rms(audio: np.ndarray) -> float:
if audio.size == 0:
return 0.0
return float(np.sqrt(np.mean(np.square(audio.astype(np.float32)))))
def is_repetitive_hallucination(text: str) -> bool:
words = re.findall(r"[a-zA-Z']+", text.lower())
if len(words) < 8:
return False
unique_words = set(words)
if len(unique_words) <= 2:
return True
most_common = max(words.count(word) for word in unique_words)
if most_common / len(words) >= 0.65:
return True
repeated_run = 1
for previous, current in zip(words, words[1:]):
repeated_run = repeated_run + 1 if previous == current else 1
if repeated_run >= 5:
return True
return False
async def evaluate_session(session_id: int | None, last_state: dict[str, Any] | None) -> str:
if not session_id:
return "Start a session first."
exchanges = await list_exchanges(session_id)
if not exchanges:
return "No exchanges to evaluate yet."
existing_evaluations = await list_evaluations(session_id)
evaluated_exchange_ids = {item["exchange_id"] for item in existing_evaluations}
pending_exchanges = [exchange for exchange in exchanges if exchange["id"] not in evaluated_exchange_ids]
if not pending_exchanges:
return await render_evaluations(session_id)
latest_state_exchange_id = (last_state or {}).get("exchange_id")
for exchange in pending_exchanges:
if exchange["id"] == latest_state_exchange_id:
steps = (last_state or {}).get("framework_steps") or (last_state or {}).get("steps") or []
else:
classification = await classify_topic_and_steps(exchange["question"])
steps = classification.get("steps", [])
if not steps:
steps = ["Clarify", "Answer", "Example", "Impact"]
result = await evaluator.evaluate(
question=exchange["question"],
answer=exchange["answer"],
framework=exchange["framework_used"],
steps=steps,
)
await add_evaluation(
exchange_id=exchange["id"],
steps_covered=result["steps_covered"],
score=result["score"],
feedback=result["feedback"],
)
return await render_evaluations(session_id)
async def render_log(session_id: int) -> str:
exchanges = await list_exchanges(session_id)
if not exchanges:
return "No exchanges yet."
lines = []
for item in exchanges:
lines.append(
f"Q{item['id']} [{item['framework_used']}]: {item['question']}\n"
f"A: {item['answer'] or '(not captured yet)'}"
)
return "\n\n".join(lines)
async def render_evaluations(session_id: int) -> str:
evaluations = await list_evaluations(session_id)
if not evaluations:
return "No evaluations yet."
blocks = []
for item in evaluations:
blocks.append(
f"Exchange {item['exchange_id']}\n"
f"Framework: {item['framework_used']}\n"
f"{item['feedback']}"
)
return "\n\n".join(blocks)
async def render_sqlite_evaluation_summary(session_id: int | None) -> tuple[str, list[list[str]]]:
evaluations = await list_evaluations(session_id) if session_id else await list_all_evaluations()
if not evaluations:
return "No evaluated exchanges found in SQLite. Run Evaluate Session first.", []
blocks = []
rows = []
for item in evaluations:
feedback_parts = split_evaluation_feedback(item["feedback"])
session_name = format_session_name(item)
session_date = format_session_date(item.get("date", ""))
session_label = ""
if item.get("session_id"):
session_label = f"{session_name} - {session_date}\n"
blocks.append(
f"{session_label}"
f"Exchange {item['exchange_id']} [{item['framework_used']}]\n\n"
f"Question:\n{item['question']}\n\n"
f"My answer:\n{item['answer'] or '(not captured yet)'}\n\n"
f"Benchmark answer:\n{feedback_parts['benchmark']}\n\n"
f"Evaluation band:\n{feedback_parts['band']}\n\n"
f"Feedback:\n{feedback_parts['feedback']}"
)
rows.append(
[
session_name,
session_date,
item["question"],
item["answer"] or "(not captured yet)",
feedback_parts["benchmark"],
feedback_parts["band"],
feedback_parts["feedback"],
]
)
return "\n\n---\n\n".join(blocks), rows
async def export_sqlite_evaluation_summary_csv(session_id: int | None) -> str | None:
_, rows = await render_sqlite_evaluation_summary(session_id)
if not rows:
return None
export_dir = BASE_DIR / ".runtime" / "exports"
export_dir.mkdir(parents=True, exist_ok=True)
export_path = export_dir / "evaluation_summary.csv"
with export_path.open("w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerow(EVALUATION_TABLE_HEADERS)
writer.writerows(rows)
return str(export_path)
EVALUATION_TABLE_HEADERS = [
"Session",
"Date",
"Question",
"Candidate Answer",
"Model Benchmark Answer",
"Evaluation Band",
"Feedback",
]
def format_session_name(item: dict[str, Any]) -> str:
session_id = item.get("session_id", "")
company = item.get("company") or "Unknown company"
role = item.get("role") or "Unknown role"
return f"Session {session_id} - {company} / {role}"
def format_session_date(value: str) -> str:
if not value:
return ""
return value.replace("T", " ").split(".")[0]
def split_evaluation_feedback(feedback: str) -> dict[str, str]:
benchmark = extract_feedback_section(feedback, "Agent benchmark answer:", "Evaluation band:")
if not benchmark:
benchmark = extract_feedback_section(feedback, "Agent benchmark answer:", "Score:")
band = extract_feedback_section(feedback, "Evaluation band:", "Strong points:")
score_and_feedback = extract_feedback_section(feedback, "Evaluation band:", "")
if band:
score_and_feedback = extract_feedback_section(feedback, "Strong points:", "")
score_and_feedback = f"Strong points:\n{score_and_feedback}".strip()
else:
score_and_feedback = extract_feedback_section(feedback, "Score:", "")
if score_and_feedback:
score_text = re.match(r"^\s*(\d+\s*/\s*5)", score_and_feedback)
band = score_text.group(1) if score_text else "Unknown"
score_and_feedback = re.sub(r"^\s*\d+\s*/\s*5\s*", "", score_and_feedback).strip()
return {
"benchmark": benchmark or "(benchmark not available)",
"band": band or "Unknown",
"feedback": score_and_feedback or feedback.strip() or "(feedback not available)",
}
def extract_feedback_section(text: str, start_label: str, end_label: str) -> str:
start = text.find(start_label)
if start == -1:
return ""
start += len(start_label)
end = text.find(end_label, start) if end_label else -1
if end == -1:
return text[start:].strip()
return text[start:end].strip()
def render_card(state: dict[str, Any], flash: bool = False) -> str:
framework = html.escape(state.get("framework", "General"))
pattern = html.escape(state.get("pattern", ""))
display_type = pattern or framework
question = html.escape(state.get("question", "Question unavailable"))
color = FRAMEWORK_COLORS.get(display_type, FRAMEWORK_COLORS.get(framework, FRAMEWORK_COLORS["General"]))
steps = "".join(f"<li>{html.escape(step)}</li>" for step in state.get("steps", []))
flash_class = " flash-card" if flash else ""
return f"""
<details class="coach-card floating-card{flash_class}" style="--framework-color: {color}" open>
<summary>
<div class="card-question">{question}</div>
<span class="card-type">Type: {display_type}</span>
</summary>
<div class="meta">Steps</div>
<ol>{steps}</ol>
</details>
"""
def render_cards(cards: list[str]) -> str:
if not cards:
return render_answer_card()
return "<div class='coach-card-stack'>" + "\n".join(cards) + "</div>"
def render_answer_card(state: dict[str, Any] | None = None) -> str:
state = state or {}
status = state.get("message") or "Waiting for answer card content."
if state.get("answer"):
status = "Answer captured. Card content to be defined."
return f"""
<div class="coach-card" style="--framework-color: #22c55e">
<h3>Answer Card</h3>
<div class="meta">{html.escape(status)}</div>
</div>
"""
with gr.Blocks(elem_id="app-shell") as demo:
session_id = gr.State(None)
last_state = gr.State({})
stream_state = gr.State(fresh_stream_state())
card_monitor_state = gr.State(fresh_card_monitor_state())
runtime_label = "Hugging Face Space browser-mic demo" if HF_SPACE_MODE else "Local and Space-style audio coaching"
gr.Markdown(
f"# InterviewCoach\n{runtime_label} with live transcript, framework cards, and post-session evaluation.",
elem_id="app-header",
)
with gr.Row(elem_classes=["form", "action-row"]):
company = gr.Textbox(label="Company", placeholder="Anthropic", scale=2)
role = gr.Textbox(label="Role", placeholder="ML Engineer", scale=2)
start = gr.Button("Create Session", variant="primary", scale=1)
status = gr.Textbox(label="Session Status", interactive=False, elem_id="status_box")
model_status_box = gr.Textbox(
label="Startup Status",
value=render_startup_status(),
interactive=False,
lines=1,
elem_id="model_status_box",
)
with gr.Tabs():
with gr.Tab("Live"):
with gr.Row(equal_height=True, elem_id="live_grid"):
with gr.Column(scale=2, min_width=180, elem_classes=["compact-panel"]):
gr.Markdown("Space-style browser mic", elem_classes=["section-title"])
mic = gr.Audio(
sources=["microphone"],
type="numpy",
label="Record from browser",
streaming=True,
elem_id="mic_box",
)
with gr.Row(elem_classes=["action-row"]):
transcribe_coach = gr.Button("Process Recording", variant="primary")
if not HF_SPACE_MODE:
gr.Markdown("System audio", elem_classes=["section-title"])
with gr.Row(elem_classes=["action-row"]):
start_live = gr.Button("Start System Audio", variant="secondary")
stop_live = gr.Button("Stop System Audio", variant="stop")
with gr.Row(elem_classes=["action-row"]):
clear = gr.Button("Clear Screen", variant="secondary")
stream_status = gr.Textbox(
label="Audio Status",
interactive=False,
lines=2,
elem_id="stream_status_box",
)
with gr.Column(scale=3, min_width=280, elem_classes=["compact-panel"]):
gr.Markdown("Transcript workspace", elem_classes=["section-title"])
live_transcript = gr.Textbox(
label="Live Transcript",
lines=11,
placeholder="Recorded or typed transcript appears here.",
elem_id="live_transcript_box",
)
with gr.Row(elem_classes=["action-row"]):
call_coaching = gr.Button("Call Coaching", variant="primary")
coach = gr.Button("Process Text", variant="secondary")
with gr.Column(scale=2, min_width=220, elem_classes=["compact-panel"]):
gr.Markdown("Coaching card", elem_classes=["section-title"])
answer_card = gr.HTML(render_answer_card())
with gr.Tab("Session Log"):
log = gr.Textbox(label="Saved Exchanges", lines=16, interactive=False, elem_id="log_box")
clear_db = gr.Button("Clear SQLite Tables", variant="stop")
with gr.Tab("Evaluate"):
with gr.Row(elem_classes=["action-row"]):
evaluate = gr.Button("Evaluate Session", variant="primary")
load_eval_summary = gr.Button("Load SQLite Summary", variant="secondary")
export_eval_csv = gr.Button("Export CSV", variant="secondary")
evaluation_table = gr.Dataframe(
headers=EVALUATION_TABLE_HEADERS,
datatype=["str", "str", "str", "str", "str", "str", "str"],
label="SQLite Evaluation History",
interactive=False,
wrap=True,
)
export_file = gr.File(label="CSV Download", interactive=False)
report = gr.Textbox(label="Evaluation Report", lines=16, interactive=False, elem_id="report_box")
start.click(start_session, inputs=[company, role], outputs=[session_id, status])
demo.load(
warmup_all_models,
outputs=[model_status_box],
queue=True,
)
transcribe_coach.click(
transcribe_and_coach,
inputs=[session_id, mic, live_transcript],
outputs=[live_transcript, answer_card, log, last_state],
queue=True,
)
mic.stream(
stream_live_transcript,
inputs=[session_id, mic, live_transcript, stream_state],
outputs=[live_transcript, answer_card, stream_status, stream_state, last_state, log],
queue=True,
)
mic.stop_recording(
transcribe_browser_recording,
inputs=[mic, live_transcript, stream_state],
outputs=[live_transcript, stream_status, stream_state],
queue=True,
)
if not HF_SPACE_MODE:
start_live.click(
start_backend_live_transcript,
inputs=[session_id],
outputs=[live_transcript, answer_card, stream_status, last_state, card_monitor_state, log],
queue=True,
)
stop_live.click(
stop_backend_live_transcript,
outputs=[stream_status],
queue=False,
)
coach.click(
process_typed_transcript,
inputs=[session_id, live_transcript],
outputs=[live_transcript, answer_card, log, last_state, card_monitor_state],
queue=True,
)
call_coaching.click(
call_coaching_from_transcript,
inputs=[session_id, live_transcript, card_monitor_state],
outputs=[answer_card, last_state, card_monitor_state, log],
queue=True,
)
live_transcript.change(
update_live_card_from_transcript,
inputs=[live_transcript, card_monitor_state],
outputs=[answer_card, last_state, card_monitor_state],
queue=True,
)
clear.click(
clear_live_state,
outputs=[live_transcript, answer_card, stream_state, stream_status, card_monitor_state],
queue=False,
)
clear_db.click(
clear_database,
outputs=[
session_id,
last_state,
stream_state,
status,
live_transcript,
answer_card,
log,
report,
card_monitor_state,
],
queue=True,
)
evaluate.click(evaluate_session, inputs=[session_id, last_state], outputs=[report], queue=True)
load_eval_summary.click(
render_sqlite_evaluation_summary,
inputs=[session_id],
outputs=[report, evaluation_table],
queue=True,
)
export_eval_csv.click(
export_sqlite_evaluation_summary_csv,
inputs=[session_id],
outputs=[export_file],
queue=True,
)
if __name__ == "__main__":
port = int(os.environ.get("INTERVIEW_COACH_PORT", APP_PORT))
demo.queue().launch(
css=CSS,
theme=gr.themes.Base(),
server_name=APP_HOST,
server_port=port,
)