sajith-0701 commited on
Commit
5094515
·
1 Parent(s): 03faf26
backend/main.py CHANGED
@@ -9,7 +9,7 @@ import os
9
  from config import get_settings
10
  from database import connect_db, close_db
11
 
12
- from routers import auth, resume, profile, interview, reports, admin
13
 
14
  settings = get_settings()
15
 
@@ -48,6 +48,7 @@ app.include_router(profile.router, prefix="/profile", tags=["Profile"])
48
  app.include_router(interview.router, prefix="/interview", tags=["Interview"])
49
  app.include_router(reports.router, prefix="/reports", tags=["Reports"])
50
  app.include_router(admin.router, prefix="/admin", tags=["Admin"])
 
51
 
52
 
53
  @app.get("/health")
 
9
  from config import get_settings
10
  from database import connect_db, close_db
11
 
12
+ from routers import auth, resume, profile, interview, reports, admin, speech
13
 
14
  settings = get_settings()
15
 
 
48
  app.include_router(interview.router, prefix="/interview", tags=["Interview"])
49
  app.include_router(reports.router, prefix="/reports", tags=["Reports"])
50
  app.include_router(admin.router, prefix="/admin", tags=["Admin"])
51
+ app.include_router(speech.router, prefix="/speech", tags=["Speech"])
52
 
53
 
54
  @app.get("/health")
backend/routers/profile.py CHANGED
@@ -24,6 +24,9 @@ async def get_profile(current_user: dict = Depends(get_current_user)):
24
  "name": current_user.get("name", ""),
25
  "email": current_user.get("email", ""),
26
  "role": current_user.get("role", "student"),
 
 
 
27
  }
28
 
29
  # Get resume info
@@ -45,6 +48,27 @@ async def get_profile(current_user: dict = Depends(get_current_user)):
45
 
46
  return profile
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  @router.put("/skills")
49
  async def update_user_skills(
50
  request_data: dict, # Or use UpdateSkillsRequest if imported
 
24
  "name": current_user.get("name", ""),
25
  "email": current_user.get("email", ""),
26
  "role": current_user.get("role", "student"),
27
+ "speech_settings": {
28
+ "voice_gender": (user or {}).get("speech_settings", {}).get("voice_gender", "female"),
29
+ },
30
  }
31
 
32
  # Get resume info
 
48
 
49
  return profile
50
 
51
+
52
+ @router.put("/speech-settings")
53
+ async def update_speech_settings(
54
+ request_data: dict,
55
+ current_user: dict = Depends(get_current_user),
56
+ ):
57
+ """Update user's speech assistant preferences."""
58
+ db = get_db()
59
+ voice_gender = (request_data.get("voice_gender") or "female").strip().lower()
60
+ if voice_gender not in {"male", "female", "auto"}:
61
+ raise HTTPException(status_code=400, detail="voice_gender must be one of: male, female, auto")
62
+
63
+ await db[USERS].update_one(
64
+ {"_id": ObjectId(current_user["user_id"])},
65
+ {"$set": {"speech_settings.voice_gender": voice_gender}},
66
+ )
67
+ return {
68
+ "message": "Speech settings updated successfully",
69
+ "speech_settings": {"voice_gender": voice_gender},
70
+ }
71
+
72
  @router.put("/skills")
