Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions cv_eval/llm_scorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,15 +304,27 @@ def unified_evaluate(self, cv_text: str, jd_text: str = "") -> dict:
else:
prompt = CV_ONLY_EVALUATION_PROMPT.format(cv_text=cv_text)

raw = self._call_llm(prompt)
cleaned = self._extract_json_from_response(raw)
return json.loads(cleaned)
return self._generate_and_parse_json(prompt)

# ---------- CV only (legacy alias) ----------
def evaluate_cv_only(self, cv_text: str) -> dict:
return self.unified_evaluate(cv_text=cv_text, jd_text="")

# ---------- Internals ----------
def _generate_and_parse_json(self, prompt: str) -> dict:
"""Retry LLM call if JSON parsing fails."""
for attempt in range(3):
try:
raw = self._call_llm(prompt)
cleaned = self._extract_json_from_response(raw)
return json.loads(cleaned)
except json.JSONDecodeError as e:
logger.warning(f"JSON parsing failed (attempt {attempt+1}/3): {e}. Retrying.")
if attempt == 2:
logger.error(f"Final JSON parsing failure. Raw response: {raw}")
raise
raise ValueError("Failed to generate valid JSON")

def _call_llm(self, prompt: str) -> str:
for attempt in range(3):
try:
Expand All @@ -324,6 +336,7 @@ def _call_llm(self, prompt: str) -> str:
],
temperature=self.temperature,
max_tokens=3500,
response_format={"type": "json_object"},
)
return resp.choices[0].message.content.strip()
except Exception as e:
Expand All @@ -337,9 +350,7 @@ def improvement(self, cv_text: str, jd_text: str) -> dict:
raise ValueError("Both CV text and JD text are required for improvement")

prompt = IMPROVEMENT_PROMPT.format(cv_text=cv_text, jd_text=jd_text)
raw = self._call_llm(prompt)
cleaned = self._extract_json_from_response(raw)
return json.loads(cleaned)
return self._generate_and_parse_json(prompt)


@staticmethod
Expand Down
20 changes: 20 additions & 0 deletions debug_groq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@

try:
from groq import Groq
import inspect

print(f"Groq imported successfully: {Groq}")
# Use dummy key just to init client
client = Groq(api_key="gsk_dummy")
print(f"Client type: {type(client)}")
print(f"Client dir: {dir(client)}")

if hasattr(client, 'audio'):
print("Client has 'audio' attribute")
print(f"Audio type: {type(client.audio)}")
print(f"Audio dir: {dir(client.audio)}")
else:
print("Client MISSING 'audio' attribute")

except Exception as e:
print(f"Error: {e}")
16 changes: 16 additions & 0 deletions debug_voice_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@

import sys
import os
import traceback

print(f"Python executable: {sys.executable}")
try:
print("Attempting to import interview.voice_analyzer...")
from interview.voice_analyzer import voice_analyzer
print(f"Successfully imported voice_analyzer: {voice_analyzer}")
except ImportError:
print("Caught ImportError!")
traceback.print_exc()
except Exception:
print("Caught unexpected exception!")
traceback.print_exc()
7 changes: 6 additions & 1 deletion interview/session_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,7 +544,12 @@ def _combine_text_voice_scores(self, tech_eval: Dict, voice_eval: Dict) -> Dict:
# If fallback local 'raw' exists, try to use its finer-grained breakdown for suggestions
raw_text_eval = tech_eval.get("raw") or {}

voice_score = voice_eval.get("voice_scores", {}).get("total", 0.0) # Out of 6
voice_data = voice_eval.get("voice_scores", {})
# Handle both flat (legacy) and nested (new) structures
if "raw" in voice_data:
voice_score = voice_data.get("raw", {}).get("total", 0.0)
else:
voice_score = voice_data.get("total", 0.0)

# If no voice data, slightly penalize overall outcome
if voice_score == 0.0:
Expand Down
9 changes: 8 additions & 1 deletion interview/speech_to_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,13 +248,20 @@ def convert_audio_to_text(self, audio_data: bytes, language: Optional[str] = Non

if isinstance(response, dict):
text = response.get("text")

if not text and "segments" in response:
text = " ".join(
seg.get("text", "") for seg in response.get("segments", [])
)
else:
# With Groq Python SDK v1.0+, response is a Transcription object
# It might have a 'text' attribute.
text = getattr(response, "text", None)
if text is None:
logger.warning(f"ASR response object content: {dir(response)}")

# Explicitly check for string "None." which might be a hallucination or artifact
if text == "None." or text == "None":
text = ""

text = (text or "").strip()
if not text:
Expand Down
15 changes: 15 additions & 0 deletions interview/voice_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,23 +63,34 @@ def analyze_voice(
return self._fail("no_audio_data")

# -------- IN-MEMORY AUDIO DECODE (NO TEMP FILES) --------
# logger.info(f"[VoiceAnalyzer] Decoding {len(audio_data)} bytes...")
audio_buffer = io.BytesIO(audio_data)
y, sr = sf.read(audio_buffer, dtype="float32")

if y is None or len(y) == 0:
logger.warning("[VoiceAnalyzer] sf.read returned empty array")
return self._fail("empty_audio_after_decode")

# logger.info(f"[VoiceAnalyzer] Decoded: {len(y)} samples at {sr}Hz (Duration: {len(y)/sr:.2f}s)")

# Convert to mono if needed
if y.ndim > 1:
y = np.mean(y, axis=1)

# Resample if needed
if sr != self.sample_rate:
# logger.info(f"[VoiceAnalyzer] Resampling from {sr} to {self.sample_rate}")
y = librosa.resample(y, orig_sr=sr, target_sr=self.sample_rate)
sr = self.sample_rate

analysis = self._analyze_audio_features(y, sr, transcript)
analysis["analysis_ok"] = True

# Check if metrics are all zero, which is suspicious
metrics = analysis.get("voice_metrics", {})
if metrics.get("duration", 0) == 0:
logger.warning(f"[VoiceAnalyzer] Processed audio but duration is 0. Metrics: {metrics}")

return analysis

except Exception as e:
Expand Down Expand Up @@ -341,3 +352,7 @@ def _fail(self, code: str) -> Dict[str, Any]:
"wpm_source": "none",
},
}


# Global instance
voice_analyzer = VoiceAnalyzer()
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ langchain-openai==0.3.31
langgraph==0.6.6

# LLM provider for CV evaluation
groq==0.4.2
groq>=1.0.0

# Vector embeddings and search
sentence-transformers==3.0.1
Expand Down
Loading
Loading