73
  async def update_user_skills(
74
  request_data: dict, # Or use UpdateSkillsRequest if imported
backend/routers/speech.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from fastapi.responses import Response
3
+ from pydantic import BaseModel
4
+
5
+ from auth.jwt import get_current_user
6
+ from services.tts_service import synthesize_wav
7
+
8
+ router = APIRouter()
9
+
10
+
11
+ class SpeechSynthesisRequest(BaseModel):
12
+ text: str
13
+ voice_gender: str = "female"
14
+
15
+
16
+ @router.get("/health")
17
+ async def speech_health(current_user: dict = Depends(get_current_user)):
18
+ """Check whether speech route is available for authenticated users."""
19
+ return {"status": "ok", "service": "speech"}
20
+
21
+
22
+ @router.post("/synthesize")
23
+ async def synthesize_speech(
24
+ request: SpeechSynthesisRequest,
25
+ current_user: dict = Depends(get_current_user),
26
+ ):
27
+ """Synthesize text to WAV bytes using Coqui TTS models."""
28
+ try:
29
+ wav_bytes = await synthesize_wav(request.text, request.voice_gender)
30
+ return Response(content=wav_bytes, media_type="audio/wav")
31
+ except ValueError as e:
32
+ raise HTTPException(status_code=400, detail=str(e))
33
+ except RuntimeError as e:
34
+ raise HTTPException(status_code=503, detail=str(e))
35
+ except Exception as e:
36
+ raise HTTPException(status_code=500, detail=f"Speech synthesis failed: {str(e)}")
backend/services/auth_service.py CHANGED
@@ -25,6 +25,9 @@ async def signup_user(name: str, email: str, password: str, role: str = None) ->
25
  "email": email,
26
  "password": hashed_password,
27
  "role": determined_role,
 
 
 
28
  "created_at": utc_now(),
29
  }
30
 
 
25
  "email": email,
26
  "password": hashed_password,
27
  "role": determined_role,
28
+ "speech_settings": {
29
+ "voice_gender": "female",
30
+ },
31
  "created_at": utc_now(),
32
  }
33
 
backend/services/tts_service.py ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import os
3
+ import tempfile
4
+ from typing import Tuple
5
+
6
+ _MODEL_CACHE = {}
7
+ _MODEL_LOCK = asyncio.Lock()
8
+
9
+
10
+ def _select_model(voice_gender: str) -> Tuple[str, str | None]:
11
+ gender = (voice_gender or "female").strip().lower()
12
+ if gender == "male":
13
+ # Multi-speaker model; use a male VCTK speaker token.
14
+ return "tts_models/en/vctk/vits", "p226"
15
+ # Default female-like English voice model.
16
+ return "tts_models/en/ljspeech/tacotron2-DDC", None
17
+
18
+
19
+ async def _get_tts_model(model_name: str):
20
+ async with _MODEL_LOCK:
21
+ if model_name in _MODEL_CACHE:
22
+ return _MODEL_CACHE[model_name]
23
+
24
+ def _load_model():
25
+ try:
26
+ from TTS.api import TTS
27
+ except Exception as exc:
28
+ raise RuntimeError(
29
+ "Coqui TTS is not installed in the active Python environment"
30
+ ) from exc
31
+
32
+ # Use CPU by default for compatibility.
33
+ return TTS(model_name=model_name, progress_bar=False, gpu=False)
34
+
35
+ model = await asyncio.to_thread(_load_model)
36
+ _MODEL_CACHE[model_name] = model
37
+ return model
38
+
39
+
40
+ async def synthesize_wav(text: str, voice_gender: str = "female") -> bytes:
41
+ content = (text or "").strip()
42
+ if not content:
43
+ raise ValueError("text is required")
44
+
45
+ model_name, speaker = _select_model(voice_gender)
46
+ tts = await _get_tts_model(model_name)
47
+
48
+ fd, tmp_path = tempfile.mkstemp(suffix=".wav")
49
+ os.close(fd)
50
+ try:
51
+ def _synthesize():
52
+ kwargs = {
53
+ "text": content,
54
+ "file_path": tmp_path,
55
+ }
56
+ if speaker:
57
+ kwargs["speaker"] = speaker
58
+ tts.tts_to_file(**kwargs)
59
+
60
+ await asyncio.to_thread(_synthesize)
61
+ with open(tmp_path, "rb") as f:
62
+ return f.read()
63
+ finally:
64
+ if os.path.exists(tmp_path):
65
+ os.remove(tmp_path)